Skip to main content

release_kit/setup/
steps.rs

1//! The setup step table, defined by what each step proves.
2//!
3//! Steps follow `method/02-setup.md`, with reporting intake after the
4//! protection inventory.
5
6/// What a step touches when it applies.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Mutates {
9    /// The step only reads and reports.
10    Nothing,
11    /// The step writes forge configuration.
12    Forge,
13    /// The step writes local repository state in the target.
14    Local,
15}
16
17/// One step of the setup, in canonical order.
18#[derive(Debug)]
19pub struct StepSpec {
20    /// The name, which is also the `rk setup step` argument and the script
21    /// file name in every forge tree.
22    pub name: &'static str,
23    /// The `method/02-setup.md` section the step executes.
24    pub chapter: &'static str,
25    /// What the step touches under apply.
26    pub mutates: Mutates,
27    /// What the step proves, from the chapter.
28    pub proves: &'static str,
29    /// Whether the step deletes anything; a destructive step carries its own
30    /// refusal beyond `--apply`.
31    pub destructive: bool,
32    /// Whether a full run skips the step: an optional step applies only
33    /// where its condition holds, by name, through `rk setup step`.
34    pub optional: bool,
35    /// Steps that must be observed satisfied before this one applies.
36    pub prereqs: &'static [&'static str],
37}
38
39/// The fifteen steps, with reporting intake last.
40///
41/// `package-check`, `branch-reminder`, and `forge-version` belong to no
42/// forge tree — the first reads its command from the technology binding,
43/// the second writes an embedded hook body into the target's own git
44/// directory, and the third is one read-only API call the observer already
45/// makes — which makes them the three steps outside the parity rule.
46pub const STEPS: [StepSpec; 15] = [
47    StepSpec {
48        name: "package-check",
49        chapter: "§0",
50        mutates: Mutates::Nothing,
51        proves: "the package is publishable with no credentials",
52        destructive: false,
53        optional: false,
54        prereqs: &[],
55    },
56    StepSpec {
57        name: "default-branch",
58        chapter: "§1",
59        mutates: Mutates::Forge,
60        proves: "the trunk is the default branch",
61        destructive: false,
62        optional: false,
63        prereqs: &[],
64    },
65    StepSpec {
66        name: "single-trunk",
67        chapter: "§1",
68        mutates: Mutates::Forge,
69        proves: "no long-lived branch besides the trunk remains",
70        destructive: true,
71        optional: false,
72        prereqs: &["default-branch"],
73    },
74    StepSpec {
75        name: "merge-cleanup",
76        chapter: "§1",
77        mutates: Mutates::Forge,
78        proves: "the forge deletes a branch when its merge lands",
79        destructive: false,
80        optional: false,
81        prereqs: &["default-branch"],
82    },
83    StepSpec {
84        name: "branch-reminder",
85        chapter: "§1",
86        mutates: Mutates::Local,
87        proves: "a pull reminds the operator when a merged branch lingers locally",
88        destructive: false,
89        optional: false,
90        prereqs: &[],
91    },
92    StepSpec {
93        name: "ci-permissions",
94        chapter: "§2",
95        mutates: Mutates::Forge,
96        proves: "CI may write and open requests",
97        destructive: false,
98        optional: false,
99        prereqs: &[],
100    },
101    StepSpec {
102        name: "install-bot",
103        chapter: "§2",
104        mutates: Mutates::Forge,
105        proves: "the bot identity can act on this project",
106        destructive: false,
107        optional: false,
108        prereqs: &[],
109    },
110    StepSpec {
111        name: "bot-secrets",
112        chapter: "§2",
113        mutates: Mutates::Forge,
114        proves: "the bot credentials are stored on the project",
115        destructive: false,
116        optional: false,
117        prereqs: &[],
118    },
119    StepSpec {
120        name: "forge-version",
121        chapter: "§3",
122        mutates: Mutates::Nothing,
123        proves: "the forge meets the convention's minimum version",
124        destructive: false,
125        optional: false,
126        prereqs: &[],
127    },
128    StepSpec {
129        name: "protect-trunk",
130        chapter: "§3",
131        mutates: Mutates::Forge,
132        proves: "the trunk takes no direct push, merges only by squash with the request's title and body as the message, and requires the named check",
133        destructive: false,
134        optional: false,
135        prereqs: &["default-branch", "forge-version"],
136    },
137    StepSpec {
138        name: "protect-tags",
139        chapter: "§3",
140        mutates: Mutates::Forge,
141        proves: "v* is protected as far as the forge allows",
142        destructive: false,
143        optional: false,
144        prereqs: &[],
145    },
146    StepSpec {
147        name: "protect-release-lines",
148        chapter: "§3",
149        mutates: Mutates::Forge,
150        proves: "release/* cannot be force-pushed or deleted",
151        destructive: false,
152        optional: true,
153        prereqs: &[],
154    },
155    StepSpec {
156        name: "auto-merge",
157        chapter: "§3",
158        mutates: Mutates::Forge,
159        proves: "a request may merge itself once its required checks pass",
160        destructive: false,
161        optional: false,
162        prereqs: &["default-branch"],
163    },
164    StepSpec {
165        name: "protections-check",
166        chapter: "§3",
167        mutates: Mutates::Nothing,
168        proves: "exactly the owned protections, with those rules",
169        destructive: false,
170        optional: false,
171        prereqs: &[],
172    },
173    StepSpec {
174        name: "private-vulnerability-reporting",
175        chapter: "§3",
176        mutates: Mutates::Forge,
177        proves: "a vulnerability report has a private intake path, with the forge's limits named",
178        destructive: false,
179        optional: false,
180        prereqs: &[],
181    },
182];
183
184/// Look one step up by name.
185#[must_use]
186pub fn spec(name: &str) -> Option<&'static StepSpec> {
187    STEPS.iter().find(|step| step.name == name)
188}
189
190#[cfg(test)]
191mod tests {
192    use super::{STEPS, spec};
193
194    /// Every prerequisite names a step that exists and comes earlier, so the
195    /// full run can never be refused by its own table.
196    #[test]
197    fn every_prereq_is_an_earlier_step() {
198        for (idx, step) in STEPS.iter().enumerate() {
199            for prereq in step.prereqs {
200                let position = STEPS
201                    .iter()
202                    .position(|other| other.name == *prereq)
203                    .unwrap_or(usize::MAX);
204                assert!(
205                    position < idx,
206                    "{}: prereq {prereq} is not an earlier step",
207                    step.name
208                );
209            }
210        }
211        let reporting = &STEPS[STEPS.len() - 1];
212        assert_eq!(reporting.name, "private-vulnerability-reporting");
213        assert_eq!(reporting.chapter, "§3");
214        assert_eq!(reporting.mutates, super::Mutates::Forge);
215        assert!(!reporting.optional && !reporting.destructive);
216        assert!(reporting.prereqs.is_empty());
217        assert_eq!(STEPS[STEPS.len() - 2].name, "protections-check");
218        assert!(spec("package-check").is_some());
219        assert!(spec("no-such-step").is_none());
220    }
221}