Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -612,18 +612,20 @@ jobs:
# build-only (tiny-keccak, a rustpython-parser build script). Every one is
# enumerated with its linkage in `NOTICE.md`.
#
# WHY THE RAW CHECK IS `continue-on-error` AND THE TRIPWIRE IS NOT:
# what to DO about the LGPL is an OWNER DECISION (`lgpl-in-shipped-binary`
# in docs/roadmaps/queue.yaml), not a decision this job may make. A job whose
# verdict is "still 9 rejections" would be red on every commit forever, and
# this repo has already recorded what that buys: `wasi` was RED on ccb95a04,
# the exact SHA tagged v0.1.617, and the release was cut anyway (PMAT-1370).
# A permanently-red advisory job is exactly as invisible as no job at all.
# So the raw check RUNS and its full output is in the log every time, while
# the job's PASS/FAIL verdict is the DRIFT tripwire below: the set of
# rejected (crate, version, license) triples must equal the set NOTICE.md
# enumerates, and each row's `linkage:` claim is re-derived from `cargo
# tree`. A tenth rejection, or a documented one vanishing, reds this job.
# TWO STEPS, BOTH ABLE TO FAIL THE JOB (PMAT-2177). This is a night-only
# LAB job: it is not in REQUIRED-CONTEXTS and never runs on a pull request
# or a merge group, so neither step can block a merge or a release. The
# DRIFT tripwire checks that the set of rejected (crate, version, license)
# triples equals the set NOTICE.md enumerates, and re-derives each row's
# `linkage:` claim from `cargo tree`: a tenth rejection, or a documented one
# vanishing, reds it. The raw `cargo deny check licenses bans sources` step
# then reds the job on ANY finding, the documented nine included. What to DO
# about the LGPL is an OWNER DECISION (`lgpl-in-shipped-binary` in
# docs/roadmaps/queue.yaml), not one this job may make, so it does not hide
# the finding behind `continue-on-error` (a check that runs and cannot fail
# is a third state, C324 #6): the red night opens or updates the ONE
# `night: license-scan red on main` issue through the last step, and that
# issue stays open until the owner decides.
license-scan:
name: license-scan
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
Expand Down Expand Up @@ -655,10 +657,9 @@ jobs:
- name: drift tripwire — NOTICE.md equals the live rejection set
run: cargo test -p xpile --test dependency_license_policy -- --nocapture

- name: report — cargo deny check licenses bans sources (advisory)
# Exits 4 today BY DESIGN; see the block comment above. The output is
# the point, not the exit code.
continue-on-error: true
- name: cargo deny check licenses bans sources
# Exits 4 on main today (the nine NOTICE.md rejections), so the night
# job is red and files its one issue; see the block comment above.
run: cargo deny check licenses bans sources

# PMAT-2174: the one issue a red night opens or updates. LAST step, so
Expand Down
15 changes: 7 additions & 8 deletions crates/xpile/tests/dependency_license_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@
//! `NOTICE.md` is "this copyleft library is linked into what we ship"; a
//! later edit that quietly downgrades it to `dev-only` reds here.
//! 4. The `license-scan` job exists, runs the check, and its *tripwire* step is
//! not `continue-on-error`. Its raw-report step deliberately is — see the
//! job comment — and a gate that let the tripwire go the same way would be
//! decorative.
//! not `continue-on-error`: a gate that let the tripwire ignore its exit
//! code would be decorative. (Since PMAT-2177 the raw `cargo deny` step is
//! not either; `night_checks_off_pr_path.rs` pins that for every night job.)
//!
//! **LIVE half** — needs `cargo-deny` and `cargo`. Skips with a printed reason
//! when they are absent; `XPILE_REQUIRE_DENY=1` (set on the `license-scan` job)
Expand Down Expand Up @@ -388,10 +388,9 @@ fn the_notice_states_derive_commands_and_no_bare_counts() {
}

