diff --git a/scripts/build-changelog.ts b/scripts/build-changelog.ts index 1b7eb86a..ae691945 100644 --- a/scripts/build-changelog.ts +++ b/scripts/build-changelog.ts @@ -233,6 +233,11 @@ function defaultListPrereleaseTags(cwd: string, baseVersion: string): string[] { // Diffs changes/*.md between the previous prerelease tag and the working tree. // Renamed fragments are treated as modifications of the new path. +// +// Like every other path in this script, git paths are resolved against `cwd`, +// which is the project root and also the repository root. Callers that point +// `cwd` elsewhere already fail earlier and loudly, when package.json and +// changes/ come back missing. function defaultResolveFragmentDelta(cwd: string, previousTag: string): FragmentDeltaEntry[] { const output = execFileSync( 'git', diff --git a/src/prerelease-workflow.test.ts b/src/prerelease-workflow.test.ts index 08c68086..b0dd3e51 100644 --- a/src/prerelease-workflow.test.ts +++ b/src/prerelease-workflow.test.ts @@ -122,3 +122,35 @@ test('prerelease workflow does not publish to AUR', () => { assert.doesNotMatch(prereleaseWorkflow, /AUR_SSH_PRIVATE_KEY/); assert.doesNotMatch(prereleaseWorkflow, /scripts\/update-aur-package\.sh/); }); + +test('prerelease workflow rejects committed notes generated for a different beta or rc', () => { + assert.equal( + packageJson.scripts['changelog:check-prerelease-notes'], + 'bun run scripts/build-changelog.ts check-prerelease-notes', + ); + assert.match( + prereleaseWorkflow, + /bun run changelog:check-prerelease-notes --version "\$RELEASE_VERSION"/, + ); + + // The staleness check has to run before the release is created or edited, + // otherwise stale notes are already published by the time it fails. + const checkIndex = prereleaseWorkflow.indexOf('changelog:check-prerelease-notes'); + const createIndex = prereleaseWorkflow.indexOf('gh release create'); + const editIndex = prereleaseWorkflow.indexOf('gh release edit'); + assert.notEqual(checkIndex, -1); + assert.ok(checkIndex < createIndex); + assert.ok(checkIndex < editIndex); +}); + +// GitHub substitutes ${{ }} into the run script before the shell parses it, so a +// tag-derived value used that way is executed as script rather than read as data. +test('tag-derived values reach shell bodies through env, not template interpolation', () => { + const rawVersionUses = prereleaseWorkflow + .split('\n') + .filter((line) => line.includes('steps.version.outputs.VERSION')) + .filter( + (line) => !/^\s*RELEASE_VERSION: \$\{\{ steps\.version\.outputs\.VERSION \}\}$/.test(line), + ); + assert.deepEqual(rawVersionUses, []); +}); diff --git a/src/release-workflow.test.ts b/src/release-workflow.test.ts index 0cfda411..7325dba7 100644 --- a/src/release-workflow.test.ts +++ b/src/release-workflow.test.ts @@ -249,7 +249,7 @@ test('release workflow publishes subminer-bin to AUR from tagged release artifac releaseWorkflow, /cp packaging\/aur\/subminer-bin\/\.SRCINFO aur-subminer-bin\/\.SRCINFO/, ); - assert.match(releaseWorkflow, /version_no_v="\$\{\{ steps\.version\.outputs\.VERSION \}\}"/); + assert.match(releaseWorkflow, /version_no_v="\$RELEASE_VERSION"/); assert.match(releaseWorkflow, /SubMiner-\$\{version_no_v\}\.AppImage/); assert.doesNotMatch( releaseWorkflow, @@ -278,3 +278,27 @@ test('Makefile uninstall targets remove bundled runtime plugin app-data copies', assert.match(makefile, /Removed:[\s\S]*\$\(LINUX_DATA_DIR\)\/plugin\/subminer/); assert.match(makefile, /Removed:[\s\S]*\$\(MACOS_DATA_DIR\)\/plugin\/subminer/); }); + +// GitHub substitutes ${{ }} into the run script before the shell parses it, so a +// tag-derived value used that way is executed as script rather than read as data. +// The release and docs workflows must route tag values through env and +// reference them as shell variables. +test('tag-derived values reach shell bodies through env, not template interpolation', () => { + const rawVersionUses = releaseWorkflow + .split('\n') + .filter((line) => line.includes('steps.version.outputs.VERSION')) + .filter( + (line) => !/^\s*RELEASE_VERSION: \$\{\{ steps\.version\.outputs\.VERSION \}\}$/.test(line), + ); + assert.deepEqual(rawVersionUses, []); + + const rawRefNameUses = docsPagesWorkflow + .split('\n') + .filter((line) => line.includes('github.ref_name')) + .filter((line) => !/^\s*TAG_NAME: \$\{\{ github\.ref_name \}\}$/.test(line)) + // `if:` conditions are evaluated by Actions itself, never handed to a shell. + .filter((line) => !/^\s*if:/.test(line)); + assert.deepEqual(rawRefNameUses, []); + + assert.match(docsPagesWorkflow, /if \[\[ ! "\$TAG_NAME" =~/); +});