Skip to content
Merged
12 changes: 10 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,18 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Check out the tagged source
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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 @@ -468,7 +477,6 @@ jobs:
gh release edit "$GITHUB_REF_NAME" \
--repo "$GITHUB_REPOSITORY" \
--draft=false

release_immutable="$(gh release view "$GITHUB_REF_NAME" \
--repo "$GITHUB_REPOSITORY" \
--json isImmutable \
Expand Down
95 changes: 95 additions & 0 deletions src/releaseExactCheckout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';

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

const RELEASE_CHECKOUT_ACTION =
'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1';

/** 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.
}

/** Extract one named workflow step so unrelated steps cannot satisfy its contract. */
function workflowStep(source: string, stepName: string, nextStepName: string): string {
const start = source.indexOf(` - name: ${stepName}`);
const end = source.indexOf(` - name: ${nextStepName}`, start + 1);
expect(start).toBeGreaterThan(-1);
expect(end).toBeGreaterThan(start);
return source.slice(start, end);
}

describe('release artifact checkout authority', () => {
it('binds the artifact build to the repository checkout baseline and exact tag SHA before setup', () => {
const workflow = repositoryFile('.github/workflows/release.yml');
const buildJob = workflowJob(
workflow,
'build-release-artifacts',
'browser-release-evidence',
);
const checkoutStep = workflowStep(
buildJob,
'Check out the tagged source',
'Verify exact checkout',
);
const verifyStep = workflowStep(
buildJob,
'Verify exact checkout',
'Set up pnpm',
);

expect(checkoutStep).toContain(`uses: ${RELEASE_CHECKOUT_ACTION}`);
expect(checkoutStep).toContain('with:');
expect(checkoutStep).toContain('ref: ${{ github.sha }}');
expect(checkoutStep).toContain('fetch-depth: 0');
expect(checkoutStep).toContain('persist-credentials: false');
expect(verifyStep).toContain('INKSPAN_EXPECTED_HEAD_SHA: ${{ github.sha }}');
expect(verifyStep).toContain('actual_head="$(git rev-parse HEAD)"');
expect(verifyStep).toContain(
'test "$actual_head" = "$INKSPAN_EXPECTED_HEAD_SHA"',
);

expect(buildJob.indexOf('name: Verify exact checkout')).toBeLessThan(
buildJob.indexOf('name: Set up pnpm'),
);
expect(buildJob.indexOf('name: Verify exact checkout')).toBeLessThan(
buildJob.indexOf('name: Set up Node.js'),
);
expect(buildJob.indexOf('name: Verify exact checkout')).toBeLessThan(
buildJob.indexOf('name: Set up Python'),
);
});

it('preserves the established prerelease tag semantics while npm publication remains gated', () => {
const workflow = repositoryFile('.github/workflows/release.yml');
const buildJob = workflowJob(
workflow,
'build-release-artifacts',
'browser-release-evidence',
);
const identityStep = workflowStep(
buildJob,
'Verify release identity and current main tip',
'Install JavaScript dependencies',
);

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