1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//! Guards the property that no commit on `main` can have its CI cancelled by
//! a later one.
//!
//! Every commit on `main` is a permanent, releasable point: `self-gate` stamps
//! it and `dogfood` publishes from it. A commit whose CI run was cancelled is
//! unverified forever, because no later run covers it.
//!
//! Main commits genuinely do arrive back-to-back — `merge-on-green` has to
//! re-dispatch main's CI after each auto-merge, since a `GITHUB_TOKEN` push
//! does not fire `on: push`. Under a ref-scoped concurrency group those
//! dispatches cancelled each other, and three merges landing together left the
//! middle two commits permanently unverified.
//!
//! The fix is to scope main's group by SHA so each commit runs alone. Note
//! that `cancel-in-progress: false` does NOT achieve this: GitHub keeps at
//! most one running and one *pending* run per group and cancels the older
//! pending one, so the middle commit of three would still lose its run. Only
//! distinct groups work, which is why this guard checks the group expression
//! rather than the cancel flag.
use std::path::{Path, PathBuf};
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("crates/<pkg> has a workspace root two levels up")
.to_path_buf()
}
/// Extracts the `group:` value from a workflow's top-level `concurrency:`
/// block. Returns `None` when the workflow declares no concurrency at all,
/// which is a valid configuration this guard says nothing about.
fn concurrency_group(workflow: &str) -> Option<String> {
let mut in_block = false;
for line in workflow.lines() {
if line.starts_with("concurrency:") {
in_block = true;
continue;
}
if in_block {
// A non-indented, non-blank line ends the block.
if !line.starts_with(char::is_whitespace) && !line.trim().is_empty() {
break;
}
if let Some(rest) = line.trim().strip_prefix("group:") {
return Some(rest.trim().to_string());
}
}
}
None
}
/// The property under test: two different commits on `main` must land in two
/// different concurrency groups.
///
/// A group qualified by the commit SHA under a `refs/heads/main` condition
/// satisfies this; one scoped only to the ref does not, because every main
/// commit shares `refs/heads/main`.
fn main_commits_get_distinct_groups(group: &str) -> bool {
group.contains("github.sha") && group.contains("refs/heads/main")
}
#[test]
fn main_commits_cannot_cancel_each_others_ci() {
let root = workspace_root();
let ci = root.join(".github/workflows/ci.yml");
let text = std::fs::read_to_string(&ci).expect("read .github/workflows/ci.yml");
let group = concurrency_group(&text).expect(
"ci.yml declares a top-level `concurrency:` block with a `group:` key — \
if that changed, this guard needs updating rather than deleting",
);
assert!(
main_commits_get_distinct_groups(&group),
"ci.yml's concurrency group does not give each `main` commit its own \
group, so one main commit's CI can cancel another's, leaving it \
permanently unverified.\n group: {group}\n\
Expected the expression to qualify the group by `github.sha` when \
`github.ref` is `refs/heads/main`."
);
}
#[test]
fn the_guard_rejects_the_ref_only_group_and_accepts_the_sha_qualified_one() {
// The exact shape that caused three commits to lose their CI runs. It is
// also what the tempting non-fix leaves behind: flipping
// `cancel-in-progress` to `false` keeps the group ref-scoped, so the
// pending-run supersede still drops the middle commit. The group is the
// thing that has to change, which is why this is what gets rejected.
let ref_only = "${{ github.workflow }}-${{ github.ref }}";
assert!(
!main_commits_get_distinct_groups(ref_only),
"the ref-only group must be rejected — it is the defect this guards"
);
let sha_qualified = "${{ github.workflow }}-${{ github.ref }}-${{ github.ref == 'refs/heads/main' && github.sha || '' }}";
assert!(
main_commits_get_distinct_groups(sha_qualified),
"the shipped group must be accepted"
);
// The self-test runs the same matcher the gate above runs, so the two
// cannot drift apart.
let root = workspace_root();
let shipped = concurrency_group(
&std::fs::read_to_string(root.join(".github/workflows/ci.yml")).expect("read ci.yml"),
)
.expect("ci.yml has a concurrency group");
assert_eq!(
shipped, sha_qualified,
"the shipped group changed; update this self-test's pinned copy so it \
keeps proving the matcher works against what actually ships"
);
}
#[test]
fn the_block_parser_finds_the_group_and_stops_at_the_block_end() {
let sample = "name: X\nconcurrency:\n group: abc\n cancel-in-progress: true\n\npermissions:\n group: NOT-THIS\n";
assert_eq!(concurrency_group(sample).as_deref(), Some("abc"));
// No concurrency block at all is a valid configuration, not a match.
assert_eq!(
concurrency_group("name: X\njobs:\n a:\n runs-on: x\n"),
None
);
}