diff --git a/.github/actions/night-issue/action.yml b/.github/actions/night-issue/action.yml new file mode 100644 index 00000000..5b9a0938 --- /dev/null +++ b/.github/actions/night-issue/action.yml @@ -0,0 +1,69 @@ +name: night-issue +description: > + A red night check opens or updates its ONE issue (PMAT-2174). Called as the + LAST step of every night-only (advisory) job in ci.yml, under + `if: failure() && github.event_name == 'schedule'`, so it runs only after + that job has already failed on a scheduled run. It finds the open issue + titled exactly `night: red on main`; if there is one it comments the + run on it, otherwise it creates it assigned to the one owner. It cannot turn + a red job green (the job has already failed) and it never runs on + pull_request, merge_group, push or workflow_dispatch: the script refuses any + event but `schedule` before it calls gh. Pinned by + crates/xpile/tests/night_checks_off_pr_path.rs (XPILE-NIGHT-CHECKS-001). + +inputs: + check: + description: The ci.yml job id of the night check that failed. + required: true + owner: + description: > + The ONE owner (a GitHub login) a new night issue is assigned to. One + login, never a list: a ticket with two owners has none. + required: false + default: noahgift + +runs: + using: composite + steps: + - name: open or update the night issue + shell: bash + env: + GH_TOKEN: ${{ github.token }} + CHECK: ${{ inputs.check }} + OWNER: ${{ inputs.owner }} + run: | + set -euo pipefail + if [ "${GITHUB_EVENT_NAME:-}" != "schedule" ]; then + echo "night-issue: refusing on event '${GITHUB_EVENT_NAME:-}': only a scheduled run files a night issue" >&2 + exit 2 + fi + : "${CHECK:?night-issue: no check named}" + : "${OWNER:?night-issue: no owner named}" + title="night: ${CHECK} red on main" + run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + # Find by EXACT title. The search narrows, the comparison decides: a + # search hit whose title merely contains this one is not this issue. + # A failed list exits here (set -e), so an API error never becomes a + # duplicate issue. + found=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \ + --search "\"${title}\" in:title" --limit 100 \ + --json number,title --jq '.[] | "\(.number)\t\(.title)"') + number="" + while IFS=$'\t' read -r n t; do + if [ -n "$n" ] && [ "$t" = "$title" ]; then + number="$n" + break + fi + done <<< "$found" + if [ -n "$number" ]; then + gh issue comment "$number" --repo "$GITHUB_REPOSITORY" \ + --body "Still red: \`${CHECK}\` failed again on the scheduled run ${run_url} (\`${GITHUB_SHA}\`)." + echo "night-issue: commented on #${number} (${title})" + else + gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" \ + --assignee "$OWNER" \ + --body "The night check \`${CHECK}\` failed on the scheduled ci run ${run_url} (\`${GITHUB_SHA}\`). + + This is the one issue for this check: later red nights comment here. A night check cannot block a merge or a release (it runs only on \`schedule\` and \`workflow_dispatch\`, PMAT-2170); this issue is its only output. Close it when the check is green again." + echo "night-issue: opened '${title}' for ${OWNER}" + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eac8faea..0d030eb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,8 +148,18 @@ jobs: # job in the marker line above carries exactly the job-level # if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' # so it runs only on the nightly schedule and on a manual dispatch (RELEASE.md - # §5 step 2 dispatches ci on the pinned SHA). A red there opens or updates one - # issue with one owner and never holds a merge or a tag. The REQUIRED jobs + # §5 step 2 reads the latest scheduled run on the pinned SHA and dispatches ci + # only if there is none). A red there never holds a merge or a tag; on a + # scheduled run it opens or updates ONE issue with ONE owner (PMAT-2174): the + # LAST step of every night job is + # if: failure() && github.event_name == 'schedule' + # uses: ./.github/actions/night-issue (with: check: ) + # which comments on the open issue titled exactly `night: red on main` + # or creates it, assigned to the action's one `owner` input. It runs only + # after its job has already failed, so it cannot change a job's result, and + # it refuses any event but `schedule`, so a pull request, a merge group or a + # manual dispatch files nothing. The night jobs carry `issues: write` for + # it. The REQUIRED jobs # carry no job-level `if:` and need no night job. Making a night job blocking # is the owner's ruling (plan row L4). Pinned by # `crates/xpile/tests/night_checks_off_pr_path.rs` (XPILE-NIGHT-CHECKS-001), @@ -238,6 +248,9 @@ jobs: name: kani if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest + permissions: + contents: read + issues: write steps: - uses: actions/checkout@v4 @@ -277,6 +290,15 @@ jobs: XPILE_REQUIRE_KANI: "1" run: cargo test -p xpile --test kani_verify -- --nocapture + # PMAT-2174: the one issue a red night opens or updates. LAST step, so + # failure() covers every step above it; schedule only, so a manual + # dispatch (RELEASE.md §5 step 2) files nothing. + - name: night issue — open or update the one issue for this check + if: failure() && github.event_name == 'schedule' + uses: ./.github/actions/night-issue + with: + check: kani + # ── Optional check: lake-build (Proof lane / PMAT-903) ──────── # Elaborates the machine-checked PILOT set in `contracts/lean/` # via `lake build` against the pinned `lean-toolchain` (v4.15.0). @@ -297,6 +319,9 @@ jobs: name: lake-build if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest + permissions: + contents: read + issues: write steps: - uses: actions/checkout@v4 @@ -319,6 +344,15 @@ jobs: working-directory: contracts/lean run: lake build + # PMAT-2174: the one issue a red night opens or updates. LAST step, so + # failure() covers every step above it; schedule only, so a manual + # dispatch (RELEASE.md §5 step 2) files nothing. + - name: night issue — open or update the one issue for this check + if: failure() && github.event_name == 'schedule' + uses: ./.github/actions/night-issue + with: + check: lake-build + # ── Optional check: docs (README/documentation quality) ─────── # Grades and enforces the documentation with `pmat`: # - validate-docs --fail-on-error : every relative + external @@ -338,6 +372,9 @@ jobs: name: docs if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest + permissions: + contents: read + issues: write env: # Pinned prebuilt musl binary: no `cargo install` (pmat needs rustc # ≥1.95 while this repo's toolchain is 1.93), no compile time, and a @@ -368,6 +405,15 @@ jobs: exit 1 } + # PMAT-2174: the one issue a red night opens or updates. LAST step, so + # failure() covers every step above it; schedule only, so a manual + # dispatch (RELEASE.md §5 step 2) files nothing. + - name: night issue — open or update the one issue for this check + if: failure() && github.event_name == 'schedule' + uses: ./.github/actions/night-issue + with: + check: docs + # ── Optional check: wasi (universal-binary path) ────────────── # Proves the "works everywhere" claim end to end: xpile emits a proven model # as a buildable crate (`transpile --emit-crate`), that crate builds to a @@ -379,6 +425,9 @@ jobs: name: wasi if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest + permissions: + contents: read + issues: write env: # Pinned prebuilt release asset, mirroring the `docs` job's PMAT_VERSION # pattern. PMAT-1370: this job used to run @@ -443,6 +492,15 @@ jobs: fi echo "OK — the universal .wasm matches CPython byte-for-byte" + # PMAT-2174: the one issue a red night opens or updates. LAST step, so + # failure() covers every step above it; schedule only, so a manual + # dispatch (RELEASE.md §5 step 2) files nothing. + - name: night issue — open or update the one issue for this check + if: failure() && github.event_name == 'schedule' + uses: ./.github/actions/night-issue + with: + check: wasi + # ── Optional check: lean-models (Mathlib model-proof lane / PMAT-956) ── # The deliberate, walled-off Mathlib lane: elaborates the OLS-uniqueness # certificate in `contracts/lean-models/` against Mathlib (fetched as prebuilt @@ -457,6 +515,9 @@ jobs: name: lean-models if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest + permissions: + contents: read + issues: write steps: - uses: actions/checkout@v4 @@ -482,6 +543,15 @@ jobs: working-directory: contracts/lean-models run: lake build + # PMAT-2174: the one issue a red night opens or updates. LAST step, so + # failure() covers every step above it; schedule only, so a manual + # dispatch (RELEASE.md §5 step 2) files nothing. + - name: night issue — open or update the one issue for this check + if: failure() && github.event_name == 'schedule' + uses: ./.github/actions/night-issue + with: + check: lean-models + # ── Optional check: shader-validate (offline WGSL+SPIR-V corpus / PMAT-482) ── # Converts the GPU-lane SKIP into a VALIDATED gate on free CI. `naga` is the # SINGLE shared oracle for the WGSL lane (front-end parse+validate) AND the @@ -502,6 +572,9 @@ jobs: name: shader-validate if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest + permissions: + contents: read + issues: write steps: - uses: actions/checkout@v4 @@ -516,6 +589,15 @@ jobs: - name: offline WGSL naga-validate + WGSL->SPIR-V corpus (no GPU) run: cargo test -p xpile-spirv-codegen --test offline_validate -- --nocapture + # PMAT-2174: the one issue a red night opens or updates. LAST step, so + # failure() covers every step above it; schedule only, so a manual + # dispatch (RELEASE.md §5 step 2) files nothing. + - name: night issue — open or update the one issue for this check + if: failure() && github.event_name == 'schedule' + uses: ./.github/actions/night-issue + with: + check: shader-validate + # ── license-scan (ADVISORY — PMAT-1409) ────────────────────── # deny.toml has configured `[licenses]`, `[bans]` and `[graph]` since the # file was written. Until this job existed NONE of them had ever executed: @@ -546,6 +628,9 @@ jobs: name: license-scan if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest + permissions: + contents: read + issues: write env: # Turns the "cargo-deny is not installed" skip in # dependency_license_policy.rs into a hard failure. Without this the @@ -575,3 +660,12 @@ jobs: # the point, not the exit code. continue-on-error: true run: cargo deny check licenses bans sources + + # PMAT-2174: the one issue a red night opens or updates. LAST step, so + # failure() covers every step above it; schedule only, so a manual + # dispatch (RELEASE.md §5 step 2) files nothing. + - name: night issue — open or update the one issue for this check + if: failure() && github.event_name == 'schedule' + uses: ./.github/actions/night-issue + with: + check: license-scan diff --git a/crates/xpile/tests/night_checks_off_pr_path.rs b/crates/xpile/tests/night_checks_off_pr_path.rs index 883bbc38..6ed4633a 100644 --- a/crates/xpile/tests/night_checks_off_pr_path.rs +++ b/crates/xpile/tests/night_checks_off_pr_path.rs @@ -29,6 +29,16 @@ //! night condition can come true: a night job in a workflow with no night //! trigger never runs, and that is a check turned off, not moved (#2167 //! adds both triggers; this branch merges after it). +//! 6. (PMAT-2174) the red has somewhere to go: the LAST step of every night +//! job is the `night-issue` action, under exactly [`ISSUE_IF`], with +//! `check:` set to its own job id and `issues: write` on the job; no other +//! job (required or not) calls it; the action names exactly one owner. +//! 7. (PMAT-2174) the action's script, EXECUTED against a stub `gh` (no +//! network): it refuses every event but `schedule` before calling gh; it +//! comments on an open issue whose title is exactly +//! `night: red on main` and creates nothing; it creates the issue, +//! assigned to the one owner, when only a near-miss title exists; and a +//! failed lookup fails the step without creating anything. //! //! RED arm, recorded in the PR: on the tree before PMAT-2170 (no `if:` on the //! seven jobs) assertion 1 fails for all seven and assertion 4 fails for @@ -188,3 +198,245 @@ fn the_night_jobs_have_a_night_to_run_in() { "{CI} `schedule:` carries no `cron:` entry, so the night never comes" ); } + +// ── PMAT-2174: a red night opens or updates its one issue ────────────────── + +const ACTION: &str = ".github/actions/night-issue/action.yml"; +const ACTION_USES: &str = "./.github/actions/night-issue"; +/// The one step-level condition on the issue step. `failure()` means it runs +/// only after its job has already failed, so it can never turn a job green or +/// red; `schedule` alone means a pull request, a merge group, a push and a +/// manual dispatch file nothing. Exact string, like [`NIGHT_IF`]. +const ISSUE_IF: &str = "failure() && github.event_name == 'schedule'"; + +fn action_yaml() -> serde_yaml::Value { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); + let text = + std::fs::read_to_string(root.join(ACTION)).unwrap_or_else(|e| panic!("read {ACTION}: {e}")); + serde_yaml::from_str(&text).unwrap_or_else(|e| panic!("parse {ACTION}: {e}")) +} + +fn calls_the_action(step: &serde_yaml::Value) -> bool { + step["uses"].as_str().map(str::trim) == Some(ACTION_USES) +} + +#[test] +fn every_night_job_ends_by_filing_its_one_issue() { + let y = ci_yaml(); + let mut wrong = Vec::new(); + for id in marker_set(ADVISORY_MARKER) { + let j = job(&y, &id); + let steps = j["steps"].as_sequence().cloned().unwrap_or_default(); + let calls: Vec<&serde_yaml::Value> = steps.iter().filter(|s| calls_the_action(s)).collect(); + if calls.len() != 1 { + wrong.push(format!( + "{id}: calls {ACTION_USES} {} times, not once", + calls.len() + )); + continue; + } + if !steps.last().is_some_and(calls_the_action) { + wrong.push(format!( + "{id}: the issue step is not the LAST step, so failure() cannot see the steps after it" + )); + } + let s = calls[0]; + if s["if"].as_str().map(str::trim) != Some(ISSUE_IF) { + wrong.push(format!( + "{id}: issue step if = {:?}, not `{ISSUE_IF}`", + s["if"] + )); + } + if !s["continue-on-error"].is_null() { + wrong.push(format!("{id}: issue step carries continue-on-error")); + } + if s["with"]["check"].as_str() != Some(id.as_str()) { + wrong.push(format!( + "{id}: issue step check = {:?}, not its own job id", + s["with"]["check"] + )); + } + if j["permissions"]["issues"].as_str() != Some("write") { + wrong.push(format!("{id}: job lacks `permissions: issues: write`")); + } + } + assert!( + wrong.is_empty(), + "a red night check must open or update its one issue (PMAT-2174):\n {}", + wrong.join("\n ") + ); +} + +#[test] +fn only_night_jobs_file_issues_and_the_action_names_one_owner() { + let y = ci_yaml(); + let night = marker_set(ADVISORY_MARKER); + let jobs = y["jobs"].as_mapping().expect("ci.yml has a jobs: mapping"); + let outside: Vec = jobs + .iter() + .filter_map(|(id, j)| { + let id = id.as_str().unwrap_or_default().to_string(); + let calls = j["steps"] + .as_sequence() + .into_iter() + .flatten() + .any(calls_the_action); + (calls && !night.contains(&id)).then_some(id) + }) + .collect(); + assert!( + outside.is_empty(), + "job(s) {outside:?} call {ACTION_USES} but are not night jobs; a gate's red is the \ + gate's own verdict, not a ticket" + ); + let a = action_yaml(); + let owner = a["inputs"]["owner"]["default"].as_str().unwrap_or_default(); + assert!( + !owner.is_empty() && owner.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'), + "{ACTION} must name exactly ONE owner login as `inputs.owner.default`, got {owner:?}" + ); + let steps = a["runs"]["steps"] + .as_sequence() + .expect("composite action has steps"); + assert_eq!(steps.len(), 1, "{ACTION} is one script step"); + let run = steps[0]["run"] + .as_str() + .expect("the step has a run: script"); + assert!( + !run.contains("${{"), + "{ACTION}'s script interpolates an expression; inputs reach it through `env:` only" + ); +} + +/// The action's script, run by bash against a stub `gh` that records every +/// call. Returns (exit code, the recorded gh calls). +fn run_action(event: &str, listed: &str, list_rc: i32) -> (i32, Vec) { + let a = action_yaml(); + let run = a["runs"]["steps"][0]["run"] + .as_str() + .expect("script") + .to_string(); + static SEQ: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + let dir = std::env::temp_dir().join(format!("xpile-night-issue-{}-{seq}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("stub dir"); + let log = dir.join("gh.log"); + let stub = dir.join("gh"); + std::fs::write( + &stub, + "#!/bin/bash\nprintf '%s\\n' \"$*\" >> \"$STUB_LOG\"\n\ + if [ \"$1 $2\" = \"issue list\" ]; then printf '%b' \"$STUB_LIST\"; exit \"$STUB_LIST_RC\"; fi\n\ + exit 0\n", + ) + .expect("write stub gh"); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&stub, std::fs::Permissions::from_mode(0o755)).expect("chmod"); + } + let path = format!( + "{}:{}", + dir.display(), + std::env::var("PATH").unwrap_or_default() + ); + let status = std::process::Command::new("bash") + .arg("-c") + .arg(&run) + .env("PATH", path) + .env("STUB_LOG", &log) + .env("STUB_LIST", listed) + .env("STUB_LIST_RC", list_rc.to_string()) + .env("GITHUB_EVENT_NAME", event) + .env("CHECK", "kani") + .env("OWNER", "the-one-owner") + .env("GITHUB_REPOSITORY", "paiml/xpile") + .env("GITHUB_SERVER_URL", "https://github.com") + .env("GITHUB_RUN_ID", "4242") + .env("GITHUB_SHA", "0123abcd") + .env_remove("GH_TOKEN") + .status() + .expect("bash runs"); + let calls = std::fs::read_to_string(&log) + .unwrap_or_default() + .lines() + .map(str::to_string) + .collect(); + let _ = std::fs::remove_dir_all(&dir); + (status.code().unwrap_or(-1), calls) +} + +#[test] +fn the_issue_script_refuses_every_event_but_schedule() { + for event in [ + "pull_request", + "merge_group", + "push", + "workflow_dispatch", + "", + ] { + let (rc, calls) = run_action(event, "", 0); + assert_ne!( + rc, 0, + "event {event:?}: the night-issue script must refuse, exited 0" + ); + assert!( + calls.is_empty(), + "event {event:?}: gh was called before the refusal: {calls:?}" + ); + } +} + +#[test] +fn an_open_issue_with_the_exact_title_is_commented_not_duplicated() { + let listed = "7\tnight: kani red on main (old)\n41\tnight: kani red on main\n"; + let (rc, calls) = run_action("schedule", listed, 0); + assert_eq!(rc, 0, "calls: {calls:?}"); + assert!( + calls.iter().any(|c| c.starts_with("issue comment 41 ")), + "no comment on #41: {calls:?}" + ); + assert!( + !calls.iter().any(|c| c.starts_with("issue create")), + "created a second issue although #41 is open: {calls:?}" + ); + assert!( + calls.iter().any(|c| c.contains("/actions/runs/4242")), + "run url not posted: {calls:?}" + ); +} + +#[test] +fn a_near_miss_title_is_not_the_issue_so_one_is_created_for_the_one_owner() { + let (rc, calls) = run_action("schedule", "7\tnight: kani red on main (old)\n", 0); + assert_eq!(rc, 0, "calls: {calls:?}"); + let created: Vec<&String> = calls + .iter() + .filter(|c| c.starts_with("issue create")) + .collect(); + assert_eq!(created.len(), 1, "expected exactly one create: {calls:?}"); + assert!( + created[0].contains("--title night: kani red on main --assignee the-one-owner "), + "create is not titled/assigned as specified: {}", + created[0] + ); + assert!( + !calls.iter().any(|c| c.starts_with("issue comment")), + "{calls:?}" + ); +} + +#[test] +fn a_failed_lookup_fails_the_step_and_creates_nothing() { + let (rc, calls) = run_action("schedule", "", 1); + assert_ne!( + rc, 0, + "a failed `gh issue list` must fail the step: {calls:?}" + ); + assert!( + !calls + .iter() + .any(|c| c.starts_with("issue create") || c.starts_with("issue comment")), + "acted on a lookup that failed: {calls:?}" + ); +} diff --git a/crates/xpile/tests/release_runbook_facts_witness.rs b/crates/xpile/tests/release_runbook_facts_witness.rs index 51c4c6fb..555c767a 100644 --- a/crates/xpile/tests/release_runbook_facts_witness.rs +++ b/crates/xpile/tests/release_runbook_facts_witness.rs @@ -427,3 +427,48 @@ fn the_runbook_scan_actually_reaches_pmat_1373() { ); } } + +/// PMAT-2175: §5 step 2 READS the night before it dispatches. Since PMAT-2166 +/// `ci.yml` runs on `main` every night, so on a frozen Friday the pinned SHA +/// normally already carries a scheduled run; the step used to dispatch and +/// wait unconditionally, spending a full ci run to re-measure a measured SHA. +/// Pinned as an ORDER inside step 2, and tied to the trigger it depends on: if +/// `ci.yml` ever loses its `schedule:`, "read the night first" is a read of +/// nothing and this reds. +#[test] +fn release_step_2_reads_the_scheduled_run_before_it_dispatches() { + let doc = read("docs/RELEASE.md"); + let s5 = doc + .split_once("\n## 5. ") + .map(|(_, rest)| rest.split_once("\n## 6. ").map_or(rest, |(s, _)| s)) + .expect("docs/RELEASE.md has a `## 5.` section"); + let step2 = s5 + .split_once("\n2. **Record the advisory statuses**") + .and_then(|(_, rest)| rest.split_once("\n3. **").map(|(s, _)| s)) + .expect("§5 still has step 2 `Record the advisory statuses`, followed by step 3"); + let read_at = step2 + .find("--event schedule") + .expect("§5 step 2 no longer reads the scheduled run (`gh run list ... --event schedule`)"); + assert!( + step2[read_at..].contains("--commit "), + "§5 step 2 reads a scheduled run but not the one on ``" + ); + let dispatch_at = step2 + .find("gh workflow run ci.yml") + .expect("§5 step 2 lost its fallback dispatch for a SHA no night has measured"); + assert!( + read_at < dispatch_at, + "§5 step 2 dispatches ci before it reads the night that may already exist" + ); + assert!( + step2.contains("only if"), + "§5 step 2 must make the dispatch conditional (`only if there is none`)" + ); + let ci: serde_yaml::Value = serde_yaml::from_str(&read(CI_WORKFLOW)).expect("ci.yml parses"); + assert!( + ci["on"]["schedule"] + .as_sequence() + .is_some_and(|s| !s.is_empty()), + "{CI_WORKFLOW} has no `schedule:` trigger, so §5 step 2's scheduled run never exists" + ); +} diff --git a/docs/RELEASE.md b/docs/RELEASE.md index bd4ce28f..7bebc88b 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -412,9 +412,22 @@ tier), which is where you should read it. 1. **Re-verify A1** (§6). All preconditions, before the first upload byte. 2. **Record the advisory statuses** on the pinned SHA and name every non-success in the release body. The advisory jobs run only on `schedule` - and `workflow_dispatch` (PMAT-2170), so dispatch them first: + and `workflow_dispatch` (PMAT-2170), and `ci.yml` runs on `main` every + night (PMAT-2166), so on a frozen `main` (A7) the pinned SHA normally + already has a scheduled run. Read the latest one first (PMAT-2175): + + ```bash + gh run list -R paiml/xpile --workflow ci.yml --event schedule \ + --commit --limit 1 \ + --json databaseId,status,conclusion,headSha,createdAt + ``` + + If it is `completed`, its statuses are the ones to record. If it is still + running, wait for it; do not dispatch a second run. **Dispatch only if + there is none** (empty output: no night has run on `` yet): `gh workflow run ci.yml --ref main` while `main` is still `` (A7), then wait for that run and check its `head_sha` is ``. + Either way, read the statuses from the commit: ```bash gh api "repos/paiml/xpile/commits//check-runs" \ diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 6e0d7932..53752cf2 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -27438,3 +27438,41 @@ SCOPE, honestly. The published crate is unaffected — one tree, one target dir, - kind:code notes: null epic: 2124 +- id: PMAT-2174 + github_issue: 2174 + item_type: task + title: 'ci: a red night check opens or updates its one issue (night-issue step)' + status: planned + priority: medium + assigned_to: null + created: 2026-10-15T04:02:46Z + updated: 2026-10-15T04:02:46Z + spec: null + acceptance_criteria: + - ci.yml says a night red opens or updates one issue with one owner, but no step does it. Add a failure()+schedule-only final step to every advisory job that finds its issue by exact title, comments if open, else creates it with one assignee; never on pull_request/merge_group; never changes a job result. Pinned by night_checks_off_pr_path.rs. + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 2124 +- id: PMAT-2175 + github_issue: 2175 + item_type: task + title: 'docs: RELEASE.md §5 step 2 reads the latest scheduled nightly on the pinned SHA; dispatch only if none' + status: planned + priority: medium + assigned_to: null + created: 2026-10-15T04:03:01Z + updated: 2026-10-15T04:03:01Z + spec: null + acceptance_criteria: + - 'RELEASE.md §5 step 2 dispatches ci.yml and waits for the run. Since #2167 ci runs nightly on main, so read the latest scheduled run whose head_sha is the pinned SHA first, and dispatch (then wait) only if there is none. Docs only, plus any test pinning RELEASE.md facts.' + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:docs + notes: null + epic: 2124