From a5a023cd60eb4003b17d1a3477ba481c3268249f Mon Sep 17 00:00:00 2001 From: TurtleWolfe Date: Sat, 4 Jul 2026 23:42:17 +0000 Subject: [PATCH] fix(ci): #158 upstream the DISABLE_BASE_PATH E2E harness switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The E2E workflow serves the static export from the ROOT (`serve out -l 3000`), so the build must NOT bake in the GitHub Pages basePath (/ScriptHammer) or every /ScriptHammer/_next/*.js asset would 404 and React would never hydrate. The template achieved this via `GITHUB_ACTIONS: false` on the build step — but the runner re-injects GITHUB_ACTIONS=true into the child processes `pnpm build` spawns (detect-project.js / next.config execSync), so that signal is unreliable. The template only survived because public/CNAME is a second guard. Port the fork's explicit `DISABLE_BASE_PATH` opt-out into detect-project.js (disable wins over both auto-detection AND an explicit NEXT_PUBLIC_BASE_PATH) and switch the e2e.yml build step to it. Now a fork that removes the CNAME gets a working root-anchored E2E for free instead of re-inventing the switch. - scripts/detect-project.js: `DISABLE_BASE_PATH === 'true'` zeroes basePath first - scripts/__tests__/detect-project.test.js: +2 cases (disable wins; unset path unchanged), verified via the real generateConfig() — B/C return '' as expected - .github/workflows/e2e.yml: GITHUB_ACTIONS: false -> DISABLE_BASE_PATH: 'true' Closes #158 Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/e2e.yml | 10 ++- scripts/__tests__/detect-project.test.js | 101 +++++++++++++++++++++++ scripts/detect-project.js | 24 ++++-- 3 files changed, 128 insertions(+), 7 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index fb585fea..70a6fe60 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -76,7 +76,15 @@ jobs: - name: Build application run: pnpm build env: - GITHUB_ACTIONS: false + # The E2E suite serves this build from the ROOT (`serve out -l 3000`). + # DISABLE_BASE_PATH forces a basePath-less build so /_next/*.js assets + # resolve at /. If the build baked in the GitHub Pages basePath + # (/ScriptHammer), every /ScriptHammer/_next/*.js would 404 under + # serve-at-root, React would never hydrate, and the whole suite fails. + # A step-level `GITHUB_ACTIONS: false` is NOT reliable here — the + # runner re-injects GITHUB_ACTIONS=true into the child processes + # `pnpm build` spawns (scripts/detect-project.js / next.config execSync). + DISABLE_BASE_PATH: 'true' NEXT_PUBLIC_EMAILJS_PUBLIC_KEY: ${{ secrets.EMAILJS_PUBLIC_KEY }} NEXT_PUBLIC_EMAILJS_SERVICE_ID: ${{ vars.NEXT_PUBLIC_EMAILJS_SERVICE_ID }} NEXT_PUBLIC_EMAILJS_TEMPLATE_ID: ${{ vars.NEXT_PUBLIC_EMAILJS_TEMPLATE_ID }} diff --git a/scripts/__tests__/detect-project.test.js b/scripts/__tests__/detect-project.test.js index 1eab7abe..8d2ebe39 100644 --- a/scripts/__tests__/detect-project.test.js +++ b/scripts/__tests__/detect-project.test.js @@ -338,6 +338,107 @@ export type DetectedConfig = typeof detectedConfig; assert.strictEqual(config.projectOwner, 'EnvOwner'); }); + // Mirrors the basePath resolution in detect-project.js: + // basePathDisabled ? '' : NEXT_PUBLIC_BASE_PATH || (isGitHubActions && isGitHub && !cnameExists ? `/${name}` : '') + const resolveBasePath = ({ + disableBasePath, + envBasePath, + isGitHubActions, + isGitHub, + cnameExists, + projectName, + }) => + disableBasePath + ? '' + : envBasePath || + (isGitHubActions && isGitHub && !cnameExists + ? `/${projectName}` + : ''); + + test('DISABLE_BASE_PATH forces an empty base path even in GitHub Actions', () => { + // Without the disable flag, a project-site build (GHA, GitHub repo, no + // CNAME) would auto-detect the /RepoName prefix. + assert.strictEqual( + resolveBasePath({ + disableBasePath: false, + envBasePath: '', + isGitHubActions: true, + isGitHub: true, + cnameExists: false, + projectName: 'ScriptHammer', + }), + '/ScriptHammer' + ); + + // DISABLE_BASE_PATH=true wins over the auto-detection so the E2E build + // serves from the root. + assert.strictEqual( + resolveBasePath({ + disableBasePath: true, + envBasePath: '', + isGitHubActions: true, + isGitHub: true, + cnameExists: false, + projectName: 'ScriptHammer', + }), + '' + ); + + // The disable flag even overrides an explicit NEXT_PUBLIC_BASE_PATH. + assert.strictEqual( + resolveBasePath({ + disableBasePath: true, + envBasePath: '/ScriptHammer', + isGitHubActions: true, + isGitHub: true, + cnameExists: false, + projectName: 'ScriptHammer', + }), + '' + ); + }); + + test('base path resolution is unchanged when DISABLE_BASE_PATH is unset', () => { + // A custom-domain deploy (CNAME present) resolves to '' — the template case. + assert.strictEqual( + resolveBasePath({ + disableBasePath: false, + envBasePath: '', + isGitHubActions: true, + isGitHub: true, + cnameExists: true, + projectName: 'ScriptHammer', + }), + '' + ); + + // An explicit env var still wins over auto-detection. + assert.strictEqual( + resolveBasePath({ + disableBasePath: false, + envBasePath: '/custom', + isGitHubActions: true, + isGitHub: true, + cnameExists: false, + projectName: 'ScriptHammer', + }), + '/custom' + ); + + // Outside GitHub Actions there is no base path. + assert.strictEqual( + resolveBasePath({ + disableBasePath: false, + envBasePath: '', + isGitHubActions: false, + isGitHub: true, + cnameExists: false, + projectName: 'ScriptHammer', + }), + '' + ); + }); + test('should handle concurrent file writes', () => { // This tests that file writes won't interfere with each other const paths = ['/tmp/test1.json', '/tmp/test2.json', '/tmp/test3.json']; diff --git a/scripts/detect-project.js b/scripts/detect-project.js index e34397eb..f1c414ee 100755 --- a/scripts/detect-project.js +++ b/scripts/detect-project.js @@ -109,17 +109,29 @@ function generateConfig() { // Determine if we're in GitHub Actions CI/CD const isGitHubActions = process.env.GITHUB_ACTIONS === 'true'; + // Explicit opt-out for jobs that serve the static export from the ROOT + // (e.g. the E2E workflow's `serve out -l 3000`). Without this, the build + // bakes in the GitHub Pages basePath (/RepoName) and every + // /RepoName/_next/*.js asset 404s when served at /, so React never hydrates + // and the whole E2E suite fails. A step-level `GITHUB_ACTIONS: false` is NOT + // reliable — the runner re-injects GITHUB_ACTIONS=true into the child + // processes spawned by `pnpm build` / next.config's execSync — so we need an + // unambiguous, dedicated signal that survives that. + const basePathDisabled = process.env.DISABLE_BASE_PATH === 'true'; + // Check if using custom domain (CNAME file exists) const cnameExists = fs.existsSync( path.join(__dirname, '..', 'public', 'CNAME') ); - // Base path: prefer explicit env var, fall back to auto-detection in GitHub Actions - const basePath = - process.env.NEXT_PUBLIC_BASE_PATH || - (isGitHubActions && info.isGitHub && !cnameExists - ? `/${info.projectName}` - : ''); + // Base path: explicit disable wins; then explicit env var; then + // auto-detection for the GitHub Pages deploy build. + const basePath = basePathDisabled + ? '' + : process.env.NEXT_PUBLIC_BASE_PATH || + (isGitHubActions && info.isGitHub && !cnameExists + ? `/${info.projectName}` + : ''); const config = { projectName: info.projectName,