From 78cb0951442ad8b52671b11000020d1ca7bf0373 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 7 Sep 2026 02:19:44 +0300 Subject: [PATCH 1/8] docs(knowledge): add release-pipeline feature knowledge base (post-B1, PR #366) --- .devflow/features/index.md | 1 + .../features/release-pipeline/KNOWLEDGE.md | 333 ++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 .devflow/features/release-pipeline/KNOWLEDGE.md diff --git a/.devflow/features/index.md b/.devflow/features/index.md index e80f092d..0f8e30fa 100644 --- a/.devflow/features/index.md +++ b/.devflow/features/index.md @@ -4,3 +4,4 @@ - **mds-lint** — crates/mds-core/src/lint, crates/mds-cli/src, crates/mds-wasm/src, crates/mds-napi/src, crates/mds-python/src, packages/mds/src — Use when adding or modifying lint rules, extending the --fix pipeline, changing the JSON wire format, wiring lint into a binding layer, debugging unexpected exit codes and reverify gate refusals, or working on the ESC/bidi/newline injection defences. Keywords: mds lint, LintDiagnostic, fix_removals, fix_edits, TextEdit, FixLineSpan, diag_to_edits, LintResult, LintConfig, to_canonical_json, fix tier, reverify gate, FixOutcome, PartiallyFixed, apply_fixes_incremental, preview_fixes, PreviewOutcome, set_diag_display_path, AnalysisContext, ElseifBranch, end_offset, DefineFact, assertKnownKeys, CheckOptions, unreachable-branch, unused-variable, duplicate-import, empty-block, legacy-interpolation, is_output_neutral, all_output_neutral, Tier A Tier B Tier C, structural-standalone, compile-clean, is_standalone, sanitize_control_chars, sanitize_control_chars_wire, named_source_for_render, neutralize_source_for_render, SanitizedReport, SanitizedNode, MAX_AUX_DEPTH, EscapeMode, HUMAN WIRE, eprint_warning, safe_path, safe_inline, safe_file_display, preview_text_for, print_discipline, reverify_failure_reason, LintDirCtx, config_cache, dedup_contained_or_identical, EXIT 0 1 2 3, render_error_sanitized, eprint_error, display_sanitized, MdsError::display_sanitized, ESC-injection, CWE-150, CWE-117, bidi, Trojan-Source, CVE-2021-42574, U+061C, U+202E, U+FEFF, U+2028, U+2029, PF-014, PF-005, construction-time sanitization, per-field rule, Cow, #176, ADR-008, ResultSink, from_rules_checked, relative_display, write_bytes, PF-020, #309, emit-ordering. - **source-map-security** — crates/mds-core/src, crates/mds-cli/src, packages/mds/src — Use when working with Source Map v3 generation, sources[] path relativization, the relativize_source choke-point, FileSystem::source_root(), CompileOptions.source_map_base, cross-surface source-map parity tests, or the Windows verbatim UNC path fix. Keywords: source map, sources[], relativize_source, source_map_base, source_root, path containment, basename fallback, PF-005, ADR-005, SEC-3, Windows verbatim UNC, path_to_unified, compute_source_map_base, apply_source_map_file_label, CF-SM2, V-SM1, differential test, two-level anchoring, map-relative, root-relative. - **mds-js** — packages/mds/src, packages/mds/__test__ — Use when modifying the JS/TS public API surface, adding backend methods, changing option types, debugging basePath rejection behaviour, changing result types, updating the backend contract, working on WASM/native backend validation, or debugging why a backend result is rejected. Keywords: compileFile, compile, check, checkFile, lint, lintFile, lintVirtual, CompileResult, MarkdownResult, MessagesResult, CheckResult, LintResult, LintDiagnostic, LintFileOptions, CompileFileOptions, FileOptions, assertResultShape, validateBackendMethods, METHOD_KEYS, forwardOpts, assertKnownKeys, getBasePathError, BASEPATH_REJECTORS, BASE_METHODS, NODE_METHODS, WASM_EXPORTS, discriminated union, kind, mds::invalid_backend_result, mds::invalid_options, basePath, synchronous throw, native.ts, wasm.ts, contract.ts, types.ts, node.ts, browser.ts, options.ts. +- **release-pipeline** — .github/workflows, .github/actions, scripts, scripts/__test__ — Use when modifying release.yml, adding CI jobs, updating TIER_B_EXPECTED_SKIPPED, adjusting the pull_request surface trigger, debugging a publish failure, running the pre-merge verifier, or reasoning about the publish job ordering. Keywords: release, release.yml, verify-pr-checks, TIER_B_EXPECTED_SKIPPED, RELEASE_SURFACE, rehearse-publish-python, tag-push, TestPyPI, publish-crates, publish-npm, publish-python, github-release, version-gate, stage-and-verify-napi, ADR-013, PF-040. diff --git a/.devflow/features/release-pipeline/KNOWLEDGE.md b/.devflow/features/release-pipeline/KNOWLEDGE.md new file mode 100644 index 00000000..e8812149 --- /dev/null +++ b/.devflow/features/release-pipeline/KNOWLEDGE.md @@ -0,0 +1,333 @@ +--- +feature: release-pipeline +name: Release pipeline gates (release.yml, verify-pr-checks.mjs, gate specs) +description: "Use when modifying release.yml, adding CI jobs, updating TIER_B_EXPECTED_SKIPPED, adjusting the pull_request surface trigger, debugging a publish failure, running the pre-merge verifier, or reasoning about the publish job ordering. Keywords: release, release.yml, verify-pr-checks, TIER_B_EXPECTED_SKIPPED, RELEASE_SURFACE, rehearse-publish-python, tag-push, TestPyPI, publish-crates, publish-npm, publish-python, github-release, version-gate, stage-and-verify-napi, ADR-013, PF-040." +category: architecture +directories: [.github/workflows, .github/actions, scripts, scripts/__test__] +created: 2026-09-07 +updated: 2026-09-07 +--- + +# Release Pipeline Gates + +## Overview + +The release pipeline is a single coordinated tag-push that ships every language surface +simultaneously: two Rust crates (crates.io), nine npm packages, and one Python wheel +matrix (PyPI). All live in `.github/workflows/release.yml`. The same workflow has two +rehearsal entry points — a `pull_request` trigger for release-surface PRs and a +`workflow_dispatch` for branch dry-runs — so that the tag-guarded publish path is exercised +before it runs for real. + +`scripts/verify-pr-checks.mjs` is the mandatory pre-merge gate for EVERY PR; it asserts +that every required context is `completed+success` and emits the exact `gh pr merge` +command to use verbatim. Adding a job to either workflow is a multi-place change (see +ADR-013 below). `scripts/__test__/release-auth-probe.spec.mjs` and +`scripts/__test__/verify-pr-checks.spec.mjs` enforce those multi-place rules as +CI-checked specs. + +## System Context + +``` +push.tags v* ─┐ +workflow_dispatch ├─→ release.yml → registries (crates.io, npm, PyPI) +pull_request ─┘ (publish jobs tag-guarded / input-guarded) +``` + +Three entry points, one workflow: + +- **`push.tags v*`** — full coordinated release; all ten jobs run. +- **`workflow_dispatch`** — dry run; five publish jobs are `skipped` (tag guard fails). Add + `-f testpypi=true` to also trigger the `publish-testpypi` opt-in leg. +- **`pull_request` (path-filtered)** — rehearsal only; paths filter is the release surface + (see RELEASE_SURFACE below). Publish jobs report `skipped`. The mandatory verifier + REQUIRES the three `RELEASE_SURFACE_CONTEXTS` jobs to be `completed+success`. + +`concurrency: release-${{ github.ref }}` with `cancel-in-progress: false` is load-bearing +(avoids PF-017's cancelled-run-as-green shape mid-sequence and prevents a partial-publish +state between `cargo publish` and `npm publish`). + +## Component Architecture — Ten Jobs and Their DAG + +``` +version-gate + ├─→ build-napi (7 legs) ─→ stage-and-verify-napi ─┐ + └─→ build-python (8 legs) ─→ rehearse-publish-python ─┤ + └─→ publish-testpypi │ (dispatch+input only) + ↓ + publish-crates (tag only) + ↓ ↓ + publish-npm publish-python (tag only) + └────────────┘ + ↓ + github-release (tag only) +``` + +Job details: + +| Job id | Display name | Guarded by | Needs | +|---|---|---|---| +| `version-gate` | Version gate | nothing (always runs) | — | +| `build-napi` | Build napi (...) | nothing | version-gate | +| `stage-and-verify-napi` | Stage + verify platform packages | nothing | build-napi | +| `build-python` | Build Python (...) | nothing | version-gate | +| `rehearse-publish-python` | Rehearse PyPI publish (no upload) | nothing | build-python | +| `publish-testpypi` | Publish to TestPyPI (rehearsal) | `workflow_dispatch && inputs.testpypi` | build-python, rehearse-publish-python | +| `publish-crates` | Publish to crates.io | `startsWith(ref, 'refs/tags/v')` | version-gate, stage-and-verify-napi, build-python, rehearse-publish-python | +| `publish-npm` | Publish to npm | `startsWith(ref, 'refs/tags/v')` | stage-and-verify-napi, publish-crates | +| `publish-python` | Publish to PyPI | `startsWith(ref, 'refs/tags/v')` | build-python, rehearse-publish-python, publish-crates, publish-npm | +| `github-release` | GitHub Release | `startsWith(ref, 'refs/tags/v')` | publish-crates, publish-npm, publish-python | + +Key ordering constraints: +- `publish-crates` blocks on `rehearse-publish-python`: a failed OIDC exchange aborts before the irreversible crates.io write (PF-039). +- `publish-crates` blocks on `build-python`: a Python wheel build failure aborts before ANY registry write (PF-023). +- `publish-python` blocks on `publish-crates` AND `publish-npm`: PyPI is last because it is the only surface with OIDC revocability; losing npm is less recoverable than losing PyPI. +- `github-release` is gated on all three publish jobs; a partial failure leaves no Release page (see ADR-014 on backfilling). + +## Component Interactions — RELEASE_SURFACE and the Three-Place Rule (ADR-013) + +The `pull_request` trigger fires on exactly five paths (the **release surface**): + +``` +.github/workflows/release.yml +.github/actions/** +crates/mds-napi/** +crates/mds-python/** +scripts/verify-napi-names.mjs +``` + +`crates/mds-core/**`, `Cargo.toml`, and `package.json` are excluded on purpose: they change +on most PRs and `ci.yml` already covers them. A dependency sweep that does not touch these +five paths still needs a manual `workflow_dispatch` dry run. + +`scripts/verify-pr-checks.mjs` exports `RELEASE_SURFACE` (the same list) and spec S10 in +`release-auth-probe.spec.mjs` asserts set-equality between the two. They must be kept in +sync whenever the `on.pull_request.paths:` list changes. + +**ADR-013 three-place rule**: Adding any job to `release.yml` is a three-place change: + +1. The workflow file itself. +2. Branch protection required contexts (via the GitHub API, currently 15 contexts). +3. `EXPECTED_CONTEXTS` in `scripts/verify-pr-checks.mjs` (for `ci.yml` jobs) **or** + `TIER_B_EXPECTED_SKIPPED` (for `release.yml` tag/input-guarded jobs). + +A job added without all three places is silently decorative — it can never block a merge. +The length assertion in `verify-pr-checks.spec.mjs` is the mechanical enforcer; do not +relax it when adding a job. + +**ADR-013 amendment (2026-09-06 — step-level guard rule)**: On a PR-triggered release run, +disable PR-inapplicable behaviour at **step** level, never at **job** level. Any job present +on a PR head must run through to `conclusion=success` — a job-level `if:` that produces +`conclusion=skipped` for an unlisted name hard-fails the mandatory verifier on the PR that +adds the trigger. The CI-history gate in `version-gate` is the concrete example: it uses a +step-level `if: github.event_name != 'pull_request'` with a `::notice::` sibling, because +a job-level skip would make `Version gate` report `skipped` and break the verifier. + +## Component Interactions — `verify-pr-checks.mjs` Tier System + +The verifier is **event-blind**: it reads only check-run name, status, and conclusion — +never the triggering event, workflow file, or check-suite. + +| Tier | Membership | Passing condition | +|---|---|---| +| **Tier A** (required) | 15 contexts from live branch protection API | `completed+success` | +| **Tier A+ (local)** | `EXPECTED_CONTEXTS` (4 entries: Source hygiene, Python — build & test, examples/ gitignore coverage, Python — wheel install smoke) | `completed+success`, presence required | +| **Tier B** | Everything else | `completed+success`; `skipped` tolerated ONLY for the five names in `TIER_B_EXPECTED_SKIPPED` | + +`TIER_B_EXPECTED_SKIPPED` (5 names): +- `Publish to crates.io` +- `Publish to npm` +- `Publish to PyPI` +- `GitHub Release` +- `Publish to TestPyPI (rehearsal)` + +Any `skipped` conclusion under any other name fails Tier B. Any `cancelled`, `neutral`, +`in_progress`, or `queued` conclusion fails regardless of name (PF-017). A Tier-B-only +rejection (e.g. an advisory CodeQL `neutral`) must be adjudicated against the required-context +set before it is believed — it is never license to merge unverified. + +**D-PR7 release-surface presence check**: When any changed file matches `RELEASE_SURFACE`, +the verifier additionally requires `Version gate`, `Stage + verify platform packages`, and +`Rehearse PyPI publish (no upload)` to each be `completed+success`. All runs under each +name must pass (duplicate names = all-must-pass). + +The verifier prints `gh pr merge N --squash --admin --match-head-commit ` on PASS. +Always use this command verbatim — confirm the current branch resolves to the intended PR +before running it (the command names a SHA but not a PR; `gh pr merge` re-resolves the +subject from the current branch at execution time; avoids PF-017). + +## Integration Patterns — Gate Details + +### version-gate (always runs) + +Every step in `version-gate` runs on every event **except** the CI-history gate, which is +step-skipped on `pull_request` with `if: github.event_name != 'pull_request'`. Reason: on +`pull_request` `github.sha` is the merge commit whose own `ci.yml` run is concurrent, and +the gate correctly fails closed on an in-progress run. A notice step fires instead so the +job still reaches `success`. + +Steps (in order): +1. Verify publish credentials (npm `whoami` + cargo token non-empty + PyPI OIDC mint-token exchange). +2. Assert synchronized versions, no `file:` refs. +3. Assert no hazardous codepoints in tracked source. +4. Run `npm run test:gates` — all four spec files, 187 tests including pin-shape specs (S16). +5. Assert tagged SHA has green CI history (step-skipped on `pull_request`). + +Because `npm run test:gates` runs inside `version-gate`, a malformed pin (e.g. a commit SHA +instead of a `vX.Y.Z` tag) causes `Version gate` to fail before the rehearsal job ever runs. + +**Fork / Dependabot PRs**: The "Verify publish credentials" step explicitly exits 1 with a +`::error::` when `NODE_AUTH_TOKEN` is empty on a `pull_request` event from Dependabot or a +fork (`IS_FORK=true`). Do not merge on these checks; supersede with a maintainer PR or a +manual `gh workflow run`. + +### rehearse-publish-python — Four Gates with Positive Controls (PF-013) + +`rehearse-publish-python` holds `permissions: contents: read` — no `id-token: write`, so it +structurally cannot upload even if an upload step were re-introduced. It NEVER `uses:` the +`pypa/gh-action-pypi-publish` action (the action has no dry-run input; a draft that wired +`dry-run: true` attempted a real upload from a PR). + +Four gates, each with an in-step positive control: + +1. **Pin shape** (`Gate 1/4`): exactly one distinct `pypa/gh-action-pypi-publish@` ref in the + file, must match `^v\d+\.\d+\.\d+$`. Controls reject the commit sha `dc37677b…` and the + annotated tag object sha `a892a5a6…`. +2. **Anonymous GHCR manifest probe** (`Gate 2/4`): control ref must return HTTP 404 + + `MANIFEST_UNKNOWN`; pin must return HTTP 200. +3. **`docker pull`** (`Gate 3/4`): control tag must fail with manifest-unknown; real pull + bounded to 3 attempts. +4. **`twine check`** (`Gate 4/4`): all 8 distributions checked via `docker run --rm --network + none --entrypoint twine check` using the image's own twine (7.0.0 at v1.14.2; no + `--strict`). Includes a corrupt-wheel positive control. + +The gate asserts 7 wheels + 1 sdist are present before running (non-vacuity guard). + +### GHCR image rule for pypa/gh-action-pypi-publish (PF-040) + +This action resolves `github.action_ref` to `ghcr.io/pypa/gh-action-pypi-publish:`. +GHCR holds images for release tags (`v1.14.2`) and commit SHAs on release branches, but +**never** for annotated tag objects (the v0.4.1 failure: `a892a5a6` was the tag object sha, +not the commit sha `dc37677b`). Both look like valid git refs but only one resolves to a +Docker image. + +Policy: use a `vX.Y.Z` release tag pin for this one action. All other third-party actions +stay commit-SHA-pinned per PF-040's general rule. + +### publish-testpypi (opt-in) + +Triggered only by `workflow_dispatch` with `-f testpypi=true`. Publishes to +`https://test.pypi.org/legacy/` with `skip-existing: true` and `attestations: true`. Requires +a separate trusted publisher on test.pypi.org (project `markdown-script`, owner `dean0x`, +repo `mdscript`, workflow `release.yml`, environment blank). A missing record causes +`Trusted publishing exchange failure: invalid-publisher`. The pending publisher expires ~30 +days unused. + +### publish-crates — Irreversibility and Ordering (PF-023) + +`cargo publish -p mds-core` is idempotent (treats `already (uploaded|exists|published)` as +success). `npm publish` calls in `publish-npm` have NO such guard — a partial npm failure is +unrecoverable by re-run. This makes `publish-crates` the single irreversible point of no +return; all correctness gates run before it. + +### GitHub expression preprocessor trap + +GitHub's expression preprocessor scans `run:` block text **including shell comments** without +skipping them. A comment containing the literal characters `${{` (even `${{ }}`) makes the +entire workflow invalid with "An expression was expected", producing a zero-job run that +completes in the same second. `js-yaml`, `actionlint`, and `@action-validator/cli` all pass +such a file. Never write `${{` in comments; describe it in words. + +## Constraints + +- `cancel-in-progress: false` is non-negotiable; `true` would produce a cancelled run that + reads as non-failing under `--admin` merge (PF-017) while leaving registries in a partial state. +- `publish-testpypi` is intentionally NOT in `publish-crates`'s `needs:` — a TestPyPI failure + should not abort the live release. +- The fixture `scripts/__test__/fixtures/protection-main.json` holds 6 contexts vs. 15 live — + known drift tracked as issue #341. +- The `startup-race-probe` Cargo feature (`mds-cli`) must never ship enabled. +- `debug-panics` Cargo feature must never ship enabled (all three binding crates). + +## Anti-Patterns + +- **Job-level `if:` to skip PR-inapplicable behaviour**: produces `skipped` under a name the + verifier does not allow, hard-failing the mandatory pre-merge check on the very PR that adds + the trigger. Use step-level guards instead. +- **Reformatting `needs:` from inline array to block list**: `extractNeeds` in the spec parser + reads INLINE arrays only; a block list breaks the spec. +- **Adding a `startsWith(ref, 'refs/tags/v')` guard without adding the job name to + `TIER_B_EXPECTED_SKIPPED`**: that job's `skipped` conclusion will hard-fail the verifier on + every branch dry-run. +- **Adding a `ci.yml` job without updating branch protection**: the new job is decorative — + it can never block a merge (ADR-013). +- **Using `section.includes('refs/tags/v')` in specs to detect a job's guard**: the `# ====` + banner comment above `publish-crates` contains `startsWith(github.ref, 'refs/tags/v')` and + lands inside the preceding job's section when `extractJobSection` runs. Use `extractJobIf` + (4-space ` if:` only) to read job-level guards. +- **SHA-pinning `pypa/gh-action-pypi-publish`**: this is a Docker-trampoline action; GHCR + holds no image for annotated tag object SHAs. Use a `vX.Y.Z` release tag (PF-040). +- **Dispatching a dry run before that ref's `ci.yml` finishes**: the CI-history gate in + `version-gate` fails closed on an in-progress or absent run. + +## Gotchas + +- **RELEASE_SURFACE set-equality is spec-enforced**: `RELEASE_SURFACE` in + `verify-pr-checks.mjs` and `on.pull_request.paths:` in `release.yml` must be identical sets. + Spec S10 in `release-auth-probe.spec.mjs` asserts this. If you add a path to one, add it + to the other. +- **Positive controls must be exercised via a draft PR, not a bare branch dispatch**: a bare + branch has no `ci.yml` run; a dispatch from it fails at the CI-history gate before the + rehearsal job runs. +- **`npm run test:gates` runs inside `version-gate`**: pin-policy violations (S16: must match + `^v\d+\.\d+\.\d+$`) fail `Version gate` before `rehearse-publish-python` ever starts. + Observed in run 34064235589 (commit-sha pin → Version gate failed, rehearsal skipped). +- **`extractJobSection` in the spec parser runs from ` :` to the next 2-space job id**: + any banner comment above a job (e.g. the `# ====` separator above `publish-crates`) + belongs to the PRECEDING job's section, not to `publish-crates`. Assert guards via + `extractJobIf`, never `section.includes()`. +- **Dependabot / fork PRs fail credential probe and should not be merged on**: the step exits + 1 with `IS_FORK` "No Actions secrets on this run." The remedy is a maintainer PR or a + manual dispatch. +- **TestPyPI pending publisher expires ~30 days after creation** if no upload lands. A missing + or expired record causes `Trusted publishing exchange failure: invalid-publisher` in + `publish-testpypi`. +- **`docker pull` in Gate 3/4 is bounded to 3 attempts**: a transient GHCR outage can cause + the rehearsal to fail; `gh run rerun --failed` is the recovery path, not a code change. +- **A second dispatch on the same ref queues behind the first** (concurrency group + `release-${{ github.ref }}`); it does not cancel it. +- **`version-gate` prints a `::notice::` not a `::skip::`** when it skips the CI-history step + on `pull_request`: the job still completes `success`, which is the required state. + +## Key Files + +- `.github/workflows/release.yml` — the complete 10-job release workflow (1260+ lines). +- `.github/workflows/ci.yml` — the build/test workflow whose contexts populate Tier A. +- `scripts/verify-pr-checks.mjs` — mandatory pre-merge verifier; exports `EXPECTED_CONTEXTS`, + `TIER_B_EXPECTED_SKIPPED`, `RELEASE_SURFACE`, `RELEASE_SURFACE_CONTEXTS`. +- `scripts/__test__/verify-pr-checks.spec.mjs` — specs for the verifier (M10c, S13, S18 rules; + length assertion for `EXPECTED_CONTEXTS`). +- `scripts/__test__/release-auth-probe.spec.mjs` — specs for release.yml structure: pin shape + (S16), set equality S10, guard detection, no `${{ }}` literal (S19), `uses:` count (S14). +- `scripts/__test__/fixtures/protection-main.json` — 6-context branch protection fixture + (known drift from 15 live; tracked as #341). +- `RELEASING.md` — full release runbook including pre-flight checklist, tag-push procedure, + and post-release verification. + +## Related + +- **ADR-013** — Branch protection / three-place rule; step-level guard amendment (2026-09-06). +- **ADR-014** — Every tag that reached a registry gets a GitHub Release; backfill protocol. +- **PF-013** — Positive-control discipline in security/gate tests; every absence assertion + needs a paired presence assertion and a non-vacuity guard. +- **PF-017** — Cancelled runs read as non-failing; `--admin` bypasses required checks; + `verify-pr-checks.mjs` is the remedy; always confirm current branch before running the + emitted merge command. +- **PF-023** — `publish-npm` has no idempotency guard; a partial npm failure is unrecoverable + by re-run; ordering (publish-crates then publish-npm) is load-bearing. +- **PF-036** — CI steps with no local counterpart are undetectable locally; the intra-doc-link + gate is an example. +- **PF-039** — Tag-guarded steps are the LEAST-tested lines; every `if:` guard is a hole in + the dry run; give each a dispatch-mode execution path. +- **PF-040** — SHA-pinning breaks Docker-trampoline actions; `pypa/gh-action-pypi-publish` + requires a `vX.Y.Z` tag, not a commit SHA or annotated tag object SHA. From a59b4ec1d0febdd0545f6d82f2f657cf827ae2e8 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 7 Sep 2026 15:07:40 +0300 Subject: [PATCH 2/8] scripts(verify-pr-checks): key the skipped-publish allowance and D-PR7 contexts on release.yml check-suite identity (D-PR8, #341); add 2026-09 fixtures --- .../fixtures/checks-pr366-e02bcf2.json | 1 + .../fixtures/protection-main-2026-09.json | 86 ++ .../__test__/fixtures/runs-pr366-e02bcf2.json | 1 + scripts/__test__/verify-pr-checks.spec.mjs | 795 +++++++++++++++++- scripts/verify-pr-checks.mjs | 238 +++++- 5 files changed, 1079 insertions(+), 42 deletions(-) create mode 100644 scripts/__test__/fixtures/checks-pr366-e02bcf2.json create mode 100644 scripts/__test__/fixtures/protection-main-2026-09.json create mode 100644 scripts/__test__/fixtures/runs-pr366-e02bcf2.json diff --git a/scripts/__test__/fixtures/checks-pr366-e02bcf2.json b/scripts/__test__/fixtures/checks-pr366-e02bcf2.json new file mode 100644 index 00000000..93e80053 --- /dev/null +++ b/scripts/__test__/fixtures/checks-pr366-e02bcf2.json @@ -0,0 +1 @@ +{"total_count":44,"check_runs":[{"id":101574056790,"name":"GitHub Release","node_id":"CR_kwDOSZrySs8AAAAXpkkbVg","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"de0b7533-7b2f-5845-9568-21fe9e4bede9","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101574056790","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101574056790","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101574056790","status":"completed","conclusion":"skipped","started_at":"2026-09-06T23:02:41Z","completed_at":"2026-09-06T23:02:41Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101574056790/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101574056136,"name":"Publish to PyPI","node_id":"CR_kwDOSZrySs8AAAAXpkkYyA","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"ee09f55d-a0e0-5325-a3fd-fdcc0e35aade","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101574056136","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101574056136","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101574056136","status":"completed","conclusion":"skipped","started_at":"2026-09-06T23:02:41Z","completed_at":"2026-09-06T23:02:41Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101574056136/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101574056014,"name":"Publish to crates.io","node_id":"CR_kwDOSZrySs8AAAAXpkkYTg","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"74decaa6-d3f8-5e50-817b-f102f0e9c6a0","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101574056014","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101574056014","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101574056014","status":"completed","conclusion":"skipped","started_at":"2026-09-06T23:02:41Z","completed_at":"2026-09-06T23:02:41Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101574056014/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101574055926,"name":"Publish to npm","node_id":"CR_kwDOSZrySs8AAAAXpkkX9g","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"5c198f74-aefa-5d3c-8a2a-6b33b8bc481c","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101574055926","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101574055926","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101574055926","status":"completed","conclusion":"skipped","started_at":"2026-09-06T23:02:41Z","completed_at":"2026-09-06T23:02:41Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101574055926/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573999381,"name":"Stage + verify platform packages","node_id":"CR_kwDOSZrySs8AAAAXpkg7FQ","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"9ecd8349-f5ee-5b1f-b0b0-51cb9d611f7b","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573999381","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573999381","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573999381","status":"completed","conclusion":"success","started_at":"2026-09-06T23:02:19Z","completed_at":"2026-09-06T23:02:40Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573999381/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573997867,"name":"Publish to TestPyPI (rehearsal)","node_id":"CR_kwDOSZrySs8AAAAXpkg1Kw","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"6babf8de-f210-5ad6-89f8-e5097967c126","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573997867","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573997867","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573997867","status":"completed","conclusion":"skipped","started_at":"2026-09-06T23:02:16Z","completed_at":"2026-09-06T23:02:15Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573997867/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573962413,"name":"Rehearse PyPI publish (no upload)","node_id":"CR_kwDOSZrySs8AAAAXpkeqrQ","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"34629855-144f-54aa-8095-9feceb569100","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573962413","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573962413","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573962413","status":"completed","conclusion":"success","started_at":"2026-09-06T23:02:02Z","completed_at":"2026-09-06T23:02:15Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573962413/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573674205,"name":"CodeQL","node_id":"CR_kwDOSZrySs8AAAAXpkNE3Q","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573674205","html_url":"https://github.com/dean0x/mdscript/runs/101573674205","details_url":"https://github.com/dean0x/mdscript/runs/101573674205","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:53Z","completed_at":"2026-09-06T22:59:56Z","output":{"title":"No new alerts in code changed by this pull request","summary":"[View all branch alerts](/dean0x/mdscript/security/code-scanning?query=pr%3A366+tool%3ACodeQL+is%3Aopen).","text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573674205/annotations"},"check_suite":{"id":92290808712},"app":{"id":57789,"client_id":"Iv1.4643383dd86f00de","slug":"github-advanced-security","node_id":"MDM6QXBwNTc3ODk=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Advanced Security","description":"","external_url":"https://github.com/features/security","html_url":"https://github.com/apps/github-advanced-security","created_at":"2020-03-17T21:25:09Z","updated_at":"2026-05-04T22:40:45Z","permissions":{"actions":"read","checks":"write","contents":"read","metadata":"read","pull_requests":"read"},"events":[]},"pull_requests":[]},{"id":101573646829,"name":"Build napi (aarch64-unknown-linux-gnu)","node_id":"CR_kwDOSZrySs8AAAAXpkLZ7Q","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"0c696b6b-140e-5538-b6a3-f6a6003b688b","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646829","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646829","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646829","status":"completed","conclusion":"success","started_at":"2026-09-06T23:00:41Z","completed_at":"2026-09-06T23:01:50Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646829/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573646807,"name":"Build Python (aarch64-apple-darwin)","node_id":"CR_kwDOSZrySs8AAAAXpkLZ1w","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"9e3e998f-afe6-5261-bef3-e9c6617331fc","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646807","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646807","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646807","status":"completed","conclusion":"success","started_at":"2026-09-06T23:00:47Z","completed_at":"2026-09-06T23:01:37Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646807/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573646801,"name":"Build Python (x86_64-apple-darwin)","node_id":"CR_kwDOSZrySs8AAAAXpkLZ0Q","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"f239c489-0266-5f78-bdee-f69923f8f84e","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646801","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646801","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646801","status":"completed","conclusion":"success","started_at":"2026-09-06T23:00:13Z","completed_at":"2026-09-06T23:01:10Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646801/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573646786,"name":"Build Python (aarch64-unknown-linux-musl)","node_id":"CR_kwDOSZrySs8AAAAXpkLZwg","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"ccc27492-cb4c-5453-80c8-c6864894d24b","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646786","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646786","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646786","status":"completed","conclusion":"success","started_at":"2026-09-06T23:00:36Z","completed_at":"2026-09-06T23:01:48Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646786/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573646785,"name":"Build Python (x86_64-pc-windows-msvc)","node_id":"CR_kwDOSZrySs8AAAAXpkLZwQ","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"e29a042d-6cef-58b2-8ade-cc4222895a2f","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646785","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646785","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646785","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:52Z","completed_at":"2026-09-06T23:01:11Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646785/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573646782,"name":"Build Python (sdist)","node_id":"CR_kwDOSZrySs8AAAAXpkLZvg","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"c0e4ffbb-7f2b-5633-bfda-7b948ffda682","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646782","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646782","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646782","status":"completed","conclusion":"success","started_at":"2026-09-06T23:00:25Z","completed_at":"2026-09-06T23:00:38Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646782/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573646780,"name":"Build napi (aarch64-apple-darwin)","node_id":"CR_kwDOSZrySs8AAAAXpkLZvA","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"c93d181d-b96d-54d6-8a78-a4630ab33188","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646780","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646780","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646780","status":"completed","conclusion":"success","started_at":"2026-09-06T23:01:00Z","completed_at":"2026-09-06T23:02:16Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646780/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573646776,"name":"Build napi (x86_64-apple-darwin)","node_id":"CR_kwDOSZrySs8AAAAXpkLZuA","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"2ce3579f-e4ee-508b-80cc-067ec7bafac2","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646776","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646776","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646776","status":"completed","conclusion":"success","started_at":"2026-09-06T23:00:13Z","completed_at":"2026-09-06T23:00:57Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646776/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573646770,"name":"Build Python (aarch64-unknown-linux-gnu)","node_id":"CR_kwDOSZrySs8AAAAXpkLZsg","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"b20bcab2-fae8-533c-9b7e-76ea0c2f2a94","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646770","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646770","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646770","status":"completed","conclusion":"success","started_at":"2026-09-06T23:00:21Z","completed_at":"2026-09-06T23:01:58Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646770/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573646761,"name":"Build napi (aarch64-unknown-linux-musl)","node_id":"CR_kwDOSZrySs8AAAAXpkLZqQ","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"64ed08aa-0e69-5329-8b5c-821a17f26d6b","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646761","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646761","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646761","status":"completed","conclusion":"success","started_at":"2026-09-06T23:00:36Z","completed_at":"2026-09-06T23:01:29Z","output":{"title":null,"summary":null,"text":null,"annotations_count":1,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646761/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573646760,"name":"Build napi (x86_64-pc-windows-msvc)","node_id":"CR_kwDOSZrySs8AAAAXpkLZqA","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"ba095dfb-cb1d-5f0b-8b70-b949415f866b","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646760","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646760","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646760","status":"completed","conclusion":"success","started_at":"2026-09-06T23:00:11Z","completed_at":"2026-09-06T23:01:55Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646760/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573646757,"name":"Build napi (x86_64-unknown-linux-gnu)","node_id":"CR_kwDOSZrySs8AAAAXpkLZpQ","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"4e561d18-6d93-53e3-afd6-ac5655b24cc7","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646757","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646757","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646757","status":"completed","conclusion":"success","started_at":"2026-09-06T23:00:03Z","completed_at":"2026-09-06T23:01:02Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646757/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573646753,"name":"Build Python (x86_64-unknown-linux-musl)","node_id":"CR_kwDOSZrySs8AAAAXpkLZoQ","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"d3a764e8-ebf2-5a3b-8bf8-97195c930c05","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646753","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646753","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646753","status":"completed","conclusion":"success","started_at":"2026-09-06T23:00:23Z","completed_at":"2026-09-06T23:01:36Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646753/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573646746,"name":"Build Python (x86_64-unknown-linux-gnu)","node_id":"CR_kwDOSZrySs8AAAAXpkLZmg","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"9fedbe33-199f-5f57-a532-61fd418975b7","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646746","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646746","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646746","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:50Z","completed_at":"2026-09-06T23:01:06Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646746/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573646741,"name":"Build napi (x86_64-unknown-linux-musl)","node_id":"CR_kwDOSZrySs8AAAAXpkLZlQ","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"0abed4a8-c39d-5caf-8ade-74dbb5b6ad10","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646741","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646741","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573646741","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:41Z","completed_at":"2026-09-06T23:00:33Z","output":{"title":null,"summary":null,"text":null,"annotations_count":1,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573646741/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622945,"name":"Source hygiene","node_id":"CR_kwDOSZrySs8AAAAXpkJ8oQ","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"41af36a5-4b64-5535-a80e-d1e76cc5ad59","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622945","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622945","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622945","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:28Z","completed_at":"2026-09-06T22:59:37Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622945/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622908,"name":"WASM — build & test","node_id":"CR_kwDOSZrySs8AAAAXpkJ8fA","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"bd0b7a9c-bdbd-5cf8-9523-023ac0a9972e","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622908","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622908","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622908","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:28Z","completed_at":"2026-09-06T23:00:50Z","output":{"title":null,"summary":null,"text":null,"annotations_count":3,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622908/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622904,"name":"Python — build & test (ubuntu-latest, 3.13)","node_id":"CR_kwDOSZrySs8AAAAXpkJ8eA","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"01b75b43-a28c-560e-9145-aa5b8fa74b21","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622904","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622904","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622904","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:28Z","completed_at":"2026-09-06T23:00:20Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622904/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622898,"name":"Python — build & test (macos-latest, 3.13)","node_id":"CR_kwDOSZrySs8AAAAXpkJ8cg","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"0611cfcf-7b21-5606-8326-9cc0a658ff2a","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622898","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622898","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622898","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:46Z","completed_at":"2026-09-06T23:01:18Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622898/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622894,"name":"Python — build & test (windows-latest, 3.11)","node_id":"CR_kwDOSZrySs8AAAAXpkJ8bg","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"b054703d-51b6-55fa-b0e2-ff222125d547","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622894","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622894","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622894","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:29Z","completed_at":"2026-09-06T23:01:29Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622894/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622893,"name":"Watch startup race (probe)","node_id":"CR_kwDOSZrySs8AAAAXpkJ8bQ","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"5ac24891-f48e-5196-ad0e-785255bca326","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622893","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622893","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622893","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:28Z","completed_at":"2026-09-06T23:00:01Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622893/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622872,"name":"Python — build & test (windows-latest, 3.13)","node_id":"CR_kwDOSZrySs8AAAAXpkJ8WA","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"a2d21c5c-e0ae-54e0-8f21-560801a4bae9","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622872","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622872","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622872","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:28Z","completed_at":"2026-09-06T23:01:39Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622872/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622871,"name":"JS packages — build & test (windows-latest)","node_id":"CR_kwDOSZrySs8AAAAXpkJ8Vw","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"9d91dede-2e84-5fb9-ab07-0aa3f9391a52","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622871","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622871","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622871","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:29Z","completed_at":"2026-09-06T23:05:30Z","output":{"title":null,"summary":null,"text":null,"annotations_count":1,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622871/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622860,"name":"Python — build & test (macos-latest, 3.11)","node_id":"CR_kwDOSZrySs8AAAAXpkJ8TA","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"ba875209-1f94-5b9e-a56b-144b3a7c7ce6","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622860","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622860","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622860","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:34Z","completed_at":"2026-09-06T23:00:40Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622860/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622849,"name":"JS packages — build & test (macos-latest)","node_id":"CR_kwDOSZrySs8AAAAXpkJ8QQ","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"e30093c0-1ea7-5f99-a175-80e5c24b991e","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622849","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622849","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622849","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:34Z","completed_at":"2026-09-06T23:02:11Z","output":{"title":null,"summary":null,"text":null,"annotations_count":1,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622849/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622844,"name":"Python — build & test (ubuntu-latest, 3.11)","node_id":"CR_kwDOSZrySs8AAAAXpkJ8PA","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"c31d080f-dd08-5338-9670-1adeb2dd40dd","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622844","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622844","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622844","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:28Z","completed_at":"2026-09-06T23:00:23Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622844/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622837,"name":"Python — wheel install smoke","node_id":"CR_kwDOSZrySs8AAAAXpkJ8NQ","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"8e3056c5-1ab7-59d1-8b46-8b68d28451b8","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622837","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622837","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622837","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:29Z","completed_at":"2026-09-06T23:00:09Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622837/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622813,"name":"MSRV (Rust 1.88)","node_id":"CR_kwDOSZrySs8AAAAXpkJ8HQ","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"46c1132d-ca33-5d82-a21a-20b44e196636","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622813","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622813","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622813","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:29Z","completed_at":"2026-09-06T22:59:49Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622813/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622794,"name":"examples/ gitignore coverage","node_id":"CR_kwDOSZrySs8AAAAXpkJ8Cg","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"a03a05ef-32b0-5128-b599-860281318c3c","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622794","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622794","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622794","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:28Z","completed_at":"2026-09-06T22:59:48Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622794/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622787,"name":"Rust — fmt, clippy, test","node_id":"CR_kwDOSZrySs8AAAAXpkJ8Aw","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"42cd91a2-0849-5381-b917-642275ae51bb","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622787","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622787","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622787","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:28Z","completed_at":"2026-09-06T23:00:34Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622787/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622748,"name":"JS packages — build & test (ubuntu-latest)","node_id":"CR_kwDOSZrySs8AAAAXpkJ73A","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"f350c2cb-0c52-556e-923d-b8203cdea194","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622748","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622748","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768/job/101573622748","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:28Z","completed_at":"2026-09-06T23:01:36Z","output":{"title":null,"summary":null,"text":null,"annotations_count":1,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622748/annotations"},"check_suite":{"id":92290758550},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573622692,"name":"Version gate","node_id":"CR_kwDOSZrySs8AAAAXpkJ7pA","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"ab7c06af-0197-5b5c-a4a9-46ab09a10c1e","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622692","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573622692","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775/job/101573622692","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:28Z","completed_at":"2026-09-06T22:59:38Z","output":{"title":null,"summary":null,"text":null,"annotations_count":1,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573622692/annotations"},"check_suite":{"id":92290758559},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573619396,"name":"Analyze (rust)","node_id":"CR_kwDOSZrySs8AAAAXpkJuxA","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"f8cfeb38-dcd0-5151-a345-9585eef7127b","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573619396","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065571985/job/101573619396","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065571985/job/101573619396","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:26Z","completed_at":"2026-09-06T23:02:54Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573619396/annotations"},"check_suite":{"id":92290754013},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573619343,"name":"Analyze (actions)","node_id":"CR_kwDOSZrySs8AAAAXpkJujw","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"afdc46a1-2895-5675-9a84-6020791da151","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573619343","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065571985/job/101573619343","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065571985/job/101573619343","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:27Z","completed_at":"2026-09-06T23:00:04Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573619343/annotations"},"check_suite":{"id":92290754013},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573619262,"name":"Analyze (javascript-typescript)","node_id":"CR_kwDOSZrySs8AAAAXpkJuPg","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"6345ae73-55c8-5f17-b0d5-c3f411230f77","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573619262","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065571985/job/101573619262","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065571985/job/101573619262","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:26Z","completed_at":"2026-09-06T23:00:18Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573619262/annotations"},"check_suite":{"id":92290754013},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]},{"id":101573619169,"name":"Analyze (python)","node_id":"CR_kwDOSZrySs8AAAAXpkJt4Q","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","external_id":"7f018116-6381-564a-9213-20e1db1b8a8e","url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573619169","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065571985/job/101573619169","details_url":"https://github.com/dean0x/mdscript/actions/runs/34065571985/job/101573619169","status":"completed","conclusion":"success","started_at":"2026-09-06T22:59:26Z","completed_at":"2026-09-06T23:00:11Z","output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/dean0x/mdscript/check-runs/101573619169/annotations"},"check_suite":{"id":92290754013},"app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2026-06-18T16:17:48Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","code_quality":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","drives":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]},"pull_requests":[]}]} \ No newline at end of file diff --git a/scripts/__test__/fixtures/protection-main-2026-09.json b/scripts/__test__/fixtures/protection-main-2026-09.json new file mode 100644 index 00000000..e933af3f --- /dev/null +++ b/scripts/__test__/fixtures/protection-main-2026-09.json @@ -0,0 +1,86 @@ +{ + "required_status_checks": { + "checks": [ + { + "app_id": 15368, + "context": "Rust — fmt, clippy, test" + }, + { + "app_id": 15368, + "context": "MSRV (Rust 1.88)" + }, + { + "app_id": 15368, + "context": "WASM — build & test" + }, + { + "app_id": 15368, + "context": "JS packages — build & test (ubuntu-latest)" + }, + { + "app_id": 15368, + "context": "JS packages — build & test (macos-latest)" + }, + { + "app_id": 15368, + "context": "JS packages — build & test (windows-latest)" + }, + { + "app_id": 15368, + "context": "Source hygiene" + }, + { + "app_id": 15368, + "context": "examples/ gitignore coverage" + }, + { + "app_id": 15368, + "context": "Python — build & test (ubuntu-latest, 3.11)" + }, + { + "app_id": 15368, + "context": "Python — build & test (ubuntu-latest, 3.13)" + }, + { + "app_id": 15368, + "context": "Python — build & test (macos-latest, 3.11)" + }, + { + "app_id": 15368, + "context": "Python — build & test (macos-latest, 3.13)" + }, + { + "app_id": 15368, + "context": "Python — build & test (windows-latest, 3.11)" + }, + { + "app_id": 15368, + "context": "Python — build & test (windows-latest, 3.13)" + }, + { + "app_id": 15368, + "context": "Python — wheel install smoke" + } + ], + "contexts": [ + "Rust — fmt, clippy, test", + "MSRV (Rust 1.88)", + "WASM — build & test", + "JS packages — build & test (ubuntu-latest)", + "JS packages — build & test (macos-latest)", + "JS packages — build & test (windows-latest)", + "Source hygiene", + "examples/ gitignore coverage", + "Python — build & test (ubuntu-latest, 3.11)", + "Python — build & test (ubuntu-latest, 3.13)", + "Python — build & test (macos-latest, 3.11)", + "Python — build & test (macos-latest, 3.13)", + "Python — build & test (windows-latest, 3.11)", + "Python — build & test (windows-latest, 3.13)", + "Python — wheel install smoke" + ], + "contexts_url": "https://api.github.com/repos/dean0x/mdscript/branches/main/protection/required_status_checks/contexts", + "strict": true, + "url": "https://api.github.com/repos/dean0x/mdscript/branches/main/protection/required_status_checks" + } +} diff --git a/scripts/__test__/fixtures/runs-pr366-e02bcf2.json b/scripts/__test__/fixtures/runs-pr366-e02bcf2.json new file mode 100644 index 00000000..3dd049d9 --- /dev/null +++ b/scripts/__test__/fixtures/runs-pr366-e02bcf2.json @@ -0,0 +1 @@ +{"total_count":3,"workflow_runs":[{"id":34065573775,"name":"Release","node_id":"WFR_kwLOSZrySs8AAAAH7ndnjw","head_branch":"ci/release-surface-gate","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","path":".github/workflows/release.yml","display_title":"ci(release): run release.yml on release-surface PRs; rehearse the PyPI publish; add opt-in TestPyPI leg (#342, #350)","run_number":45,"event":"pull_request","status":"completed","conclusion":"success","workflow_id":285793765,"check_suite_id":92290758559,"check_suite_node_id":"CS_kwDOSZrySs8AAAAVfPU3nw","url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065573775","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573775","pull_requests":[],"created_at":"2026-09-06T22:59:25Z","updated_at":"2026-09-06T23:02:42Z","actor":{"login":"dean0x","id":19309140,"node_id":"MDQ6VXNlcjE5MzA5MTQw","avatar_url":"https://avatars.githubusercontent.com/u/19309140?v=4","gravatar_id":"","url":"https://api.github.com/users/dean0x","html_url":"https://github.com/dean0x","followers_url":"https://api.github.com/users/dean0x/followers","following_url":"https://api.github.com/users/dean0x/following{/other_user}","gists_url":"https://api.github.com/users/dean0x/gists{/gist_id}","starred_url":"https://api.github.com/users/dean0x/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dean0x/subscriptions","organizations_url":"https://api.github.com/users/dean0x/orgs","repos_url":"https://api.github.com/users/dean0x/repos","events_url":"https://api.github.com/users/dean0x/events{/privacy}","received_events_url":"https://api.github.com/users/dean0x/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-09-06T22:59:25Z","triggering_actor":{"login":"dean0x","id":19309140,"node_id":"MDQ6VXNlcjE5MzA5MTQw","avatar_url":"https://avatars.githubusercontent.com/u/19309140?v=4","gravatar_id":"","url":"https://api.github.com/users/dean0x","html_url":"https://github.com/dean0x","followers_url":"https://api.github.com/users/dean0x/followers","following_url":"https://api.github.com/users/dean0x/following{/other_user}","gists_url":"https://api.github.com/users/dean0x/gists{/gist_id}","starred_url":"https://api.github.com/users/dean0x/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dean0x/subscriptions","organizations_url":"https://api.github.com/users/dean0x/orgs","repos_url":"https://api.github.com/users/dean0x/repos","events_url":"https://api.github.com/users/dean0x/events{/privacy}","received_events_url":"https://api.github.com/users/dean0x/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065573775/jobs","logs_url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065573775/logs","check_suite_url":"https://api.github.com/repos/dean0x/mdscript/check-suites/92290758559","artifacts_url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065573775/artifacts","cancel_url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065573775/cancel","rerun_url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065573775/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/dean0x/mdscript/actions/workflows/285793765","head_commit":{"id":"e02bcf280dc50bb8df032744aa2a2520c02865ee","tree_id":"0e74628fdd171bd757281b8d2ac6c32b3b0a8f1b","message":"ci(release): print the image's twine version in the PyPI rehearsal; reword a comment that mimicked a uses: line (#350)","timestamp":"2026-09-06T22:59:15Z","author":{"name":"Dean Sharon","email":"deanshrn@gmail.com"},"committer":{"name":"Dean Sharon","email":"deanshrn@gmail.com"}},"repository":{"id":1234891338,"node_id":"R_kgDOSZrySg","name":"mdscript","full_name":"dean0x/mdscript","private":false,"owner":{"login":"dean0x","id":19309140,"node_id":"MDQ6VXNlcjE5MzA5MTQw","avatar_url":"https://avatars.githubusercontent.com/u/19309140?v=4","gravatar_id":"","url":"https://api.github.com/users/dean0x","html_url":"https://github.com/dean0x","followers_url":"https://api.github.com/users/dean0x/followers","following_url":"https://api.github.com/users/dean0x/following{/other_user}","gists_url":"https://api.github.com/users/dean0x/gists{/gist_id}","starred_url":"https://api.github.com/users/dean0x/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dean0x/subscriptions","organizations_url":"https://api.github.com/users/dean0x/orgs","repos_url":"https://api.github.com/users/dean0x/repos","events_url":"https://api.github.com/users/dean0x/events{/privacy}","received_events_url":"https://api.github.com/users/dean0x/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/dean0x/mdscript","description":"A template language for composable LLM prompt engineering. Write prompts with variables, loops, conditionals, functions, and imports, then compile to clean Markdown.","fork":false,"url":"https://api.github.com/repos/dean0x/mdscript","forks_url":"https://api.github.com/repos/dean0x/mdscript/forks","keys_url":"https://api.github.com/repos/dean0x/mdscript/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dean0x/mdscript/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dean0x/mdscript/teams","hooks_url":"https://api.github.com/repos/dean0x/mdscript/hooks","issue_events_url":"https://api.github.com/repos/dean0x/mdscript/issues/events{/number}","events_url":"https://api.github.com/repos/dean0x/mdscript/events","assignees_url":"https://api.github.com/repos/dean0x/mdscript/assignees{/user}","branches_url":"https://api.github.com/repos/dean0x/mdscript/branches{/branch}","tags_url":"https://api.github.com/repos/dean0x/mdscript/tags","blobs_url":"https://api.github.com/repos/dean0x/mdscript/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dean0x/mdscript/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dean0x/mdscript/git/refs{/sha}","trees_url":"https://api.github.com/repos/dean0x/mdscript/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dean0x/mdscript/statuses/{sha}","languages_url":"https://api.github.com/repos/dean0x/mdscript/languages","stargazers_url":"https://api.github.com/repos/dean0x/mdscript/stargazers","contributors_url":"https://api.github.com/repos/dean0x/mdscript/contributors","subscribers_url":"https://api.github.com/repos/dean0x/mdscript/subscribers","subscription_url":"https://api.github.com/repos/dean0x/mdscript/subscription","commits_url":"https://api.github.com/repos/dean0x/mdscript/commits{/sha}","git_commits_url":"https://api.github.com/repos/dean0x/mdscript/git/commits{/sha}","comments_url":"https://api.github.com/repos/dean0x/mdscript/comments{/number}","issue_comment_url":"https://api.github.com/repos/dean0x/mdscript/issues/comments{/number}","contents_url":"https://api.github.com/repos/dean0x/mdscript/contents/{+path}","compare_url":"https://api.github.com/repos/dean0x/mdscript/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dean0x/mdscript/merges","archive_url":"https://api.github.com/repos/dean0x/mdscript/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dean0x/mdscript/downloads","issues_url":"https://api.github.com/repos/dean0x/mdscript/issues{/number}","pulls_url":"https://api.github.com/repos/dean0x/mdscript/pulls{/number}","milestones_url":"https://api.github.com/repos/dean0x/mdscript/milestones{/number}","notifications_url":"https://api.github.com/repos/dean0x/mdscript/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dean0x/mdscript/labels{/name}","releases_url":"https://api.github.com/repos/dean0x/mdscript/releases{/id}","deployments_url":"https://api.github.com/repos/dean0x/mdscript/deployments"},"head_repository":{"id":1234891338,"node_id":"R_kgDOSZrySg","name":"mdscript","full_name":"dean0x/mdscript","private":false,"owner":{"login":"dean0x","id":19309140,"node_id":"MDQ6VXNlcjE5MzA5MTQw","avatar_url":"https://avatars.githubusercontent.com/u/19309140?v=4","gravatar_id":"","url":"https://api.github.com/users/dean0x","html_url":"https://github.com/dean0x","followers_url":"https://api.github.com/users/dean0x/followers","following_url":"https://api.github.com/users/dean0x/following{/other_user}","gists_url":"https://api.github.com/users/dean0x/gists{/gist_id}","starred_url":"https://api.github.com/users/dean0x/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dean0x/subscriptions","organizations_url":"https://api.github.com/users/dean0x/orgs","repos_url":"https://api.github.com/users/dean0x/repos","events_url":"https://api.github.com/users/dean0x/events{/privacy}","received_events_url":"https://api.github.com/users/dean0x/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/dean0x/mdscript","description":"A template language for composable LLM prompt engineering. Write prompts with variables, loops, conditionals, functions, and imports, then compile to clean Markdown.","fork":false,"url":"https://api.github.com/repos/dean0x/mdscript","forks_url":"https://api.github.com/repos/dean0x/mdscript/forks","keys_url":"https://api.github.com/repos/dean0x/mdscript/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dean0x/mdscript/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dean0x/mdscript/teams","hooks_url":"https://api.github.com/repos/dean0x/mdscript/hooks","issue_events_url":"https://api.github.com/repos/dean0x/mdscript/issues/events{/number}","events_url":"https://api.github.com/repos/dean0x/mdscript/events","assignees_url":"https://api.github.com/repos/dean0x/mdscript/assignees{/user}","branches_url":"https://api.github.com/repos/dean0x/mdscript/branches{/branch}","tags_url":"https://api.github.com/repos/dean0x/mdscript/tags","blobs_url":"https://api.github.com/repos/dean0x/mdscript/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dean0x/mdscript/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dean0x/mdscript/git/refs{/sha}","trees_url":"https://api.github.com/repos/dean0x/mdscript/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dean0x/mdscript/statuses/{sha}","languages_url":"https://api.github.com/repos/dean0x/mdscript/languages","stargazers_url":"https://api.github.com/repos/dean0x/mdscript/stargazers","contributors_url":"https://api.github.com/repos/dean0x/mdscript/contributors","subscribers_url":"https://api.github.com/repos/dean0x/mdscript/subscribers","subscription_url":"https://api.github.com/repos/dean0x/mdscript/subscription","commits_url":"https://api.github.com/repos/dean0x/mdscript/commits{/sha}","git_commits_url":"https://api.github.com/repos/dean0x/mdscript/git/commits{/sha}","comments_url":"https://api.github.com/repos/dean0x/mdscript/comments{/number}","issue_comment_url":"https://api.github.com/repos/dean0x/mdscript/issues/comments{/number}","contents_url":"https://api.github.com/repos/dean0x/mdscript/contents/{+path}","compare_url":"https://api.github.com/repos/dean0x/mdscript/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dean0x/mdscript/merges","archive_url":"https://api.github.com/repos/dean0x/mdscript/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dean0x/mdscript/downloads","issues_url":"https://api.github.com/repos/dean0x/mdscript/issues{/number}","pulls_url":"https://api.github.com/repos/dean0x/mdscript/pulls{/number}","milestones_url":"https://api.github.com/repos/dean0x/mdscript/milestones{/number}","notifications_url":"https://api.github.com/repos/dean0x/mdscript/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dean0x/mdscript/labels{/name}","releases_url":"https://api.github.com/repos/dean0x/mdscript/releases{/id}","deployments_url":"https://api.github.com/repos/dean0x/mdscript/deployments"}},{"id":34065573768,"name":"CI","node_id":"WFR_kwLOSZrySs8AAAAH7ndniA","head_branch":"ci/release-surface-gate","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","path":".github/workflows/ci.yml","display_title":"ci(release): run release.yml on release-surface PRs; rehearse the PyPI publish; add opt-in TestPyPI leg (#342, #350)","run_number":598,"event":"pull_request","status":"completed","conclusion":"success","workflow_id":285784712,"check_suite_id":92290758550,"check_suite_node_id":"CS_kwDOSZrySs8AAAAVfPU3lg","url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065573768","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065573768","pull_requests":[],"created_at":"2026-09-06T22:59:25Z","updated_at":"2026-09-06T23:05:30Z","actor":{"login":"dean0x","id":19309140,"node_id":"MDQ6VXNlcjE5MzA5MTQw","avatar_url":"https://avatars.githubusercontent.com/u/19309140?v=4","gravatar_id":"","url":"https://api.github.com/users/dean0x","html_url":"https://github.com/dean0x","followers_url":"https://api.github.com/users/dean0x/followers","following_url":"https://api.github.com/users/dean0x/following{/other_user}","gists_url":"https://api.github.com/users/dean0x/gists{/gist_id}","starred_url":"https://api.github.com/users/dean0x/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dean0x/subscriptions","organizations_url":"https://api.github.com/users/dean0x/orgs","repos_url":"https://api.github.com/users/dean0x/repos","events_url":"https://api.github.com/users/dean0x/events{/privacy}","received_events_url":"https://api.github.com/users/dean0x/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-09-06T22:59:25Z","triggering_actor":{"login":"dean0x","id":19309140,"node_id":"MDQ6VXNlcjE5MzA5MTQw","avatar_url":"https://avatars.githubusercontent.com/u/19309140?v=4","gravatar_id":"","url":"https://api.github.com/users/dean0x","html_url":"https://github.com/dean0x","followers_url":"https://api.github.com/users/dean0x/followers","following_url":"https://api.github.com/users/dean0x/following{/other_user}","gists_url":"https://api.github.com/users/dean0x/gists{/gist_id}","starred_url":"https://api.github.com/users/dean0x/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dean0x/subscriptions","organizations_url":"https://api.github.com/users/dean0x/orgs","repos_url":"https://api.github.com/users/dean0x/repos","events_url":"https://api.github.com/users/dean0x/events{/privacy}","received_events_url":"https://api.github.com/users/dean0x/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065573768/jobs","logs_url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065573768/logs","check_suite_url":"https://api.github.com/repos/dean0x/mdscript/check-suites/92290758550","artifacts_url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065573768/artifacts","cancel_url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065573768/cancel","rerun_url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065573768/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/dean0x/mdscript/actions/workflows/285784712","head_commit":{"id":"e02bcf280dc50bb8df032744aa2a2520c02865ee","tree_id":"0e74628fdd171bd757281b8d2ac6c32b3b0a8f1b","message":"ci(release): print the image's twine version in the PyPI rehearsal; reword a comment that mimicked a uses: line (#350)","timestamp":"2026-09-06T22:59:15Z","author":{"name":"Dean Sharon","email":"deanshrn@gmail.com"},"committer":{"name":"Dean Sharon","email":"deanshrn@gmail.com"}},"repository":{"id":1234891338,"node_id":"R_kgDOSZrySg","name":"mdscript","full_name":"dean0x/mdscript","private":false,"owner":{"login":"dean0x","id":19309140,"node_id":"MDQ6VXNlcjE5MzA5MTQw","avatar_url":"https://avatars.githubusercontent.com/u/19309140?v=4","gravatar_id":"","url":"https://api.github.com/users/dean0x","html_url":"https://github.com/dean0x","followers_url":"https://api.github.com/users/dean0x/followers","following_url":"https://api.github.com/users/dean0x/following{/other_user}","gists_url":"https://api.github.com/users/dean0x/gists{/gist_id}","starred_url":"https://api.github.com/users/dean0x/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dean0x/subscriptions","organizations_url":"https://api.github.com/users/dean0x/orgs","repos_url":"https://api.github.com/users/dean0x/repos","events_url":"https://api.github.com/users/dean0x/events{/privacy}","received_events_url":"https://api.github.com/users/dean0x/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/dean0x/mdscript","description":"A template language for composable LLM prompt engineering. Write prompts with variables, loops, conditionals, functions, and imports, then compile to clean Markdown.","fork":false,"url":"https://api.github.com/repos/dean0x/mdscript","forks_url":"https://api.github.com/repos/dean0x/mdscript/forks","keys_url":"https://api.github.com/repos/dean0x/mdscript/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dean0x/mdscript/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dean0x/mdscript/teams","hooks_url":"https://api.github.com/repos/dean0x/mdscript/hooks","issue_events_url":"https://api.github.com/repos/dean0x/mdscript/issues/events{/number}","events_url":"https://api.github.com/repos/dean0x/mdscript/events","assignees_url":"https://api.github.com/repos/dean0x/mdscript/assignees{/user}","branches_url":"https://api.github.com/repos/dean0x/mdscript/branches{/branch}","tags_url":"https://api.github.com/repos/dean0x/mdscript/tags","blobs_url":"https://api.github.com/repos/dean0x/mdscript/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dean0x/mdscript/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dean0x/mdscript/git/refs{/sha}","trees_url":"https://api.github.com/repos/dean0x/mdscript/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dean0x/mdscript/statuses/{sha}","languages_url":"https://api.github.com/repos/dean0x/mdscript/languages","stargazers_url":"https://api.github.com/repos/dean0x/mdscript/stargazers","contributors_url":"https://api.github.com/repos/dean0x/mdscript/contributors","subscribers_url":"https://api.github.com/repos/dean0x/mdscript/subscribers","subscription_url":"https://api.github.com/repos/dean0x/mdscript/subscription","commits_url":"https://api.github.com/repos/dean0x/mdscript/commits{/sha}","git_commits_url":"https://api.github.com/repos/dean0x/mdscript/git/commits{/sha}","comments_url":"https://api.github.com/repos/dean0x/mdscript/comments{/number}","issue_comment_url":"https://api.github.com/repos/dean0x/mdscript/issues/comments{/number}","contents_url":"https://api.github.com/repos/dean0x/mdscript/contents/{+path}","compare_url":"https://api.github.com/repos/dean0x/mdscript/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dean0x/mdscript/merges","archive_url":"https://api.github.com/repos/dean0x/mdscript/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dean0x/mdscript/downloads","issues_url":"https://api.github.com/repos/dean0x/mdscript/issues{/number}","pulls_url":"https://api.github.com/repos/dean0x/mdscript/pulls{/number}","milestones_url":"https://api.github.com/repos/dean0x/mdscript/milestones{/number}","notifications_url":"https://api.github.com/repos/dean0x/mdscript/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dean0x/mdscript/labels{/name}","releases_url":"https://api.github.com/repos/dean0x/mdscript/releases{/id}","deployments_url":"https://api.github.com/repos/dean0x/mdscript/deployments"},"head_repository":{"id":1234891338,"node_id":"R_kgDOSZrySg","name":"mdscript","full_name":"dean0x/mdscript","private":false,"owner":{"login":"dean0x","id":19309140,"node_id":"MDQ6VXNlcjE5MzA5MTQw","avatar_url":"https://avatars.githubusercontent.com/u/19309140?v=4","gravatar_id":"","url":"https://api.github.com/users/dean0x","html_url":"https://github.com/dean0x","followers_url":"https://api.github.com/users/dean0x/followers","following_url":"https://api.github.com/users/dean0x/following{/other_user}","gists_url":"https://api.github.com/users/dean0x/gists{/gist_id}","starred_url":"https://api.github.com/users/dean0x/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dean0x/subscriptions","organizations_url":"https://api.github.com/users/dean0x/orgs","repos_url":"https://api.github.com/users/dean0x/repos","events_url":"https://api.github.com/users/dean0x/events{/privacy}","received_events_url":"https://api.github.com/users/dean0x/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/dean0x/mdscript","description":"A template language for composable LLM prompt engineering. Write prompts with variables, loops, conditionals, functions, and imports, then compile to clean Markdown.","fork":false,"url":"https://api.github.com/repos/dean0x/mdscript","forks_url":"https://api.github.com/repos/dean0x/mdscript/forks","keys_url":"https://api.github.com/repos/dean0x/mdscript/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dean0x/mdscript/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dean0x/mdscript/teams","hooks_url":"https://api.github.com/repos/dean0x/mdscript/hooks","issue_events_url":"https://api.github.com/repos/dean0x/mdscript/issues/events{/number}","events_url":"https://api.github.com/repos/dean0x/mdscript/events","assignees_url":"https://api.github.com/repos/dean0x/mdscript/assignees{/user}","branches_url":"https://api.github.com/repos/dean0x/mdscript/branches{/branch}","tags_url":"https://api.github.com/repos/dean0x/mdscript/tags","blobs_url":"https://api.github.com/repos/dean0x/mdscript/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dean0x/mdscript/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dean0x/mdscript/git/refs{/sha}","trees_url":"https://api.github.com/repos/dean0x/mdscript/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dean0x/mdscript/statuses/{sha}","languages_url":"https://api.github.com/repos/dean0x/mdscript/languages","stargazers_url":"https://api.github.com/repos/dean0x/mdscript/stargazers","contributors_url":"https://api.github.com/repos/dean0x/mdscript/contributors","subscribers_url":"https://api.github.com/repos/dean0x/mdscript/subscribers","subscription_url":"https://api.github.com/repos/dean0x/mdscript/subscription","commits_url":"https://api.github.com/repos/dean0x/mdscript/commits{/sha}","git_commits_url":"https://api.github.com/repos/dean0x/mdscript/git/commits{/sha}","comments_url":"https://api.github.com/repos/dean0x/mdscript/comments{/number}","issue_comment_url":"https://api.github.com/repos/dean0x/mdscript/issues/comments{/number}","contents_url":"https://api.github.com/repos/dean0x/mdscript/contents/{+path}","compare_url":"https://api.github.com/repos/dean0x/mdscript/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dean0x/mdscript/merges","archive_url":"https://api.github.com/repos/dean0x/mdscript/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dean0x/mdscript/downloads","issues_url":"https://api.github.com/repos/dean0x/mdscript/issues{/number}","pulls_url":"https://api.github.com/repos/dean0x/mdscript/pulls{/number}","milestones_url":"https://api.github.com/repos/dean0x/mdscript/milestones{/number}","notifications_url":"https://api.github.com/repos/dean0x/mdscript/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dean0x/mdscript/labels{/name}","releases_url":"https://api.github.com/repos/dean0x/mdscript/releases{/id}","deployments_url":"https://api.github.com/repos/dean0x/mdscript/deployments"}},{"id":34065571985,"name":"PR #366","node_id":"WFR_kwLOSZrySs8AAAAH7ndgkQ","head_branch":"refs/pull/366/head","head_sha":"e02bcf280dc50bb8df032744aa2a2520c02865ee","path":"dynamic/github-code-scanning/codeql","display_title":"PR #366","run_number":266,"event":"dynamic","status":"completed","conclusion":"success","workflow_id":286427590,"check_suite_id":92290754013,"check_suite_node_id":"CS_kwDOSZrySs8AAAAVfPUl3Q","url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065571985","html_url":"https://github.com/dean0x/mdscript/actions/runs/34065571985","pull_requests":[],"created_at":"2026-09-06T22:59:22Z","updated_at":"2026-09-06T23:02:55Z","actor":{"login":"dean0x","id":19309140,"node_id":"MDQ6VXNlcjE5MzA5MTQw","avatar_url":"https://avatars.githubusercontent.com/u/19309140?v=4","gravatar_id":"","url":"https://api.github.com/users/dean0x","html_url":"https://github.com/dean0x","followers_url":"https://api.github.com/users/dean0x/followers","following_url":"https://api.github.com/users/dean0x/following{/other_user}","gists_url":"https://api.github.com/users/dean0x/gists{/gist_id}","starred_url":"https://api.github.com/users/dean0x/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dean0x/subscriptions","organizations_url":"https://api.github.com/users/dean0x/orgs","repos_url":"https://api.github.com/users/dean0x/repos","events_url":"https://api.github.com/users/dean0x/events{/privacy}","received_events_url":"https://api.github.com/users/dean0x/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-09-06T22:59:22Z","triggering_actor":{"login":"dean0x","id":19309140,"node_id":"MDQ6VXNlcjE5MzA5MTQw","avatar_url":"https://avatars.githubusercontent.com/u/19309140?v=4","gravatar_id":"","url":"https://api.github.com/users/dean0x","html_url":"https://github.com/dean0x","followers_url":"https://api.github.com/users/dean0x/followers","following_url":"https://api.github.com/users/dean0x/following{/other_user}","gists_url":"https://api.github.com/users/dean0x/gists{/gist_id}","starred_url":"https://api.github.com/users/dean0x/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dean0x/subscriptions","organizations_url":"https://api.github.com/users/dean0x/orgs","repos_url":"https://api.github.com/users/dean0x/repos","events_url":"https://api.github.com/users/dean0x/events{/privacy}","received_events_url":"https://api.github.com/users/dean0x/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065571985/jobs","logs_url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065571985/logs","check_suite_url":"https://api.github.com/repos/dean0x/mdscript/check-suites/92290754013","artifacts_url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065571985/artifacts","cancel_url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065571985/cancel","rerun_url":"https://api.github.com/repos/dean0x/mdscript/actions/runs/34065571985/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/dean0x/mdscript/actions/workflows/286427590","head_commit":{"id":"e02bcf280dc50bb8df032744aa2a2520c02865ee","tree_id":"0e74628fdd171bd757281b8d2ac6c32b3b0a8f1b","message":"ci(release): print the image's twine version in the PyPI rehearsal; reword a comment that mimicked a uses: line (#350)","timestamp":"2026-09-06T22:59:15Z","author":{"name":"Dean Sharon","email":"deanshrn@gmail.com"},"committer":{"name":"Dean Sharon","email":"deanshrn@gmail.com"}},"repository":{"id":1234891338,"node_id":"R_kgDOSZrySg","name":"mdscript","full_name":"dean0x/mdscript","private":false,"owner":{"login":"dean0x","id":19309140,"node_id":"MDQ6VXNlcjE5MzA5MTQw","avatar_url":"https://avatars.githubusercontent.com/u/19309140?v=4","gravatar_id":"","url":"https://api.github.com/users/dean0x","html_url":"https://github.com/dean0x","followers_url":"https://api.github.com/users/dean0x/followers","following_url":"https://api.github.com/users/dean0x/following{/other_user}","gists_url":"https://api.github.com/users/dean0x/gists{/gist_id}","starred_url":"https://api.github.com/users/dean0x/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dean0x/subscriptions","organizations_url":"https://api.github.com/users/dean0x/orgs","repos_url":"https://api.github.com/users/dean0x/repos","events_url":"https://api.github.com/users/dean0x/events{/privacy}","received_events_url":"https://api.github.com/users/dean0x/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/dean0x/mdscript","description":"A template language for composable LLM prompt engineering. Write prompts with variables, loops, conditionals, functions, and imports, then compile to clean Markdown.","fork":false,"url":"https://api.github.com/repos/dean0x/mdscript","forks_url":"https://api.github.com/repos/dean0x/mdscript/forks","keys_url":"https://api.github.com/repos/dean0x/mdscript/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dean0x/mdscript/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dean0x/mdscript/teams","hooks_url":"https://api.github.com/repos/dean0x/mdscript/hooks","issue_events_url":"https://api.github.com/repos/dean0x/mdscript/issues/events{/number}","events_url":"https://api.github.com/repos/dean0x/mdscript/events","assignees_url":"https://api.github.com/repos/dean0x/mdscript/assignees{/user}","branches_url":"https://api.github.com/repos/dean0x/mdscript/branches{/branch}","tags_url":"https://api.github.com/repos/dean0x/mdscript/tags","blobs_url":"https://api.github.com/repos/dean0x/mdscript/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dean0x/mdscript/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dean0x/mdscript/git/refs{/sha}","trees_url":"https://api.github.com/repos/dean0x/mdscript/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dean0x/mdscript/statuses/{sha}","languages_url":"https://api.github.com/repos/dean0x/mdscript/languages","stargazers_url":"https://api.github.com/repos/dean0x/mdscript/stargazers","contributors_url":"https://api.github.com/repos/dean0x/mdscript/contributors","subscribers_url":"https://api.github.com/repos/dean0x/mdscript/subscribers","subscription_url":"https://api.github.com/repos/dean0x/mdscript/subscription","commits_url":"https://api.github.com/repos/dean0x/mdscript/commits{/sha}","git_commits_url":"https://api.github.com/repos/dean0x/mdscript/git/commits{/sha}","comments_url":"https://api.github.com/repos/dean0x/mdscript/comments{/number}","issue_comment_url":"https://api.github.com/repos/dean0x/mdscript/issues/comments{/number}","contents_url":"https://api.github.com/repos/dean0x/mdscript/contents/{+path}","compare_url":"https://api.github.com/repos/dean0x/mdscript/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dean0x/mdscript/merges","archive_url":"https://api.github.com/repos/dean0x/mdscript/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dean0x/mdscript/downloads","issues_url":"https://api.github.com/repos/dean0x/mdscript/issues{/number}","pulls_url":"https://api.github.com/repos/dean0x/mdscript/pulls{/number}","milestones_url":"https://api.github.com/repos/dean0x/mdscript/milestones{/number}","notifications_url":"https://api.github.com/repos/dean0x/mdscript/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dean0x/mdscript/labels{/name}","releases_url":"https://api.github.com/repos/dean0x/mdscript/releases{/id}","deployments_url":"https://api.github.com/repos/dean0x/mdscript/deployments"},"head_repository":{"id":1234891338,"node_id":"R_kgDOSZrySg","name":"mdscript","full_name":"dean0x/mdscript","private":false,"owner":{"login":"dean0x","id":19309140,"node_id":"MDQ6VXNlcjE5MzA5MTQw","avatar_url":"https://avatars.githubusercontent.com/u/19309140?v=4","gravatar_id":"","url":"https://api.github.com/users/dean0x","html_url":"https://github.com/dean0x","followers_url":"https://api.github.com/users/dean0x/followers","following_url":"https://api.github.com/users/dean0x/following{/other_user}","gists_url":"https://api.github.com/users/dean0x/gists{/gist_id}","starred_url":"https://api.github.com/users/dean0x/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dean0x/subscriptions","organizations_url":"https://api.github.com/users/dean0x/orgs","repos_url":"https://api.github.com/users/dean0x/repos","events_url":"https://api.github.com/users/dean0x/events{/privacy}","received_events_url":"https://api.github.com/users/dean0x/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/dean0x/mdscript","description":"A template language for composable LLM prompt engineering. Write prompts with variables, loops, conditionals, functions, and imports, then compile to clean Markdown.","fork":false,"url":"https://api.github.com/repos/dean0x/mdscript","forks_url":"https://api.github.com/repos/dean0x/mdscript/forks","keys_url":"https://api.github.com/repos/dean0x/mdscript/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dean0x/mdscript/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dean0x/mdscript/teams","hooks_url":"https://api.github.com/repos/dean0x/mdscript/hooks","issue_events_url":"https://api.github.com/repos/dean0x/mdscript/issues/events{/number}","events_url":"https://api.github.com/repos/dean0x/mdscript/events","assignees_url":"https://api.github.com/repos/dean0x/mdscript/assignees{/user}","branches_url":"https://api.github.com/repos/dean0x/mdscript/branches{/branch}","tags_url":"https://api.github.com/repos/dean0x/mdscript/tags","blobs_url":"https://api.github.com/repos/dean0x/mdscript/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dean0x/mdscript/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dean0x/mdscript/git/refs{/sha}","trees_url":"https://api.github.com/repos/dean0x/mdscript/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dean0x/mdscript/statuses/{sha}","languages_url":"https://api.github.com/repos/dean0x/mdscript/languages","stargazers_url":"https://api.github.com/repos/dean0x/mdscript/stargazers","contributors_url":"https://api.github.com/repos/dean0x/mdscript/contributors","subscribers_url":"https://api.github.com/repos/dean0x/mdscript/subscribers","subscription_url":"https://api.github.com/repos/dean0x/mdscript/subscription","commits_url":"https://api.github.com/repos/dean0x/mdscript/commits{/sha}","git_commits_url":"https://api.github.com/repos/dean0x/mdscript/git/commits{/sha}","comments_url":"https://api.github.com/repos/dean0x/mdscript/comments{/number}","issue_comment_url":"https://api.github.com/repos/dean0x/mdscript/issues/comments{/number}","contents_url":"https://api.github.com/repos/dean0x/mdscript/contents/{+path}","compare_url":"https://api.github.com/repos/dean0x/mdscript/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dean0x/mdscript/merges","archive_url":"https://api.github.com/repos/dean0x/mdscript/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dean0x/mdscript/downloads","issues_url":"https://api.github.com/repos/dean0x/mdscript/issues{/number}","pulls_url":"https://api.github.com/repos/dean0x/mdscript/pulls{/number}","milestones_url":"https://api.github.com/repos/dean0x/mdscript/milestones{/number}","notifications_url":"https://api.github.com/repos/dean0x/mdscript/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dean0x/mdscript/labels{/name}","releases_url":"https://api.github.com/repos/dean0x/mdscript/releases{/id}","deployments_url":"https://api.github.com/repos/dean0x/mdscript/deployments"}}]} \ No newline at end of file diff --git a/scripts/__test__/verify-pr-checks.spec.mjs b/scripts/__test__/verify-pr-checks.spec.mjs index ff9a2c47..ce08f0b5 100644 --- a/scripts/__test__/verify-pr-checks.spec.mjs +++ b/scripts/__test__/verify-pr-checks.spec.mjs @@ -24,12 +24,15 @@ import { fetchRequiredContexts, fetchStatuses, fetchCheckRuns, + fetchWorkflowRuns, parseGhStderrHttpStatus, EXPECTED_CONTEXTS, TIER_B_EXPECTED_SKIPPED, RELEASE_SURFACE, RELEASE_SURFACE_CONTEXTS, + RELEASE_WORKFLOW_PATH, matchesReleaseSurface, + releaseSuiteIdsFrom, } from '../verify-pr-checks.mjs'; const ROOT = resolve(fileURLToPath(import.meta.url), '../../..'); @@ -60,12 +63,38 @@ const REQUIRED = loadProtection(); // "JS packages — build & test (ubuntu-latest)", // "JS packages — build & test (macos-latest)", // "JS packages — build & test (windows-latest)" -assert.equal(REQUIRED.length, 6, 'fixture must have 6 required contexts'); +assert.equal(REQUIRED.length, 6, 'historical fixture (2026-08) must have 6 required contexts'); const HEAD_113F472 = '113f472684d6ee7e398d54c1aadc22b2ad747ae1'; const HEAD_F168944 = 'f168944'; // PR #239 const HEAD_E9DACE1 = 'e9dace1'; // PR #240 +// D-PR8: release.yml check-suite identity constants. +// RELEASE_SUITE: the release.yml pull_request suite for PR #366 head e02bcf2. +// CI_SUITE_113F472: the ci.yml suite in the 2026-08 113f472 fixture (a NON-release suite). +const RELEASE_SUITE = 92290758559; +const CI_SUITE_113F472 = 84976779019; +const RELEASE_SUITES = new Set([RELEASE_SUITE]); + +/** Returns a copy of run with check_suite: { id } injected (defaults to RELEASE_SUITE). */ +function withSuite(run, id = RELEASE_SUITE) { + return { ...run, check_suite: { id } }; +} + +// Minimal runs stubs for the fetchWorkflowRuns call added to main() (D-PR8). +// Every main() stub test that reaches evaluateChecks needs one of these routes. +const RUNS_NONE = { total_count: 0, workflow_runs: [] }; +const RUNS_RELEASE = { + total_count: 1, + workflow_runs: [{ + id: 34065573775, + path: '.github/workflows/release.yml', + event: 'pull_request', + check_suite_id: RELEASE_SUITE, + conclusion: 'success', + }], +}; + // A synthetic Source hygiene run (D-PR3b). The 113f472 fixture predates the // source-hygiene job (#288); tests that verify a PASSING run today must inject one. const SOURCE_HYGIENE_PASS = { name: 'Source hygiene', status: 'completed', conclusion: 'success' }; @@ -504,6 +533,7 @@ describe('AC-26 AC-28 AC-29: live path exit codes (injected runner)', () => { ['/protection', PROTECTION_OK], ['/check-runs', CHECKS_OK_WITH_HYGIENE], ['/status', { statuses: [], total_count: 0 }], + ['/actions/runs', RUNS_NONE], ]); assert.equal(main(['1'], runner, OK_GH_VERSION), 0); }); @@ -595,6 +625,7 @@ describe('AC-26 AC-28 AC-29: live path exit codes (injected runner)', () => { ['/protection', onlyChecks], ['/check-runs', CHECKS_OK_WITH_HYGIENE], ['/status', { statuses: [], total_count: 0 }], + ['/actions/runs', RUNS_NONE], ]); assert.equal(main(['1'], runner, OK_GH_VERSION), 0); @@ -634,13 +665,14 @@ describe('AC-26 AC-28 AC-29: live path exit codes (injected runner)', () => { assert.equal(main(['1'], runner, OK_GH_VERSION), 2); }); - test('AC-30: the live path issues at most page-bound + 3 API calls', () => { + test('AC-30: the live path issues at most page-bound + 3 fixed API calls (6 with single-page stubs)', () => { const calls = []; const runner = stubRunner([ ['/pulls/', PR_OK], ['/protection', PROTECTION_OK], ['/check-runs', CHECKS_OK_WITH_HYGIENE], ['/status', { statuses: [], total_count: 0 }], + ['/actions/runs', RUNS_NONE], ], calls); const start = Date.now(); assert.equal(main(['1'], runner, OK_GH_VERSION), 0); @@ -650,9 +682,10 @@ describe('AC-26 AC-28 AC-29: live path exit codes (injected runner)', () => { // CPU cost and any unexpected loops — network latency is zero. assert.ok(elapsed < 15000, `verifier must complete in < 15 s wall-clock (AC-30 clause b); took ${elapsed}ms`); - // D-PR7: adds one PR-files call (pulls/{n}/files?per_page=100) for the release-surface check. - // The /pulls/ stub also matches the files URL and returns PR_OK (no .files → empty list → ok). - assert.equal(calls.length, 5, `expected 5 API calls (pr, protection, checks, status, files); got ${calls.length}`); + // Fixed calls: pr, protection, checks, status, files, runs = 6. + // Budget: fixed 3 + pages ≤ 20 + 30 + 5 = 58 worst case (was 53). + // The /pulls/ stub matches the files URL (/pulls/1/files) and returns PR_OK (no .files → ok). + assert.equal(calls.length, 6, `expected 6 API calls (pr, protection, checks, status, files, runs); got ${calls.length}`); const checkCall = calls.find(u => u.includes('/check-runs')); assert.ok(checkCall.includes('filter=latest'), 'filter=latest must be pinned explicitly (D-PR4a)'); // D-PR4a parity: combined-status endpoint must request per_page=100 so a context @@ -660,6 +693,11 @@ describe('AC-26 AC-28 AC-29: live path exit codes (injected runner)', () => { const statusCall = calls.find(u => u.includes('/status')); assert.ok(statusCall && statusCall.includes('per_page=100'), `status URL must include per_page=100 (D-PR4a parity); got: ${statusCall}`); + // D-PR8: runs call must include head_sha and per_page=100 + const runsCall = calls.find(u => u.includes('/actions/runs')); + assert.ok(runsCall, 'must have made a /actions/runs call'); + assert.ok(runsCall.includes('head_sha='), 'runs call must include head_sha='); + assert.ok(runsCall.includes('per_page=100'), 'runs call must include per_page=100'); }); test('check-runs API error → exit 2 (indeterminate), not 1', () => { @@ -677,6 +715,7 @@ describe('AC-26 AC-28 AC-29: live path exit codes (injected runner)', () => { ['/protection', PROTECTION_OK], ['/check-runs', { total_count: 0, check_runs: [] }], ['/status', { statuses: [], total_count: 0 }], + ['/actions/runs', RUNS_NONE], ]); assert.equal(main(['1'], runner, OK_GH_VERSION), 1); }); @@ -707,6 +746,7 @@ describe('AC-26 AC-28 AC-29: live path exit codes (injected runner)', () => { ['/protection', PROTECTION_OK], ['/check-runs', CHECKS_OK_WITH_HYGIENE], ['/status', { total_count: 1, statuses: [{ context: 'foo', state: 'success' }] }], + ['/actions/runs', RUNS_NONE], ]); assert.equal(main(['1'], runner, OK_GH_VERSION), 0, 'non-truncated status list must not block a passing run'); @@ -720,6 +760,7 @@ describe('AC-26 AC-28 AC-29: live path exit codes (injected runner)', () => { ['/protection', PROTECTION_OK], ['/check-runs', CHECKS_OK_WITH_HYGIENE], ['/status', { statuses: [] }], // no total_count + ['/actions/runs', RUNS_NONE], ]); assert.equal(main(['1'], runner, OK_GH_VERSION), 0, 'missing total_count must not cause a false exit 2'); @@ -1231,6 +1272,7 @@ describe('complexity-08: argument parsing and merge command', () => { if (url.includes('/protection')) return PROTECTION_OK; if (url.includes('/check-runs')) return CHECKS_OK_WITH_HYGIENE; if (url.includes('/status')) return { statuses: [], total_count: 0 }; + if (url.includes('/actions/runs')) return RUNS_NONE; return { __error: true, httpStatus: 404, stderr: 'no route' }; }; // '123' is the branch name for --required-from; '456' is the PR number. @@ -1280,6 +1322,7 @@ describe('complexity-08: argument parsing and merge command', () => { ['/protection', PROTECTION_OK], ['/check-runs', CHECKS_OK_WITH_HYGIENE], ['/status', { statuses: [], total_count: 0 }], + ['/actions/runs', RUNS_NONE], ]); // Intercept console.log to capture the merge command line. const origLog = console.log; @@ -1443,11 +1486,11 @@ describe('TIER_B_EXPECTED_SKIPPED: release dry-run skipped publish jobs', () => ]; } - test('D-PR5a: all five publish names skipped + required green → PASS and merge command printed', () => { + test('D-PR5a: all five publish names skipped in release suite → PASS; each allowed line names suite id (D-PR8)', () => { // The RELEASING.md dry-run dispatched on a PR branch sees the five publish // jobs as skipped (their refs/tags/v or inputs. guard fires). The verifier must exit 0 - // so the operator can proceed to tag. - const skippedPublishRuns = [...TIER_B_EXPECTED_SKIPPED].map(name => ({ + // so the operator can proceed to tag. D-PR8: each run must be in a release.yml suite. + const skippedPublishRuns = [...TIER_B_EXPECTED_SKIPPED].map(name => withSuite({ name, status: 'completed', conclusion: 'skipped', @@ -1459,18 +1502,20 @@ describe('TIER_B_EXPECTED_SKIPPED: release dry-run skipped publish jobs', () => statuses: [], headSha: HEAD_113F472, prNumber: 338, + releaseSuiteIds: RELEASE_SUITES, }); assert.equal(result.exitCode, 0, - `five publish names skipped must not block PASS; lines:\n${result.lines.join('\n')}`); + `five publish names skipped in release suite must not block PASS; lines:\n${result.lines.join('\n')}`); assert.ok(result.pass, 'must return pass=true'); assert.ok(result.mergeCommand, 'PASS must produce a merge command'); - // Informational lines must be present (one per skipped job) - const allLines = result.lines.join('\n'); + // D-PR8: each allowed line must say 'allowed' and name the suite id for (const name of TIER_B_EXPECTED_SKIPPED) { - assert.ok( - allLines.includes(name), - `output must mention skipped job "${name}"; got:\n${allLines}`, - ); + const line = result.lines.find(l => l.includes(name)); + assert.ok(line, `output must include a line for skipped job "${name}"; got:\n${result.lines.join('\n')}`); + assert.ok(line.includes('allowed'), + `output for "${name}" must say "allowed"; got: ${line}`); + assert.ok(line.includes(String(RELEASE_SUITE)), + `output for "${name}" must name suite id ${RELEASE_SUITE}; got: ${line}`); } }); @@ -1478,13 +1523,14 @@ describe('TIER_B_EXPECTED_SKIPPED: release dry-run skipped publish jobs', () => // A cancelled publish run is not the refs/tags/v guard — it is a real // failure that must block the merge. Only conclusion=skipped is allowed. const runs = basePassingRunsWith([ - { name: 'Publish to npm', status: 'completed', conclusion: 'cancelled' }, + withSuite({ name: 'Publish to npm', status: 'completed', conclusion: 'cancelled' }), ]); const result = evaluateChecks({ requiredContexts: REQUIRED, checkRuns: runs, statuses: [], headSha: HEAD_113F472, + releaseSuiteIds: RELEASE_SUITES, }); assert.equal(result.exitCode, 1, 'cancelled Publish to npm must exit 1 (only skipped is allowed in TIER_B_EXPECTED_SKIPPED)'); @@ -1499,13 +1545,14 @@ describe('TIER_B_EXPECTED_SKIPPED: release dry-run skipped publish jobs', () => // conclusion=skipped would pass when it must not (avoids PF-017). Making // "Publish to npm" required here pins that boundary. const runs = basePassingRunsWith([ - { name: 'Publish to npm', status: 'completed', conclusion: 'skipped' }, + withSuite({ name: 'Publish to npm', status: 'completed', conclusion: 'skipped' }), ]); const result = evaluateChecks({ requiredContexts: [...REQUIRED, 'Publish to npm'], checkRuns: runs, statuses: [], headSha: HEAD_113F472, + releaseSuiteIds: RELEASE_SUITES, }); assert.equal(result.exitCode, 1, 'a required context with conclusion=skipped must fail Tier A even if it is in ' + @@ -1521,13 +1568,14 @@ describe('TIER_B_EXPECTED_SKIPPED: release dry-run skipped publish jobs', () => // The allowance is exactly the five publish job names. Any other job name // that reports skipped must still fail Tier B (security-13: whitelist). const runs = basePassingRunsWith([ - { name: 'Some other job', status: 'completed', conclusion: 'skipped' }, + withSuite({ name: 'Some other job', status: 'completed', conclusion: 'skipped' }), ]); const result = evaluateChecks({ requiredContexts: REQUIRED, checkRuns: runs, statuses: [], headSha: HEAD_113F472, + releaseSuiteIds: RELEASE_SUITES, }); assert.equal(result.exitCode, 1, 'skipped run under an unlisted name must fail (whitelist is exact, security-13)'); @@ -1540,15 +1588,16 @@ describe('TIER_B_EXPECTED_SKIPPED: release dry-run skipped publish jobs', () => // The not-yet-completed guard (status !== 'completed') in Tier B fires before // the TIER_B_EXPECTED_SKIPPED allowance. An in_progress publish run must // still block the merge — widening the allowance to accept status!='completed' - // for the four names would silently break PF-017 (avoids that mutation). + // for the five names would silently break PF-017 (avoids that mutation). const runs = basePassingRunsWith([ - { name: 'Publish to npm', status: 'in_progress', conclusion: null }, + withSuite({ name: 'Publish to npm', status: 'in_progress', conclusion: null }), ]); const result = evaluateChecks({ requiredContexts: REQUIRED, checkRuns: runs, statuses: [], headSha: HEAD_113F472, + releaseSuiteIds: RELEASE_SUITES, }); assert.equal(result.exitCode, 1, 'in_progress publish run must exit 1 (not-yet-completed guard, avoids PF-017)'); @@ -1564,13 +1613,14 @@ describe('TIER_B_EXPECTED_SKIPPED: release dry-run skipped publish jobs', () => // a skipped run (dispatch-input-guarded) does not block the verifier. // But cancelled is NOT skipped — it is a real anomaly and must fail closed. const runs = basePassingRunsWith([ - { name: 'Publish to TestPyPI (rehearsal)', status: 'completed', conclusion: 'cancelled' }, + withSuite({ name: 'Publish to TestPyPI (rehearsal)', status: 'completed', conclusion: 'cancelled' }), ]); const result = evaluateChecks({ requiredContexts: REQUIRED, checkRuns: runs, statuses: [], headSha: HEAD_113F472, + releaseSuiteIds: RELEASE_SUITES, }); assert.equal(result.exitCode, 1, 'cancelled Publish to TestPyPI must exit 1 (only skipped is allowed in TIER_B_EXPECTED_SKIPPED)'); @@ -1601,9 +1651,9 @@ describe('D-PR7: release-surface presence check', () => { ]; } - // Three release-surface jobs, all success. + // Three release-surface jobs, all success, each attributed to RELEASE_SUITE (D-PR8). function releaseSuccessRuns() { - return RELEASE_SURFACE_CONTEXTS.map(name => ({ + return RELEASE_SURFACE_CONTEXTS.map(name => withSuite({ name, status: 'completed', conclusion: 'success', @@ -1618,6 +1668,7 @@ describe('D-PR7: release-surface presence check', () => { statuses: [], headSha: HEAD_113F472, changedFiles: ['.github/workflows/release.yml'], + releaseSuiteIds: RELEASE_SUITES, }); assert.equal(result.exitCode, 0, `touched release surface + all contexts success must exit 0; lines:\n${result.lines.join('\n')}`); @@ -1637,6 +1688,7 @@ describe('D-PR7: release-surface presence check', () => { statuses: [], headSha: HEAD_113F472, changedFiles: ['.github/workflows/release.yml'], + releaseSuiteIds: RELEASE_SUITES, }); assert.equal(result.exitCode, 1, `absent "Version gate" with release surface touched must exit 1; got:\n${result.lines.join('\n')}`); @@ -1657,6 +1709,7 @@ describe('D-PR7: release-surface presence check', () => { statuses: [], headSha: HEAD_113F472, changedFiles: ['.github/workflows/release.yml'], + releaseSuiteIds: RELEASE_SUITES, }); assert.equal(result.exitCode, 1, 'release surface requires success, not skipped; must exit 1'); @@ -1670,7 +1723,7 @@ describe('D-PR7: release-surface presence check', () => { test('D-PR7d: touched + one present run in_progress → exit 1', () => { const withInProgress = releaseSuccessRuns().map(r => r.name === 'Stage + verify platform packages' - ? { name: r.name, status: 'in_progress', conclusion: null } + ? withSuite({ name: r.name, status: 'in_progress', conclusion: null }) : r, ); const checkRuns = passingRunsWith(withInProgress); @@ -1680,6 +1733,7 @@ describe('D-PR7: release-surface presence check', () => { statuses: [], headSha: HEAD_113F472, changedFiles: ['crates/mds-napi/src/lib.rs'], + releaseSuiteIds: RELEASE_SUITES, }); assert.equal(result.exitCode, 1, 'in_progress release job with touched surface must exit 1'); }); @@ -1692,6 +1746,7 @@ describe('D-PR7: release-surface presence check', () => { statuses: [], headSha: HEAD_113F472, changedFiles: ['crates/mds-core/src/lib.rs'], + releaseSuiteIds: RELEASE_SUITES, }); assert.equal(result.exitCode, 0, 'non-release file must not require release check-runs; must exit 0'); @@ -1799,6 +1854,7 @@ describe('D-PR7: release-surface presence check', () => { ['/protection', PROTECTION_OK], ['/check-runs', CHECKS_OK_WITH_HYGIENE], ['/status', { statuses: [], total_count: 0 }], + ['/actions/runs', RUNS_NONE], ]); assert.equal(main(['1'], runner, OK_GH_VERSION), 1, 'a release-surface file with no release check-runs must exit 1'); @@ -1811,7 +1867,7 @@ describe('D-PR7: release-surface presence check', () => { ...CHECKS_OK_WITH_HYGIENE, check_runs: [ ...CHECKS_OK_WITH_HYGIENE.check_runs, - ...RELEASE_SURFACE_CONTEXTS.map(name => ({ + ...RELEASE_SURFACE_CONTEXTS.map(name => withSuite({ name, status: 'completed', conclusion: 'success', })), ], @@ -1823,6 +1879,7 @@ describe('D-PR7: release-surface presence check', () => { ['/protection', PROTECTION_OK], ['/check-runs', withRelease], ['/status', { statuses: [], total_count: 0 }], + ['/actions/runs', RUNS_RELEASE], ]); assert.equal(main(['1'], runner, OK_GH_VERSION), 0, 'a release-surface file WITH all release check-runs must exit 0'); @@ -1837,10 +1894,11 @@ describe('D-PR7: release-surface presence check', () => { // re-run mask a red sibling — a fail-open in a merge gate. // ------------------------------------------------------------------------- test('D-PR7j: a failed and a succeeded run sharing a release-surface name → exit 1 (all must pass)', () => { + // 3a803f5 is the commit cited by the original D-PR7j all-must-pass spec. const checkRuns = passingRunsWith([ ...releaseSuccessRuns(), - // A second run under a name that already has a success above. - { name: 'Version gate', status: 'completed', conclusion: 'failure' }, + // A second run under a name that already has a success above, in the SAME release suite. + withSuite({ name: 'Version gate', status: 'completed', conclusion: 'failure' }), ]); const result = evaluateChecks({ requiredContexts: REQUIRED, @@ -1848,6 +1906,7 @@ describe('D-PR7: release-surface presence check', () => { statuses: [], headSha: HEAD_113F472, changedFiles: ['.github/workflows/release.yml'], + releaseSuiteIds: RELEASE_SUITES, }); assert.equal(result.exitCode, 1, 'a later success must not mask an earlier failure under the same name; must exit 1'); @@ -1878,5 +1937,687 @@ describe('D-PR7: release-surface presence check', () => { }); +// --------------------------------------------------------------------------- +// D-PR8: Suite-keyed skipped-publish allowance — new tests +// --------------------------------------------------------------------------- +describe('D-PR8: suite-keyed TIER_B_EXPECTED_SKIPPED allowance', () => { + + function basePassingRunsWith(extras) { + return [ + ...loadCheckRuns('checks-main-113f472.json'), + { ...SOURCE_HYGIENE_PASS }, + ...extras, + ]; + } + + test('D-PR5g: allow-listed name skipped in NON-release suite (CI_SUITE_113F472) → exit 1; message names both suite ids (D-PR8)', () => { + const runs = basePassingRunsWith([ + { name: 'Publish to npm', status: 'completed', conclusion: 'skipped', check_suite: { id: CI_SUITE_113F472 } }, + ]); + const result = evaluateChecks({ + requiredContexts: REQUIRED, + checkRuns: runs, + statuses: [], + headSha: HEAD_113F472, + releaseSuiteIds: RELEASE_SUITES, + }); + assert.equal(result.exitCode, 1, + 'allow-listed name in non-release suite must fail (D-PR8)'); + const allLines = result.lines.join('\n'); + assert.ok(allLines.includes('Publish to npm'), `must name the job; got:\n${allLines}`); + assert.ok( + allLines.includes(String(CI_SUITE_113F472)), + `failure must name the check-run's suite id (${CI_SUITE_113F472}); got:\n${allLines}`, + ); + assert.ok( + allLines.includes(String(RELEASE_SUITE)) || allLines.includes('none'), + `failure must mention the release suite list; got:\n${allLines}`, + ); + // Positive control: same check-run but treat CI_SUITE as a release suite → PASS + const result2 = evaluateChecks({ + requiredContexts: REQUIRED, + checkRuns: runs, + statuses: [], + headSha: HEAD_113F472, + releaseSuiteIds: new Set([CI_SUITE_113F472]), + }); + assert.equal(result2.exitCode, 0, + 'control: when the run\'s suite IS in releaseSuiteIds, must pass'); + }); + + test('D-PR5g2: allow-listed name skipped but check_suite field absent → exit 1; "check_suite.id missing" (D-PR8)', () => { + const runs = basePassingRunsWith([ + { name: 'Publish to npm', status: 'completed', conclusion: 'skipped' }, // no check_suite + ]); + const result = evaluateChecks({ + requiredContexts: REQUIRED, + checkRuns: runs, + statuses: [], + headSha: HEAD_113F472, + releaseSuiteIds: RELEASE_SUITES, + }); + assert.equal(result.exitCode, 1, 'missing check_suite must fail (D-PR8)'); + const allLines = result.lines.join('\n'); + assert.ok( + allLines.includes('check_suite.id missing'), + `failure must say "check_suite.id missing"; got:\n${allLines}`, + ); + // Positive control: add check_suite in release suite → PASS + const runs2 = basePassingRunsWith([ + withSuite({ name: 'Publish to npm', status: 'completed', conclusion: 'skipped' }), + ]); + const result2 = evaluateChecks({ + requiredContexts: REQUIRED, + checkRuns: runs2, + statuses: [], + headSha: HEAD_113F472, + releaseSuiteIds: RELEASE_SUITES, + }); + assert.equal(result2.exitCode, 0, 'control: check_suite present in release suite must pass'); + }); + + test('D-PR5h: releaseSuiteIds undefined → all five skipped allow-listed names FAIL with DISABLED in message (D-PR8, fails closed)', () => { + const skippedPublishRuns = [...TIER_B_EXPECTED_SKIPPED].map(name => ({ + name, status: 'completed', conclusion: 'skipped', + })); + const runs = basePassingRunsWith(skippedPublishRuns); + const result = evaluateChecks({ + requiredContexts: REQUIRED, + checkRuns: runs, + statuses: [], + headSha: HEAD_113F472, + // releaseSuiteIds: undefined — not provided + }); + assert.equal(result.exitCode, 1, + 'undefined releaseSuiteIds must fail the five skipped allow-listed names'); + const allLines = result.lines.join('\n'); + assert.ok(allLines.includes('DISABLED'), + `failure must say DISABLED when releaseSuiteIds is undefined; got:\n${allLines}`); + // Each of the five names must appear in a failure + for (const name of TIER_B_EXPECTED_SKIPPED) { + assert.ok( + allLines.includes(name), + `failure must name "${name}"; got:\n${allLines}`, + ); + } + }); + + test('D-PR5h2: releaseSuiteIds undefined but no skipped allow-listed names and untouched surface → PASS; prints "not provided" line', () => { + const runs = basePassingRunsWith([]); + const result = evaluateChecks({ + requiredContexts: REQUIRED, + checkRuns: runs, + statuses: [], + headSha: HEAD_113F472, + changedFiles: ['README.md'], + // relaseSuiteIds: undefined — not provided + }); + assert.equal(result.exitCode, 0, + 'undefined releaseSuiteIds with no skipped names and untouched surface must pass'); + const allLines = result.lines.join('\n'); + assert.ok( + allLines.includes('not provided'), + `output must print the "not provided" state; got:\n${allLines}`, + ); + }); + + test('D-PR5i: live path, runs-API error → exit 2 (D-PR8, fail closed)', () => { + const runner = stubRunner([ + ['/pulls/', PR_OK], + ['/protection', PROTECTION_OK], + ['/check-runs', CHECKS_OK_WITH_HYGIENE], + ['/status', { statuses: [], total_count: 0 }], + ['/actions/runs', { __error: true, httpStatus: 500, stderr: 'server error' }], + ]); + assert.equal(main(['1'], runner, OK_GH_VERSION), 2, + 'runs API error must exit 2 (D-PR8, fail closed)'); + }); + +}); + +// --------------------------------------------------------------------------- +// D-PR5i2: fetchWorkflowRuns unit tests (bounded loop, total_count guard, +// field projection). Mirrors architecture-08 for fetchCheckRuns. +// --------------------------------------------------------------------------- +describe('D-PR5i2: fetchWorkflowRuns — bounded loop, total_count guard, projection', () => { + + test('page cap (≤ 5 requests) then ok:false/exitCode 2 (D-PR8)', () => { + let calls = 0; + const fullPage = { + total_count: 100_000, + workflow_runs: Array.from({ length: 100 }, (_, i) => ({ + id: i, path: '.github/workflows/ci.yml', event: 'pull_request', + check_suite_id: i + 1, conclusion: 'success', + })), + }; + const runner = (_args) => { calls++; return fullPage; }; + const result = fetchWorkflowRuns('abc123sha', runner); + assert.ok(!result.ok, 'must return ok:false when pagination exceeds MAX_RUNS_PAGES'); + assert.equal(result.exitCode, 2, 'pagination cap must produce exit 2 (D-PR8)'); + assert.ok(calls <= 5, `must not exceed 5 page requests; made ${calls} (pinned to MAX_RUNS_PAGES=5)`); + assert.ok(calls >= 1, 'must have made at least one request'); + }); + + test('total_count mismatch → ok:false/exitCode 2 (D-PR8)', () => { + const runner = (_args) => ({ + total_count: 50, + workflow_runs: [{ id: 1, path: '.github/workflows/release.yml', event: 'pull_request', check_suite_id: 123, conclusion: 'success' }], + }); + const result = fetchWorkflowRuns('abc123sha', runner); + assert.ok(!result.ok, 'total_count mismatch must return ok:false'); + assert.equal(result.exitCode, 2, 'total_count mismatch must produce exit 2 (D-PR8)'); + assert.ok( + result.message.includes('total_count') || result.message.includes('D-PR8'), + `message must mention total_count or D-PR8; got: ${result.message}`, + ); + }); + + test('single complete page → ok:true with ONLY the projected fields', () => { + const rawRun = { + id: 123, + path: '.github/workflows/release.yml', + event: 'pull_request', + check_suite_id: RELEASE_SUITE, + conclusion: 'success', + // Extra fields that must NOT appear in the projected output: + name: 'Release', html_url: 'https://...', actor: { login: 'user' }, + }; + const runner = (_args) => ({ total_count: 1, workflow_runs: [rawRun] }); + const result = fetchWorkflowRuns('abc123sha', runner); + assert.ok(result.ok, `must return ok:true for a complete single-page result; got: ${JSON.stringify(result)}`); + assert.equal(result.runs.length, 1, 'must return one run'); + const projected = result.runs[0]; + assert.equal(projected.id, 123, 'id must be projected'); + assert.equal(projected.path, '.github/workflows/release.yml', 'path must be projected'); + assert.equal(projected.event, 'pull_request', 'event must be projected'); + assert.equal(projected.check_suite_id, RELEASE_SUITE, 'check_suite_id must be projected'); + assert.equal(projected.conclusion, 'success', 'conclusion must be projected'); + assert.ok(!('name' in projected), 'extra field "name" must be excluded'); + assert.ok(!('html_url' in projected), 'extra field "html_url" must be excluded'); + assert.ok(!('actor' in projected), 'extra field "actor" must be excluded'); + }); + + test('API error → ok:false/exitCode 2 (D-PR8)', () => { + const runner = (_args) => ({ __error: true, httpStatus: 500, stderr: 'server error' }); + const result = fetchWorkflowRuns('abc123sha', runner); + assert.ok(!result.ok, 'API error must return ok:false'); + assert.equal(result.exitCode, 2, 'API error must produce exit 2'); + }); + +}); + +// --------------------------------------------------------------------------- +// D-PR5j: live path with RUNS_RELEASE → 6 API calls, exit 0; control with +// ci.yml path → exit 1 (D-PR8) +// --------------------------------------------------------------------------- +describe('D-PR5j: live path with workflow runs routing (D-PR8)', () => { + + test('D-PR5j: RUNS_RELEASE → exit 0 and 6 URLs; control: run path = ci.yml → releaseSuiteIds empty → exit 1', () => { + // Add a skipped publish run so that the "no release suite → fail" control fires. + const withSkipped = { + ...CHECKS_OK_WITH_HYGIENE, + check_runs: [ + ...CHECKS_OK_WITH_HYGIENE.check_runs, + { name: 'Publish to npm', status: 'completed', conclusion: 'skipped', check_suite: { id: RELEASE_SUITE } }, + ], + total_count: CHECKS_OK_WITH_HYGIENE.total_count + 1, + }; + const calls = []; + const runner = stubRunner([ + ['/pulls/', PR_OK], + ['/protection', PROTECTION_OK], + ['/check-runs', withSkipped], + ['/status', { statuses: [], total_count: 0 }], + ['/actions/runs', RUNS_RELEASE], + ], calls); + assert.equal(main(['1'], runner, OK_GH_VERSION), 0, + 'RUNS_RELEASE must allow the skipped publish run and exit 0'); + assert.equal(calls.length, 6, `expected 6 API calls (pr, protection, checks, status, files, runs); got ${calls.length}`); + const runsCall = calls.find(u => u.includes('/actions/runs')); + assert.ok(runsCall, 'must have made a /actions/runs call'); + assert.ok(runsCall.includes('head_sha='), 'runs call must include head_sha='); + assert.ok(runsCall.includes('per_page=100'), 'runs call must include per_page=100'); + + // Control: same check-runs but run path = ci.yml → empty releaseSuiteIds → skipped run fails + const CI_RUNS = { + total_count: 1, + workflow_runs: [{ id: 99, path: '.github/workflows/ci.yml', event: 'pull_request', check_suite_id: CI_SUITE_113F472, conclusion: 'success' }], + }; + const runner2 = stubRunner([ + ['/pulls/', PR_OK], + ['/protection', PROTECTION_OK], + ['/check-runs', withSkipped], + ['/status', { statuses: [], total_count: 0 }], + ['/actions/runs', CI_RUNS], + ]); + assert.equal(main(['1'], runner2, OK_GH_VERSION), 1, + 'control: ci.yml path → releaseSuiteIds empty → skipped publish run fails → exit 1'); + }); + +}); + +// --------------------------------------------------------------------------- +// D-PR5k: two release suites on one head — each run attributed on its own +// --------------------------------------------------------------------------- +describe('D-PR5k: multiple release suites on one head (dispatch + pull_request)', () => { + + test('D-PR5k: two release suites → each skip attributed to its own suite; PASS; control: failure in second suite → exit 1', () => { + const SUITE_A = RELEASE_SUITE; + const SUITE_B = 92246852351; // PR #365 dispatch-only release suite (event-agnostic) + const twoSuites = new Set([SUITE_A, SUITE_B]); + + function basePassingRunsWith(extras) { + return [ + ...loadCheckRuns('checks-main-113f472.json'), + { ...SOURCE_HYGIENE_PASS }, + ...extras, + ]; + } + + // Four names in SUITE_A, TestPyPI in SUITE_B + const runs = basePassingRunsWith([ + ...['Publish to crates.io', 'Publish to npm', 'Publish to PyPI', 'GitHub Release'].map(name => + withSuite({ name, status: 'completed', conclusion: 'skipped' }, SUITE_A), + ), + withSuite({ name: 'Publish to TestPyPI (rehearsal)', status: 'completed', conclusion: 'skipped' }, SUITE_B), + ]); + const result = evaluateChecks({ + requiredContexts: REQUIRED, + checkRuns: runs, + statuses: [], + headSha: HEAD_113F472, + releaseSuiteIds: twoSuites, + }); + assert.equal(result.exitCode, 0, + 'both release suites must allow their skipped runs → pass'); + + // Control: flip TestPyPI in SUITE_B to failure → exit 1 (all-must-pass, D-PR7j ref 3a803f5) + const runs2 = basePassingRunsWith([ + ...['Publish to crates.io', 'Publish to npm', 'Publish to PyPI', 'GitHub Release'].map(name => + withSuite({ name, status: 'completed', conclusion: 'skipped' }, SUITE_A), + ), + withSuite({ name: 'Publish to TestPyPI (rehearsal)', status: 'completed', conclusion: 'failure' }, SUITE_B), + ]); + const result2 = evaluateChecks({ + requiredContexts: REQUIRED, + checkRuns: runs2, + statuses: [], + headSha: HEAD_113F472, + releaseSuiteIds: twoSuites, + }); + assert.equal(result2.exitCode, 1, + 'control: failure in second release suite must exit 1 (all-must-pass)'); + const allLines2 = result2.lines.join('\n'); + assert.ok(allLines2.includes('Publish to TestPyPI'), 'must name the failing job'); + }); + +}); + +// --------------------------------------------------------------------------- +// D-PR7k, D-PR7l: release-surface attribution via releaseSuiteIds (D-PR8) +// --------------------------------------------------------------------------- +describe('D-PR7k, D-PR7l: D-PR7 attribution keyed on release suite identity', () => { + + function passingRunsWith(extras) { + return [ + ...loadCheckRuns('checks-main-113f472.json'), + { ...SOURCE_HYGIENE_PASS }, + ...extras, + ]; + } + + test('D-PR7k: release-surface contexts present ONLY in ci.yml suite → exit 1; "absent from every … check-suite" AND "other suites ignored"', () => { + const releaseRunsInCiSuite = RELEASE_SURFACE_CONTEXTS.map(name => ({ + name, status: 'completed', conclusion: 'success', check_suite: { id: CI_SUITE_113F472 }, + })); + const checkRuns = passingRunsWith(releaseRunsInCiSuite); + const result = evaluateChecks({ + requiredContexts: REQUIRED, + checkRuns, + statuses: [], + headSha: HEAD_113F472, + changedFiles: ['.github/workflows/release.yml'], + relaseSuiteIds: RELEASE_SUITES, // intentional typo for RED — see correction in code + }); + // This test is RED until the code is implemented (relaseSuiteIds → undefined → fail closed) + // After fix: releaseSuiteIds will be used correctly + assert.equal(result.exitCode, 1, + 'release-surface contexts in non-release suite must exit 1 (D-PR8)'); + }); + + test('D-PR7k (corrected): contexts in ci.yml suite → exit 1; message cites "absent from every .github/workflows/release.yml" and N same-name runs ignored', () => { + const releaseRunsInCiSuite = RELEASE_SURFACE_CONTEXTS.map(name => ({ + name, status: 'completed', conclusion: 'success', check_suite: { id: CI_SUITE_113F472 }, + })); + const checkRuns = passingRunsWith(releaseRunsInCiSuite); + const result = evaluateChecks({ + requiredContexts: REQUIRED, + checkRuns, + statuses: [], + headSha: HEAD_113F472, + changedFiles: ['.github/workflows/release.yml'], + releaseSuiteIds: RELEASE_SUITES, // correct spelling + }); + assert.equal(result.exitCode, 1, 'contexts only in ci.yml suite must fail (D-PR8)'); + const allLines = result.lines.join('\n'); + assert.ok( + allLines.includes('absent from every .github/workflows/release.yml') || + allLines.includes('absent from every'), + `must say "absent from every .github/workflows/release.yml"; got:\n${allLines}`, + ); + assert.ok( + allLines.includes('other suites ignored') || allLines.includes('ignored'), + `must mention that same-name runs in other suites are ignored; got:\n${allLines}`, + ); + // Positive control: same runs but CI_SUITE_113F472 IS a release suite → PASS + const result2 = evaluateChecks({ + requiredContexts: REQUIRED, + checkRuns, + statuses: [], + headSha: HEAD_113F472, + changedFiles: ['.github/workflows/release.yml'], + releaseSuiteIds: new Set([CI_SUITE_113F472]), + }); + assert.equal(result2.exitCode, 0, + 'control: when ci.yml suite is treated as a release suite, must pass'); + }); + + test('D-PR7l: releaseSuiteIds undefined + surface touched → 3 D-PR7 failures ("not provided")', () => { + const releaseRuns = RELEASE_SURFACE_CONTEXTS.map(name => ({ + name, status: 'completed', conclusion: 'success', + })); + const checkRuns = passingRunsWith(releaseRuns); + const result = evaluateChecks({ + requiredContexts: REQUIRED, + checkRuns, + statuses: [], + headSha: HEAD_113F472, + changedFiles: ['.github/workflows/release.yml'], + // releaseSuiteIds: undefined — not provided + }); + assert.equal(result.exitCode, 1, + 'undefined releaseSuiteIds + surface touched must fail (D-PR8, fails closed)'); + const allLines = result.lines.join('\n'); + assert.ok( + allLines.includes('not provided'), + `must say "not provided" in D-PR7 failure; got:\n${allLines}`, + ); + // All 3 RELEASE_SURFACE_CONTEXTS should fail + for (const ctx of RELEASE_SURFACE_CONTEXTS) { + assert.ok(allLines.includes(ctx), `must name context "${ctx}"; got:\n${allLines}`); + } + }); + +}); + +// --------------------------------------------------------------------------- +// releaseSuiteIdsFrom unit tests (D-PR8) +// --------------------------------------------------------------------------- +describe('releaseSuiteIdsFrom: builds Set of check_suite_ids for release.yml runs', () => { + + test('returns Set with release.yml suite ids only; non-release paths excluded; non-integer ids excluded', () => { + const runs = [ + { id: 1, path: '.github/workflows/release.yml', event: 'pull_request', check_suite_id: 123, conclusion: 'success' }, + { id: 2, path: '.github/workflows/ci.yml', event: 'pull_request', check_suite_id: 456, conclusion: 'success' }, + { id: 3, path: '.github/workflows/release.yml', event: 'workflow_dispatch', check_suite_id: 789, conclusion: 'success' }, + { id: 4, path: '.github/workflows/release.yml', event: 'push', check_suite_id: 'not-a-number', conclusion: 'success' }, + ]; + const result = releaseSuiteIdsFrom(runs); + assert.ok(result instanceof Set, 'must return a Set'); + assert.ok(result.has(123), 'must include pull_request release suite 123'); + assert.ok(result.has(789), 'must include workflow_dispatch release suite 789 (any event counts)'); + assert.ok(!result.has(456), 'must exclude ci.yml suite 456'); + assert.ok(!result.has('not-a-number'), 'must exclude non-integer ids'); + assert.equal(result.size, 2, 'must contain exactly 2 entries'); + }); + + test('empty runs array → empty Set (no release suites on this head)', () => { + const result = releaseSuiteIdsFrom([]); + assert.ok(result instanceof Set, 'must return a Set'); + assert.equal(result.size, 0, 'empty runs must yield empty Set'); + }); + + test('drift guard: RELEASE_SURFACE includes RELEASE_WORKFLOW_PATH (ADR-013 amendment)', () => { + // releaseSuiteIdsFrom uses RELEASE_WORKFLOW_PATH to filter runs. + // RELEASE_SURFACE must include that path so the release-surface presence check + // and the suite attribution use the same path string (ADR-013). + assert.ok( + RELEASE_SURFACE.includes(RELEASE_WORKFLOW_PATH), + `RELEASE_WORKFLOW_PATH "${RELEASE_WORKFLOW_PATH}" must be in RELEASE_SURFACE: ${RELEASE_SURFACE.join(', ')}`, + ); + }); + +}); + +// --------------------------------------------------------------------------- +// Current fixtures (2026-09): live-shaped evaluation with the PR #366 fixture +// --------------------------------------------------------------------------- +describe('current fixtures (2026-09): live-shaped evaluation', () => { + + const CHECKS_PR366 = JSON.parse(readFileSync(join(FIXTURES, 'checks-pr366-e02bcf2.json'), 'utf8')); + const RUNS_PR366 = JSON.parse(readFileSync(join(FIXTURES, 'runs-pr366-e02bcf2.json'), 'utf8')); + const PROTECTION_2026_09 = JSON.parse(readFileSync(join(FIXTURES, 'protection-main-2026-09.json'), 'utf8')); + + const REQUIRED_2026_09 = PROTECTION_2026_09.required_status_checks.contexts; + const CHECK_RUNS_PR366 = CHECKS_PR366.check_runs; + const PR366_HEAD = 'e02bcf280dc50bb8df032744aa2a2520c02865ee'; + // B1 files changed in PR #366: + const PR366_FILES = [ + { filename: '.github/workflows/release.yml' }, + { filename: 'CHANGELOG.md' }, + { filename: 'RELEASING.md' }, + { filename: 'scripts/__test__/release-auth-probe.spec.mjs' }, + { filename: 'scripts/__test__/verify-pr-checks.spec.mjs' }, + { filename: 'scripts/verify-pr-checks.mjs' }, + ]; + const CI_SUITE_PR366 = 92290758550; // ci.yml suite for PR #366 + + // Build releaseSuiteIds from the runs fixture + function pr366ReleaseSuiteIds() { + return releaseSuiteIdsFrom(RUNS_PR366.workflow_runs.map(r => ({ + id: r.id, path: r.path, event: r.event, check_suite_id: r.check_suite_id, conclusion: r.conclusion, + }))); + } + + test('fixture shapes: 15 required contexts; 44 check-runs across 4 suites; app.slug ∈ {github-actions, github-advanced-security}; 3 workflow runs with exactly 1 release.yml run (check_suite_id = RELEASE_SUITE)', () => { + assert.equal(REQUIRED_2026_09.length, 15, + 'protection-main-2026-09 must have 15 required contexts'); + assert.equal(CHECK_RUNS_PR366.length, 44, + 'checks-pr366-e02bcf2 must have 44 check-runs'); + const suiteIds = new Set(CHECK_RUNS_PR366.map(cr => cr.check_suite.id)); + assert.equal(suiteIds.size, 4, 'must have 4 distinct check-suites'); + const appSlugs = new Set(CHECK_RUNS_PR366.map(cr => cr.app.slug)); + for (const slug of appSlugs) { + assert.ok( + ['github-actions', 'github-advanced-security'].includes(slug), + `unexpected app.slug "${slug}" — only github-actions and github-advanced-security expected`, + ); + } + assert.equal(RUNS_PR366.total_count, 3, 'runs fixture must declare total_count=3'); + const releaseRuns = RUNS_PR366.workflow_runs.filter(r => r.path === RELEASE_WORKFLOW_PATH); + assert.equal(releaseRuns.length, 1, 'exactly one release.yml workflow run expected'); + assert.equal(releaseRuns[0].check_suite_id, RELEASE_SUITE, + `release run must have check_suite_id ${RELEASE_SUITE}`); + }); + + test('CF-1: pure evaluateChecks with PR #366 fixture → PASS; "release surface touched"; five allowed (D-PR8) lines with suite id; control: mutate Version gate → exit 1', () => { + const releaseSuiteIds = pr366ReleaseSuiteIds(); + + const result = evaluateChecks({ + requiredContexts: REQUIRED_2026_09, + checkRuns: CHECK_RUNS_PR366, + statuses: [], + headSha: PR366_HEAD, + changedFiles: ['.github/workflows/release.yml'], + releaseSuiteIds, + }); + assert.equal(result.exitCode, 0, `must PASS; lines:\n${result.lines.join('\n')}`); + const allLines = result.lines.join('\n'); + assert.ok(allLines.includes('release surface touched'), 'must say "release surface touched"'); + + // Five allowed lines (D-PR8): each must say "allowed" and name the release suite id + for (const name of TIER_B_EXPECTED_SKIPPED) { + const line = result.lines.find(l => l.includes(name)); + assert.ok(line, `output must include a line for "${name}"; got:\n${allLines}`); + assert.ok(line.includes('allowed'), `"${name}" allowed line must say "allowed"; got: ${line}`); + assert.ok( + line.includes(String(RELEASE_SUITE)), + `"${name}" allowed line must name suite id ${RELEASE_SUITE}; got: ${line}`, + ); + } + + // Control: mutate "Version gate" to failure → exit 1 + const withFailedGate = CHECK_RUNS_PR366.map(cr => + (cr.name === 'Version gate' && cr.check_suite.id === RELEASE_SUITE) + ? { ...cr, conclusion: 'failure' } + : cr, + ); + const result2 = evaluateChecks({ + requiredContexts: REQUIRED_2026_09, + checkRuns: withFailedGate, + statuses: [], + headSha: PR366_HEAD, + changedFiles: ['.github/workflows/release.yml'], + releaseSuiteIds, + }); + assert.equal(result2.exitCode, 1, 'control: failed Version gate must exit 1'); + assert.ok(result2.lines.join('\n').includes('Version gate'), 'must name Version gate in failure'); + }); + + test('CF-2: "Publish to npm" re-suited to ci.yml suite → exit 1 (D-PR8: skipped allowance requires release suite)', () => { + const releaseSuiteIds = pr366ReleaseSuiteIds(); + // Move "Publish to npm" from release suite to ci.yml suite + const resuitedChecks = CHECK_RUNS_PR366.map(cr => + (cr.name === 'Publish to npm' && cr.check_suite.id === RELEASE_SUITE) + ? { ...cr, check_suite: { id: CI_SUITE_PR366 } } + : cr, + ); + const result = evaluateChecks({ + requiredContexts: REQUIRED_2026_09, + checkRuns: resuitedChecks, + statuses: [], + headSha: PR366_HEAD, + changedFiles: ['.github/workflows/release.yml'], + releaseSuiteIds, + }); + assert.equal(result.exitCode, 1, + '"Publish to npm" re-suited to ci.yml must fail the skipped-allowance check (D-PR8)'); + const allLines = result.lines.join('\n'); + assert.ok(allLines.includes('Publish to npm'), 'must name the failing job'); + assert.ok( + allLines.includes(String(CI_SUITE_PR366)) || allLines.includes(String(RELEASE_SUITE)), + 'must mention a suite id in the failure', + ); + }); + + test('CF-3: live main() over fixtures (stubbed runner) → exit 0; 6 API calls; merge line contains "366"; control: run path = ci.yml → exit 1', () => { + const PR366_DATA = { head: { sha: PR366_HEAD }, base: { ref: 'main' }, changed_files: 6 }; + const calls = []; + const runner = stubRunner([ + ['/files', PR366_FILES], + ['/pulls/', PR366_DATA], + ['/protection', PROTECTION_2026_09], + ['/check-runs', CHECKS_PR366], + ['/status', { statuses: [], total_count: 0 }], + ['/actions/runs', RUNS_PR366], + ], calls); + + const lines = []; + const origLog = console.log; + console.log = (...args) => lines.push(args.join(' ')); + let exitCode; + try { + exitCode = main(['366'], runner, OK_GH_VERSION); + } finally { + console.log = origLog; + } + assert.equal(exitCode, 0, `must exit 0; output:\n${lines.join('\n')}`); + assert.equal(calls.length, 6, `expected 6 API calls; got ${calls.length}: ${calls.join(', ')}`); + const mergeLine = lines.find(l => l.includes('gh pr merge')); + assert.ok(mergeLine, `output must include a merge command; got:\n${lines.join('\n')}`); + assert.ok(mergeLine.includes('366'), `merge command must contain PR number 366; got: ${mergeLine}`); + + // Control: rewrite the release run's path to ci.yml → releaseSuiteIds empty → 5+3 failures → exit 1 + const RUNS_NO_RELEASE_PATH = { + ...RUNS_PR366, + workflow_runs: RUNS_PR366.workflow_runs.map(r => + r.path === RELEASE_WORKFLOW_PATH ? { ...r, path: '.github/workflows/ci.yml' } : r, + ), + }; + const runner2 = stubRunner([ + ['/files', PR366_FILES], + ['/pulls/', PR366_DATA], + ['/protection', PROTECTION_2026_09], + ['/check-runs', CHECKS_PR366], + ['/status', { statuses: [], total_count: 0 }], + ['/actions/runs', RUNS_NO_RELEASE_PATH], + ]); + assert.equal(main(['366'], runner2, OK_GH_VERSION), 1, + 'control: no release.yml run in the runs fixture → exit 1'); + }); + + test('CF-4: release run removed from runs fixture → Tier B (5 skipped) + D-PR7 (3 contexts) all FAIL', () => { + const RUNS_STRIPPED = { + total_count: 2, + workflow_runs: RUNS_PR366.workflow_runs.filter(r => r.path !== RELEASE_WORKFLOW_PATH), + }; + const releaseSuiteIds = releaseSuiteIdsFrom(RUNS_STRIPPED.workflow_runs.map(r => ({ + id: r.id, path: r.path, event: r.event, check_suite_id: r.check_suite_id, conclusion: r.conclusion, + }))); + const result = evaluateChecks({ + requiredContexts: REQUIRED_2026_09, + checkRuns: CHECK_RUNS_PR366, + statuses: [], + headSha: PR366_HEAD, + changedFiles: ['.github/workflows/release.yml'], + releaseSuiteIds, + }); + assert.equal(result.exitCode, 1, 'no release suite → must fail'); + // 5 Tier B failures + 3 D-PR7 failures = 8 content failures + 1 summary line = 9 ✖ lines + const failureLines = result.lines.filter(l => l.startsWith('✖')); + assert.ok( + failureLines.length >= 9, + `expected ≥9 failure lines (5 Tier B + 3 D-PR7 + summary); got ${failureLines.length}:\n${result.lines.join('\n')}`, + ); + }); + + test('CF-5: with 2026-09 protection, EXPECTED_CONTEXTS are now Tier A — no double-report; Tier A catches planted Source hygiene failure', () => { + const releaseSuiteIds = pr366ReleaseSuiteIds(); + + // PASS: all 15 required contexts + surface contexts → no double-report + const result = evaluateChecks({ + requiredContexts: REQUIRED_2026_09, + checkRuns: CHECK_RUNS_PR366, + statuses: [], + headSha: PR366_HEAD, + changedFiles: ['.github/workflows/release.yml'], + releaseSuiteIds, + }); + assert.equal(result.exitCode, 0, `must PASS; lines:\n${result.lines.join('\n')}`); + // Source hygiene is now Tier A (in the 2026-09 required set); must NOT appear in Tier A+ + const tierAplusLines = result.lines.filter(l => l.includes('Tier A+') && l.includes('Source hygiene')); + assert.equal(tierAplusLines.length, 0, + 'Source hygiene must not appear in Tier A+ — it is now in Tier A (2026-09 protection)'); + + // Tier A catches planted Source hygiene failure + const withFailedHygiene = CHECK_RUNS_PR366.map(cr => + cr.name === 'Source hygiene' ? { ...cr, conclusion: 'failure' } : cr, + ); + const result2 = evaluateChecks({ + requiredContexts: REQUIRED_2026_09, + checkRuns: withFailedHygiene, + statuses: [], + headSha: PR366_HEAD, + changedFiles: ['.github/workflows/release.yml'], + releaseSuiteIds, + }); + assert.equal(result2.exitCode, 1, 'planted Source hygiene failure must fail'); + const allLines2 = result2.lines.join('\n'); + assert.ok(allLines2.includes('Source hygiene'), 'must name Source hygiene'); + assert.ok(allLines2.includes('Tier A'), 'must report as Tier A failure (not Tier A+)'); + }); + +}); + // Code of Conduct tests (AC-1, AC-2) live in code-of-conduct.spec.mjs — // split in commit 2e9482f to follow the one-spec-per-module convention. diff --git a/scripts/verify-pr-checks.mjs b/scripts/verify-pr-checks.mjs index d7f28878..a16eaa17 100644 --- a/scripts/verify-pr-checks.mjs +++ b/scripts/verify-pr-checks.mjs @@ -28,10 +28,11 @@ * (failure, cancelled, timed_out, action_required, stale, * skipped, neutral, null, any future value) = FAIL. * not-yet-completed = FAIL (avoids PF-017). - * Exception: the four release publish jobs are guarded by + * Exception: the five release publish jobs are guarded by * startsWith(github.ref,'refs/tags/v') and report as skipped - * on a PR-branch dry-run; only those four names, only when - * skipped, are allowed (TIER_B_EXPECTED_SKIPPED). + * on a PR-branch dry-run; only those five names, only when + * skipped AND in a release.yml check-suite, are allowed + * (TIER_B_EXPECTED_SKIPPED + D-PR8 suite keying). * Tier C (legacy statuses): advisory unless the context is required * * D-PR3b: EXPECTED_CONTEXTS lists jobs that must be present and passing even though @@ -61,10 +62,20 @@ * * D-PR7: Release-surface presence check — when a PR touches the release * surface (paths matching release.yml's pull_request.paths filter), - * each RELEASE_SURFACE_CONTEXTS job must be completed+success. - * Absence or non-success is FAIL (Tier A semantics applied to release - * check-runs). The changed-file list is fetched via the PR-files API - * and fails closed on every indeterminate outcome (avoids PF-013). + * each RELEASE_SURFACE_CONTEXTS job must be completed+success IN A + * release.yml check-suite (D-PR8). Absence or non-success is FAIL + * (Tier A semantics applied to release check-runs). The changed-file + * list is fetched via the PR-files API and fails closed on every + * indeterminate outcome (avoids PF-013). + * + * D-PR8: Suite-keyed skipped-publish allowance — Tier B's allow-listed skip + * (TIER_B_EXPECTED_SKIPPED) and D-PR7's release-surface context check + * now additionally require that the check-run belongs to a check-suite + * originating from .github/workflows/release.yml. The mapping is built + * by fetchWorkflowRuns (bounded at MAX_RUNS_PAGES=5 pages) which maps + * check_suite_id → workflow path via the Actions runs API. A skipped + * allow-listed run in a ci.yml or CodeQL suite is NOT allowed — only + * release.yml suites grant the skipped-publish exception. * * Usage: * node scripts/verify-pr-checks.mjs @@ -105,6 +116,8 @@ function isMainModule(metaUrl) { // D-PR4a: hard page cap — exit 2 rather than evaluating a partial result const MAX_PAGES = 20; +// D-PR8: hard page cap for workflow-runs fetch (suites per head are small, 5 is ample) +const MAX_RUNS_PAGES = 5; // D-PR5: minimum gh version required for --match-head-commit const MIN_GH_MAJOR = 2; const MIN_GH_MINOR = 31; @@ -208,6 +221,37 @@ export function matchesReleaseSurface(file) { return false; } +/** + * The workflow path used to identify release.yml check-suites (D-PR8). + * releaseSuiteIdsFrom filters Actions runs by this value to build the set of + * suite ids that grant the Tier B skipped-publish exception and D-PR7 credit. + * + * Must equal the workflow file path that GitHub populates in `workflow_runs[].path`. + */ +export const RELEASE_WORKFLOW_PATH = '.github/workflows/release.yml'; + +/** + * Build the set of check_suite_id values for workflow runs whose path is + * RELEASE_WORKFLOW_PATH (D-PR8). + * + * Only numeric (integer) ids are included — non-integer values are excluded + * as anomalous. Every id in the result is guaranteed to be a GitHub-Actions + * check-suite id originating from release.yml (by construction, the CodeQL + * umbrella never appears in /actions/runs). + * + * @param {Array<{path: string, check_suite_id: any}>} runs — projected run objects + * @returns {Set} + */ +export function releaseSuiteIdsFrom(runs) { + const ids = new Set(); + for (const run of runs) { + if (run.path === RELEASE_WORKFLOW_PATH && Number.isInteger(run.check_suite_id)) { + ids.add(run.check_suite_id); + } + } + return ids; +} + // --------------------------------------------------------------------------- // gh runner (thin IO shim; injected in tests for offline operation) // --------------------------------------------------------------------------- @@ -283,6 +327,7 @@ function defaultGhRunner(args) { * name: string; * status: string; // 'completed' | 'queued' | 'in_progress' | ... * conclusion: string | null; // 'success' | 'failure' | 'cancelled' | ... + * check_suite?: { id: number }; // D-PR8: suite identity (absent on legacy fixtures) * }} CheckRun * * @typedef {{ @@ -298,6 +343,7 @@ function defaultGhRunner(args) { * prNumber?: number; // included in the emitted merge command (D-PR5) * expectedContexts?: string[]; // defaults to EXPECTED_CONTEXTS * changedFiles?: string[]; // D-PR7: when present, release-surface presence check runs + * releaseSuiteIds?: Set; // D-PR8: check_suite_id values for release.yml runs * }} EvaluateInput * * @typedef {{ @@ -326,6 +372,7 @@ export function evaluateChecks({ prNumber, expectedContexts = EXPECTED_CONTEXTS, changedFiles, + releaseSuiteIds, }) { const lines = []; const failures = []; @@ -367,6 +414,21 @@ export function evaluateChecks({ // Always print counts (D-PR4 / avoids PF-013) lines.push(` check-runs: ${nChecks}, statuses: ${nStatuses}, required contexts: ${nRequired}`); + // D-PR8: announce whether the release-suite mapping is available. + // When undefined, skipped allow-listed runs and release-surface contexts will + // fail closed (the notice here makes the state visible even on a PASS). + if (releaseSuiteIds === undefined) { + lines.push( + ' · D-PR8: releaseSuiteIds not provided — skipped allow-listed runs and ' + + 'D-PR7 release-surface contexts CANNOT be suite-verified (disabled path)', + ); + } else { + lines.push( + ` · D-PR8: ${releaseSuiteIds.size} release.yml suite(s) on this head` + + (releaseSuiteIds.size > 0 ? `: ${[...releaseSuiteIds].join(', ')}` : ' — none found'), + ); + } + // Build lookup maps. // A name maps to EVERY check-run carrying it, not just the last one seen: // `filter=latest` de-duplicates within a check-suite, but two suites (two @@ -511,13 +573,42 @@ export function evaluateChecks({ // Allowance: release.yml publish jobs are skipped on a PR-branch dry-run // because they are guarded by startsWith(github.ref, 'refs/tags/v'). - // That skip IS the guard working; allow exactly these four names, only - // when skipped. Cancelled/failed/neutral publish runs still fail, and a - // skipped run under any other name still fails (TIER_B_EXPECTED_SKIPPED). + // That skip IS the guard working; allow exactly these five names, only + // when skipped AND the run belongs to a release.yml check-suite (D-PR8). + // Cancelled/failed/neutral publish runs still fail, and a skipped run + // under any other name still fails (TIER_B_EXPECTED_SKIPPED). if (cr.conclusion === 'skipped' && TIER_B_EXPECTED_SKIPPED.has(cr.name)) { + // D-PR8: fail closed when the suite mapping is unavailable — a skipped + // publish run cannot be attributed without the workflow-run data. + if (releaseSuiteIds === undefined) { + failures.push( + `Tier B (non-required): "${cr.name}" — conclusion=skipped (DISABLED: ` + + `releaseSuiteIds not provided; D-PR8 suite-keying requires fetchWorkflowRuns to run first)`, + ); + pass = false; + continue; + } + const suiteId = cr.check_suite?.id; + if (!Number.isInteger(suiteId)) { + failures.push( + `Tier B (non-required): "${cr.name}" — conclusion=skipped but check_suite.id missing ` + + `(D-PR8: skipped allow-listed run must carry a verifiable release.yml suite id)`, + ); + pass = false; + continue; + } + if (!releaseSuiteIds.has(suiteId)) { + const relIds = [...releaseSuiteIds].join(', ') || 'none'; + failures.push( + `Tier B (non-required): "${cr.name}" — conclusion=skipped in suite ${suiteId} ` + + `which is NOT a release.yml suite (D-PR8: allowed release suite ids: ${relIds})`, + ); + pass = false; + continue; + } lines.push( ` · Tier B: "${cr.name}" skipped by its refs/tags/v guard` + - ` (release dry-run on PR head) — allowed`, + ` (release dry-run on PR head, suite ${suiteId}) — allowed (D-PR8)`, ); continue; } @@ -574,19 +665,42 @@ export function evaluateChecks({ ') — requiring release check-runs', ); for (const ctx of RELEASE_SURFACE_CONTEXTS) { - const releaseRuns = checkRuns.filter(cr => cr.name === ctx); + // D-PR8: fail closed when suite mapping unavailable — cannot verify + // that a success run belongs to release.yml and not ci.yml. + if (releaseSuiteIds === undefined) { + failures.push( + `D-PR7 (release surface): "${ctx}" — releaseSuiteIds not provided (D-PR8: ` + + `suite attribution required for release-surface contexts; fetchWorkflowRuns must run first)`, + ); + pass = false; + continue; + } + const allRunsForName = checkRuns.filter(cr => cr.name === ctx); + // Only count runs belonging to a release.yml check-suite (D-PR8). + // Runs in ci.yml or other suites sharing the same name are ignored. + const releaseRuns = allRunsForName.filter( + cr => Number.isInteger(cr.check_suite?.id) && releaseSuiteIds.has(cr.check_suite.id), + ); + const ignoredCount = allRunsForName.length - releaseRuns.length; if (releaseRuns.length === 0) { + const ignoredNote = ignoredCount > 0 + ? ` (${ignoredCount} same-name run(s) in other suites ignored — D-PR8: absent from every .github/workflows/release.yml suite)` + : ''; failures.push( - `D-PR7 (release surface): "${ctx}" absent — the PR touches the release ` + - `surface but release.yml's pull_request run is missing, not finished, or failed (#342)`, + `D-PR7 (release surface): "${ctx}" absent from every .github/workflows/release.yml` + + ` check-suite — the PR touches the release surface but the release.yml pull_request ` + + `run is missing, not finished, or in another suite (#342)${ignoredNote}`, ); pass = false; } else { + if (ignoredCount > 0) { + lines.push(` · D-PR7: ${ignoredCount} same-name run(s) for "${ctx}" in other suites ignored (D-PR8)`); + } for (const cr of releaseRuns) { if (cr.status !== 'completed' || cr.conclusion !== 'success') { failures.push( `D-PR7 (release surface): "${ctx}" — status=${cr.status}, conclusion=${cr.conclusion ?? 'null'} ` + - `— the PR touches the release surface but release.yml's pull_request run is missing, not finished, or failed (#342)`, + `— the PR touches the release surface but release.yml's pull_request run failed (#342)`, ); pass = false; } @@ -871,6 +985,82 @@ export function fetchChangedFiles(prNumber, declaredCount, runner) { return { ok: true, files }; } +/** + * Fetch workflow runs for a given head SHA, bounded at MAX_RUNS_PAGES (D-PR8). + * + * Maps check_suite_id → workflow path so Tier B and D-PR7 can verify that a + * skipped allow-listed run belongs to a release.yml check-suite. Only the five + * fields needed by releaseSuiteIdsFrom and evaluateChecks are retained (minimal + * projection); the rest of the run object is discarded. + * + * Fails closed on API error, pagination overflow, and total_count mismatch — + * consistent with fetchCheckRuns and fetchChangedFiles (D-PR4a parity). + * + * @param {string} headSha + * @param {function} runner + * @returns {{ ok: true, runs: Array<{id:number, path:string, event:string, check_suite_id:number, conclusion:string|null}> } + * | { ok: false, exitCode: 2, message: string }} + */ +export function fetchWorkflowRuns(headSha, runner) { + const perPage = 100; + let page = 1; + const allRuns = []; + let totalCount = null; + + // Bounded loop (reliability rule): at most MAX_RUNS_PAGES iterations, always. + // security-11: headSha is network-derived data; encode for safe URL construction. + while (page <= MAX_RUNS_PAGES) { + const url = + `/repos/{owner}/{repo}/actions/runs?head_sha=${encodeURIComponent(headSha)}&per_page=${perPage}&page=${page}`; + const data = runner(['api', url]); + if (data.__error) { + return { + ok: false, + exitCode: 2, + message: `workflow-runs API error (page ${page}): ${data.stderr} (D-PR8)`, + }; + } + if (totalCount === null) { + totalCount = data.total_count ?? 0; + } + const runs = data.workflow_runs ?? []; + // Project to only the five fields we need — avoids leaking unneeded data + // into evaluateChecks and makes the contract explicit (D-PR8: minimal surface). + allRuns.push(...runs.map(r => ({ + id: r.id, + path: r.path, + event: r.event, + check_suite_id: r.check_suite_id, + conclusion: r.conclusion, + }))); + if (runs.length < perPage || allRuns.length >= totalCount) break; + page++; + } + + if (page > MAX_RUNS_PAGES) { + return { + ok: false, + exitCode: 2, + message: + `workflow-runs pagination exceeded ${MAX_RUNS_PAGES} pages (D-PR8) — ` + + `refusing to evaluate partial result`, + }; + } + + // D-PR4a parity: assert we collected everything declared by total_count + if (totalCount !== null && allRuns.length !== totalCount) { + return { + ok: false, + exitCode: 2, + message: + `collected ${allRuns.length} workflow runs but total_count=${totalCount} — ` + + `partial page set (D-PR8 total_count guard)`, + }; + } + + return { ok: true, runs: allRuns }; +} + const USAGE = 'Usage: node scripts/verify-pr-checks.mjs [--required-from ]'; @@ -984,6 +1174,23 @@ export function main(argv = process.argv.slice(2), runner = defaultGhRunner, ghV const changedFiles = filesResult.files; console.log(` changed files: ${changedFiles.length}`); + // ---- D-PR8: fetch workflow runs for release.yml suite attribution ---- + // Must be the LAST fetch so that earlier exit-2 paths (fetchCheckRuns, + // fetchStatuses, fetchChangedFiles) still fire before reaching this call. + // The result feeds releaseSuiteIdsFrom so Tier B and D-PR7 can verify that + // skipped allow-listed runs and release-surface contexts belong to a + // release.yml check-suite rather than ci.yml or another workflow. + const runsResult = fetchWorkflowRuns(headSha, runner); + if (!runsResult.ok) { + fail(runsResult.message); + return runsResult.exitCode; + } + const releaseSuiteIds = releaseSuiteIdsFrom(runsResult.runs); + console.log( + ` workflow runs: ${runsResult.runs.length}; release.yml suite(s): ${releaseSuiteIds.size}` + + (releaseSuiteIds.size > 0 ? ` (${[...releaseSuiteIds].join(', ')})` : ''), + ); + // ---- Evaluate (D-PR1: pure function) ---- const result = evaluateChecks({ requiredContexts: req.contexts, @@ -992,6 +1199,7 @@ export function main(argv = process.argv.slice(2), runner = defaultGhRunner, ghV headSha, prNumber, changedFiles, + releaseSuiteIds, // D-PR8: suite attribution for Tier B and D-PR7 }); for (const line of result.lines) { From 5e3a8915372d33a154cfc9af1eefa74fccc2575b Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 7 Sep 2026 15:25:22 +0300 Subject: [PATCH 3/8] ci(release): per-leg rust-cache keys in build-napi (PF-041, #352); re-scope #345 to documentation; specs S3+S20 --- .../features/release-pipeline/KNOWLEDGE.md | 58 +++-- .github/workflows/release.yml | 23 +- CHANGELOG.md | 2 + RELEASING.md | 69 +++-- scripts/__test__/release-auth-probe.spec.mjs | 236 ++++++++++++++++++ 5 files changed, 350 insertions(+), 38 deletions(-) diff --git a/.devflow/features/release-pipeline/KNOWLEDGE.md b/.devflow/features/release-pipeline/KNOWLEDGE.md index e8812149..bdb8ca60 100644 --- a/.devflow/features/release-pipeline/KNOWLEDGE.md +++ b/.devflow/features/release-pipeline/KNOWLEDGE.md @@ -125,14 +125,15 @@ a job-level skip would make `Version gate` report `skipped` and break the verifi ## Component Interactions — `verify-pr-checks.mjs` Tier System -The verifier is **event-blind**: it reads only check-run name, status, and conclusion — -never the triggering event, workflow file, or check-suite. +The verifier reads check-run name, status, conclusion, and check-suite identity — one +bounded `GET /actions/runs?head_sha=` call maps each check-suite id to its workflow file +(D-PR8, #341). The verifier exits 2 when that run-list cannot be enumerated. | Tier | Membership | Passing condition | |---|---|---| | **Tier A** (required) | 15 contexts from live branch protection API | `completed+success` | | **Tier A+ (local)** | `EXPECTED_CONTEXTS` (4 entries: Source hygiene, Python — build & test, examples/ gitignore coverage, Python — wheel install smoke) | `completed+success`, presence required | -| **Tier B** | Everything else | `completed+success`; `skipped` tolerated ONLY for the five names in `TIER_B_EXPECTED_SKIPPED` | +| **Tier B** | Everything else | `completed+success`; `skipped` tolerated ONLY for the five names in `TIER_B_EXPECTED_SKIPPED` AND only when the check-run belongs to a `release.yml` check suite | `TIER_B_EXPECTED_SKIPPED` (5 names): - `Publish to crates.io` @@ -141,15 +142,17 @@ never the triggering event, workflow file, or check-suite. - `GitHub Release` - `Publish to TestPyPI (rehearsal)` -Any `skipped` conclusion under any other name fails Tier B. Any `cancelled`, `neutral`, -`in_progress`, or `queued` conclusion fails regardless of name (PF-017). A Tier-B-only -rejection (e.g. an advisory CodeQL `neutral`) must be adjudicated against the required-context -set before it is believed — it is never license to merge unverified. +Any `skipped` conclusion under any other name, or for a name in the list whose check-suite +is NOT a `release.yml` run, fails Tier B. Any `cancelled`, `neutral`, `in_progress`, or +`queued` conclusion fails regardless of name (PF-017). A Tier-B-only rejection +(e.g. an advisory CodeQL `neutral`) must be adjudicated against the required-context set +before it is believed — it is never license to merge unverified. **D-PR7 release-surface presence check**: When any changed file matches `RELEASE_SURFACE`, the verifier additionally requires `Version gate`, `Stage + verify platform packages`, and -`Rehearse PyPI publish (no upload)` to each be `completed+success`. All runs under each -name must pass (duplicate names = all-must-pass). +`Rehearse PyPI publish (no upload)` to each be `completed+success`. These are attributed +by suite — the check-run must belong to a `release.yml` run; any event counts. All runs +under each name must pass (duplicate names = all-must-pass). The verifier prints `gh pr merge N --squash --admin --match-head-commit ` on PASS. Always use this command verbatim — confirm the current branch resolves to the intended PR @@ -170,7 +173,7 @@ Steps (in order): 1. Verify publish credentials (npm `whoami` + cargo token non-empty + PyPI OIDC mint-token exchange). 2. Assert synchronized versions, no `file:` refs. 3. Assert no hazardous codepoints in tracked source. -4. Run `npm run test:gates` — all four spec files, 187 tests including pin-shape specs (S16). +4. Run `npm run test:gates` — all four spec files, 211 tests including pin-shape specs (S16) and per-leg cache key spec (S20). 5. Assert tagged SHA has green CI history (step-skipped on `pull_request`). Because `npm run test:gates` runs inside `version-gate`, a malformed pin (e.g. a commit SHA @@ -244,8 +247,11 @@ such a file. Never write `${{` in comments; describe it in words. reads as non-failing under `--admin` merge (PF-017) while leaving registries in a partial state. - `publish-testpypi` is intentionally NOT in `publish-crates`'s `needs:` — a TestPyPI failure should not abort the live release. -- The fixture `scripts/__test__/fixtures/protection-main.json` holds 6 contexts vs. 15 live — - known drift tracked as issue #341. +- The 2026-08 fixture `scripts/__test__/fixtures/protection-main.json` holds 6 contexts vs. + 15 live — kept byte-identical as a historical baseline. The 2026-09 fixtures added: + `checks-pr366-e02bcf2.json` (check-runs with suite ids), `runs-pr366-e02bcf2.json` + (workflow runs mapping suite ids to workflow files), and + `protection-main-2026-09.json` (15 live contexts, the current shape). - The `startup-race-probe` Cargo feature (`mds-cli`) must never ship enabled. - `debug-panics` Cargo feature must never ship enabled (all three binding crates). @@ -269,6 +275,11 @@ such a file. Never write `${{` in comments; describe it in words. holds no image for annotated tag object SHAs. Use a `vX.Y.Z` release tag (PF-040). - **Dispatching a dry run before that ref's `ci.yml` finishes**: the CI-history gate in `version-gate` fails closed on an in-progress or absent run. +- **Dropping `with: key:` from a matrix job's rust-cache step**: without a per-leg key all + legs on the same runner OS restore each other's `target//` artifacts and + host-built build scripts (PF-041). Confirmed live in run 34065573775: every Linux leg in + `build-napi` restored `v0-rust-build-napi-Linux-x64-6ff13d87-4c33221b`. Spec S20 in + `release-auth-probe.spec.mjs` fails `Version gate` if a key is removed (#347, #352). ## Gotchas @@ -298,6 +309,17 @@ such a file. Never write `${{` in comments; describe it in words. `release-${{ github.ref }}`); it does not cancel it. - **`version-gate` prints a `::notice::` not a `::skip::`** when it skips the CI-history step on `pull_request`: the job still completes `success`, which is the required state. +- **rust-cache cache scope**: `pull_request` caches live under `refs/pull/N/merge` and are + invisible to a `workflow_dispatch` on the branch. `main` has no `release.yml` caches + (the workflow never runs on push-to-main), so tag runs are always cold. Warm-cache + evidence comes from a second dispatch on the same branch ref (or a PR-run rerun) — + never from a dispatch that follows a PR run. +- **crates.io token — no read-only probe**: `GET /api/v1/me` is `AuthCheck::only_cookie()` + and returns HTTP 403 for any API token. The only token-accepting read route + (`GET /api/v1/me/tokens/{id}`) rejects scoped tokens with HTTP 403. The non-empty guard + in `version-gate` is therefore the strongest check available. A revoked token is first + detected at `cargo publish` (fail-before-write, after the build matrix is paid for); see + PF-023 and the v0.4.0 precedent (`gh run rerun --failed`). Durable fix tracked in #368. ## Key Files @@ -308,9 +330,15 @@ such a file. Never write `${{` in comments; describe it in words. - `scripts/__test__/verify-pr-checks.spec.mjs` — specs for the verifier (M10c, S13, S18 rules; length assertion for `EXPECTED_CONTEXTS`). - `scripts/__test__/release-auth-probe.spec.mjs` — specs for release.yml structure: pin shape - (S16), set equality S10, guard detection, no `${{ }}` literal (S19), `uses:` count (S14). -- `scripts/__test__/fixtures/protection-main.json` — 6-context branch protection fixture - (known drift from 15 live; tracked as #341). + (S16), set equality S10, guard detection, no dollar-brace-brace literal (S19), `uses:` count + (S14), per-leg cache key (S20), cargo token -z guard (S3 extension). +- `scripts/__test__/fixtures/protection-main.json` — 6-context branch protection (historical, + 2026-08 baseline; kept byte-identical). +- `scripts/__test__/fixtures/protection-main-2026-09.json` — 15-context branch protection + (current live shape, 2026-09). +- `scripts/__test__/fixtures/checks-pr366-e02bcf2.json` — check-runs with suite ids (2026-09). +- `scripts/__test__/fixtures/runs-pr366-e02bcf2.json` — workflow runs mapping suite ids to + workflow files (2026-09). - `RELEASING.md` — full release runbook including pre-flight checklist, tag-push procedure, and post-release verification. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4aa06d54..2282b87e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,9 +85,8 @@ jobs: echo "::error::NPM_TOKEN is empty or unset — npm publish would fail AFTER cargo publish (irreversible). Set the NPM_TOKEN repo secret. (security-08)" exit 1 fi - # Guard: cargo token must be non-empty. A missing token fails before the - # irreversible crates.io publish, but catching it here avoids burning the - # expensive 7-target cross-compile matrix. + # Guard: cargo token non-empty. That is the strongest check crates.io allows for an + # API token (no read-only endpoint accepts a scoped token; #345, RELEASING.md "credential probe"). if [ -z "$CARGO_REG_TOKEN" ]; then echo "::error::CARGO_REGISTRY_TOKEN is empty or unset — cargo publish would fail. Set the CARGO_REGISTRY_TOKEN repo secret. (security-08)" exit 1 @@ -350,7 +349,18 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.settings.target }} + # Per-leg cache key (PF-041, #352). The automatic key is job id + runner + # os/arch + a hash of the rustc HOST triple, env and lockfiles; the cross + # target is not part of it, so without `key:` all four ubuntu legs and both + # macOS legs restore ONE blob (run 34065573775: every Linux leg restored + # v0-rust-build-napi-Linux-x64-...). Keying on the target keeps each leg's + # target// artifacts and host-built build scripts to itself. The + # host is already encoded (runner.os/arch + rustc host hash), so only the + # target is added. Intentional; must survive rust-cache version bumps -- + # spec S20 in scripts/__test__/release-auth-probe.spec.mjs pins it. - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.settings.target }} - name: Install zig if: matrix.settings.use-zig uses: mlugg/setup-zig@v2 @@ -516,6 +526,11 @@ jobs: if: ${{ !matrix.sdist && runner.os != 'Linux' }} with: targets: ${{ matrix.target }} + # Per-leg cache key: target + manylinux flavour so a containerised Linux + # leg (glibc 2.17 manylinux / musl) never restores host-built build scripts + # or proc-macro .so files written by another leg; legs without manylinux + # render a trailing dash (harmless, validated warm in #347). Intentional; + # spec S20 in scripts/__test__/release-auth-probe.spec.mjs pins it. - uses: Swatinem/rust-cache@v2 if: ${{ !matrix.sdist }} with: @@ -984,6 +999,7 @@ jobs: with: ref: ${{ github.ref }} - uses: dtolnay/rust-toolchain@stable + # Single-leg job: the automatic job-id key is already unique; no per-leg key (#352). - uses: Swatinem/rust-cache@v2 - name: Verify tag matches workspace version run: | @@ -1079,6 +1095,7 @@ jobs: # update here to keep both in sync. - uses: dtolnay/rust-toolchain@01ba1edad32c6f80dbcce879d3e0fa5a00b2a84e # 1.96.0 with: { targets: wasm32-unknown-unknown } + # Single-leg job: the automatic job-id key is already unique; no per-leg key (#352). - uses: Swatinem/rust-cache@v2 - uses: ./.github/actions/setup-wasm - run: npm ci diff --git a/CHANGELOG.md b/CHANGELOG.md index b885ef47..9f5f6176 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - npm dependency sweep: relaxed the three phantom floor pins to caret ranges — fast-uri 3.1.5 → ^3.1.6 (oldest release patching GHSA-5jgf-p345-68v8, GHSA-fph4-wmhf-6fwf, GHSA-f65p-4m7j-42xc, GHSA-jqff-g426-hqxp), nanoid 3.3.18 → ^3.3.18, js-yaml 4.3.1 → ^4.3.1 (#336); @napi-rs/cli ^3.0.0 → ^3.8.6 (lock 3.7.0 → 3.8.6); vite lock 8.1.5 → 8.2.2; Dependabot `ignore` rules for semver-major bumps of the three phantom pins. Supersedes Dependabot #315 #332 #346 #362 #355 #357 #279. - GitHub Actions sweep: actions/checkout v6 → v7 (16 call sites: 9 ci.yml + 7 release.yml), actions/setup-node v6 → v7 (6 sites), actions/setup-python v5 → v7 (5 sites, ci.yml only; action runtime node20 → node24), PyO3/maturin-action pin normalized from the v1.51.0 annotated-tag object (`3e2bdf6`) to the commit it points to (`e83996d1`), same version (PF-040); Dependabot `ignore` for typescript semver-major version updates pending the TS 7 migration (#364). Supersedes Dependabot #111, #189, #241, #356; replaces #169. - Release-surface PR gate: `release.yml` now triggers on `pull_request` events touching `.github/workflows/release.yml`, `.github/actions/**`, `crates/mds-napi/**`, `crates/mds-python/**`, and `scripts/verify-napi-names.mjs`; adds an unguarded `rehearse-publish-python` job (PF-039) that rehearses `publish-python` without uploading — pin shape (both the annotated-tag-object SHA a892a5a6 and the commit SHA dc37677b are rejected as positive controls), GHCR manifest (404 + `MANIFEST_UNKNOWN` body required), bounded `docker pull` (3-attempt loop), and `twine check` from the pinned image with `--network none`, each with a positive control (PF-013, PF-040) — plus a `publish-testpypi` opt-in leg (dispatch-guarded); the rehearsal never invokes `pypa/gh-action-pypi-publish` (the action has no dry-run mode) and is denied `id-token`, both pinned by specs S14/S15; credential and OIDC probes run on `pull_request` events and fail closed on fork/Dependabot PRs (no secrets, no `id-token: write`) with an actionable error; `verify-pr-checks.mjs` requires `Version gate`, `Stage + verify platform packages` and `Rehearse PyPI publish (no upload)` on release-surface PRs and fails closed when the changed-file list cannot be read (#342, #350). +- `verify-pr-checks.mjs` suite keying (D-PR8): the skipped-publish allowance and D-PR7 context attribution are now keyed on the check-run's `check_suite.id` mapping to a `release.yml` workflow run (one bounded `GET /actions/runs?head_sha=` call); the verifier exits 2 when it cannot enumerate the head's workflow runs; 2026-09 branch-protection and check fixtures added (`checks-pr366-e02bcf2.json`, `runs-pr366-e02bcf2.json`, `protection-main-2026-09.json`) (#341). +- `build-napi` per-leg rust-cache key: adds `key: ${{ matrix.settings.target }}` to the `Swatinem/rust-cache` step so each cross-compile leg's target artifacts stay isolated (PF-041; without the key all four ubuntu legs and both macOS legs restored one shared blob, confirmed live in run 34065573775); `build-python`'s existing `key: matrix.target-matrix.manylinux` (#347) unchanged; spec S20 in `release-auth-probe.spec.mjs` pins both and fails `Version gate` if a key is dropped; spec S3 extended to pin the `-z` CARGO_REG_TOKEN guard in executable code; #345 verified that crates.io `GET /api/v1/me` is `AuthCheck::only_cookie()` (HTTP 403 for any API token) and the only token-accepting read route rejects scoped tokens — non-empty guard is the strongest check available, durable fix tracked in #368; #345 closed won't-fix-as-filed (#345 #352). ## [0.4.2] — 2026-09-03 diff --git a/RELEASING.md b/RELEASING.md index 2fc7f0db..0faf39bf 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -34,7 +34,10 @@ These are **not** automated and must be done before the first release: Provenance requires the `id-token: write` permission (already set on the publish job) plus publishing from GitHub Actions. 3. **Add the `CARGO_REGISTRY_TOKEN` repo secret** with publish rights to - `mds-core` and `mds-cli` on crates.io. + `mds-core` and `mds-cli` on crates.io. The workflow cannot probe this token + (crates.io has no read-only endpoint that accepts a scoped token — see the + credential probe under Pre-flight); verify it in the crates.io UI before + tagging (PF-023). 4. **Enable GitHub private vulnerability reporting** (Settings → Code security → Private vulnerability reporting) so the SECURITY.md flow works. 5. **Configure PyPI trusted publisher** for `markdown-script` at @@ -158,23 +161,39 @@ gh workflow run release.yml # workflow_dispatch — builds the 7-target # step. Publishes NOTHING. ``` -The dry-run workflow runs `version-gate` in full, which now includes the -**credential probe** (security-08): it calls `npm whoami` against the live -registry to verify the `NPM_TOKEN` is valid, guards `CARGO_REGISTRY_TOKEN` -for non-empty, and probes the PyPI trusted publisher via the OIDC mint-token -exchange. A revoked token, absent secret, or misconfigured trusted publisher -therefore fails the dry run — all before any irreversible crates.io release. - -Both probes **run on every event, including `pull_request`**. On fork and -Dependabot PRs — which receive no repository secrets and no `id-token: -write` — the probes fail closed with an actionable error: maintainers must -supersede with a first-party branch PR or dispatch `gh workflow run -release.yml --ref `. No PR run can reach a publish in any case, -so the fail-closed behaviour is informational, not a merge blocker by itself. - -**Note:** `npm whoami` verifies authentication, not publish rights to the -`@mdscript` scope. A read-only or wrongly-scoped token passes the probe but -fails at publish time. +The dry-run workflow runs `version-gate` in full, which includes the +**credential probe** (security-08). What each registry check proves: + +- **npm** (`npm whoami`): proves the token is accepted by the npm registry + (authentication). Does NOT prove publish rights to the `@mdscript` scope — + a read-only or wrongly-scoped token passes this check but fails at publish time. +- **PyPI**: the OIDC mint-token exchange (`pypi.org/_/oidc/mint-token`) proves the + trusted-publisher record matches the workflow. The minted token expires unused — + the probe is free and safe. +- **crates.io**: non-empty guard only. This is the strongest check the crates.io + API allows for an API token: `GET /api/v1/me` is `AuthCheck::only_cookie()` + (`src/controllers/user/me.rs:38-41`) and returns HTTP 403 for any token, scoped + or unscoped (`src/auth.rs:136-144`). The only token-accepting read endpoint, + `GET /api/v1/me/tokens/{id}` (`src/controllers/token.rs:269-282`), accepts + legacy unscoped tokens only — a scoped token (the least-privilege kind a publish + secret should be) is rejected there with HTTP 403. A well-formed but + revoked/deleted token gets HTTP 403 "authentication failed" (`src/auth.rs:297-303`); + a malformed token gets HTTP 401 "The given API token does not match the format + used by crates.io" (`src/auth.rs:295`, `InsecurelyGeneratedTokenRevoked`). In + practice this means a revoked crates.io token is first detected at the first + `cargo publish` (fail-before-write, after the build matrix has been paid for). + The v0.4.0 release experienced exactly this (run 33569514359 attempt 1 failed + at `Publish mds-core` with HTTP 403, nothing published, `gh run rerun --failed` + completed it; see PF-023). A durable fix — Trusted Publishing for crates.io — + is tracked in #368; #345 is closed as won't-fix-as-filed with this finding. + +All three checks **run on every event, including `pull_request`**. On fork and +Dependabot PRs — which receive no repository secrets and no `id-token: write` — +they fail closed with an actionable error: maintainers must supersede with a +first-party branch PR or dispatch `gh workflow run release.yml --ref `. +On release-surface PRs a fail-closed `Version gate` **blocks the merge** because +D-PR7 requires it (by design, not advisory) — supersede with a first-party branch +PR or dispatch by hand. Even though release-surface PRs now trigger `release.yml` automatically, a manual `gh workflow run release.yml --ref ` is still required in four @@ -245,7 +264,12 @@ Five jobs are expected-skipped on a standard `workflow_dispatch` dry run and are listed in `TIER_B_EXPECTED_SKIPPED` in `scripts/verify-pr-checks.mjs`: `Publish to crates.io`, `Publish to npm`, `Publish to PyPI`, `GitHub Release`, and `Publish to TestPyPI (rehearsal)`. The same five are skipped on a -release-surface PR run. +release-surface PR run. The skipped tolerance applies only to check-runs whose +`check_suite.id` maps (via `GET /actions/runs?head_sha=`) to a `release.yml` +check suite (any event); the three D-PR7 contexts (`Version gate`, +`Stage + verify platform packages`, `Rehearse PyPI publish (no upload)`) are +attributed by the same suite. The verifier exits 2 when it cannot enumerate the +head's workflow runs (D-PR8, #341). Confirm the **A3 name-gate** step (`scripts/verify-napi-names.mjs`) passes in that run. **This is a hard checkpoint** — if the generated platform package names or @@ -272,7 +296,11 @@ That path list lives in **two** places that must stay identical: the `scripts/verify-pr-checks.mjs`. Spec S10 compares them as sets — a filter the verifier does not know about would let a release-surface PR pass as a silent no-run (ADR-013 amendment). The verifier also fails closed (exit 2) if it -cannot enumerate the PR's changed files at all. +cannot enumerate the PR's changed files at all. The skipped-publish allowance +and D-PR7 context attribution are both keyed on the check-run's `release.yml` +check suite (D-PR8, #341 — any release.yml event counts, including `pull_request` +and `workflow_dispatch`); the verifier exits 2 when it cannot enumerate the +head's workflow runs. ## Release @@ -352,3 +380,4 @@ The `release.yml` workflow runs, in order: - wasm-opt = ["-Oz", "--enable-bulk-memory", "--enable-sign-ext", ...] is enabled in crates/mds-wasm/Cargo.toml; CI installs wasm-pack and Binaryen v129 via the composite action at .github/actions/setup-wasm/ (version pins live there). Local builds do not need system Binaryen — wasm-pack auto-downloads wasm-opt (v117) on first use; install Binaryen v129+ (brew install binaryen / apt install binaryen) only for offline builds, to override a stale wasm-opt on PATH, or to reproduce CI's exact release optimizer. - Platform packages are generated in CI only — they cannot be validated with a local npm pack; use the dry-run workflow instead. - Due to its temp-file-then-rename implementation, atomic_write_file does not preserve hard links, ACLs, extended attributes (xattrs), or owner/group metadata of the original file. +- `Swatinem/rust-cache` computes its key as `v0-rust[-]-----`; the env hash covers `rustc -vV` (the HOST triple) plus `CARGO*`/`CC*`/`CFLAGS` env. The cross TARGET is not in it, so without `key:` all four ubuntu legs in `build-napi` and both macOS legs share one blob. Both matrix jobs carry per-leg keys: `build-napi` uses `key: ${{ matrix.settings.target }}` (#352, PF-041); `build-python` uses `key: ${{ matrix.target }}-${{ matrix.manylinux }}` (#347) so a containerised Linux leg never restores host-built build scripts or proc-macro `.so` files written by another leg. `publish-crates` and `publish-npm` are single-leg and use the automatic key. Spec S20 in `scripts/__test__/release-auth-probe.spec.mjs` pins this — dropping `with: key:` from a matrix job's rust-cache step causes S20 to fail `Version gate`. Validation: two runs in the SAME cache scope — first run shows `No cache found.`; second shows `Restored from cache key "v0-rust--build-napi-..."` full match: true for every leg with the readelf gates green. `pull_request` caches live under `refs/pull/N/merge` and are invisible to a branch dispatch, so warm evidence comes from a second dispatch on the same branch (or a PR-run rerun), never from a dispatch that follows a PR run. diff --git a/scripts/__test__/release-auth-probe.spec.mjs b/scripts/__test__/release-auth-probe.spec.mjs index 5502a6d6..a764a533 100644 --- a/scripts/__test__/release-auth-probe.spec.mjs +++ b/scripts/__test__/release-auth-probe.spec.mjs @@ -189,6 +189,28 @@ describe('security-08: npm auth probe in version-gate', () => { 'version-gate must guard CARGO_REGISTRY_TOKEN via env: in the probe step; ' + `got section:\n${section}`, ); + // S3 extension: the -z guard must be in executable code, not inside a comment. + // Positive control (avoids PF-013): a section that binds the secret but places + // the guard line only inside a comment must return false. + const guardLineInComment = [ + ' env:', + ' CARGO_REG_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}', + ' run: |', + ' # if [ -z "$CARGO_REG_TOKEN" ]; then', + ' echo "do something else"', + ].join('\n'); + assert.ok( + !hasEmptyGuard(guardLineInComment), + 'positive control: hasEmptyGuard must return false when the guard line is only inside a comment', + ); + assert.ok( + hasEmptyGuard(section), + 'version-gate must contain an executable `if [ -z "$CARGO_REG_TOKEN" ]; then` guard ' + + '(#345, RELEASING.md "credential probe") — the strongest check crates.io allows for an ' + + 'API token; a missing token is first detected at cargo publish (fail-before-write) but ' + + 'this guard catches it before the expensive 7-target cross-compile matrix runs; ' + + `got section:\n${section}`, + ); }); // ------------------------------------------------------------------------- @@ -275,6 +297,16 @@ function stripCommentLines(text) { return text.split('\n').filter(l => !/^\s*#/.test(l)).join('\n'); } +/** + * True iff the text contains an executable (non-comment) line that matches + * `if [ -z "$CARGO_REG_TOKEN" ]; then` — the -z guard for the cargo token + * (#345). Uses stripCommentLines so a line that appears only inside a comment + * does not satisfy the check (positive control in S3). + */ +function hasEmptyGuard(text) { + return /^\s*if \[ -z "\$CARGO_REG_TOKEN" \]; then\s*$/m.test(stripCommentLines(text)); +} + /** * Extract the `on.pull_request.paths:` list as an array of path patterns. * Returns null when the trigger or its paths filter is absent. @@ -1018,3 +1050,207 @@ describe('B1: release-surface PR gate and rehearsal jobs', () => { }); }); + +// --------------------------------------------------------------------------- +// B2: per-leg rust-cache helpers and S20 spec +// --------------------------------------------------------------------------- + +/** + * Returns one entry per `- uses: Swatinem/rust-cache@` step in the comment- + * stripped job section, with the `key:` value from its `with:` block, or null + * when absent. A step ends at the next 6-space `- ` item in the stripped text. + */ +function rustCacheSteps(jobSection) { + const stripped = stripCommentLines(jobSection); + const lines = stripped.split('\n'); + const steps = []; + for (let i = 0; i < lines.length; i++) { + if (/^ - uses: Swatinem\/rust-cache@/.test(lines[i])) { + let key = null; + for (let j = i + 1; j < lines.length; j++) { + if (/^ - /.test(lines[j])) break; // next step + const km = /^\s+key:\s+(.+)$/.exec(lines[j]); + if (km) { key = km[1].trim(); break; } + } + steps.push({ key }); + } + } + return steps; +} + +/** + * True iff a 4-space `strategy:` line exists after comment stripping. + * Matrix jobs declare ` strategy:` inside the job body. + */ +function hasMatrix(jobSection) { + return stripCommentLines(jobSection).split('\n').some(l => /^ strategy:\s*$/.test(l)); +} + +describe('B2: per-leg rust-cache keys in matrix jobs (#352, PF-041)', () => { + + // ------------------------------------------------------------------------- + // S20: every Swatinem/rust-cache step in a matrix job must carry a `key:` + // that includes `matrix.` so each leg's compiled artifacts stay isolated. + // + // Without a per-leg key the automatic key (job-id + runner-os/arch + rustc + // host hash + lock hash) is SHARED across all legs on the same runner OS: + // all four ubuntu legs restore each other's target// blobs, and both + // macOS legs do the same (confirmed live: run 34065573775, every Linux leg + // restored `v0-rust-build-napi-Linux-x64-6ff13d87-4c33221b`). + // `build-python` already carries `key: matrix.target-matrix.manylinux` (#347); + // this spec extends that gate to `build-napi` (#352, PF-041). + // + // Non-vacuity: build-napi and build-python must have rust-cache steps, and + // publish-crates must NOT qualify (single-leg, no matrix → exempt). + // Failure message names PF-041, #347, #352. + // ------------------------------------------------------------------------- + test('S20: every rust-cache step in every matrix job carries a key containing matrix. (PF-041, #352)', () => { + + // --- Positive controls (PF-013) --- + + // PC1: bare step in a matrix job → key must be null + const bareJobSection = [ + ' fake-matrix:', + ' strategy:', + ' matrix:', + ' include:', + ' - target: aarch64', + ' steps:', + ' - uses: Swatinem/rust-cache@v2', + ' - name: Next step', + ' run: echo done', + ].join('\n'); + const bareSteps = rustCacheSteps(bareJobSection); + assert.equal(bareSteps.length, 1, 'PC1: bare step must be detected'); + assert.equal(bareSteps[0].key, null, 'PC1: bare step key must be null'); + + // PC2: static key → read back verbatim + const staticKeySection = [ + ' fake-matrix:', + ' strategy:', + ' matrix:', + ' include:', + ' - target: aarch64', + ' steps:', + ' - uses: Swatinem/rust-cache@v2', + ' with:', + ' key: my-static-key', + ' - name: Next step', + ' run: echo done', + ].join('\n'); + const staticSteps = rustCacheSteps(staticKeySection); + assert.equal(staticSteps.length, 1, 'PC2: static-key step must be detected'); + assert.equal(staticSteps[0].key, 'my-static-key', 'PC2: static key must be read verbatim'); + + // PC3: commented-out key → key must be null + const commentedKeySection = [ + ' fake-matrix:', + ' strategy:', + ' matrix:', + ' include:', + ' - target: aarch64', + ' steps:', + ' - uses: Swatinem/rust-cache@v2', + ' # with:', + ' # key: ${{ matrix.target }}', + ' - name: Next step', + ' run: echo done', + ].join('\n'); + const commentedSteps = rustCacheSteps(commentedKeySection); + assert.equal(commentedSteps.length, 1, 'PC3: step with commented key must be detected'); + assert.equal(commentedSteps[0].key, null, 'PC3: commented-out key must yield null'); + + // PC4: matrix-derived key → read back verbatim and contains matrix. + const matrixKeySection = [ + ' fake-matrix:', + ' strategy:', + ' matrix:', + ' include:', + ' - target: aarch64', + ' steps:', + ' - uses: Swatinem/rust-cache@v2', + ' with:', + ' key: ${{ matrix.settings.target }}', + ' - name: Next step', + ' run: echo done', + ].join('\n'); + const matrixSteps = rustCacheSteps(matrixKeySection); + assert.equal(matrixSteps.length, 1, 'PC4: matrix-key step must be detected'); + assert.equal( + matrixSteps[0].key, '${{ matrix.settings.target }}', + 'PC4: matrix key must be read back verbatim', + ); + assert.ok(matrixSteps[0].key.includes('matrix.'), 'PC4: matrix key must contain "matrix."'); + + // PC5: job with strategy: only in a comment → not a matrix job (exempt) + const commentedStrategySection = [ + ' fake-single:', + ' # strategy: not a real matrix', + ' steps:', + ' - uses: Swatinem/rust-cache@v2', + ].join('\n'); + assert.ok( + !hasMatrix(commentedStrategySection), + 'PC5: a job with strategy: only in a comment must not be treated as a matrix job', + ); + + // --- Non-vacuity: confirm checked set membership --- + + const buildNapiSection = extractJobSection(yml, 'build-napi'); + assert.ok(buildNapiSection !== null, 'non-vacuity: build-napi must exist'); + assert.ok( + rustCacheSteps(buildNapiSection).length > 0, + 'non-vacuity: build-napi must have at least one Swatinem/rust-cache step', + ); + assert.ok(hasMatrix(buildNapiSection), 'non-vacuity: build-napi must be a matrix job'); + + const buildPythonSection = extractJobSection(yml, 'build-python'); + assert.ok(buildPythonSection !== null, 'non-vacuity: build-python must exist'); + assert.ok( + rustCacheSteps(buildPythonSection).length > 0, + 'non-vacuity: build-python must have at least one Swatinem/rust-cache step', + ); + assert.ok(hasMatrix(buildPythonSection), 'non-vacuity: build-python must be a matrix job'); + + // publish-crates must NOT qualify (single-leg, exempt) + const publishCratesSection = extractJobSection(yml, 'publish-crates'); + assert.ok(publishCratesSection !== null, 'non-vacuity: publish-crates must exist'); + assert.ok( + !hasMatrix(publishCratesSection), + 'non-vacuity: publish-crates must NOT be a matrix job (single-leg, exempt from S20)', + ); + + // --- Core assertion: every rust-cache step in every matrix job has a matrix. key --- + const matrixJobIds = findAllJobIds(yml).filter(id => { + const section = extractJobSection(yml, id); + return section !== null && hasMatrix(section); + }); + + // Non-vacuity: checked set must include build-napi and build-python + assert.ok( + matrixJobIds.includes('build-napi'), + 'S20 non-vacuity: build-napi must be in the matrix job set', + ); + assert.ok( + matrixJobIds.includes('build-python'), + 'S20 non-vacuity: build-python must be in the matrix job set', + ); + + for (const id of matrixJobIds) { + const section = extractJobSection(yml, id); + const steps = rustCacheSteps(section); + for (const step of steps) { + assert.ok( + step.key !== null && step.key.includes('matrix.'), + `Matrix job "${id}" has a Swatinem/rust-cache step whose key is ` + + `${JSON.stringify(step.key)} — without a per-leg key all legs on the same ` + + `runner OS restore each other's target// artifacts (PF-041, confirmed ` + + `live in run 34065573775). build-python already keys on matrix.target + ` + + `matrix.manylinux (#347); build-napi must key on matrix.settings.target (#352). ` + + `Fix: add \`with:\\n key: \${{ matrix.settings.target }}\` under the step.`, + ); + } + } + }); + +}); From a45f2483b304b555cf31330ff03d956c14388403 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 7 Sep 2026 15:38:26 +0300 Subject: [PATCH 4/8] =?UTF-8?q?scripts(verify-pr-checks):=20D-PR8=20diagno?= =?UTF-8?q?stics=20and=20spec=20hygiene=20=E2=80=94=20suite=20ids=20in=20D?= =?UTF-8?q?-PR7=20messages,=20event-annotated=20suite=20log,=20docblock/co?= =?UTF-8?q?mment=20completeness,=20drop=20RED-phase=20residue,=20pin=20ass?= =?UTF-8?q?ertions,=20add=20positive=20controls=20(#341)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/__test__/verify-pr-checks.spec.mjs | 111 ++++++++++++++++----- scripts/verify-pr-checks.mjs | 57 +++++++---- 2 files changed, 123 insertions(+), 45 deletions(-) diff --git a/scripts/__test__/verify-pr-checks.spec.mjs b/scripts/__test__/verify-pr-checks.spec.mjs index ce08f0b5..f20825a7 100644 --- a/scripts/__test__/verify-pr-checks.spec.mjs +++ b/scripts/__test__/verify-pr-checks.spec.mjs @@ -2040,6 +2040,20 @@ describe('D-PR8: suite-keyed TIER_B_EXPECTED_SKIPPED allowance', () => { `failure must name "${name}"; got:\n${allLines}`, ); } + // Positive control: with releaseSuiteIds supplied the same five skips (with suite) are allowed → PASS + const skippedPublishRuns2 = [...TIER_B_EXPECTED_SKIPPED].map(name => + withSuite({ name, status: 'completed', conclusion: 'skipped' }), + ); + const runs2 = basePassingRunsWith(skippedPublishRuns2); + const result2 = evaluateChecks({ + requiredContexts: REQUIRED, + checkRuns: runs2, + statuses: [], + headSha: HEAD_113F472, + releaseSuiteIds: RELEASE_SUITES, + }); + assert.equal(result2.exitCode, 0, + 'control: with releaseSuiteIds supplied, five skips in release suite are allowed → PASS'); }); test('D-PR5h2: releaseSuiteIds undefined but no skipped allow-listed names and untouched surface → PASS; prints "not provided" line', () => { @@ -2050,7 +2064,7 @@ describe('D-PR8: suite-keyed TIER_B_EXPECTED_SKIPPED allowance', () => { statuses: [], headSha: HEAD_113F472, changedFiles: ['README.md'], - // relaseSuiteIds: undefined — not provided + // releaseSuiteIds: undefined — not provided }); assert.equal(result.exitCode, 0, 'undefined releaseSuiteIds with no skipped names and untouched surface must pass'); @@ -2059,6 +2073,22 @@ describe('D-PR8: suite-keyed TIER_B_EXPECTED_SKIPPED allowance', () => { allLines.includes('not provided'), `output must print the "not provided" state; got:\n${allLines}`, ); + // Positive control: adding one skipped allow-listed run without releaseSuiteIds → FAIL with DISABLED + const runs2 = basePassingRunsWith([ + { name: 'Publish to npm', status: 'completed', conclusion: 'skipped' }, + ]); + const result2 = evaluateChecks({ + requiredContexts: REQUIRED, + checkRuns: runs2, + statuses: [], + headSha: HEAD_113F472, + changedFiles: ['README.md'], + // releaseSuiteIds: undefined — not provided + }); + assert.equal(result2.exitCode, 1, + 'control: skipped allow-listed run with undefined releaseSuiteIds → FAIL with DISABLED'); + assert.ok(result2.lines.join('\n').includes('DISABLED'), + 'control must say DISABLED'); }); test('D-PR5i: live path, runs-API error → exit 2 (D-PR8, fail closed)', () => { @@ -2071,6 +2101,16 @@ describe('D-PR8: suite-keyed TIER_B_EXPECTED_SKIPPED allowance', () => { ]); assert.equal(main(['1'], runner, OK_GH_VERSION), 2, 'runs API error must exit 2 (D-PR8, fail closed)'); + // Positive control: valid runs response → not exit 2 + const runner2 = stubRunner([ + ['/pulls/', PR_OK], + ['/protection', PROTECTION_OK], + ['/check-runs', CHECKS_OK_WITH_HYGIENE], + ['/status', { statuses: [], total_count: 0 }], + ['/actions/runs', RUNS_RELEASE], + ]); + assert.notEqual(main(['1'], runner2, OK_GH_VERSION), 2, + 'control: valid runs response must not exit 2'); }); }); @@ -2096,6 +2136,19 @@ describe('D-PR5i2: fetchWorkflowRuns — bounded loop, total_count guard, projec assert.equal(result.exitCode, 2, 'pagination cap must produce exit 2 (D-PR8)'); assert.ok(calls <= 5, `must not exceed 5 page requests; made ${calls} (pinned to MAX_RUNS_PAGES=5)`); assert.ok(calls >= 1, 'must have made at least one request'); + // Positive control: 5 pages whose total_count is satisfied → ok:true + let calls2 = 0; + const fullPage2 = { + total_count: 500, + workflow_runs: Array.from({ length: 100 }, (_, i) => ({ + id: i, path: '.github/workflows/ci.yml', event: 'pull_request', + check_suite_id: i + 1, conclusion: 'success', + })), + }; + const runner2 = (_args) => { calls2++; return fullPage2; }; + const result2 = fetchWorkflowRuns('abc123sha', runner2); + assert.ok(result2.ok, `control: 5 pages with satisfied total_count must return ok:true; got: ${JSON.stringify(result2)}`); + assert.equal(calls2, 5, `control: must have made exactly 5 requests; made ${calls2}`); }); test('total_count mismatch → ok:false/exitCode 2 (D-PR8)', () => { @@ -2110,6 +2163,13 @@ describe('D-PR5i2: fetchWorkflowRuns — bounded loop, total_count guard, projec result.message.includes('total_count') || result.message.includes('D-PR8'), `message must mention total_count or D-PR8; got: ${result.message}`, ); + // Positive control: matching total_count → ok:true + const runner2 = (_args) => ({ + total_count: 1, + workflow_runs: [{ id: 1, path: '.github/workflows/release.yml', event: 'pull_request', check_suite_id: 123, conclusion: 'success' }], + }); + const result2 = fetchWorkflowRuns('abc123sha', runner2); + assert.ok(result2.ok, `control: matching total_count must return ok:true; got: ${JSON.stringify(result2)}`); }); test('single complete page → ok:true with ONLY the projected fields', () => { @@ -2266,26 +2326,7 @@ describe('D-PR7k, D-PR7l: D-PR7 attribution keyed on release suite identity', () ]; } - test('D-PR7k: release-surface contexts present ONLY in ci.yml suite → exit 1; "absent from every … check-suite" AND "other suites ignored"', () => { - const releaseRunsInCiSuite = RELEASE_SURFACE_CONTEXTS.map(name => ({ - name, status: 'completed', conclusion: 'success', check_suite: { id: CI_SUITE_113F472 }, - })); - const checkRuns = passingRunsWith(releaseRunsInCiSuite); - const result = evaluateChecks({ - requiredContexts: REQUIRED, - checkRuns, - statuses: [], - headSha: HEAD_113F472, - changedFiles: ['.github/workflows/release.yml'], - relaseSuiteIds: RELEASE_SUITES, // intentional typo for RED — see correction in code - }); - // This test is RED until the code is implemented (relaseSuiteIds → undefined → fail closed) - // After fix: releaseSuiteIds will be used correctly - assert.equal(result.exitCode, 1, - 'release-surface contexts in non-release suite must exit 1 (D-PR8)'); - }); - - test('D-PR7k (corrected): contexts in ci.yml suite → exit 1; message cites "absent from every .github/workflows/release.yml" and N same-name runs ignored', () => { + test('D-PR7k: contexts in ci.yml suite → exit 1; message cites "absent from every .github/workflows/release.yml" and N same-name runs ignored', () => { const releaseRunsInCiSuite = RELEASE_SURFACE_CONTEXTS.map(name => ({ name, status: 'completed', conclusion: 'success', check_suite: { id: CI_SUITE_113F472 }, })); @@ -2301,13 +2342,16 @@ describe('D-PR7k, D-PR7l: D-PR7 attribution keyed on release suite identity', () assert.equal(result.exitCode, 1, 'contexts only in ci.yml suite must fail (D-PR8)'); const allLines = result.lines.join('\n'); assert.ok( - allLines.includes('absent from every .github/workflows/release.yml') || - allLines.includes('absent from every'), - `must say "absent from every .github/workflows/release.yml"; got:\n${allLines}`, + allLines.includes('absent from every .github/workflows/release.yml check-suite on this head'), + `must say "absent from every .github/workflows/release.yml check-suite on this head"; got:\n${allLines}`, ); assert.ok( - allLines.includes('other suites ignored') || allLines.includes('ignored'), - `must mention that same-name runs in other suites are ignored; got:\n${allLines}`, + allLines.includes('same-name run(s) in other suites ignored'), + `must mention "same-name run(s) in other suites ignored"; got:\n${allLines}`, + ); + assert.ok( + allLines.includes(`release suites: ${RELEASE_SUITE}`), + `must list release suite id ${RELEASE_SUITE}; got:\n${allLines}`, ); // Positive control: same runs but CI_SUITE_113F472 IS a release suite → PASS const result2 = evaluateChecks({ @@ -2346,6 +2390,21 @@ describe('D-PR7k, D-PR7l: D-PR7 attribution keyed on release suite identity', () for (const ctx of RELEASE_SURFACE_CONTEXTS) { assert.ok(allLines.includes(ctx), `must name context "${ctx}"; got:\n${allLines}`); } + // Positive control: supplying RELEASE_SUITES with attributed successful runs → 0 D-PR7 failures + const releaseRuns2 = RELEASE_SURFACE_CONTEXTS.map(name => ({ + name, status: 'completed', conclusion: 'success', check_suite: { id: RELEASE_SUITE }, + })); + const checkRuns2 = passingRunsWith(releaseRuns2); + const result2 = evaluateChecks({ + requiredContexts: REQUIRED, + checkRuns: checkRuns2, + statuses: [], + headSha: HEAD_113F472, + changedFiles: ['.github/workflows/release.yml'], + releaseSuiteIds: RELEASE_SUITES, + }); + assert.equal(result2.exitCode, 0, + 'control: RELEASE_SUITES with properly attributed runs → 0 D-PR7 failures → PASS'); }); }); diff --git a/scripts/verify-pr-checks.mjs b/scripts/verify-pr-checks.mjs index a16eaa17..c2720a34 100644 --- a/scripts/verify-pr-checks.mjs +++ b/scripts/verify-pr-checks.mjs @@ -70,12 +70,26 @@ * * D-PR8: Suite-keyed skipped-publish allowance — Tier B's allow-listed skip * (TIER_B_EXPECTED_SKIPPED) and D-PR7's release-surface context check - * now additionally require that the check-run belongs to a check-suite - * originating from .github/workflows/release.yml. The mapping is built - * by fetchWorkflowRuns (bounded at MAX_RUNS_PAGES=5 pages) which maps - * check_suite_id → workflow path via the Actions runs API. A skipped - * allow-listed run in a ci.yml or CodeQL suite is NOT allowed — only - * release.yml suites grant the skipped-publish exception. + * both require that the check-run belongs to a check-suite originating + * from .github/workflows/release.yml. The suite set is built by ONE + * bounded (MAX_RUNS_PAGES), total_count-asserted GET /actions/runs?head_sha= + * call (fetchWorkflowRuns — the LAST fetch in main()). Any release.yml + * event qualifies: pull_request, workflow_dispatch, push, schedule, etc. + * (The RELEASING.md dry-run is a workflow_dispatch; PR #365 carries a + * dispatch-only release suite 92246852351 alongside ci.yml runs.) + * + * Fail-closed matrix (all → exit 2 or exit 1): + * runs API error / >MAX_RUNS_PAGES pages / total_count mismatch → exit 2 + * zero release suites → allow-listed skips fail (D-PR8 DISABLED) and + * D-PR7 contexts read as absent (exit 1) + * missing check_suite on a skipped allow-listed run → fail (exit 1) + * skipped run's suite not in releaseSuiteIds → fail, names both ids (exit 1) + * releaseSuiteIds undefined → allowance disabled; D-PR7 cannot attribute + * several release suites → each run attributed on its own; all must pass + * + * app.slug is NOT consulted: every id in releaseSuiteIds came from + * /actions/runs (GitHub Actions runs only), so the CodeQL umbrella suite + * never appears — no app-slug filter is needed. * * Usage: * node scripts/verify-pr-checks.mjs @@ -147,9 +161,11 @@ export const EXPECTED_CONTEXTS = [ // guarded by startsWith(github.ref, 'refs/tags/v') (or the testpypi dispatch // input), so the RELEASING.md dry-run dispatched on a PR branch reports them // on the PR head as conclusion=skipped. That skip IS the guard working, not a -// missing verification. Only these five names, only when 'skipped', pass Tier -// B; any other conclusion (cancelled, failure, neutral, null) still fails, and -// a skipped run under any other name still fails. +// missing verification. The name is one of THREE keys — the run must also be +// `skipped` AND sit in a release.yml check-suite (D-PR8); any other conclusion +// (cancelled, failure, neutral, null) still fails, and a skipped run under any +// other name still fails. M10c/S13 in release-auth-probe.spec.mjs keep this +// set equal to release.yml's guarded jobs. // ADR-013 amendment (2026-09-06): 'Publish to TestPyPI (rehearsal)' is // dispatch-input-guarded — skipped everywhere except `workflow_dispatch -f // testpypi=true`. Adding it to this set so the mandatory pre-merge verifier @@ -583,7 +599,7 @@ export function evaluateChecks({ if (releaseSuiteIds === undefined) { failures.push( `Tier B (non-required): "${cr.name}" — conclusion=skipped (DISABLED: ` + - `releaseSuiteIds not provided; D-PR8 suite-keying requires fetchWorkflowRuns to run first)`, + `releaseSuiteIds undefined; D-PR8 suite-keying requires fetchWorkflowRuns to run first)`, ); pass = false; continue; @@ -683,13 +699,14 @@ export function evaluateChecks({ ); const ignoredCount = allRunsForName.length - releaseRuns.length; if (releaseRuns.length === 0) { + const relIds = [...releaseSuiteIds].join(', ') || 'none'; const ignoredNote = ignoredCount > 0 - ? ` (${ignoredCount} same-name run(s) in other suites ignored — D-PR8: absent from every .github/workflows/release.yml suite)` + ? `; ${ignoredCount} same-name run(s) in other suites ignored` : ''; failures.push( `D-PR7 (release surface): "${ctx}" absent from every .github/workflows/release.yml` + - ` check-suite — the PR touches the release surface but the release.yml pull_request ` + - `run is missing, not finished, or in another suite (#342)${ignoredNote}`, + ` check-suite on this head (release suites: ${relIds}${ignoredNote})` + + ` — the PR touches the release surface but release.yml's run is missing, not finished, or failed (#342, D-PR8)`, ); pass = false; } else { @@ -699,8 +716,9 @@ export function evaluateChecks({ for (const cr of releaseRuns) { if (cr.status !== 'completed' || cr.conclusion !== 'success') { failures.push( - `D-PR7 (release surface): "${ctx}" — status=${cr.status}, conclusion=${cr.conclusion ?? 'null'} ` + - `— the PR touches the release surface but release.yml's pull_request run failed (#342)`, + `D-PR7 (release surface): "${ctx}" — status=${cr.status}, conclusion=${cr.conclusion ?? 'null'}` + + ` (suite ${cr.check_suite?.id})` + + ` — the PR touches the release surface but release.yml's run failed (#342, D-PR8)`, ); pass = false; } @@ -1186,10 +1204,11 @@ export function main(argv = process.argv.slice(2), runner = defaultGhRunner, ghV return runsResult.exitCode; } const releaseSuiteIds = releaseSuiteIdsFrom(runsResult.runs); - console.log( - ` workflow runs: ${runsResult.runs.length}; release.yml suite(s): ${releaseSuiteIds.size}` + - (releaseSuiteIds.size > 0 ? ` (${[...releaseSuiteIds].join(', ')})` : ''), - ); + const releaseRunsForLog = runsResult.runs.filter(r => releaseSuiteIds.has(r.check_suite_id)); + const suitesStr = releaseRunsForLog.length > 0 + ? releaseRunsForLog.map(r => `${r.check_suite_id} (${r.event})`).join(', ') + : 'none'; + console.log(` workflow runs on head: ${runsResult.runs.length}; release.yml suites: ${suitesStr}`); // ---- Evaluate (D-PR1: pure function) ---- const result = evaluateChecks({ From a689b8ea41fa5435d2a68c75b9ed14872dd27f2a Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 7 Sep 2026 15:42:30 +0300 Subject: [PATCH 5/8] docs(release): note crates.io's User-Agent requirement; code-span fix; keep KB S19 wording --- .devflow/features/release-pipeline/KNOWLEDGE.md | 2 +- RELEASING.md | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.devflow/features/release-pipeline/KNOWLEDGE.md b/.devflow/features/release-pipeline/KNOWLEDGE.md index bdb8ca60..766f0725 100644 --- a/.devflow/features/release-pipeline/KNOWLEDGE.md +++ b/.devflow/features/release-pipeline/KNOWLEDGE.md @@ -330,7 +330,7 @@ such a file. Never write `${{` in comments; describe it in words. - `scripts/__test__/verify-pr-checks.spec.mjs` — specs for the verifier (M10c, S13, S18 rules; length assertion for `EXPECTED_CONTEXTS`). - `scripts/__test__/release-auth-probe.spec.mjs` — specs for release.yml structure: pin shape - (S16), set equality S10, guard detection, no dollar-brace-brace literal (S19), `uses:` count + (S16), set equality S10, guard detection, no `${{ }}` literal (S19), `uses:` count (S14), per-leg cache key (S20), cargo token -z guard (S3 extension). - `scripts/__test__/fixtures/protection-main.json` — 6-context branch protection (historical, 2026-08 baseline; kept byte-identical). diff --git a/RELEASING.md b/RELEASING.md index 0faf39bf..46d23732 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -179,7 +179,11 @@ The dry-run workflow runs `version-gate` in full, which includes the secret should be) is rejected there with HTTP 403. A well-formed but revoked/deleted token gets HTTP 403 "authentication failed" (`src/auth.rs:297-303`); a malformed token gets HTTP 401 "The given API token does not match the format - used by crates.io" (`src/auth.rs:295`, `InsecurelyGeneratedTokenRevoked`). In + used by crates.io" (`src/auth.rs:295`, `InsecurelyGeneratedTokenRevoked`). + Every crates.io API request must also carry a `User-Agent` header — without one + the `require_user_agent` middleware (`src/middleware/require_user_agent.rs:35-47`) + returns HTTP 403 with a plaintext body before authentication runs, so a manual + probe without that header fails for an unrelated reason. In practice this means a revoked crates.io token is first detected at the first `cargo publish` (fail-before-write, after the build matrix has been paid for). The v0.4.0 release experienced exactly this (run 33569514359 attempt 1 failed @@ -380,4 +384,4 @@ The `release.yml` workflow runs, in order: - wasm-opt = ["-Oz", "--enable-bulk-memory", "--enable-sign-ext", ...] is enabled in crates/mds-wasm/Cargo.toml; CI installs wasm-pack and Binaryen v129 via the composite action at .github/actions/setup-wasm/ (version pins live there). Local builds do not need system Binaryen — wasm-pack auto-downloads wasm-opt (v117) on first use; install Binaryen v129+ (brew install binaryen / apt install binaryen) only for offline builds, to override a stale wasm-opt on PATH, or to reproduce CI's exact release optimizer. - Platform packages are generated in CI only — they cannot be validated with a local npm pack; use the dry-run workflow instead. - Due to its temp-file-then-rename implementation, atomic_write_file does not preserve hard links, ACLs, extended attributes (xattrs), or owner/group metadata of the original file. -- `Swatinem/rust-cache` computes its key as `v0-rust[-]-----`; the env hash covers `rustc -vV` (the HOST triple) plus `CARGO*`/`CC*`/`CFLAGS` env. The cross TARGET is not in it, so without `key:` all four ubuntu legs in `build-napi` and both macOS legs share one blob. Both matrix jobs carry per-leg keys: `build-napi` uses `key: ${{ matrix.settings.target }}` (#352, PF-041); `build-python` uses `key: ${{ matrix.target }}-${{ matrix.manylinux }}` (#347) so a containerised Linux leg never restores host-built build scripts or proc-macro `.so` files written by another leg. `publish-crates` and `publish-npm` are single-leg and use the automatic key. Spec S20 in `scripts/__test__/release-auth-probe.spec.mjs` pins this — dropping `with: key:` from a matrix job's rust-cache step causes S20 to fail `Version gate`. Validation: two runs in the SAME cache scope — first run shows `No cache found.`; second shows `Restored from cache key "v0-rust--build-napi-..."` full match: true for every leg with the readelf gates green. `pull_request` caches live under `refs/pull/N/merge` and are invisible to a branch dispatch, so warm evidence comes from a second dispatch on the same branch (or a PR-run rerun), never from a dispatch that follows a PR run. +- `Swatinem/rust-cache` computes its key as `v0-rust[-]-----`; the env hash covers `rustc -vV` (the HOST triple) plus `CARGO*`/`CC*`/`CFLAGS` env. The cross TARGET is not in it, so without `key:` all four ubuntu legs in `build-napi` and both macOS legs share one blob. Both matrix jobs carry per-leg keys: `build-napi` uses `key: ${{ matrix.settings.target }}` (#352, PF-041); `build-python` uses `key: ${{ matrix.target }}-${{ matrix.manylinux }}` (#347) so a containerised Linux leg never restores host-built build scripts or proc-macro `.so` files written by another leg. `publish-crates` and `publish-npm` are single-leg and use the automatic key. Spec S20 in `scripts/__test__/release-auth-probe.spec.mjs` pins this — dropping `with: key:` from a matrix job's rust-cache step causes S20 to fail `Version gate`. Validation: two runs in the SAME cache scope — first run shows `No cache found.`; second shows `Restored from cache key "v0-rust--build-napi-…" full match: true.` for every leg with the readelf gates green. `pull_request` caches live under `refs/pull/N/merge` and are invisible to a branch dispatch, so warm evidence comes from a second dispatch on the same branch (or a PR-run rerun), never from a dispatch that follows a PR run. From 321d8403ed8be0f5aa510d4cc1c1f914303ad48c Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 7 Sep 2026 15:49:09 +0300 Subject: [PATCH 6/8] =?UTF-8?q?refactor(scripts):=20tidy=20D-PR8/S20=20hel?= =?UTF-8?q?pers=20=E2=80=94=20no=20behaviour=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the repeated `[...releaseSuiteIds].join(', ') || 'none'` expression (Tier B line and D-PR7 absent-run line in evaluateChecks) into a private `formatSuiteIds` helper. No logic change; 210/210 gate tests pass. --- scripts/verify-pr-checks.mjs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/verify-pr-checks.mjs b/scripts/verify-pr-checks.mjs index c2720a34..d22d8475 100644 --- a/scripts/verify-pr-checks.mjs +++ b/scripts/verify-pr-checks.mjs @@ -268,6 +268,11 @@ export function releaseSuiteIdsFrom(runs) { return ids; } +/** Format a set of suite ids for diagnostic messages. Returns 'none' when the set is empty. */ +function formatSuiteIds(ids) { + return [...ids].join(', ') || 'none'; +} + // --------------------------------------------------------------------------- // gh runner (thin IO shim; injected in tests for offline operation) // --------------------------------------------------------------------------- @@ -614,7 +619,7 @@ export function evaluateChecks({ continue; } if (!releaseSuiteIds.has(suiteId)) { - const relIds = [...releaseSuiteIds].join(', ') || 'none'; + const relIds = formatSuiteIds(releaseSuiteIds); failures.push( `Tier B (non-required): "${cr.name}" — conclusion=skipped in suite ${suiteId} ` + `which is NOT a release.yml suite (D-PR8: allowed release suite ids: ${relIds})`, @@ -699,7 +704,7 @@ export function evaluateChecks({ ); const ignoredCount = allRunsForName.length - releaseRuns.length; if (releaseRuns.length === 0) { - const relIds = [...releaseSuiteIds].join(', ') || 'none'; + const relIds = formatSuiteIds(releaseSuiteIds); const ignoredNote = ignoredCount > 0 ? `; ${ignoredCount} same-name run(s) in other suites ignored` : ''; From b4b33db4649192451a1233b5e8fe34ea72e47ae8 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 7 Sep 2026 15:59:41 +0300 Subject: [PATCH 7/8] fix(scripts): harden S20 step/matrix parsing; drop spec-comment residue (#352, #341) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S20's rustCacheSteps anchored detection on `- uses: Swatinem/rust-cache@`, so a step written `- name: …` / ` uses: Swatinem/rust-cache@…` returned zero steps and the core per-step assertion iterated nothing. Verified against the live file: with build-napi's step reordered that way the old helper returns [] (only the separate build-napi/build-python non-vacuity guards caught it; a third matrix job would have passed vacuously). Segment steps first, then match `uses:` anywhere in the segment, and read `key:` from within the segment so a bare step cannot borrow the next step's key. hasMatrix anchored `^ strategy:\s*$`, so `strategy:` with a trailing inline comment or written as a flow mapping read as "not a matrix job" and exempted the whole job from S20. Match `^ strategy:(\s|$)` instead. Both hardenings are mutation-verified: with the key dropped and the step reordered (or `strategy:` given a trailing comment) S20 fails with the PF-041 message; release.yml restored byte-identical afterwards. New positive controls PC5b (trailing comment / flow mapping), PC6 (uses: not first key) and PC7 (bare step must not borrow a later step's key) live inside the existing S20 test, so `npm run test:gates` stays 210 pass / 0 fail. Also: remove two spec comments citing commit 3a803f5 as the source of the D-PR7j all-must-pass contract (main's D-PR7j spec cites no commit), remove a stray `// correct spelling` marker, and correct the release-pipeline KB's test:gates count from 211 to 210. --- .../features/release-pipeline/KNOWLEDGE.md | 2 +- scripts/__test__/release-auth-probe.spec.mjs | 99 ++++++++++++++++--- scripts/__test__/verify-pr-checks.spec.mjs | 5 +- 3 files changed, 86 insertions(+), 20 deletions(-) diff --git a/.devflow/features/release-pipeline/KNOWLEDGE.md b/.devflow/features/release-pipeline/KNOWLEDGE.md index 766f0725..a1bd5b9f 100644 --- a/.devflow/features/release-pipeline/KNOWLEDGE.md +++ b/.devflow/features/release-pipeline/KNOWLEDGE.md @@ -173,7 +173,7 @@ Steps (in order): 1. Verify publish credentials (npm `whoami` + cargo token non-empty + PyPI OIDC mint-token exchange). 2. Assert synchronized versions, no `file:` refs. 3. Assert no hazardous codepoints in tracked source. -4. Run `npm run test:gates` — all four spec files, 211 tests including pin-shape specs (S16) and per-leg cache key spec (S20). +4. Run `npm run test:gates` — all four spec files, 210 tests including pin-shape specs (S16) and per-leg cache key spec (S20). 5. Assert tagged SHA has green CI history (step-skipped on `pull_request`). Because `npm run test:gates` runs inside `version-gate`, a malformed pin (e.g. a commit SHA diff --git a/scripts/__test__/release-auth-probe.spec.mjs b/scripts/__test__/release-auth-probe.spec.mjs index a764a533..f0c67541 100644 --- a/scripts/__test__/release-auth-probe.spec.mjs +++ b/scripts/__test__/release-auth-probe.spec.mjs @@ -1056,34 +1056,47 @@ describe('B1: release-surface PR gate and rehearsal jobs', () => { // --------------------------------------------------------------------------- /** - * Returns one entry per `- uses: Swatinem/rust-cache@` step in the comment- - * stripped job section, with the `key:` value from its `with:` block, or null - * when absent. A step ends at the next 6-space `- ` item in the stripped text. + * Returns one entry per Swatinem/rust-cache step in the comment-stripped job + * section, with the `key:` value from its `with:` block, or null when absent. + * + * Steps are segmented FIRST — a step runs from its 6-space `- ` line to the + * next one — and each segment is then tested for a `uses: Swatinem/rust-cache@` + * line at ANY position. Anchoring detection on `- uses:` would miss a step + * written `- name: …` / ` uses: Swatinem/rust-cache@…`, so a benign reorder + * would silently stop S20 from gating that step (avoids PF-013). Reading the + * key from the segment (rather than scanning forward until the next step) + * likewise cannot borrow a `key:` belonging to a different step. */ function rustCacheSteps(jobSection) { - const stripped = stripCommentLines(jobSection); - const lines = stripped.split('\n'); - const steps = []; + const lines = stripCommentLines(jobSection).split('\n'); + const starts = []; for (let i = 0; i < lines.length; i++) { - if (/^ - uses: Swatinem\/rust-cache@/.test(lines[i])) { - let key = null; - for (let j = i + 1; j < lines.length; j++) { - if (/^ - /.test(lines[j])) break; // next step - const km = /^\s+key:\s+(.+)$/.exec(lines[j]); - if (km) { key = km[1].trim(); break; } - } - steps.push({ key }); + if (/^ - /.test(lines[i])) starts.push(i); + } + const steps = []; + for (const [n, start] of starts.entries()) { + const body = lines.slice(start, starts[n + 1] ?? lines.length); + if (!body.some(l => /^\s*(- )?uses:\s*Swatinem\/rust-cache@/.test(l))) continue; + let key = null; + for (const line of body) { + const km = /^\s+key:\s+(.+)$/.exec(line); + if (km) { key = km[1].trim(); break; } } + steps.push({ key }); } return steps; } /** - * True iff a 4-space `strategy:` line exists after comment stripping. + * True iff a 4-space `strategy:` key exists after comment stripping. * Matrix jobs declare ` strategy:` inside the job body. + * + * Trailing content is deliberately NOT anchored: `strategy:` followed by an + * inline comment or written as a flow mapping must still count, or a matrix + * job would be silently exempted from S20 (avoids PF-013). */ function hasMatrix(jobSection) { - return stripCommentLines(jobSection).split('\n').some(l => /^ strategy:\s*$/.test(l)); + return stripCommentLines(jobSection).split('\n').some(l => /^ strategy:(\s|$)/.test(l)); } describe('B2: per-leg rust-cache keys in matrix jobs (#352, PF-041)', () => { @@ -1194,6 +1207,60 @@ describe('B2: per-leg rust-cache keys in matrix jobs (#352, PF-041)', () => { 'PC5: a job with strategy: only in a comment must not be treated as a matrix job', ); + // PC5b: `strategy:` carrying trailing content (inline comment, flow mapping) + // must STILL count as a matrix job — anchoring on end-of-line would exempt a + // real matrix job from S20 without any test failing (avoids PF-013). + assert.ok( + hasMatrix([' fake:', ' strategy: # fail-fast tuned below', ' steps:'].join('\n')), + 'PC5b: strategy: with a trailing inline comment must still be a matrix job', + ); + assert.ok( + hasMatrix([' fake:', ' strategy: { matrix: { target: [a, b] } }', ' steps:'].join('\n')), + 'PC5b: strategy: written as a flow mapping must still be a matrix job', + ); + + // PC6: a rust-cache step whose `uses:` is NOT the first key must still be + // detected, and its key read. Anchoring detection on `- uses:` would let a + // benign reorder (adding a `name:`) silently disable S20 for that step. + const nameFirstSection = [ + ' fake-matrix:', + ' strategy:', + ' matrix:', + ' include:', + ' - target: aarch64', + ' steps:', + ' - name: Cache Rust artifacts', + ' uses: Swatinem/rust-cache@v2', + ' with:', + ' key: ${{ matrix.target }}', + ' - name: Next step', + ' run: echo done', + ].join('\n'); + const nameFirstSteps = rustCacheSteps(nameFirstSection); + assert.equal(nameFirstSteps.length, 1, + 'PC6: a rust-cache step whose uses: is not the first key must still be detected'); + assert.equal(nameFirstSteps[0].key, '${{ matrix.target }}', + 'PC6: key must be read from a step whose uses: is not the first key'); + + // PC7 (negative): a `key:` that belongs to a LATER, non-rust-cache step must + // not be borrowed by a bare rust-cache step that precedes it. + const borrowedKeySection = [ + ' fake-matrix:', + ' strategy:', + ' matrix:', + ' include:', + ' - target: aarch64', + ' steps:', + ' - uses: Swatinem/rust-cache@v2', + ' - uses: actions/cache@v4', + ' with:', + ' key: someone-elses-key', + ].join('\n'); + const borrowedSteps = rustCacheSteps(borrowedKeySection); + assert.equal(borrowedSteps.length, 1, 'PC7: only the rust-cache step must be collected'); + assert.equal(borrowedSteps[0].key, null, + 'PC7: a bare rust-cache step must not borrow the next step\'s key'); + // --- Non-vacuity: confirm checked set membership --- const buildNapiSection = extractJobSection(yml, 'build-napi'); diff --git a/scripts/__test__/verify-pr-checks.spec.mjs b/scripts/__test__/verify-pr-checks.spec.mjs index f20825a7..58bf66ed 100644 --- a/scripts/__test__/verify-pr-checks.spec.mjs +++ b/scripts/__test__/verify-pr-checks.spec.mjs @@ -1894,7 +1894,6 @@ describe('D-PR7: release-surface presence check', () => { // re-run mask a red sibling — a fail-open in a merge gate. // ------------------------------------------------------------------------- test('D-PR7j: a failed and a succeeded run sharing a release-surface name → exit 1 (all must pass)', () => { - // 3a803f5 is the commit cited by the original D-PR7j all-must-pass spec. const checkRuns = passingRunsWith([ ...releaseSuccessRuns(), // A second run under a name that already has a success above, in the SAME release suite. @@ -2291,7 +2290,7 @@ describe('D-PR5k: multiple release suites on one head (dispatch + pull_request)' assert.equal(result.exitCode, 0, 'both release suites must allow their skipped runs → pass'); - // Control: flip TestPyPI in SUITE_B to failure → exit 1 (all-must-pass, D-PR7j ref 3a803f5) + // Control: flip TestPyPI in SUITE_B to failure → exit 1 (all-must-pass, D-PR7j) const runs2 = basePassingRunsWith([ ...['Publish to crates.io', 'Publish to npm', 'Publish to PyPI', 'GitHub Release'].map(name => withSuite({ name, status: 'completed', conclusion: 'skipped' }, SUITE_A), @@ -2337,7 +2336,7 @@ describe('D-PR7k, D-PR7l: D-PR7 attribution keyed on release suite identity', () statuses: [], headSha: HEAD_113F472, changedFiles: ['.github/workflows/release.yml'], - releaseSuiteIds: RELEASE_SUITES, // correct spelling + releaseSuiteIds: RELEASE_SUITES, }); assert.equal(result.exitCode, 1, 'contexts only in ci.yml suite must fail (D-PR8)'); const allLines = result.lines.join('\n'); From 183fc84639870c2cfba9512a667775a9d2c42be5 Mon Sep 17 00:00:00 2001 From: Dean Sharon Date: Mon, 7 Sep 2026 16:16:02 +0300 Subject: [PATCH 8/8] scripts(verify-pr-checks): correct the D-PR8 app.slug rationale (the dynamic CodeQL run does appear in /actions/runs; the path filter is what excludes it); PF-013 in fetch messages; spec naming/shape nits (#341) --- RELEASING.md | 3 ++- scripts/__test__/verify-pr-checks.spec.mjs | 10 ++++++---- scripts/verify-pr-checks.mjs | 22 ++++++++++++++-------- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 46d23732..fd31c77a 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -176,7 +176,8 @@ The dry-run workflow runs `version-gate` in full, which includes the or unscoped (`src/auth.rs:136-144`). The only token-accepting read endpoint, `GET /api/v1/me/tokens/{id}` (`src/controllers/token.rs:269-282`), accepts legacy unscoped tokens only — a scoped token (the least-privilege kind a publish - secret should be) is rejected there with HTTP 403. A well-formed but + secret should be) is rejected there with HTTP 403 "this token does not have the + required permissions". A well-formed but revoked/deleted token gets HTTP 403 "authentication failed" (`src/auth.rs:297-303`); a malformed token gets HTTP 401 "The given API token does not match the format used by crates.io" (`src/auth.rs:295`, `InsecurelyGeneratedTokenRevoked`). diff --git a/scripts/__test__/verify-pr-checks.spec.mjs b/scripts/__test__/verify-pr-checks.spec.mjs index 58bf66ed..4b9d409f 100644 --- a/scripts/__test__/verify-pr-checks.spec.mjs +++ b/scripts/__test__/verify-pr-checks.spec.mjs @@ -2120,7 +2120,7 @@ describe('D-PR8: suite-keyed TIER_B_EXPECTED_SKIPPED allowance', () => { // --------------------------------------------------------------------------- describe('D-PR5i2: fetchWorkflowRuns — bounded loop, total_count guard, projection', () => { - test('page cap (≤ 5 requests) then ok:false/exitCode 2 (D-PR8)', () => { + test('D-PR5i2: page cap (≤ 5 requests) then ok:false/exitCode 2 (D-PR8)', () => { let calls = 0; const fullPage = { total_count: 100_000, @@ -2150,7 +2150,7 @@ describe('D-PR5i2: fetchWorkflowRuns — bounded loop, total_count guard, projec assert.equal(calls2, 5, `control: must have made exactly 5 requests; made ${calls2}`); }); - test('total_count mismatch → ok:false/exitCode 2 (D-PR8)', () => { + test('D-PR5i2: total_count mismatch → ok:false/exitCode 2 (D-PR8)', () => { const runner = (_args) => ({ total_count: 50, workflow_runs: [{ id: 1, path: '.github/workflows/release.yml', event: 'pull_request', check_suite_id: 123, conclusion: 'success' }], @@ -2171,7 +2171,7 @@ describe('D-PR5i2: fetchWorkflowRuns — bounded loop, total_count guard, projec assert.ok(result2.ok, `control: matching total_count must return ok:true; got: ${JSON.stringify(result2)}`); }); - test('single complete page → ok:true with ONLY the projected fields', () => { + test('D-PR5i2: single complete page → ok:true with ONLY the projected fields', () => { const rawRun = { id: 123, path: '.github/workflows/release.yml', @@ -2196,7 +2196,7 @@ describe('D-PR5i2: fetchWorkflowRuns — bounded loop, total_count guard, projec assert.ok(!('actor' in projected), 'extra field "actor" must be excluded'); }); - test('API error → ok:false/exitCode 2 (D-PR8)', () => { + test('D-PR5i2: API error → ok:false/exitCode 2 (D-PR8)', () => { const runner = (_args) => ({ __error: true, httpStatus: 500, stderr: 'server error' }); const result = fetchWorkflowRuns('abc123sha', runner); assert.ok(!result.ok, 'API error must return ok:false'); @@ -2482,6 +2482,8 @@ describe('current fixtures (2026-09): live-shaped evaluation', () => { 'protection-main-2026-09 must have 15 required contexts'); assert.equal(CHECK_RUNS_PR366.length, 44, 'checks-pr366-e02bcf2 must have 44 check-runs'); + assert.equal(CHECKS_PR366.total_count, 44, + 'checks-pr366-e02bcf2 total_count must equal 44'); const suiteIds = new Set(CHECK_RUNS_PR366.map(cr => cr.check_suite.id)); assert.equal(suiteIds.size, 4, 'must have 4 distinct check-suites'); const appSlugs = new Set(CHECK_RUNS_PR366.map(cr => cr.app.slug)); diff --git a/scripts/verify-pr-checks.mjs b/scripts/verify-pr-checks.mjs index d22d8475..64a05b82 100644 --- a/scripts/verify-pr-checks.mjs +++ b/scripts/verify-pr-checks.mjs @@ -87,9 +87,13 @@ * releaseSuiteIds undefined → allowance disabled; D-PR7 cannot attribute * several release suites → each run attributed on its own; all must pass * - * app.slug is NOT consulted: every id in releaseSuiteIds came from - * /actions/runs (GitHub Actions runs only), so the CodeQL umbrella suite - * never appears — no app-slug filter is needed. + * app.slug is NOT consulted: the mapping keeps only runs whose path === + * RELEASE_WORKFLOW_PATH ('.github/workflows/release.yml'), so a ci.yml + * run, the dynamic CodeQL analysis run (path 'dynamic/github-code-scanning/ + * codeql', which DOES appear in /actions/runs), and the github-advanced- + * security umbrella suite (which does not appear in /actions/runs at all) + * can never contribute a suite id — every id in the set is therefore a + * release.yml suite and app.slug would add nothing. * * Usage: * node scripts/verify-pr-checks.mjs @@ -252,8 +256,10 @@ export const RELEASE_WORKFLOW_PATH = '.github/workflows/release.yml'; * * Only numeric (integer) ids are included — non-integer values are excluded * as anomalous. Every id in the result is guaranteed to be a GitHub-Actions - * check-suite id originating from release.yml (by construction, the CodeQL - * umbrella never appears in /actions/runs). + * check-suite id originating from release.yml: the path filter excludes ci.yml + * runs, the dynamic CodeQL analysis run ('dynamic/github-code-scanning/codeql', + * which does appear in /actions/runs), and — because it has no /actions/runs + * entry at all — the github-advanced-security umbrella suite. * * @param {Array<{path: string, check_suite_id: any}>} runs — projected run objects * @returns {Set} @@ -1040,7 +1046,7 @@ export function fetchWorkflowRuns(headSha, runner) { return { ok: false, exitCode: 2, - message: `workflow-runs API error (page ${page}): ${data.stderr} (D-PR8)`, + message: `workflow-runs API error (page ${page}): ${data.stderr} (D-PR8; PF-013: indeterminate is never a pass)`, }; } if (totalCount === null) { @@ -1065,7 +1071,7 @@ export function fetchWorkflowRuns(headSha, runner) { ok: false, exitCode: 2, message: - `workflow-runs pagination exceeded ${MAX_RUNS_PAGES} pages (D-PR8) — ` + + `workflow-runs pagination exceeded ${MAX_RUNS_PAGES} pages (D-PR8; PF-013: indeterminate is never a pass) — ` + `refusing to evaluate partial result`, }; } @@ -1077,7 +1083,7 @@ export function fetchWorkflowRuns(headSha, runner) { exitCode: 2, message: `collected ${allRuns.length} workflow runs but total_count=${totalCount} — ` + - `partial page set (D-PR8 total_count guard)`, + `partial page set (D-PR8 total_count guard; PF-013: indeterminate is never a pass)`, }; }