diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b627534..dec7707 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -139,6 +139,8 @@ jobs: release-server: needs: [preflight, release-extension-draft] runs-on: ubuntu-latest + env: + IMAGE: ghcr.io/shik3i/koalasync permissions: contents: read packages: write @@ -162,7 +164,7 @@ jobs: id: meta uses: docker/metadata-action@v6 with: - images: ghcr.io/${{ github.repository }} + images: ${{ env.IMAGE }} tags: | type=raw,value=latest type=ref,event=tag @@ -183,14 +185,13 @@ jobs: - name: Attest relay image uses: actions/attest@v4 with: - subject-name: ghcr.io/${{ github.repository }} + subject-name: ${{ env.IMAGE }} subject-digest: ${{ steps.build.outputs.digest }} push-to-registry: true - name: Verify manifest, provenance, and running image env: DIGEST: ${{ steps.build.outputs.digest }} - IMAGE: ghcr.io/${{ github.repository }} SOURCE_DIGEST: ${{ needs.preflight.outputs.tag-commit }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/AGENTS.md b/AGENTS.md index d73d95d..82d20e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,9 @@ These rules are mandatory for every automated agent working in this repository. `linux/amd64`, Ubuntu Noble, and `CI=1`; it must run clean installs, full verification, all browser E2E tests, the relay image build, and health smoke. - A failed, interrupted, ARM64, skipped, or partial run is not a passing gate. +- All OCI image references must use the canonical lowercase + `ghcr.io/shik3i/koalasync`; never construct Docker references from the + case-preserving `${{ github.repository }}` value. The local gate enforces this. - Do not say "release-ready" until the candidate gate and PR required checks are green for the exact commit. diff --git a/docs/devops.md b/docs/devops.md index b27c63c..81ff77a 100644 --- a/docs/devops.md +++ b/docs/devops.md @@ -81,3 +81,8 @@ the public GitHub assets and GHCR digest before calling the release complete. `release:gate`. The gate pins the official Playwright image to the exact lockfile version and forces `linux/amd64`, matching GitHub's Ubuntu runner even when the developer host is macOS or ARM64. + +The relay registry reference is always the canonical lowercase +`ghcr.io/shik3i/koalasync`. Docker repository names reject uppercase characters; +`release:gate` rejects workflows that derive this reference from the +case-preserving `${{ github.repository }}` value. diff --git a/scripts/release-local-gate.mjs b/scripts/release-local-gate.mjs index e755b14..e978a93 100644 --- a/scripts/release-local-gate.mjs +++ b/scripts/release-local-gate.mjs @@ -64,6 +64,21 @@ export function parseRemoteMain(text) { return match[1]; } +export function validateReleaseWorkflowContract(text) { + const workflow = String(text); + const image = 'ghcr.io/shik3i/koalasync'; + if (!workflow.includes(`IMAGE: ${image}`)) { + throw new Error(`release workflow must define the lowercase canonical image ${image}`); + } + for (const reference of ['images: ${{ env.IMAGE }}', 'subject-name: ${{ env.IMAGE }}']) { + if (!workflow.includes(reference)) throw new Error(`release workflow must use ${reference}`); + } + if (/ghcr\.io\/\$\{\{\s*github\.repository\s*\}\}/u.test(workflow)) { + throw new Error('release workflow must not derive a Docker image from case-preserving github.repository'); + } + return image; +} + function assertCleanTree() { const status = capture('git', ['status', '--porcelain=v1']); if (status) throw new Error(`release gate requires a clean working tree:\n${status}`); @@ -120,6 +135,9 @@ async function smokeRelayImage(image) { export async function runReleaseGate({ version, candidate }) { assertCleanTree(); validateReleaseSourceVersion(version, repoRoot); + validateReleaseWorkflowContract(fs.readFileSync( + path.join(repoRoot, '.github/workflows/release.yml'), 'utf8' + )); if (!candidate) assertFinalMainChecks(); const lock = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package-lock.json'), 'utf8')); diff --git a/scripts/release-local-gate.test.mjs b/scripts/release-local-gate.test.mjs index 1eadb23..82836b5 100644 --- a/scripts/release-local-gate.test.mjs +++ b/scripts/release-local-gate.test.mjs @@ -3,7 +3,8 @@ import { linuxGateCommand, parseGateArgs, parseRemoteMain, - playwrightImageFromLock + playwrightImageFromLock, + validateReleaseWorkflowContract } from './release-local-gate.mjs'; describe('local release gate contract', () => { @@ -28,6 +29,21 @@ describe('local release gate contract', () => { expect(() => parseRemoteMain(`${sha}\trefs/heads/not-main`)).toThrow('could not resolve origin main'); }); + it('requires one lowercase registry image throughout the release workflow', () => { + const valid = [ + 'IMAGE: ghcr.io/shik3i/koalasync', + 'images: ${{ env.IMAGE }}', + 'subject-name: ${{ env.IMAGE }}' + ].join('\n'); + expect(validateReleaseWorkflowContract(valid)).toBe('ghcr.io/shik3i/koalasync'); + expect(() => validateReleaseWorkflowContract(valid.replace( + 'IMAGE: ghcr.io/shik3i/koalasync', + 'IMAGE: ghcr.io/${{ github.repository }}' + ))).toThrow('lowercase canonical image'); + expect(() => validateReleaseWorkflowContract(`${valid}\n${'ghcr.io/${{ github.repository }}'}`)) + .toThrow('case-preserving github.repository'); + }); + it('runs the complete CI-equivalent dependency, verify, and browser sequence', () => { expect(linuxGateCommand()).toBe([ 'git clone --no-local /src /work',