diff --git a/.gitignore b/.gitignore index fb6ecb0907379c5a5bfdcde51c78d9a2d2b3872d..de39c339a9c61dc7e961a8513cd3a24e12cc602e 100644 --- a/.gitignore +++ b/.gitignore @@ -60,7 +60,7 @@ eslint-report.html /jsconfig.json /lefthook-local.yml /log/*.log* -/node_modules +node_modules /nohup.out /public/assets/ /public/uploads.* diff --git a/.gitlab/ci/caching.gitlab-ci.yml b/.gitlab/ci/caching.gitlab-ci.yml index 0f8369c3ddc3d219790bcf512e2561800339904b..0b20b86ab7c6a99d4844046902f8b5634bf0fc7f 100644 --- a/.gitlab/ci/caching.gitlab-ci.yml +++ b/.gitlab/ci/caching.gitlab-ci.yml @@ -117,3 +117,41 @@ cache:assets-hash: reports: dotenv: build.env allow_failure: false + +cache:frontend-islands-hash: + extends: + - .cache-pull-push-base + - .with-alpine-ruby-image + - .caching:rules:frontend-islands-hash + script: + - echo "GLCI_FRONTEND_ISLANDS_HASH=$(ruby scripts/lib/frontend_islands_sha.rb)" > build.env + - echo "Calculated frontend islands checksum - $(cat build.env)" + artifacts: + reports: + dotenv: build.env + +.frontend-islands-cache: &frontend-islands-cache + key: "frontend-islands-${BUILD_OS}-${OS_VERSION}-node-${NODE_VERSION}-${GLCI_FRONTEND_ISLANDS_HASH}" + paths: + - ee/frontend_islands/apps/duo_next/dist/ + policy: pull-push + +.frontend-islands-cache-pull: + - <<: *frontend-islands-cache + policy: pull + +cache:frontend-islands: + extends: + - .cache-pull-push-base + - .with-ci-node-image + - .caching:rules:update-cache-on-frontend-islands-code-changes + cache: + - !reference [.yarn-cache, cache] + - <<: *frontend-islands-cache + needs: + - job: cache:frontend-islands-hash + optional: true + variables: + USE_BUNDLE_INSTALL: "false" + script: + - scripts/build_frontend_islands diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index 09c43aab20112aea3f8eb586484ea4d31912a571..3e3b24573426ea3dd5eac07ce9d2c3b8d8a7601c 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -13,6 +13,9 @@ - .default-before_script - .assets-compile-cache - .with-ci-node-image + cache: + - !reference [.assets-compile-cache, cache] + - !reference [.frontend-islands-cache-pull] variables: SETUP_DB: "false" USE_BUNDLE_INSTALL: "false" # skip bundle install from default prepare_build script @@ -24,7 +27,10 @@ stage: prepare needs: - cache:assets-hash + - job: cache:frontend-islands-hash + optional: true script: + - scripts/build_frontend_islands - scripts/compile_assets - scripts/build_assets_image @@ -110,6 +116,9 @@ gdk:compile-test-assets: - compile-test-assets - .assets-compile-cache-pull - .build-images:rules:build-gdk-image + before_script: + - !reference [.compile-assets-base, before_script] + - !reference [cache:frontend-islands, script] .frontend-fixtures-base: extends: @@ -230,7 +239,6 @@ upload-graphql-schema-dump: .frontend-test-base: extends: - .default-retry - - .yarn-cache - .with-ci-node-image variables: # Disable warnings in browserslist which can break on backports @@ -241,13 +249,19 @@ upload-graphql-schema-dump: before_script: - !reference [.default-before_script, before_script] - yarn_install_script + - scripts/build_frontend_islands stage: test-frontend + cache: + - !reference [.yarn-cache, cache] + - !reference [.frontend-islands-cache-pull] + needs: + - job: cache:frontend-islands-hash + optional: true jest-build-cache: extends: - .frontend-test-base - .frontend:rules:jest - needs: [] artifacts: name: jest-cache expire_in: 12h @@ -271,6 +285,23 @@ jest-build-cache: NODE_OPTIONS: --max-old-space-size=7680 allow_failure: true +test-fe-island: + extends: + - .frontend-test-base + - .frontend:rules:fe-islands:test + stage: test-frontend + artifacts: + name: coverage-frontend + expire_in: 31d + when: always + paths: + - ee/frontend_islands/apps/duo_next/coverage/ + - ee/frontend_islands/apps/duo_next/junit_jest.xml + reports: + junit: junit_jest.xml + script: + - cd ee/frontend_islands/apps/duo_next && yarn run test + .with-jest-build-cache-vue3-ensure-compilable-sfcs-needs: needs: - job: jest-build-cache-vue3-ensure-compilable-sfcs @@ -291,6 +322,8 @@ jest: needs: - job: jest-build-cache optional: true + - job: cache:frontend-islands-hash + optional: true artifacts: name: coverage-frontend expire_in: 31d @@ -348,6 +381,8 @@ jest vue3 mr: - .frontend:rules:jest - .vue3 needs: + - job: cache:frontend-islands-hash + optional: true - !reference [.with-jest-build-cache-vue3-ensure-compilable-sfcs-needs, needs] artifacts: name: vue3-mr-junit @@ -492,6 +527,8 @@ jest-integration: - !reference [.repo-from-artifacts, needs] - !reference [.with-fixtures-needs, needs] - !reference [.with-graphql-schema-dump-needs, needs] + - job: cache:frontend-islands-hash + optional: true jest-snapshot-vue3: extends: @@ -502,6 +539,8 @@ jest-snapshot-vue3: # it's ok to wait for the repo artifact as we're waiting for the fixtures (which wait for the repo artifact) anyway - !reference [.repo-from-artifacts, needs] - !reference [.with-fixtures-needs, needs] + - job: cache:frontend-islands-hash + optional: true variables: VUE_VERSION: 3 JEST_REPORT: jest-test-report.json @@ -536,7 +575,6 @@ jest-linters: extends: - .frontend-test-base - .frontend:rules:jest-linters - needs: [] script: - yarn jest:eslint --ci @@ -607,6 +645,8 @@ compile-storybook: - !reference [.repo-from-artifacts, needs] - !reference [.with-fixtures-needs, needs] - !reference [.with-graphql-schema-dump-needs, needs] + - job: cache:frontend-islands-hash + optional: true artifacts: name: storybook expire_in: 31d @@ -629,6 +669,8 @@ test-storybook: - !reference [.repo-from-artifacts, needs] - !reference [.with-fixtures-needs, needs] - !reference [.with-graphql-schema-dump-needs, needs] + - job: cache:frontend-islands-hash + optional: true allow_failure: true timeout: 30m artifacts: diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index cca69c6cae88f90df27e26aa2eeb126e199d9557..575d9540823cc3aa051009ed7ebd0873a8e0b247 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -447,10 +447,11 @@ key: files: - yarn.lock - - scripts/frontend/postinstall.js + - ee/frontend_islands/apps/duo_next/yarn.lock prefix: node-modules-${BUILD_OS}-${OS_VERSION}-${NODE_ENV}-node-${NODE_VERSION} paths: - node_modules/ + - ee/frontend_islands/apps/duo_next/node_modules/ policy: pull .assets-cache: &assets-cache @@ -580,7 +581,8 @@ policy: pull-push .assets-compile-cache: - extends: .cache-analytics + extends: + - .cache-analytics cache: - !reference [.ruby-gems-cache] - *node-modules-cache @@ -588,7 +590,8 @@ - *assets-tmp-cache .assets-compile-cache-pull: # this cache is used only for gdk:compile-test-assets which should only pull, never push - extends: .cache-analytics + extends: + - .cache-analytics cache: - !reference [.ruby-gems-cache] - *node-modules-cache diff --git a/.gitlab/ci/preflight.gitlab-ci.yml b/.gitlab/ci/preflight.gitlab-ci.yml index 5ae6e43d6a75626a0a95da8b45241d518eac0564..cf997340b49b5219eeb4c31a00efee6ff63656ed 100644 --- a/.gitlab/ci/preflight.gitlab-ci.yml +++ b/.gitlab/ci/preflight.gitlab-ci.yml @@ -83,6 +83,7 @@ bundle-size-review: SETUP_DB: "false" script: - yarn_install_script + - scripts/build_frontend_islands - scripts/bundle_size_review artifacts: when: always diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 1928c27893d4f0f53d99ebf5fd8c63bb6c29b794..4e22c49f414f35b4319b5c1f5ac9b726b23479d8 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -402,6 +402,8 @@ - "babel.config.js" - "vite.config.js" - "config/webpack.config.js" + - "**/tsconfig*.json" + - "**/*.config.{js,ts,cjs}" - "config/**/*.js" - "{,ee/,jh/}app/assets/**/*" - "vendor/assets/**/*" @@ -554,14 +556,27 @@ # for the reasons why we only match those patterns. .frontend-predictive-patterns: &frontend-predictive-patterns - "{,ee/,jh/}{app/assets/javascripts,spec/frontend}/**/*" + - "ee/frontend_islands/apps/**/*" .frontend-code-patterns: &frontend-code-patterns - "**/{package.json,yarn.lock}" - - "**/*.{js,cjs,mjs,vue,graphql}" + - "**/*.{js,cjs,mjs,ts,tsx,vue,graphql}" + - "**/*.{css,scss,sass}" + - "**/*.html" + - "**/tsconfig*.json" + - "**/*.config.{js,ts,cjs}" - ".eslint_todo/*.mjs" # match the .rc config files like .browserslistrc, .stylelintrc etc., - "{.,}*rc" +.code-frontend-islands-patterns: &code-frontend-islands-patterns + - "ee/frontend_islands/apps/**/.{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" + - "ee/frontend_islands/apps/**/{package.json,yarn.lock}" + - "ee/frontend_islands/apps/**/*/src/**/*.{js,cjs,mjs,ts,tsx,vue,graphql,css}" + - "ee/frontend_islands/apps/**/tsconfig*.json" + - "ee/frontend_islands/apps/**/components.json" + - "ee/frontend_islands/apps/**/*.config.{js,ts,cjs}" + # Code patterns + .ci-patterns .code-patterns: &code-patterns - ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" @@ -1229,6 +1244,12 @@ changes: *code-backstage-patterns - *if-as-if-foss-mr-refs +.caching:rules:update-cache-on-frontend-islands-code-changes: + rules: + - <<: *if-default-refs + changes: *code-frontend-islands-patterns + - *if-as-if-foss-mr-refs + # e2e runners are only available in .com and in gitlab-org grouped projects .caching:rules:e2e-gem-cache-e2e-runners: rules: @@ -1251,6 +1272,15 @@ - !reference [.frontend:rules:assets-shared, rules] - !reference [.releases:rules:canonical-dot-com-security-gitlab-stable-branch-only, rules] +.caching:rules:frontend-islands-hash: + rules: + - <<: *if-default-refs + changes: *code-frontend-islands-patterns + - <<: *if-merge-request + changes: *code-frontend-islands-patterns + - <<: *if-tag + - <<: *if-sync-changes-on-stable-branches + ###################### # CI Templates Rules # ###################### @@ -1577,6 +1607,26 @@ - '{,ee/}app/**/*.vue' - '{,ee/}app/**/*.stories.js' +.frontend:rules:fe-islands:test: + rules: + - <<: *if-merge-request-labels-pipeline-expedite + when: never + - <<: *if-fork-merge-request + when: never + - <<: *if-automated-merge-request + changes: *code-frontend-islands-patterns + - <<: *if-security-merge-request + changes: *code-frontend-islands-patterns + - <<: *if-merge-request-labels-run-all-jest + - <<: *if-merge-request-labels-frontend-and-feature-flag + changes: *code-frontend-islands-patterns + - <<: *if-merge-request + changes: *code-frontend-islands-patterns + - <<: *if-merge-request + changes: [".gitlab/ci/frontend.gitlab-ci.yml"] + - <<: *if-default-refs + changes: *code-frontend-islands-patterns + .frontend:rules:jest: rules: - <<: *if-merge-request-labels-pipeline-expedite diff --git a/.gitlab/ci/static-analysis.gitlab-ci.yml b/.gitlab/ci/static-analysis.gitlab-ci.yml index 3f1831b35b42c7138bf817cebe3854e8314308da..84873dca5a3334fb98b04edbeed01f5345fd4bd1 100644 --- a/.gitlab/ci/static-analysis.gitlab-ci.yml +++ b/.gitlab/ci/static-analysis.gitlab-ci.yml @@ -64,14 +64,20 @@ generate-apollo-graphql-schema: .eslint-base: extends: - .static-analysis-base - - .yarn-cache - needs: ['generate-apollo-graphql-schema'] + needs: + - generate-apollo-graphql-schema + - job: cache:frontend-islands-hash + optional: true variables: USE_BUNDLE_INSTALL: "false" ESLINT_CODE_QUALITY_REPORT: "gl-code-quality-report.json" before_script: - !reference [.default-before_script, before_script] - yarn_install_script + - scripts/build_frontend_islands + cache: + - !reference [.yarn-cache, cache] + - !reference [.frontend-islands-cache-pull] eslint-changed-files: extends: @@ -90,6 +96,27 @@ eslint: reports: codequality: gl-code-quality-report.json +eslint:frontend-islands: + extends: + - .eslint-base + - .frontend:rules:fe-islands:test + needs: + - job: cache:frontend-islands-hash + optional: true + cache: + - !reference [.yarn-cache, cache] + - !reference [.frontend-islands-cache-pull] + before_script: + - !reference [.default-before_script, before_script] + - yarn_install_script + - scripts/build_frontend_islands + script: + - cd ee/frontend_islands/apps/duo_next + - yarn run lint --format gitlab + artifacts: + reports: + codequality: gl-code-quality-report.json + eslint-docs: extends: - .static-analysis-base diff --git a/app/assets/javascripts/commons/vue.js b/app/assets/javascripts/commons/vue.js index 534cd61e8573cbf74df94b44e2291d177b0b4f34..d9552e427e82fc5a662c032c1ff4c1524daa3a71 100644 --- a/app/assets/javascripts/commons/vue.js +++ b/app/assets/javascripts/commons/vue.js @@ -14,4 +14,4 @@ Vue.use(GlFeatureFlagsPlugin); Vue.use(GlAbilitiesPlugin); Vue.use(Translate); -Vue.config.ignoredElements = ['gl-emoji']; +Vue.config.ignoredElements = ['gl-emoji', 'fe-island-duo-next']; diff --git a/config/helpers/vite_plugin_prebuild_duo_next.js b/config/helpers/vite_plugin_prebuild_duo_next.js new file mode 100644 index 0000000000000000000000000000000000000000..eac2fe914f4f2919e4df2ab398d43b28849b10e9 --- /dev/null +++ b/config/helpers/vite_plugin_prebuild_duo_next.js @@ -0,0 +1,47 @@ +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; +import fs from 'node:fs'; + +export const PrebuildDuoNext = (options = {}) => { + const { + appDir = 'ee/frontend_islands/apps/duo_next', + command = 'yarn', + args = ['build'], + skipEnv = 'SKIP_PRE_LAUNCH', // set to "1" to bypass: SKIP_PRE_LAUNCH=1 vite dev + } = options; + + let ran = false; + + return { + name: 'prebuild-duo-next', + apply: 'serve', // never runs in "vite build" + enforce: 'pre', + + configureServer() { + if (ran) return; + if (process.env[skipEnv]) return; + + const cwd = path.resolve(process.cwd(), appDir); + const pkg = path.join(cwd, 'package.json'); + + if (!fs.existsSync(pkg)) { + throw new Error(`[prebuild-duo-next] package.json not found at ${pkg}`); + } + + const res = spawnSync(command, args, { + cwd, + stdio: 'inherit', + shell: true, + env: process.env, + }); + + if (res.status !== 0) { + throw new Error( + `[prebuild-duo-next] "${command} ${args.join(' ')}" failed in ${cwd} (code ${res.status})`, + ); + } + + ran = true; + }, + }; +}; diff --git a/config/webpack.config.js b/config/webpack.config.js index 09502a998b3a757435cee3c0ff93fe35122e4219..7ff06bb3f7917ee700040ddd8d71903b8e0db8d2 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -162,6 +162,7 @@ if (IS_EE) { ee_else_ce: path.join(ROOT_PATH, 'ee/app/assets/javascripts'), jh_else_ee: path.join(ROOT_PATH, 'ee/app/assets/javascripts'), any_else_ce: path.join(ROOT_PATH, 'ee/app/assets/javascripts'), + fe_islands: path.join(ROOT_PATH, 'ee/frontend_islands/apps'), // test-environment-only aliases duplicated from Jest config ee_else_ce_jest: path.join(ROOT_PATH, 'ee/spec/frontend'), diff --git a/ee/app/assets/javascripts/ai/duo_agentic_chat/components/app.vue b/ee/app/assets/javascripts/ai/duo_agentic_chat/components/app.vue index ad8a8380c9d1a4719b392330fa43fdb637194aa6..3e15860343dac848d951f3f41b0fbbf939646963 100644 --- a/ee/app/assets/javascripts/ai/duo_agentic_chat/components/app.vue +++ b/ee/app/assets/javascripts/ai/duo_agentic_chat/components/app.vue @@ -4,6 +4,7 @@ import { duoChatGlobalState } from '~/super_sidebar/constants'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { __ } from '~/locale'; +import { logError } from '~/lib/logger'; import { WIDTH_OFFSET } from '../../tanuki_bot/constants'; export default { @@ -100,7 +101,7 @@ export default { mounted() { this.setDimensions(); window.addEventListener('resize', this.onWindowResize); - + this.loadDuoNextIfNeeded(); if (this.$route?.path !== '/current') { this.$router.push('/current'); } @@ -110,6 +111,15 @@ export default { window.removeEventListener('resize', this.onWindowResize); }, methods: { + async loadDuoNextIfNeeded() { + if (this.glFeatures.duoUiNext) { + try { + await import('fe_islands/duo_next/dist/duo_next'); + } catch (err) { + logError('Failed to load frontend islands duo_next module', err); + } + } + }, setDimensions() { this.updateDimensions(); }, @@ -142,7 +152,22 @@ export default {