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>
105 lines
5.1 KiB
TypeScript
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]");
|
|
});
|
|
});
|