diff --git a/docker-images/core/codecc/gateway/scripts/codecc.env b/docker-images/core/codecc/gateway/scripts/codecc.env index 6845dd87..da6820dd 100644 --- a/docker-images/core/codecc/gateway/scripts/codecc.env +++ b/docker-images/core/codecc/gateway/scripts/codecc.env @@ -24,8 +24,9 @@ BK_CODECC_PUBLIC_URL=$BK_CODECC_HOST BK_CODECC_GATEWAY_REGION_NAME=$BK_CODECC_GATEWAY_REGION_NAME BK_CI_PUBLIC_URL=$BK_CI_PUBLIC_URL BK_CI_FQDN=$BK_CI_FQDN +BK_SITE_PATH=$BK_SITE_PATH BK_API_TENANT_HOST=$BK_API_TENANT_HOST BK_API_TENANT_BASE_URL=$BK_API_TENANT_BASE_URL BK_CODECC_ENABLE_MULTI_TENANT=$BK_CODECC_ENABLE_MULTI_TENANT BK_LOGIN_PATH=$BK_LOGIN_PATH -BK_CI_JWT_RSA_PRIVATE_KEY=$BK_CI_JWT_RSA_PRIVATE_KEY \ No newline at end of file +BK_CI_JWT_RSA_PRIVATE_KEY=$BK_CI_JWT_RSA_PRIVATE_KEY diff --git a/docs/superpowers/plans/2026-05-13-codecc-subpath-implementation.md b/docs/superpowers/plans/2026-05-13-codecc-subpath-implementation.md new file mode 100644 index 00000000..9efd8b58 --- /dev/null +++ b/docs/superpowers/plans/2026-05-13-codecc-subpath-implementation.md @@ -0,0 +1,1012 @@ +# CodeCC Frontend Subpath Support Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make CodeCC frontend work when deployed at `/` or any configured subpath such as `/codecc-demo/`, without rebuilding per environment. + +**Architecture:** Inject `BK_SITE_PATH` and `BK_STATIC_URL` in the rendered entry HTML, normalize both at runtime, and let Vue Router use `BK_SITE_PATH` as its history base. Use webpack runtime public path plus a small URL helper so static assets, login callback URLs, internal links, and project cookie path respect the configured deployment path while backend API URLs remain controlled by `AJAX_URL_PREFIX`. + +**Tech Stack:** Vue 2.7, Vue Router 3, bk-cli-service-webpack, webpack runtime public path, nginx/OpenResty gateway templates, Helm templates, shell deployment env rendering. + +--- + +## File Structure + +- Create `src/frontend/devops-codecc/static/webpack_public_path.js`: sets webpack runtime public path before app imports. +- Create `src/frontend/devops-codecc/src/utils/path.js`: shared path normalization and URL joining helpers for JavaScript modules. +- Create `src/frontend/devops-codecc/scripts/verify-path-utils.js`: Node verifier for path helpers. +- Modify `src/frontend/devops-codecc/index.html`: inject `BK_SITE_PATH`, normalize entry-page globals, define `__loadAssetsUrl__`, fix project-id cookie path, and fix iframe utility prefix handling. +- Modify `src/frontend/devops-codecc/src/main.js`: import runtime public path before all other imports. +- Modify `src/frontend/devops-codecc/src/router/index.js`: configure Vue Router history base. +- Modify `src/frontend/devops-codecc/src/api/index.js`: build login modal success callback with the static asset prefix. +- Modify CodeCC-owned internal URL call sites that manually construct root-relative URLs: + - `src/frontend/devops-codecc/src/views/paas/test/design-report.vue` + - any additional call sites found by the audit command in Task 4. +- Modify deploy-time env sources: + - `src/frontend/devops-codecc/.bk.production.env` + - `scripts/deploy-codecc/codecc.properties` + - `docker-images/core/codecc/gateway/scripts/codecc.env` + - `helm-charts/core/codecc/templates/gateway/deployment.yaml` +- Modify `support-files/codecc/templates/core/gateway#core#vhosts#codecc.frontend.conf`: strip the configured site path before frontend `try_files` checks. +- Create `src/frontend/devops-codecc/scripts/verify-subpath-dist.js`: local build-output verifier for root and subpath modes. +- Create `src/frontend/devops-codecc/scripts/serve-subpath-dist.js`: local static server with history fallback for manual browser verification. + +## Task 1: Add Path Helpers + +**Files:** +- Create: `src/frontend/devops-codecc/src/utils/path.js` +- Create: `src/frontend/devops-codecc/scripts/verify-path-utils.js` + +- [ ] **Step 1: Write the failing path helper verifier** + +Create `src/frontend/devops-codecc/scripts/verify-path-utils.js`: + +```js +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vm = require('vm'); + +const sourcePath = path.resolve(__dirname, '../src/utils/path.js'); +const source = fs.readFileSync(sourcePath, 'utf8'); +const transformedSource = source + .replace(/export function /g, 'function ') + .concat('\nmodule.exports = { normalizeBasePath, normalizeAssetBase, joinUrl, getSitePath, getStaticUrl, withSitePath, withStaticUrl };'); + +const sandbox = { + module: { exports: {} }, + exports: {}, + window: {}, + URL, +}; + +vm.runInNewContext(transformedSource, sandbox, { filename: sourcePath }); + +const { + normalizeBasePath, + normalizeAssetBase, + joinUrl, + getSitePath, + getStaticUrl, + withSitePath, + withStaticUrl, +} = sandbox.module.exports; + +assert.strictEqual(normalizeBasePath(), '/'); +assert.strictEqual(normalizeBasePath(''), '/'); +assert.strictEqual(normalizeBasePath('/'), '/'); +assert.strictEqual(normalizeBasePath('codecc'), '/codecc/'); +assert.strictEqual(normalizeBasePath('/codecc'), '/codecc/'); +assert.strictEqual(normalizeBasePath('codecc/'), '/codecc/'); +assert.strictEqual(normalizeBasePath('/bk/codecc/'), '/bk/codecc/'); +assert.strictEqual(normalizeAssetBase('https://cdn.example.com/codecc'), 'https://cdn.example.com/codecc/'); + +assert.strictEqual(joinUrl('/codecc-demo/', '/static/login_success.html'), '/codecc-demo/static/login_success.html'); +assert.strictEqual(joinUrl('/', '/static/login_success.html'), '/static/login_success.html'); +assert.strictEqual(joinUrl('https://example.com/codecc/', '/static/a.js'), 'https://example.com/codecc/static/a.js'); +assert.strictEqual(joinUrl('/codecc-demo/', 'https://cdn.example.com/a.js'), 'https://cdn.example.com/a.js'); + +sandbox.window.BK_SITE_PATH = '/codecc-demo/'; +sandbox.window.BK_STATIC_URL = ''; +assert.strictEqual(getSitePath(), '/codecc-demo/'); +assert.strictEqual(getStaticUrl(), '/codecc-demo/'); +assert.strictEqual(withSitePath('/codecc/demo/task/list'), '/codecc-demo/codecc/demo/task/list'); +assert.strictEqual(withStaticUrl('/static/login_success.html'), '/codecc-demo/static/login_success.html'); + +sandbox.window.BK_STATIC_URL = '/static-prefix/'; +assert.strictEqual(getStaticUrl(), '/static-prefix/'); +assert.strictEqual(withStaticUrl('/static/login_success.html'), '/static-prefix/static/login_success.html'); + +sandbox.window.BK_STATIC_URL = 'https://cdn.example.com/codecc'; +assert.strictEqual(getStaticUrl(), 'https://cdn.example.com/codecc/'); +assert.strictEqual(withStaticUrl('/static/login_success.html'), 'https://cdn.example.com/codecc/static/login_success.html'); + +console.log('path utils verifier passed'); +``` + +- [ ] **Step 2: Run the focused test and verify it fails** + +Run: + +```bash +cd /Users/brooklin/project/github/bk-codecc/src/frontend/devops-codecc +node scripts/verify-path-utils.js +``` + +Expected: FAIL with `ENOENT` because `src/utils/path.js` does not exist yet. + +- [ ] **Step 3: Implement the path helper** + +Create `src/frontend/devops-codecc/src/utils/path.js`: + +```js +export function normalizeBasePath(value = '/') { + const rawValue = String(value || '/').trim(); + if (!rawValue || rawValue === '/') { + return '/'; + } + + const normalized = rawValue.replace(/^\/+|\/+$/g, ''); + return normalized ? `/${normalized}/` : '/'; +} + +export function normalizeAssetBase(value = '/', fallback = '/') { + const rawValue = String(value || fallback || '/').trim(); + if (/^https?:\/\//.test(rawValue) || rawValue.startsWith('//')) { + return rawValue.endsWith('/') ? rawValue : `${rawValue}/`; + } + return normalizeBasePath(rawValue); +} + +export function joinUrl(base = '/', path = '') { + const baseValue = String(base || '/'); + const pathValue = String(path || ''); + + if (/^https?:\/\//.test(pathValue) || pathValue.startsWith('//')) { + return pathValue; + } + + if (/^https?:\/\//.test(baseValue)) { + const url = new URL(baseValue); + url.pathname = [url.pathname, pathValue].join('/').replace(/\/+/g, '/'); + return url.toString().replace(/\/$/, pathValue ? '' : '/'); + } + + const startsWithRoot = baseValue.startsWith('/'); + const joined = [baseValue, pathValue] + .filter(item => item !== '') + .join('/') + .replace(/\/+/g, '/'); + const result = startsWithRoot ? `/${joined.replace(/^\/+/, '')}` : joined; + return result || (startsWithRoot ? '/' : ''); +} + +export function getSitePath() { + return normalizeBasePath(window.BK_SITE_PATH || '/'); +} + +export function getStaticUrl() { + return normalizeAssetBase(window.BK_STATIC_URL, getSitePath()); +} + +export function withSitePath(path) { + return joinUrl(getSitePath(), path); +} + +export function withStaticUrl(path) { + return joinUrl(getStaticUrl(), path); +} +``` + +- [ ] **Step 4: Run the path helper verifier** + +Run: + +```bash +cd /Users/brooklin/project/github/bk-codecc/src/frontend/devops-codecc +node scripts/verify-path-utils.js +``` + +Expected: `path utils verifier passed`. + +- [ ] **Step 5: Commit Task 1** + +Run: + +```bash +git add src/frontend/devops-codecc/src/utils/path.js src/frontend/devops-codecc/scripts/verify-path-utils.js +git commit -m "feat(frontend): add subpath url helpers" +``` + +Expected: commit succeeds. + +## Task 2: Inject Entry Variables and Runtime Public Path + +**Files:** +- Create: `src/frontend/devops-codecc/static/webpack_public_path.js` +- Modify: `src/frontend/devops-codecc/index.html` +- Modify: `src/frontend/devops-codecc/src/main.js` +- Modify: `src/frontend/devops-codecc/.bk.production.env` + +- [ ] **Step 1: Add webpack runtime public path file** + +Create `src/frontend/devops-codecc/static/webpack_public_path.js`: + +```js +__webpack_public_path__ = window.BK_STATIC_URL || '/'; +``` + +- [ ] **Step 2: Import runtime public path before all app imports** + +Modify the top of `src/frontend/devops-codecc/src/main.js` so the first executable import is: + +```js +import '../static/webpack_public_path'; +``` + +Expected beginning of file: + +```js +/** + * @file main entry + * @author blueking + */ + +import '../static/webpack_public_path'; +import Vue from 'vue'; +``` + +- [ ] **Step 3: Add `BK_SITE_PATH` to production env** + +Modify `src/frontend/devops-codecc/.bk.production.env`: + +```env +BK_SITE_PATH = '__BK_SITE_PATH__' +BK_STATIC_URL = '__BK_PUBLIC_PATH_PREFIX__' +``` + +Place `BK_SITE_PATH` immediately before `BK_STATIC_URL`. + +- [ ] **Step 4: Add entry-page path normalization and `__loadAssetsUrl__`** + +In `src/frontend/devops-codecc/index.html`, immediately before the current environment variable declarations inside the first ` diff --git a/src/frontend/devops-codecc/src/views/defect/coverity-list.vue b/src/frontend/devops-codecc/src/views/defect/coverity-list.vue index 6f3fdcec..ffa10c5b 100755 --- a/src/frontend/devops-codecc/src/views/defect/coverity-list.vue +++ b/src/frontend/devops-codecc/src/views/defect/coverity-list.vue @@ -1481,6 +1481,7 @@ import { export_json_to_excel } from '@/vendor/export2Excel'; import DefectBlock from './defect-block/defect-block'; import OperateDialog from '@/components/operate-dialog'; import UserSelector from '@/components/user-selector/index.vue'; +import { withSitePath } from '@/utils/path'; // 搜索过滤项缓存 const COVERITY_SEARCH_OPTION_CACHE = 'search_option_columns_coverity'; @@ -2972,10 +2973,12 @@ export default { const { projectId, taskId } = this.$route.params; const { toolName, entityId, status } = this.currentFile; let prefix = `${location.protocol}//${location.host}`; + let path = withSitePath(`/codecc/${projectId}/task/${taskId}/defect/compile/${toolName}/list`); if (window.self !== window.top) { prefix = `${location.protocol}${window.DEVOPS_SITE_URL}/console`; + path = `/codecc/${projectId}/task/${taskId}/defect/compile/${toolName}/list`; } - const url = `${prefix}/codecc/${projectId}/task/${taskId}/defect/compile/${toolName}/list + const url = `${prefix}${path} ?entityId=${entityId}&status=${status}`; const input = document.createElement('input'); document.body.appendChild(input); diff --git a/src/frontend/devops-codecc/src/views/defect/detail.vue b/src/frontend/devops-codecc/src/views/defect/detail.vue index 38666028..0d1e110f 100644 --- a/src/frontend/devops-codecc/src/views/defect/detail.vue +++ b/src/frontend/devops-codecc/src/views/defect/detail.vue @@ -586,6 +586,7 @@ import AiSuggestion from './ai-suggestion.vue'; import CheckerDetail from './checker-detail.vue'; import DEPLOY_ENV from '@/constants/env'; import ignoreApprovalIng from '@/images/ignore-approval-ing.svg'; +import { withSitePath } from '@/utils/path'; export default { components: { @@ -1615,18 +1616,25 @@ export default { shareDefect() { const { projectId, taskId } = this.$route.params; const { toolName, entityId, status } = this.currentFile; - let prefix = `${location.host}`; + let url = `${location.host}${withSitePath(`/codecc/${projectId}/task/${taskId}/defect/lint/${toolName}/list`)} +?entityId=${entityId}&status=${status}`; if (window.self !== window.top) { - prefix = `${window.DEVOPS_SITE_URL}/console`; - } - let url = `${prefix}/codecc/${projectId}/task/${taskId}/defect/lint/${toolName}/list + url = `${window.DEVOPS_SITE_URL}/console/codecc/${projectId}/task/${taskId}/defect/lint/${toolName}/list ?entityId=${entityId}&status=${status}`; + } if (this.isProjectDefect) { - url = `${prefix}/codecc/${projectId}/defect/list + url = `${location.host}${withSitePath(`/codecc/${projectId}/defect/list`)} ?entityId=${entityId}&status=${status}`; + if (window.self !== window.top) { + url = `${window.DEVOPS_SITE_URL}/console/codecc/${projectId}/defect/list +?entityId=${entityId}&status=${status}`; + } } if (this.isPaas) { - url = `${prefix}/paas/ignored/${toolName}/list?entityId=${entityId}`; + url = `${location.host}${withSitePath(`/paas/ignored/${toolName}/list`)}?entityId=${entityId}`; + if (window.self !== window.top) { + url = `${window.DEVOPS_SITE_URL}/console/paas/ignored/${toolName}/list?entityId=${entityId}`; + } } const input = document.createElement('input'); document.body.appendChild(input); diff --git a/src/frontend/devops-codecc/src/views/paas/test/design-report.vue b/src/frontend/devops-codecc/src/views/paas/test/design-report.vue index 03d2b51a..7c678685 100644 --- a/src/frontend/devops-codecc/src/views/paas/test/design-report.vue +++ b/src/frontend/devops-codecc/src/views/paas/test/design-report.vue @@ -168,6 +168,7 @@ import { format } from 'date-fns'; // eslint-disable-next-line import { export_json_to_excel } from '@/vendor/export2Excel'; +import { withSitePath } from '@/utils/path'; export default { name: 'DesignReport', @@ -393,7 +394,8 @@ export default { this.queryTestReportDetailList(searchParams); }, openTaskOverview(row) { - const url = `${window.location.origin}/codecc/${row.projectId}/task/${row.taskId}/detail`; + const taskPath = withSitePath(`/codecc/${row.projectId}/task/${row.taskId}/detail`); + const url = `${window.location.origin}${taskPath}`; window.open(url, '_blank'); }, openGitRepo(row) { diff --git a/src/frontend/devops-codecc/static/webpack_public_path.js b/src/frontend/devops-codecc/static/webpack_public_path.js new file mode 100644 index 00000000..ccb8bef3 --- /dev/null +++ b/src/frontend/devops-codecc/static/webpack_public_path.js @@ -0,0 +1 @@ +__webpack_public_path__ = window.BK_STATIC_URL || '/'; diff --git a/support-files/codecc/templates/core/gateway#core#vhosts#codecc.frontend.conf b/support-files/codecc/templates/core/gateway#core#vhosts#codecc.frontend.conf index 61c86af0..42083715 100644 --- a/support-files/codecc/templates/core/gateway#core#vhosts#codecc.frontend.conf +++ b/support-files/codecc/templates/core/gateway#core#vhosts#codecc.frontend.conf @@ -12,12 +12,14 @@ header_filter_by_lua_file 'conf/lua/cors_filter.lua'; add_header Cache-Control no-store; index index.html index.htm; + rewrite ^__BK_SITE_PATH__(.*)$ /$1 break; try_files $uri @fallback; } location ~* \.(html)$ { header_filter_by_lua_file 'conf/lua/cors_filter.lua'; add_header Cache-Control no-store; + rewrite ^__BK_SITE_PATH__(.*)$ /$1 break; try_files $uri @fallback; # 匹配所有以 html结尾的请求 } @@ -25,6 +27,7 @@ location ~* \.(js|css|ttf)$ { header_filter_by_lua_file 'conf/lua/cors_filter.lua'; add_header Cache-Control max-age=2592000; + rewrite ^__BK_SITE_PATH__(.*)$ /$1 break; try_files $uri @fallback; # 匹配所有以 js,css或tff 结尾的请求 }