mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-29 03:57:09 +00:00
fix: canonicalize release image references (#39)
fix: canonicalize release image references
This commit is contained in:
@@ -139,6 +139,8 @@ jobs:
|
|||||||
release-server:
|
release-server:
|
||||||
needs: [preflight, release-extension-draft]
|
needs: [preflight, release-extension-draft]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
IMAGE: ghcr.io/shik3i/koalasync
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@@ -162,7 +164,7 @@ jobs:
|
|||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository }}
|
images: ${{ env.IMAGE }}
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
@@ -183,14 +185,13 @@ jobs:
|
|||||||
- name: Attest relay image
|
- name: Attest relay image
|
||||||
uses: actions/attest@v4
|
uses: actions/attest@v4
|
||||||
with:
|
with:
|
||||||
subject-name: ghcr.io/${{ github.repository }}
|
subject-name: ${{ env.IMAGE }}
|
||||||
subject-digest: ${{ steps.build.outputs.digest }}
|
subject-digest: ${{ steps.build.outputs.digest }}
|
||||||
push-to-registry: true
|
push-to-registry: true
|
||||||
|
|
||||||
- name: Verify manifest, provenance, and running image
|
- name: Verify manifest, provenance, and running image
|
||||||
env:
|
env:
|
||||||
DIGEST: ${{ steps.build.outputs.digest }}
|
DIGEST: ${{ steps.build.outputs.digest }}
|
||||||
IMAGE: ghcr.io/${{ github.repository }}
|
|
||||||
SOURCE_DIGEST: ${{ needs.preflight.outputs.tag-commit }}
|
SOURCE_DIGEST: ${{ needs.preflight.outputs.tag-commit }}
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -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
|
`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.
|
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.
|
- 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
|
- Do not say "release-ready" until the candidate gate and PR required checks
|
||||||
are green for the exact commit.
|
are green for the exact commit.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
`release:gate`. The gate pins the official Playwright image to the exact
|
||||||
lockfile version and forces `linux/amd64`, matching GitHub's Ubuntu runner even
|
lockfile version and forces `linux/amd64`, matching GitHub's Ubuntu runner even
|
||||||
when the developer host is macOS or ARM64.
|
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.
|
||||||
|
|||||||
@@ -64,6 +64,21 @@ export function parseRemoteMain(text) {
|
|||||||
return match[1];
|
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() {
|
function assertCleanTree() {
|
||||||
const status = capture('git', ['status', '--porcelain=v1']);
|
const status = capture('git', ['status', '--porcelain=v1']);
|
||||||
if (status) throw new Error(`release gate requires a clean working tree:\n${status}`);
|
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 }) {
|
export async function runReleaseGate({ version, candidate }) {
|
||||||
assertCleanTree();
|
assertCleanTree();
|
||||||
validateReleaseSourceVersion(version, repoRoot);
|
validateReleaseSourceVersion(version, repoRoot);
|
||||||
|
validateReleaseWorkflowContract(fs.readFileSync(
|
||||||
|
path.join(repoRoot, '.github/workflows/release.yml'), 'utf8'
|
||||||
|
));
|
||||||
if (!candidate) assertFinalMainChecks();
|
if (!candidate) assertFinalMainChecks();
|
||||||
|
|
||||||
const lock = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package-lock.json'), 'utf8'));
|
const lock = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package-lock.json'), 'utf8'));
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import {
|
|||||||
linuxGateCommand,
|
linuxGateCommand,
|
||||||
parseGateArgs,
|
parseGateArgs,
|
||||||
parseRemoteMain,
|
parseRemoteMain,
|
||||||
playwrightImageFromLock
|
playwrightImageFromLock,
|
||||||
|
validateReleaseWorkflowContract
|
||||||
} from './release-local-gate.mjs';
|
} from './release-local-gate.mjs';
|
||||||
|
|
||||||
describe('local release gate contract', () => {
|
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');
|
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', () => {
|
it('runs the complete CI-equivalent dependency, verify, and browser sequence', () => {
|
||||||
expect(linuxGateCommand()).toBe([
|
expect(linuxGateCommand()).toBe([
|
||||||
'git clone --no-local /src /work',
|
'git clone --no-local /src /work',
|
||||||
|
|||||||
Reference in New Issue
Block a user