Files
NetMesh/scripts/github-workflow-ci.test.cjs
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

469 lines
22 KiB
JavaScript

const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const workflowsDir = path.join(__dirname, "..", ".github", "workflows");
const readWorkflow = (name) => fs.readFileSync(path.join(workflowsDir, name), "utf8");
const testWorkflow = readWorkflow("test.yml");
const buildWorkflow = readWorkflow("build.yml");
const aiWorkflow = readWorkflow("ai-automation.yml");
const etWorkflow = readWorkflow("build-et-binaries.yml");
const appBuilderPatch = fs.readFileSync(
path.join(__dirname, "..", "patches", "app-builder-lib+26.15.2.patch"),
"utf8",
);
const windowsEtBuild = fs.readFileSync(
path.join(__dirname, "build-et", "build-windows.ps1"),
"utf8",
);
const homebrewBump = fs.readFileSync(
path.join(__dirname, "..", ".github", "scripts", "bump-homebrew-cask.sh"),
"utf8",
);
const pullRequestPaths = buildWorkflow
.match(/pull_request:\s*\n\s*paths:\s*\n((?:\s+- "[^"]+"\s*\n)+)/)?.[1]
?.match(/^\s+- "([^"]+)"$/gm)
?.map((line) => line.match(/^\s+- "([^"]+)"$/)?.[1])
.filter(Boolean);
// Portable glob matcher for Node >=22 (path.matchesGlob only landed in 22.5).
const matchesGlob = (filePath, pattern) => {
let regex = "^";
for (let i = 0; i < pattern.length; ) {
if (pattern[i] === "*" && pattern[i + 1] === "*") {
if (pattern[i + 2] === "/") {
regex += "(?:.*/)?";
i += 3;
} else {
regex += ".*";
i += 2;
}
continue;
}
if (pattern[i] === "*") {
regex += "[^/]*";
i += 1;
continue;
}
if (pattern[i] === "?") {
regex += "[^/]";
i += 1;
continue;
}
regex += pattern[i].replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
i += 1;
}
regex += "$";
return new RegExp(regex).test(filePath);
};
const triggersPackageValidation = (filePath) => {
assert.ok(pullRequestPaths, "package workflow pull_request paths must be readable");
return pullRequestPaths.reduce((included, pattern) => {
const excluded = pattern.startsWith("!");
const glob = excluded ? pattern.slice(1) : pattern;
return matchesGlob(filePath, glob) ? !excluded : included;
}, false);
};
test("PR validation runs once per commit and includes a production build", () => {
assert.match(testWorkflow, /push:\s*\n\s*branches:\s*\n\s*- main/);
assert.doesNotMatch(testWorkflow, /branches:\s*\n\s*- "\*\*"/);
assert.match(testWorkflow, /name: lint-and-test\s*\n\s*runs-on: ubuntu-latest\s*\n\s*timeout-minutes: 20/);
assert.match(testWorkflow, /sudo apt-get install -y fish xvfb/);
assert.match(
testWorkflow,
/- name: Test terminal keyword highlight performance\s*\n\s*env:\s*\n\s*NETCATTY_TERMINAL_PERF_SHOW_WINDOW: "1"\s*\n\s*# GitHub-hosted runners do not configure Electron's SUID sandbox helper\.\s*\n\s*run: xvfb-run -a \.\/node_modules\/\.bin\/electron --no-sandbox scripts\/xterm-keyword-highlight-performance\.live\.test\.cjs/,
);
assert.match(
buildWorkflow,
/- name: Test macOS Option column selection\s*\n\s*if: matrix\.name == 'macos'\s*\n\s*run: npm run test:xterm-macos-selection/,
);
assert.match(testWorkflow, /- name: Build\s*\n\s*run: npm run build/);
assert.doesNotMatch(testWorkflow, /\n mosh-windows-conpty:/);
});
test("package release concurrency is isolated per tag", () => {
assert.match(buildWorkflow, /format\('release-\{0\}', github\.ref\)/);
assert.doesNotMatch(buildWorkflow, /&& 'release' \|\| github\.ref/);
});
test("manual package validations do not share push concurrency", () => {
assert.match(
buildWorkflow,
/github\.event_name == 'workflow_dispatch' && format\('manual-\{0\}', github\.run_id\)/,
);
assert.ok(
buildWorkflow.indexOf("format('release-{0}', github.ref)") <
buildWorkflow.indexOf("format('manual-{0}', github.run_id)"),
"publishing a tag manually must still share that tag's release group",
);
});
test("package validation avoids duplicate branch runs and scopes PR builds", () => {
assert.match(buildWorkflow, /push:\s*\n\s*branches:\s*\n\s*- main/);
assert.doesNotMatch(buildWorkflow, /branches:\s*\n\s*- "\*\*"/);
assert.match(buildWorkflow, /pull_request:\s*\n\s*paths:/);
assert.doesNotMatch(buildWorkflow, /\n dedupe:/);
assert.doesNotMatch(buildWorkflow, /\n dedupe-result:/);
for (const packagedInput of [
"electron/**",
"infrastructure/config/terminalFlowConstants.*",
"public/icon*",
"scripts/afterPackMacUuid.cjs",
"scripts/beforePackCursorSdk.cjs",
"scripts/nodePtyConptyPatch.cjs",
"scripts/patch-xterm-macos-column-selection.cjs",
"scripts/xterm-macos-column-selection.live.test.cjs",
"scripts/linux/**",
"skills/**",
]) {
assert.ok(buildWorkflow.includes(`- "${packagedInput}"`), `${packagedInput} must trigger package validation`);
}
for (const excludedTestInput of [
"!electron/**/*.test.*",
"!electron/**/*.spec.*",
"!electron/**/__tests__/**",
"!electron/**/test/**",
"!electron/**/tests/**",
"!electron/**/example/**",
"!electron/**/examples/**",
"!electron/plugins/fixtures/**",
]) {
assert.ok(buildWorkflow.includes(`- "${excludedTestInput}"`), `${excludedTestInput} must stay out of package validation`);
}
assert.ok(
buildWorkflow.indexOf('- "electron/**"') < buildWorkflow.indexOf('- "!electron/**/*.test.*"'),
"packaged Electron files must be included before test-only exclusions",
);
for (const packagedPath of [
"electron/main.cjs",
"electron/entitlements.mac.plist",
"electron/bridges/terminalBridge.cjs",
"electron/preload/api.cjs",
"electron/shared/protocol.cjs",
"electron/mcp/server.cjs",
"electron/plugins/pluginManager.cjs",
"scripts/linux/after-install.tpl",
]) {
assert.equal(triggersPackageValidation(packagedPath), true, `${packagedPath} must trigger package validation`);
}
for (const testOnlyPath of [
"electron/main.test.cjs",
"electron/bridges/moshHandshake.test.cjs",
"electron/plugins/pluginManager.test.cjs",
"electron/plugins/fixtures/example/plugin.cjs",
]) {
assert.equal(triggersPackageValidation(testOnlyPath), false, `${testOnlyPath} must not trigger package validation`);
}
});
test("Windows packaging reuses its dependency install for the ConPTY smoke test", () => {
const packageMatrix = buildWorkflow.match(/\n build:\n[\s\S]*?(?=\n build-linux-x64:)/);
assert.ok(packageMatrix, "build matrix job must exist before build-linux-x64");
assert.match(packageMatrix[0], /Compile ConPTY test helpers/);
assert.match(packageMatrix[0], /Test Mosh handshake through ConPTY/);
assert.match(packageMatrix[0], /if: matrix\.name == 'windows'/);
assert.match(packageMatrix[0], /Restore Electron download cache/);
assert.match(packageMatrix[0], /actions\/cache@v6/);
assert.match(packageMatrix[0], /node electron\/bridges\/terminalBridge\.moshConpty\.integration\.cjs/);
});
test("package downloads use bounded retries and reusable caches", () => {
assert.match(buildWorkflow, /NPM_CONFIG_FETCH_RETRIES: "4"/);
assert.match(buildWorkflow, /NPM_CONFIG_FETCH_RETRY_MINTIMEOUT: "1000"/);
assert.match(buildWorkflow, /NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT: "10000"/);
assert.equal(
(buildWorkflow.match(/restore-keys:\s*\|\s*\n\s*electron-\$\{\{ runner\.os \}\}-\$\{\{ runner\.arch \}\}-/g) ?? [])
.length,
3,
"all package jobs must reuse compatible Electron downloads after lockfile changes",
);
const linuxX64 = buildWorkflow.match(/\n build-linux-x64:\n[\s\S]*?(?=\n build-linux-arm64:)/)?.[0];
const linuxArm64 = buildWorkflow.match(/\n build-linux-arm64:\n[\s\S]*?(?=\n release:)/)?.[0];
assert.ok(linuxX64, "Linux x64 package job must be readable");
assert.ok(linuxArm64, "Linux arm64 package job must be readable");
assert.match(linuxX64, /image: quay\.io\/almalinuxorg\/almalinux:8/);
assert.match(linuxX64, /dnf -y --setopt=retries=4 --setopt=timeout=30 install/);
assert.match(
linuxX64,
/curl -fsSL --retry 4 --retry-connrefused --connect-timeout 20 --max-time 300/g,
);
assert.match(linuxArm64, /apt-get -o Acquire::Retries=4 update/);
assert.match(
linuxArm64,
/name: Install build dependencies\s*\n\s*shell: bash\s*\n\s*run: \|\s*\n\s*set -euo pipefail/,
);
assert.match(linuxArm64, /apt-get -o Acquire::Retries=4 install -y/);
assert.match(
linuxArm64,
/curl -fsSL --retry 4 --retry-all-errors --connect-timeout 20 --max-time 300/,
);
});
test("stable releases propose Nix metadata through a pull request", () => {
const nixJob = buildWorkflow.match(/\n update-nix-release:\n[\s\S]*?(?=\n homebrew-tap:)/);
assert.ok(nixJob, "update-nix-release job must exist before homebrew-tap");
assert.doesNotMatch(nixJob[0], /git push origin HEAD:\$\{\{ github\.event\.repository\.default_branch \}\}/);
assert.match(nixJob[0], /gh pr create/);
assert.ok(
nixJob[0].includes("GH_TOKEN: ${{ secrets.TRIAGE_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}"),
"Nix PR creation must prefer the triage-capable token and safely fall back to the job token",
);
assert.ok(
nixJob[0].includes("token: ${{ secrets.RELEASE_TOKEN }}"),
"Nix branch pushes must keep using the release token",
);
assert.match(nixJob[0], /automation\/nix-release-/);
assert.match(nixJob[0], /candidate_tree/);
assert.match(nixJob[0], /remote_tree/);
assert.match(nixJob[0], /branch_prefix/);
assert.match(nixJob[0], /headRefName/);
assert.match(nixJob[0], /headRepositoryOwner\.login == \$owner/);
assert.match(nixJob[0], /desired_nix_blob="\$\(git hash-object -w nix\/release\.nix\)"/);
assert.match(nixJob[0], /existing_branch/);
assert.match(nixJob[0], /refs\/heads\/\$\{existing_branch\}/);
assert.match(nixJob[0], /git cat-file blob "\$desired_nix_blob" > nix\/release\.nix/);
assert.match(nixJob[0], /git diff --quiet -- nix\/release\.nix/);
assert.match(nixJob[0], /while IFS='\|' read -r existing existing_branch/);
assert.match(nixJob[0], /done <<<"\$existing_prs"/);
assert.doesNotMatch(nixJob[0], /\.\[0\] \/\//);
assert.match(
nixJob[0],
/--force-with-lease="refs\/heads\/\$\{existing_branch\}:\$\{remote_before\}"/,
);
assert.match(nixJob[0], /origin "HEAD:\$\{existing_branch\}"/);
assert.match(nixJob[0], /gh api --method GET "repos\/\$\{GITHUB_REPOSITORY\}\/pulls"/);
assert.match(nixJob[0], /-f head="\$\{REPO_OWNER\}:\$\{branch\}"/);
assert.doesNotMatch(nixJob[0], /gh pr list[^\n]*--head "\$\{REPO_OWNER\}:/);
assert.match(nixJob[0], /\.headRefName == \$prefix/);
assert.doesNotMatch(
nixJob[0],
/\.headRefName \| startswith\(\$prefix\)/,
"v1.2.30 must not be treated as a v1.2.3 metadata branch",
);
assert.match(nixJob[0], /test\("\^\[0-9\]\+-\[0-9\]\+\$"\)/);
assert.match(nixJob[0], /suffix=.*branch_prefix/);
assert.match(nixJob[0], /\[\[ "\$suffix" =~ \^\[0-9\]\+-\[0-9\]\+\$ \]\]/);
assert.match(nixJob[0], /ls-remote --heads origin "refs\/heads\/\$\{branch_prefix\}\*"/);
assert.match(nixJob[0], /GITHUB_RUN_ID/);
assert.match(nixJob[0], /--force-with-lease="refs\/heads\/\$\{branch\}:"/);
assert.doesNotMatch(nixJob[0], /--force-with-lease="\$\{branch\}:\$\{expected\}"/);
assert.ok(
nixJob[0].indexOf('gh pr list') < nixJob[0].indexOf('git switch -C'),
"an existing Nix PR must be reused before rebuilding its branch",
);
});
test("Homebrew tap updates retry push races without downgrading newer releases", () => {
assert.match(homebrewBump, /MAX_PUSH_ATTEMPTS/);
assert.match(homebrewBump, /version_is_newer/);
assert.match(homebrewBump, /git fetch --depth=1 origin main/);
assert.match(homebrewBump, /git switch -C main origin\/main/);
assert.match(homebrewBump, /for \(\(attempt=1; attempt<=MAX_PUSH_ATTEMPTS; attempt\+\+\)\)/);
assert.match(homebrewBump, /if version_is_newer "\$current_version" "\$VERSION"/);
assert.match(homebrewBump, /if push_output="\$\(git push origin HEAD:main 2>&1\)"/);
assert.match(homebrewBump, /grep -Eqi 'non-fast-forward\|fetch first' <<<"\$push_output"/);
assert.doesNotMatch(homebrewBump, /2> >\(tee/);
assert.match(homebrewBump, /Tap already has newer version/);
assert.match(homebrewBump, /Push raced with another release/);
});
test("Codex fix publishing treats a moved PR head as a stale result", () => {
const publishJob = aiWorkflow.match(/\n publish_codex_fix:\n[\s\S]*?(?=\n own_rerequest_codex:)/);
assert.ok(publishJob, "publish_codex_fix job must exist before own_rerequest_codex");
assert.match(publishJob[0], /--force-with-lease/);
assert.match(publishJob[0], /published=false/);
assert.match(publishJob[0], /remote_after/);
assert.match(publishJob[0], /exit 1/);
assert.match(publishJob[0], /steps\.publish\.outputs\.published == 'true'/);
});
test("issue implementation publishing tolerates competing automation runs", () => {
const publishJob = aiWorkflow.match(/\n publish_implement:\n[\s\S]*?(?=\n codex_loop:)/);
assert.ok(publishJob, "publish_implement job must exist before codex_loop");
assert.match(publishJob[0], /--force-with-lease/);
assert.match(publishJob[0], /candidate_tree/);
assert.match(publishJob[0], /remote_tree/);
assert.match(publishJob[0], /refs\/heads\/\$\{BRANCH\}:/);
assert.match(publishJob[0], /fetch --depth=1 origin[\s\\]*"\+refs\/heads\/\$\{BRANCH\}:refs\/remotes\/origin\/\$\{BRANCH\}"/);
assert.match(publishJob[0], /live_after/);
assert.match(publishJob[0], /changed while it was being checked/);
assert.match(publishJob[0], /reuse it without rewriting history/);
assert.doesNotMatch(publishJob[0], /:\$\{expected\}/);
assert.doesNotMatch(publishJob[0], /if \[\[ -n "\$expected" \]\]/);
assert.doesNotMatch(publishJob[0], /published=false/);
assert.match(publishJob[0], /remote_after/);
assert.match(publishJob[0], /exit 1/);
assert.match(publishJob[0], /echo "handoff=true" >> "\$GITHUB_OUTPUT"/);
assert.match(
publishJob[0],
/if: failure\(\) && steps\.existing\.outputs\.exists != 'true' && steps\.publish\.outputs\.handoff == 'true'/,
);
assert.doesNotMatch(publishJob[0], /steps\.publish\.outcome == 'failure'/);
assert.match(publishJob[0], /labels: \['ready-for-human'\]/);
assert.match(publishJob[0], /could not safely publish the implementation branch/);
assert.match(publishJob[0], /ai-publish-handoff:/);
assert.match(publishJob[0], /github\.paginate\(github\.rest\.issues\.listComments/);
assert.match(publishJob[0], /steps\.publish\.outputs\.published == 'true'/);
assert.match(publishJob[0], /group: ai-codex-head-/);
assert.match(publishJob[0], /status === 403 && createPermissionDenied/);
assert.match(publishJob[0], /resource not accessible by integration/);
assert.match(publishJob[0], /resource not accessible by personal access token/);
assert.match(publishJob[0], /not permitted to create/);
});
test("reused automation PRs still receive labels and one source-issue backlink", () => {
const openPr = aiWorkflow.match(
/\n - name: Open draft PR[\s\S]*?(?=\n - name: Request Codex review on implement PR)/,
);
assert.ok(openPr, "open-PR step must exist before Codex review request");
assert.match(openPr[0], /github\.rest\.issues\.addLabels/);
assert.match(openPr[0], /github\.paginate\(github\.rest\.issues\.listComments/);
assert.match(openPr[0], /auto\.hasAutomationPullRequestBacklink/);
assert.doesNotMatch(openPr[0], /if \(created\)/);
});
test("reused implementation PRs do not duplicate Codex requests for the same head", () => {
const requestCodex = aiWorkflow.match(
/\n - name: Request Codex review on implement PR[\s\S]*?(?=\n codex_loop:)/,
);
assert.ok(requestCodex, "implement Codex request step must exist before codex_loop");
assert.match(requestCodex[0], /github\.paginate\(github\.rest\.issues\.listComments/);
assert.match(requestCodex[0], /github\.rest\.pulls\.get/);
assert.match(requestCodex[0], /auto\.shouldSkipExternalCodexRerequest/);
assert.match(requestCodex[0], /headSha/);
assert.match(requestCodex[0], /OWN_ACTORS/);
assert.doesNotMatch(requestCodex[0], /process\.env\.HEAD_SHA/);
});
test("all own-PR Codex request paths serialize on the head branch", () => {
const codexLoop = aiWorkflow.match(/\n codex_loop:\n[\s\S]*?(?=\n publish_codex_fix:)/);
const ownRerequest = aiWorkflow.match(/\n own_rerequest_codex:\n[\s\S]*?(?=\n external_rerequest_codex:)/);
assert.ok(codexLoop, "codex_loop job must exist before publish_codex_fix");
assert.ok(ownRerequest, "own_rerequest_codex must exist before external_rerequest_codex");
assert.match(aiWorkflow, /head_ref: \$\{\{ steps\.decide\.outputs\.head_ref \}\}/);
assert.match(codexLoop[0], /group: ai-codex-head-/);
assert.match(ownRerequest[0], /group: ai-codex-head-/);
assert.match(codexLoop[0], /needs\.route\.outputs\.head_ref/);
assert.match(ownRerequest[0], /needs\.route\.outputs\.head_ref/);
assert.match(aiWorkflow, /head_ref: pr\.head\?\.ref \|\| ''/);
assert.match(
aiWorkflow,
/issue_comment[\s\S]*?github\.rest\.pulls\.get[\s\S]*?head_ref: pr\.head\?\.ref \|\| ''/,
);
});
test("scheduled Codex polling ignores review comments remapped from old heads", () => {
const pollJob = aiWorkflow.match(/\n codex_poll:\n[\s\S]*$/);
assert.ok(pollJob, "codex_poll job must exist");
assert.match(pollJob[0], /auto\.filterCodexReviewCommentsForHead\(\s*reviewComments,\s*pr\.head\.sha/);
assert.doesNotMatch(pollJob[0], /c\.commit_id \|\| c\.original_commit_id/);
});
test("clean Codex handoff updates labels without GraphQL-only organization scopes", () => {
const markReady = aiWorkflow.match(/\n - name: Mark PR ready after clean Codex[\s\S]*?(?=\n - name: Give up after max rounds)/);
assert.ok(markReady, "mark-ready step must exist before give-up step");
assert.match(markReady[0], /gh api/);
assert.match(markReady[0], /issues\/\$\{PULL_NUMBER\}\/labels/);
assert.match(markReady[0], /automation%3Acodex-loop/);
assert.match(markReady[0], /ready-for-human/);
assert.match(markReady[0], /labels\[\]=automation:codex-clean/);
assert.match(markReady[0], /labels\[\]=automation:bot-pr/);
assert.doesNotMatch(markReady[0], /nextCodexTerminalLabels/);
assert.doesNotMatch(markReady[0], /"PUT"/);
assert.doesNotMatch(markReady[0], /gh pr edit/);
});
test("permission handoffs use the established ready-for-human label", () => {
assert.doesNotMatch(aiWorkflow, /automation:needs-human/);
assert.match(aiWorkflow, /labels: \['ready-for-human'\]/);
assert.match(aiWorkflow, /-f 'labels\[\]=ready-for-human'/);
});
test("ET binary validation runs once and retries transient container pulls", () => {
assert.match(etWorkflow, /push:\s*\n\s*branches:\s*\n\s*- main/);
assert.doesNotMatch(etWorkflow, /branches:\s*\n\s*- "\*\*"/);
assert.match(etWorkflow, /Pull build container with retry/g);
assert.match(etWorkflow, /docker pull/);
assert.match(etWorkflow, /--pull=never/);
assert.match(etWorkflow, /Restore vcpkg download cache/g);
assert.match(etWorkflow, /VCPKG_DOWNLOADS/g);
assert.match(windowsEtBuild, /Invoke-WithRetry/);
});
test("ET pull requests reuse exact platform builds without weakening release builds", () => {
const buildJobs = [
["linux-x64", etWorkflow.match(/\n build-linux-x64:\n[\s\S]*?(?=\n build-linux-arm64:)/)?.[0]],
["linux-arm64", etWorkflow.match(/\n build-linux-arm64:\n[\s\S]*?(?=\n build-macos-universal:)/)?.[0]],
["macos-universal", etWorkflow.match(/\n build-macos-universal:\n[\s\S]*?(?=\n build-windows-x64:)/)?.[0]],
["windows-x64", etWorkflow.match(/\n build-windows-x64:\n[\s\S]*?(?=\n # ------------------------------------------------------------------\n # Windows arm64)/)?.[0]],
];
const skipOnExactPrCacheHit =
"if: github.event_name != 'pull_request' || steps.et-build-cache.outputs.cache-hit != 'true'";
for (const [platform, job] of buildJobs) {
assert.ok(job, `${platform} ET build job must be readable`);
assert.match(job, /- name: Restore cached PR build\s*\n\s*id: et-build-cache/);
assert.match(job, /if: github\.event_name == 'pull_request'/);
assert.match(job, /path: out\//);
assert.match(
job,
/key: et-pr-build-v1-\$\{\{ runner\.os \}\}-\$\{\{ runner\.arch \}\}-\$\{\{ env\.ET_REF \}\}-\$\{\{ hashFiles\('\.github\/workflows\/build-et-binaries\.yml', 'scripts\/build-et\/\*\*'\) \}\}/,
);
assert.ok(job.includes(skipOnExactPrCacheHit), `${platform} must skip compilation on an exact PR cache hit`);
assert.match(
job,
/- name: Upload artifact[\s\S]*?if-no-files-found: error/,
`${platform} must fail instead of publishing an empty cached build`,
);
}
assert.equal(
(etWorkflow.match(new RegExp(skipOnExactPrCacheHit.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g")) ?? []).length,
14,
"all dependency setup and compilation steps must be skipped when the exact PR build is cached",
);
assert.doesNotMatch(
etWorkflow.match(/\n release:\n[\s\S]*$/)?.[0] ?? "",
/et-build-cache|Restore cached PR build/,
"manual releases must never reuse PR build outputs",
);
});
test("GitHub-owned actions use current Node 24 releases", () => {
const workflows = fs.readdirSync(workflowsDir)
.filter((name) => name.endsWith(".yml"))
.map((name) => [name, readWorkflow(name)]);
const expectedMajors = new Map([
["actions/checkout", "v7"],
["actions/setup-node", "v7"],
["actions/upload-artifact", "v7"],
["actions/download-artifact", "v8"],
["actions/github-script", "v9"],
["actions/cache", "v6"],
]);
for (const [name, source] of workflows) {
for (const [action, major] of expectedMajors) {
const uses = [...source.matchAll(new RegExp(`${action.replace("/", "\\/")}@(v\\d+)`, "g"))];
for (const match of uses) {
assert.equal(match[1], major, `${name} must use ${action}@${major}`);
}
}
}
});
test("electron-builder retries Fetch API server errors", () => {
assert.match(appBuilderPatch, /e\?\.response\?\.status/);
assert.match(appBuilderPatch, /responseStatus >= 500/);
});