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}
13
14/// One step of the setup, in canonical order.
15#[derive(Debug)]
16pub struct StepSpec {
17    /// The name, which is also the `rk setup step` argument and the script
18    /// file name in every forge tree.
19    pub name: &'static str,
20    /// The `method/02-setup.md` section the step executes.
21    pub chapter: &'static str,
22    /// What the step touches under apply.
23    pub mutates: Mutates,
24    /// What the step proves, from the chapter.
25    pub proves: &'static str,
26    /// Whether the step deletes anything; a destructive step carries its own
27    /// refusal beyond `--apply`.
28    pub destructive: bool,
29    /// Whether a full run skips the step: an optional step applies only
30    /// where its condition holds, by name, through `rk setup step`.
31    pub optional: bool,
32    /// Steps that must be observed satisfied before this one applies.
33    pub prereqs: &'static [&'static str],
34}
35
36/// The ten steps, in the chapter's order. `package-check` belongs to no
37/// forge tree — it reads its command from the technology binding — which
38/// makes it the one step outside the parity rule.
39pub const STEPS: [StepSpec; 10] = [
40    StepSpec {
41        name: "package-check",
42        chapter: "§0",
43        mutates: Mutates::Nothing,
44        proves: "the package is publishable with no credentials",
45        destructive: false,
46        optional: false,
47        prereqs: &[],
48    },
49    StepSpec {
50        name: "default-branch",
51        chapter: "§1",
52        mutates: Mutates::Forge,
53        proves: "the trunk is the default branch",
54        destructive: false,
55        optional: false,
56        prereqs: &[],
57    },
58    StepSpec {
59        name: "single-trunk",
60        chapter: "§1",
61        mutates: Mutates::Forge,
62        proves: "no long-lived branch besides the trunk remains",
63        destructive: true,
64        optional: false,
65        prereqs: &["default-branch"],
66    },
67    StepSpec {
68        name: "ci-permissions",
69        chapter: "§2",
70        mutates: Mutates::Forge,
71        proves: "CI may write and open requests",
72        destructive: false,
73        optional: false,
74        prereqs: &[],
75    },
76    StepSpec {
77        name: "install-bot",
78        chapter: "§2",
79        mutates: Mutates::Forge,
80        proves: "the bot identity can act on this project",
81        destructive: false,
82        optional: false,
83        prereqs: &[],
84    },
85    StepSpec {
86        name: "bot-secrets",
87        chapter: "§2",
88        mutates: Mutates::Forge,
89        proves: "the bot credentials are stored on the project",
90        destructive: false,
91        optional: false,
92        prereqs: &[],
93    },
94    StepSpec {
95        name: "protect-trunk",
96        chapter: "§3",
97        mutates: Mutates::Forge,
98        proves: "the trunk takes no direct push, merges only by squash, and requires the named check",
99        destructive: false,
100        optional: false,
101        prereqs: &["default-branch"],
102    },
103    StepSpec {
104        name: "protect-tags",
105        chapter: "§3",
106        mutates: Mutates::Forge,
107        proves: "v* is protected as far as the forge allows",
108        destructive: false,
109        optional: false,
110        prereqs: &[],
111    },
112    StepSpec {
113        name: "protect-release-lines",
114        chapter: "§3",
115        mutates: Mutates::Forge,
116        proves: "release/* cannot be force-pushed or deleted",
117        destructive: false,
118        optional: true,
119        prereqs: &[],
120    },
121    StepSpec {
122        name: "protections-check",
123        chapter: "§3",
124        mutates: Mutates::Nothing,
125        proves: "exactly the owned protections, with those rules",
126        destructive: false,
127        optional: false,
128        prereqs: &[],
129    },
130];
131
132/// Look one step up by name.
133#[must_use]
134pub fn spec(name: &str) -> Option<&'static StepSpec> {
135    STEPS.iter().find(|step| step.name == name)
136}
137
138#[cfg(test)]
139mod tests {
140    use super::{STEPS, spec};
141
142    /// Every prerequisite names a step that exists and comes earlier, so the
143    /// full run can never be refused by its own table.
144    #[test]
145    fn every_prereq_is_an_earlier_step() {
146        for (idx, step) in STEPS.iter().enumerate() {
147            for prereq in step.prereqs {
148                let position = STEPS
149                    .iter()
150                    .position(|other| other.name == *prereq)
151                    .unwrap_or(usize::MAX);
152                assert!(
153                    position < idx,
154                    "{}: prereq {prereq} is not an earlier step",
155                    step.name
156                );
157            }
158        }
159        assert!(spec("package-check").is_some());
160        assert!(spec("no-such-step").is_none());
161    }
162}