Skip to main content

release_kit/setup/
steps.rs

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