From 93e764abeea3c69b5682e8eda5f074c6eb4d6429 Mon Sep 17 00:00:00 2001 From: AlexZhang Date: Wed, 19 Aug 2026 20:04:31 +0800 Subject: [PATCH 1/2] ci: fix main.yml YAML (quote step names containing ': ') and guard against it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub refused to parse .github/workflows/main.yml since #326 — two step names ("Tests (workspace: lib, ...)", "Static UX audit (gate policy: ...)") contained an unquoted ': ', which YAML reads as a nested mapping. Every push to main since then failed with "workflow file issue" and PRs showed no `spec gate` check at all (the file could not even be scheduled), so the breakage looked green. Quote both names and add `ci_workflow_scalars_with_colon_space_are_quoted` to tests/ci_policy.rs so an unquoted ': ' scalar in name/run/if/key lines fails `cargo test` locally before it reaches CI. Co-Authored-By: Claude Fable 5 --- .github/workflows/main.yml | 4 ++-- tests/ci_policy.rs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dee5cb290..d6d64042c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -134,10 +134,10 @@ jobs: fi agent-spec --version - - name: Tests (workspace: lib, bins, doctests, ux-harness) + - name: "Tests (workspace: lib, bins, doctests, ux-harness)" run: cargo test --workspace - - name: Static UX audit (gate policy: tools/ux-harness/gate.json) + - name: "Static UX audit (gate policy: tools/ux-harness/gate.json)" # Source-only audit: token contrast, token discipline, translation # coverage. Thresholds are a ratchet documented in gate.json. run: | diff --git a/tests/ci_policy.rs b/tests/ci_policy.rs index 6dccddbd0..678609148 100644 --- a/tests/ci_policy.rs +++ b/tests/ci_policy.rs @@ -61,6 +61,41 @@ fn run_lines(job: &str) -> Vec { out } +/// GitHub silently refuses to run a workflow whose YAML does not parse — the +/// run shows up as "workflow file issue" and, worse, the PR simply has no +/// `spec gate` check at all, so a broken file looks green. Without a YAML +/// dependency we enforce the one mistake that bit us: an unquoted scalar +/// containing `: ` (e.g. `name: Tests (workspace: lib)`) is a nested mapping +/// to YAML. Every single-line `name:` / `run:` value with `: ` must be quoted. +#[test] +fn ci_workflow_scalars_with_colon_space_are_quoted() { + for rel in [".github/workflows/main.yml", ".github/workflows/builds.yml"] { + let wf = read(rel); + for (i, line) in wf.lines().enumerate() { + let t = line.trim_start(); + if t.starts_with('#') { + continue; + } + for key in ["name:", "run:", "if:", "key:"] { + let Some(rest) = t.strip_prefix(key) else { continue }; + let v = rest.trim(); + if v.is_empty() || v == "|" || v == ">" || v == "|-" { + continue; + } + let quoted = (v.starts_with('"') && v.ends_with('"')) + || (v.starts_with('\'') && v.ends_with('\'')); + // `${{ ... }}` expressions are fine; a bare `: ` elsewhere is not. + let stripped = v.replace("${{", "").replace("}}", ""); + assert!( + quoted || !stripped.contains(": "), + "{rel}:{}: unquoted YAML scalar contains `: ` — quote it: {line}", + i + 1 + ); + } + } + } +} + #[test] fn ci_workflow_runs_all_cargo_test_targets() { let wf = read(".github/workflows/main.yml"); From f18c9bc84e3db1619758a62b444e5cdd5ab68fbf Mon Sep 17 00:00:00 2001 From: AlexZhang Date: Wed, 19 Aug 2026 20:05:21 +0800 Subject: [PATCH 2/2] test(ci_policy): strip the list bullet before matching step keys (guard was inert) Co-Authored-By: Claude Fable 5 --- tests/ci_policy.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/ci_policy.rs b/tests/ci_policy.rs index 678609148..2982d7c84 100644 --- a/tests/ci_policy.rs +++ b/tests/ci_policy.rs @@ -76,6 +76,8 @@ fn ci_workflow_scalars_with_colon_space_are_quoted() { if t.starts_with('#') { continue; } + // Steps are list items: `- name: …` — drop the bullet before matching keys. + let t = t.strip_prefix("- ").map(str::trim_start).unwrap_or(t); for key in ["name:", "run:", "if:", "key:"] { let Some(rest) = t.strip_prefix(key) else { continue }; let v = rest.trim();