Skip to content
Merged
13 changes: 11 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,16 @@ jobs:
- name: Check out the tagged source
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Verify exact checkout
env:
INKSPAN_EXPECTED_HEAD_SHA: ${{ github.sha }}
run: |
set -euo pipefail
actual_head="$(git rev-parse HEAD)"
test "$actual_head" = "$INKSPAN_EXPECTED_HEAD_SHA"
- name: Set up pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: Set up Node.js
Expand Down Expand Up @@ -52,8 +61,8 @@ jobs:
const expectedTag = `v${packageMetadata.version}`;
const repositoryUrl = 'git+https://github.com/ContextualWisdomLab/inkspan.git';

if (!/^v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/.test(releaseTag)) {
throw new Error(`Release tag is not valid semantic version syntax: ${releaseTag}`);
if (!/^v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(releaseTag)) {
throw new Error(`Release tag is not valid stable semantic version syntax: ${releaseTag}`);
}
if (releaseTag !== expectedTag) {
throw new Error(`Release tag ${releaseTag} does not match package version ${expectedTag}`);
Expand Down
64 changes: 64 additions & 0 deletions src/releaseExactCheckout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';

import { describe, expect, it } from 'vitest';

/** Read one authoritative repository file as UTF-8 text. */
function repositoryFile(path: string): string {
return readFileSync(resolve(process.cwd(), path), 'utf8');
}

/** Extract one top-level workflow job without allowing another job to satisfy it. */
function workflowJob(source: string, jobName: string, nextJobName: string): string {
const start = source.indexOf(` ${jobName}:`);
const end = source.indexOf(` ${nextJobName}:`, start + 1);
expect(start).toBeGreaterThan(-1);
expect(end).toBeGreaterThan(start);
return source.slice(start, end);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

describe('release artifact checkout authority', () => {
it('binds the artifact build to the exact tag SHA without persisted credentials before setup', () => {
const workflow = repositoryFile('.github/workflows/release.yml');
const buildJob = workflowJob(
workflow,
'build-release-artifacts',
'browser-release-evidence',
);

const checkoutIndex = buildJob.indexOf('name: Check out the tagged source');
const verifyIndex = buildJob.indexOf('name: Verify exact checkout');
const pnpmIndex = buildJob.indexOf('name: Set up pnpm');
const nodeIndex = buildJob.indexOf('name: Set up Node.js');
const pythonIndex = buildJob.indexOf('name: Set up Python');

expect(checkoutIndex).toBeGreaterThan(-1);
expect(buildJob).toContain('ref: ${{ github.sha }}');
expect(buildJob).toContain('persist-credentials: false');
expect(verifyIndex).toBeGreaterThan(checkoutIndex);
expect(verifyIndex).toBeLessThan(pnpmIndex);
expect(verifyIndex).toBeLessThan(nodeIndex);
expect(verifyIndex).toBeLessThan(pythonIndex);
expect(buildJob).toContain('INKSPAN_EXPECTED_HEAD_SHA: ${{ github.sha }}');
expect(buildJob).toContain('actual_head="$(git rev-parse HEAD)"');
expect(buildJob).toContain('test "$actual_head" = "$INKSPAN_EXPECTED_HEAD_SHA"');
});

it('fails closed on prerelease tags instead of allowing a registry-skipping release path', () => {
const workflow = repositoryFile('.github/workflows/release.yml');
const buildJob = workflowJob(
workflow,
'build-release-artifacts',
'browser-release-evidence',
);

expect(buildJob).toContain(
"if (!/^v(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$/.test(releaseTag))",
);
expect(buildJob).toContain(
'Release tag is not valid stable semantic version syntax',
);
expect(buildJob).not.toContain('(?:-[0-9A-Za-z.-]+)?');
expect(workflow).toContain("!contains(github.ref_name, '-')");
});
});
Loading