evOo-local/apps/desktop/src/release-workflow.test.ts
cynarAI a746783c48
ci: keep every job on internal runners without GitHub Actions storage (#192)
Runs used GitHub's artifact storage and cache service, which ran out of
the monthly included quota and blocked publishing. Caches and job
handoffs now stay on the self-hosted runner host, the desktop release
hands builds over through its draft release, and Windows waits for a
self-hosted runner instead of a GitHub-hosted one. A test enforces it.


Claude-Session: https://claude.ai/code/session_01R8L4XgqjAemFR2remB5hVY

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-10 07:44:16 +02:00

105 lines
5.1 KiB
TypeScript

import { readFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
const workflow = readFileSync(
path.resolve(import.meta.dirname, "../../../.github/workflows/release-desktop.yml"),
"utf8",
);
const expression = (inner: string) => `\${{ ${inner} }}`;
describe("desktop release workflow", () => {
it("cannot execute contributor pull-request code with release credentials", () => {
expect(workflow).not.toMatch(/^\s*pull_request:/m);
expect(workflow).toContain("permissions:\n contents: read");
expect(workflow.match(/persist-credentials: false/g)).toHaveLength(3);
});
it("pins every third-party action to an immutable commit", () => {
const actionReferences = [...workflow.matchAll(/uses:\s+([^\s#]+)/g)].map((match) => match[1]);
expect(actionReferences.length).toBeGreaterThan(0);
for (const reference of actionReferences) {
expect(reference, reference).toMatch(/@[0-9a-f]{40}$/);
}
});
it("requires signed platform builds before a single publication job", () => {
expect(workflow).toContain("-c.forceCodeSigning=true");
expect(workflow).toContain("codesign --verify --deep --strict");
expect(workflow).toContain("Get-AuthenticodeSignature");
expect(workflow).toContain("needs: [validate, build]");
expect(workflow).toContain("attestations: write");
expect(workflow).toContain("actions/attest-build-provenance@");
expect(workflow).not.toContain("--publish always");
expect(workflow).toContain("DESKTOP_MAC_CSC_LINK");
expect(workflow).toContain("DESKTOP_WIN_CSC_LINK");
expect(workflow).not.toMatch(/secrets\.DESKTOP_CSC_(?:LINK|KEY_PASSWORD)/);
expect(workflow).not.toContain("cache: pnpm");
expect(workflow).toContain("apps/desktop/out/latest*.yml");
expect(workflow).not.toContain("apps/desktop/out/*.yml");
});
it("notarizes macOS with a short-lived App Store Connect API key file", () => {
for (const secret of ["APPLE_API_KEY_ID", "APPLE_API_ISSUER", "APPLE_API_KEY_P8"]) {
expect(workflow).toContain(`secrets.${secret}`);
}
expect(workflow).not.toMatch(/secrets\.(?:APPLE_ID|APPLE_APP_SPECIFIC_PASSWORD)\b/);
// The same temp path is declared by the setup, packaging, and cleanup steps.
const keyFile = `APPLE_API_KEY: ${expression("runner.temp")}/apple-api-key.p8`;
expect(workflow.split(keyFile)).toHaveLength(4);
expect(workflow).toContain(`printf '%s\\n' "$APPLE_API_KEY_P8" > "$APPLE_API_KEY"`);
expect(workflow).toContain('chmod 600 "$APPLE_API_KEY"');
expect(workflow).toMatch(
/if: always\(\) && runner\.os == 'macOS'\n(?:.*\n){3}\s+run: rm -f "\$APPLE_API_KEY"/,
);
});
it("builds Windows only when an Authenticode certificate is configured", () => {
expect(workflow).toContain(
`WINDOWS_SIGNING: ${expression("secrets.DESKTOP_WIN_CSC_LINK != ''")}`,
);
expect(workflow).toContain(
`include: ${expression("fromJSON(needs.validate.outputs.platforms)")}`,
);
expect(workflow).toContain(
`include='[{"runner":["self-hosted","evoo-ci","macOS"],"artifact":"macos"},{"runner":["self-hosted","evoo-ci","Linux"],"artifact":"linux"}]'`,
);
expect(workflow).toMatch(
/if \[\[ "\$WINDOWS_SIGNING" == "true" \]\]; then\n(?:\s*#[^\n]*\n)*\s+include=.*\{"runner":\["self-hosted","evoo-ci","Windows"\],"artifact":"windows"\}/,
);
expect(workflow).not.toMatch(/windows-\d|windows-latest/);
expect(workflow).toContain(`WINDOWS_BUILT: ${expression("needs.validate.outputs.windows")}`);
expect(workflow).toContain('if [[ "$WINDOWS_BUILT" == "true" ]]; then');
expect(workflow).toContain("feeds+=(release-artifacts/latest.yml)");
});
it("pins every platform update feed to the official GitHub owner and repo", () => {
expect(workflow).toContain('grep -Fqx "provider: github"');
expect(workflow).toContain('grep -Fqx "owner: cynarAI"');
expect(workflow).toContain('grep -Fqx "repo: evOo-local"');
expect(workflow).toContain("Verify Linux update feed is pinned to the official GitHub channel");
expect(workflow).toContain("Windows update config missing");
expect(workflow).toContain("RELEASE_VERSION:");
expect(workflow).toContain('grep -Fqx "version: $RELEASE_VERSION"');
});
it("publishes only a complete stable, upgrade-only feed", () => {
expect(workflow).toContain("^v([0-9]+)\\.([0-9]+)\\.([0-9]+)$");
expect(workflow).toContain("must be newer than published release");
expect(workflow).toContain("group: release-desktop-stable");
expect(workflow).toContain("latest.yml");
expect(workflow).toContain("latest-mac.yml");
expect(workflow).toContain("latest-linux.yml");
expect(workflow).toContain("--draft --generate-notes");
expect(workflow).toContain("--draft=false --latest");
});
it("hands platform builds to the draft release, never to Actions artifact storage", () => {
expect(workflow).not.toMatch(/actions\/(?:upload|download)-artifact@/);
expect(workflow).toContain("bash scripts/upload-release-assets.sh");
expect(workflow).toContain('gh release download "$RELEASE_TAG" --dir release-artifacts');
expect(workflow).toContain("needs: [validate, build, publish]");
});
});