/// A job that reports and a job whose verdict means something are different
/// things. The raw `cargo deny` step is `continue-on-error` on purpose (it
/// exits 4 today and a permanently-red advisory job is invisible — `wasi` was
/// RED on the SHA tagged v0.1.617 and the release was cut anyway). The drift
/// tripwire must NOT be, or the whole job is decorative.
/// things. The drift tripwire must NOT be `continue-on-error`, or the whole
/// job is decorative. (The raw `cargo deny` step was, until PMAT-2177; no step
/// of a night job may be now, see `night_checks_off_pr_path.rs`.)
#[test]
fn the_license_scan_job_runs_the_check_and_its_tripwire_can_fail() {
let ci = read(CI);
Expand Down Expand Up @@ -424,7 +423,7 @@ fn the_license_scan_job_runs_the_check_and_its_tripwire_can_fail() {
!tripwire.contains("continue-on-error"),
"the `license-scan` tripwire step is `continue-on-error`, which makes \
the whole job decorative: it would stay green through any drift it is \
supposed to catch. Only the RAW report step may ignore its exit code."
supposed to catch."
);
}

Expand Down
44 changes: 42 additions & 2 deletions crates/xpile/tests/night_checks_off_pr_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
//! would skip the gate);
//! 4. every `continue-on-error` in the workflow sits inside a job that carries
//! [`NIGHT_IF`] (being listed as advisory is not enough), so
//! none is left on the pull-request path.
//! none is left on the pull-request path; and (PMAT-2177) no night job,
//! nor any step in one, carries `continue-on-error` either, so a red
//! night step fails its job and reaches the issue step of assertion 6.
//! 5. the workflow triggers on `schedule` and on `workflow_dispatch`, so the
//! 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
Expand All @@ -42,7 +44,9 @@
//!
//! 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
//! `license-scan`'s raw report step.
//! `license-scan`'s raw report step. On the tree before PMAT-2177 (850ffff6)
//! `no_step_in_a_night_job_has_continue_on_error` fails, naming exactly
//! `license-scan`'s `cargo deny check licenses bans sources` step.

use std::collections::BTreeSet;
use std::path::PathBuf;
Expand Down Expand Up @@ -178,6 +182,42 @@ fn no_continue_on_error_is_left_on_the_pull_request_path() {
);
}

/// PMAT-2177: moving a `continue-on-error` step to the night does not make it
/// two-state. It still runs and cannot fail, so a red there neither fails the
/// night job nor reaches the `night-issue` step (`failure()` never comes
/// true). Every step of a night job can fail its job; so can the job itself.
#[test]
fn no_step_in_a_night_job_has_continue_on_error() {
let y = ci_yaml();
let jobs = y["jobs"].as_mapping().expect("ci.yml has a jobs: mapping");
let night = marker_set(ADVISORY_MARKER);
let mut found = Vec::new();
for (id, j) in jobs {
let id = id.as_str().unwrap_or_default().to_string();
// A night job is one listed as advisory OR one carrying the night
// condition; either way its red is meant to file the one issue.
let is_night = night.contains(&id) || j["if"].as_str().map(str::trim) == Some(NIGHT_IF);
if !is_night {
continue;
}
if !j["continue-on-error"].is_null() {
found.push(format!("{id} (job level)"));
}
for step in j["steps"].as_sequence().into_iter().flatten() {
if !step["continue-on-error"].is_null() {
let name = step["name"].as_str().unwrap_or("<unnamed step>");
found.push(format!("{id}: {name}"));
}
}
}
assert!(
found.is_empty(),
"`continue-on-error` in a night job is a third state (C324 #6): the step \
runs, cannot fail its job, and so never files the night issue; remove it:\n {}",
found.join("\n ")
);
}

#[test]
fn the_night_jobs_have_a_night_to_run_in() {
let y = ci_yaml();
Expand Down
19 changes: 19 additions & 0 deletions docs/roadmaps/roadmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27476,3 +27476,22 @@ SCOPE, honestly. The published crate is unaffected — one tree, one target dir,
- kind:docs
notes: null
epic: 2124
- id: PMAT-2177
github_issue: 2177
item_type: task
title: 'ci: license-scan''s cargo deny step fails its night job (no continue-on-error), so a deny finding files the night issue'
status: planned
priority: medium
assigned_to: null
created: 2026-10-15T04:46:24Z
updated: 2026-10-15T04:46:24Z
spec: null
acceptance_criteria:
- 'The license-scan step `cargo deny check licenses bans sources` in ci.yml carries no continue-on-error, so a deny finding fails the night-only license-scan job and its last step (night-issue) opens or updates the one issue. license-scan stays out of the required set (gate, workspace-test). night_checks_off_pr_path.rs asserts that no night job, and no step in one, carries continue-on-error; it is red on 850ffff6 and green after.'
phases: []
subtasks: []
estimated_effort: null
labels:
- kind:code
notes: null
epic: 2124
Loading