ci(release): enforce canonical image references

This commit is contained in:
Timo
2026-09-01 03:45:18 +02:00
parent bc5d1a0ea6
commit 742386064c
3 changed files with 40 additions and 3 deletions
+23
View File
@@ -108,6 +108,22 @@ export function validateReleaseWorkflowContract(text) {
return image;
}
export function validateWorkflowImageReferences(workflows) {
const canonicalImage = 'ghcr.io/shik3i/koalasync';
for (const [name, rawText] of Object.entries(workflows)) {
const text = String(rawText);
if (/ghcr\.io\/\$\{\{\s*github\.repository\s*\}\}/iu.test(text)) {
throw new Error(`${name} must not derive a Docker image from case-preserving github.repository`);
}
for (const reference of text.match(/ghcr\.io\/[a-zA-Z0-9._/-]+/gu) || []) {
if (reference !== canonicalImage) {
throw new Error(`${name} must use the lowercase canonical image ${canonicalImage}, found ${reference}`);
}
}
}
return canonicalImage;
}
function assertCleanTree() {
const status = capture('git', ['status', '--porcelain=v1']);
if (status) throw new Error(`release gate requires a clean working tree:\n${status}`);
@@ -163,6 +179,13 @@ async function smokeRelayImage(image) {
export async function runReleaseGate({ version, candidate }) {
assertCleanTree();
const workflowDir = path.join(repoRoot, '.github', 'workflows');
const workflowFiles = fs.readdirSync(workflowDir)
.filter(name => /\.ya?ml$/u.test(name));
validateWorkflowImageReferences(Object.fromEntries(workflowFiles.map(name => [
name,
fs.readFileSync(path.join(workflowDir, name), 'utf8')
])));
validateReleaseWorkflowContract(fs.readFileSync(
path.join(repoRoot, '.github/workflows/release.yml'), 'utf8'
));
+15 -1
View File
@@ -5,7 +5,8 @@ import {
parseGateArgs,
parseRemoteMain,
playwrightImageFromLock,
validateReleaseWorkflowContract
validateReleaseWorkflowContract,
validateWorkflowImageReferences
} from './release-local-gate.mjs';
describe('local release gate contract', () => {
@@ -60,6 +61,19 @@ describe('local release gate contract', () => {
.toThrow('case-preserving github.repository');
});
it('enforces the canonical lowercase image across every workflow', () => {
expect(validateWorkflowImageReferences({
'release.yml': 'IMAGE: ghcr.io/shik3i/koalasync',
'beta.yml': 'images: ghcr.io/shik3i/koalasync:beta'
})).toBe('ghcr.io/shik3i/koalasync');
expect(() => validateWorkflowImageReferences({
'beta.yml': 'images: ghcr.io/${{ github.repository }}'
})).toThrow('case-preserving github.repository');
expect(() => validateWorkflowImageReferences({
'beta.yml': 'images: ghcr.io/Shik3i/KoalaSync'
})).toThrow('lowercase canonical image');
});
it('enforces the automatic version commit, direct push, prepared source, and final publication contract', () => {
const workflow = fs.readFileSync('.github/workflows/release.yml', 'utf8');
expect(validateReleaseWorkflowContract(workflow)).toBe('ghcr.io/shik3i/koalasync');