mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
npm rejects provenance-signed uploads from self-hosted runners: 422 Unprocessable Entity - Error verifying sigstore provenance bundle: Unsupported GitHub Actions runner environment: "self-hosted". Only "github-hosted" runners are supported when publishing with provenance. v3.8.50 learned that at minute 76 of its 10th publish attempt, after the tag, the GitHub Release and the Docker images were already out. USE_VPS_RUNNER had routed the job to the .113 pool on 2026-08-02; no release ran between 07-30 and 08-28, so the pairing sat latent for four weeks. It is pure text — a job whose runs-on resolves to self-hosted and a step whose run contains --provenance — so the workflow lint now checks it as a hard rule: reported in plain mode, blocking under --strict and --ratchet (the CI mode), emitted as provenanceRunnerFindings=<n> next to the other counters. Against origin/main the rule finds the two real offenders (the staged upload AND the DIRECT emergency fallback in npm-publish.yml); against the #11877 split it finds none. --provenance-file is deliberately not matched (different flag, pre-built bundle) and an opaque runs-on expression with no literal self-hosted is classified unknown and skipped — the check never guesses. The unit suite's last case walks the real .github/workflows and asserts zero findings, so it is red on main until #11877 lands and green after; that is the regression guard working, not a flake.
84 lines
3.2 KiB
JavaScript
84 lines
3.2 KiB
JavaScript
/**
|
|
* scripts/check/lib/provenanceRunner.mjs
|
|
*
|
|
* npm refuses `--provenance` from a self-hosted runner:
|
|
*
|
|
* 422 Unprocessable Entity - Error verifying sigstore provenance bundle:
|
|
* Unsupported GitHub Actions runner environment: "self-hosted".
|
|
* Only "github-hosted" runners are supported when publishing with provenance.
|
|
*
|
|
* v3.8.50 hit this at the very end of a 76-minute publish job — after the tag,
|
|
* the GitHub Release and the Docker images were already public — because
|
|
* `USE_VPS_RUNNER` had been turned on (2026-08-02) with no release in between to
|
|
* surface it. The combination is greppable, so it must fail in CI the moment a
|
|
* workflow introduces it, not four weeks later at the registry.
|
|
*
|
|
* Pure: takes workflow YAML text, returns the offending (job, step) pairs.
|
|
*/
|
|
import { load as yamlLoad } from "js-yaml";
|
|
|
|
const SELF_HOSTED = /\bself-hosted\b/;
|
|
const EXPRESSION = /\$\{\{/;
|
|
// Lookahead, not \b: `--provenance-file=…` is a different flag (a pre-built
|
|
// bundle) and must not match — a word boundary sits between "e" and "-".
|
|
const PROVENANCE = /(^|\s)--provenance(?=\s|=|$)/m;
|
|
|
|
/**
|
|
* Classifies a job's `runs-on` value.
|
|
* @returns {"self-hosted"|"hosted"|"unknown"}
|
|
* "unknown" = an expression with no literal `self-hosted` in it (e.g.
|
|
* `${{ matrix.os }}`); the check does not guess, it skips.
|
|
*/
|
|
export function classifyRunsOn(runsOn) {
|
|
if (runsOn == null) return "unknown";
|
|
if (typeof runsOn === "string") {
|
|
if (SELF_HOSTED.test(runsOn)) return "self-hosted";
|
|
return EXPRESSION.test(runsOn) ? "unknown" : "hosted";
|
|
}
|
|
if (Array.isArray(runsOn)) {
|
|
return runsOn.some((v) => typeof v === "string" && SELF_HOSTED.test(v))
|
|
? "self-hosted"
|
|
: "hosted";
|
|
}
|
|
if (typeof runsOn === "object") {
|
|
// { group: ..., labels: ... } form
|
|
const labels = runsOn.labels;
|
|
return classifyRunsOn(Array.isArray(labels) ? labels : labels == null ? "" : String(labels));
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
/**
|
|
* @param {string} yamlText
|
|
* @param {string} fileName used only for reporting
|
|
* @returns {{ file: string, job: string, step: string }[]}
|
|
*/
|
|
export function findProvenanceOnSelfHosted(yamlText, fileName = "<workflow>") {
|
|
let doc;
|
|
try {
|
|
doc = yamlLoad(yamlText);
|
|
} catch {
|
|
// actionlint owns syntax; an unparseable file is not this rule's finding.
|
|
return [];
|
|
}
|
|
const jobs =
|
|
doc && typeof doc === "object" && doc.jobs && typeof doc.jobs === "object" ? doc.jobs : {};
|
|
const findings = [];
|
|
for (const [jobName, job] of Object.entries(jobs)) {
|
|
if (!job || typeof job !== "object") continue;
|
|
if (classifyRunsOn(job["runs-on"]) !== "self-hosted") continue;
|
|
const steps = Array.isArray(job.steps) ? job.steps : [];
|
|
steps.forEach((step, i) => {
|
|
if (step && typeof step.run === "string" && PROVENANCE.test(step.run)) {
|
|
findings.push({ file: fileName, job: jobName, step: step.name || `#${i + 1}` });
|
|
}
|
|
});
|
|
}
|
|
return findings;
|
|
}
|
|
|
|
/** Human-readable line per finding, used by the CLI. */
|
|
export function formatProvenanceFinding(f) {
|
|
return `${f.file}: job "${f.job}", step "${f.step}" runs \`--provenance\` on a self-hosted runner — npm rejects that (422). Move the upload to a github-hosted job.`;
|
|
}
|