1use include_dir::{Dir, include_dir};
10
11pub use crate::payload_roots::PAYLOAD_ROOTS;
12
13pub static METHOD: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/method");
15
16pub static BINDINGS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/bindings");
18
19pub static RUNBOOKS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/runbooks");
21
22pub static FORGES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/forges");
24
25pub static SETUP: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/setup");
28
29pub static SNIPPETS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/snippets");
32
33pub static BLOCKS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/blocks");
40
41pub static SKILLS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skills");
43
44pub static SKILL_SHARED: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skill-shared");
46
47pub static VERSIONS: &str = include_str!("../versions.toml");
49
50pub static LICENSE: &str = include_str!("../LICENSE");
52
53pub static LICENSE_MIT: &str = include_str!("../LICENSE-MIT");
55
56pub static LICENSE_CC_BY: &str = include_str!("../LICENSE-CC-BY-4.0");
58
59pub const SENTINEL: &str = "TODO(release-kit)";
62
63pub(crate) fn walk<'a>(dir: &Dir<'a>) -> Vec<(String, &'a [u8])> {
66 let mut out = Vec::new();
67 for file in dir.files() {
68 out.push((file.path().to_string_lossy().into_owned(), file.contents()));
69 }
70 for sub in dir.dirs() {
71 out.extend(walk(sub));
72 }
73 out.sort_by(|a, b| a.0.cmp(&b.0));
74 out
75}
76
77#[must_use]
81pub fn root_files(root: &str) -> Option<Vec<(String, &'static [u8])>> {
82 let dir = match root {
83 "method" => &METHOD,
84 "bindings" => &BINDINGS,
85 "runbooks" => &RUNBOOKS,
86 "forges" => &FORGES,
87 "snippets" => &SNIPPETS,
88 "blocks" => &BLOCKS,
89 "setup" => &SETUP,
90 "skills" => &SKILLS,
91 "skill-shared" => &SKILL_SHARED,
92 "versions.toml" => return Some(vec![(root.to_owned(), VERSIONS.as_bytes())]),
93 _ => return None,
94 };
95 Some(
96 walk(dir)
97 .into_iter()
98 .map(|(path, bytes)| (format!("{root}/{path}"), bytes))
99 .collect(),
100 )
101}
102
103#[must_use]
109pub fn artifacts() -> Vec<(String, &'static [u8])> {
110 PAYLOAD_ROOTS
111 .iter()
112 .filter_map(|root| root_files(root))
113 .flatten()
114 .collect()
115}
116
117#[cfg(test)]
118mod tests {
119 use super::{PAYLOAD_ROOTS, artifacts, root_files};
120
121 #[test]
127 fn the_inventory_and_the_embed_declare_the_same_roots() {
128 let source = include_str!("embedded.rs");
129 let mut embedded: Vec<String> = source
130 .lines()
131 .filter_map(|line| {
132 let (_, rest) = line.split_once("include_dir!(\"$CARGO_MANIFEST_DIR/")?;
133 let (root, _) = rest.split_once('"')?;
134 Some(root.to_owned())
135 })
136 .collect();
137 embedded.extend(source.lines().filter_map(|line| {
138 let (_, rest) = line.split_once("include_str!(\"../")?;
139 let (name, _) = rest.split_once('"')?;
140 (!name.starts_with("LICENSE")).then(|| name.to_owned())
141 }));
142 embedded.sort();
143 let mut declared: Vec<String> = PAYLOAD_ROOTS.iter().map(ToString::to_string).collect();
144 declared.sort();
145 assert_eq!(
146 embedded, declared,
147 "src/embedded.rs and src/payload_roots.rs disagree on the payload roots"
148 );
149 }
150
151 #[test]
152 fn every_declared_root_serves_at_least_one_file() {
153 for root in PAYLOAD_ROOTS {
154 let files = root_files(root).expect("a declared root resolves");
155 assert!(!files.is_empty(), "{root}: the root carries no file");
156 for (path, _) in &files {
157 assert!(
158 path == root || path.starts_with(&format!("{root}/")),
159 "{path}: an artifact path must carry its root"
160 );
161 }
162 }
163 assert!(root_files("no-such-root").is_none());
164 }
165
166 #[test]
171 fn every_block_is_authored_with_one_final_newline() {
172 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("blocks");
173 for file in super::BLOCKS.files() {
174 let name = file.path().to_string_lossy().into_owned();
175 let disk = std::fs::read(root.join(&name)).expect("an embedded block exists on disk");
176 assert_eq!(disk, file.contents(), "{name}: embed and disk disagree");
177 let text = std::str::from_utf8(file.contents()).expect("a block is UTF-8");
178 assert!(text.ends_with('\n'), "{name}: a block ends in a newline");
179 assert!(
180 !text.ends_with("\n\n"),
181 "{name}: a block ends in exactly one newline"
182 );
183 }
184 }
185
186 #[test]
197 fn no_artifact_body_lives_as_a_source_literal() {
198 let needles = [
199 "## Releases",
200 "Installed by rk setup step branch-reminder",
201 "This project works in worktrees:",
202 "Branches are worked in the main checkout",
203 "stages: [commit-msg]",
204 "ROUTING_BLOCK",
205 "ROUTING_WORKTREE_LINE",
206 "ROUTING_BRANCHES_LINE",
207 "HOOKS_BLOCK",
208 "WORKTREE_GUARD_ENTRY",
209 "HOOK_BODY",
210 "use flake",
211 "rk devshell sync --apply",
212 "release-kit.packages.",
213 "inputs.nixpkgs.follows",
214 ];
215 let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
216 let mut offenders = Vec::new();
217 scan(&src, &needles, &mut offenders);
218 assert!(
219 offenders.is_empty(),
220 "an artifact body belongs under blocks/, not in the sources: {offenders:?}"
221 );
222 }
223
224 fn scan(dir: &std::path::Path, needles: &[&str], offenders: &mut Vec<String>) {
225 for entry in std::fs::read_dir(dir).expect("the source tree is readable") {
226 let entry = entry.expect("a directory entry is readable");
227 let path = entry.path();
228 if path.is_dir() {
229 scan(&path, needles, offenders);
230 continue;
231 }
232 if path.extension().is_none_or(|ext| ext != "rs") {
233 continue;
234 }
235 let text = std::fs::read_to_string(&path).expect("a source file is UTF-8");
236 let production = text.split("#[cfg(test)]").next().unwrap_or("");
237 for (index, line) in production.lines().enumerate() {
238 if line.trim_start().starts_with("//") {
239 continue;
240 }
241 for needle in needles {
242 if line.contains(needle) {
243 offenders.push(format!("{}:{}: {needle}", path.display(), index + 1));
244 }
245 }
246 }
247 for (line, span) in multiline_literals(production) {
248 offenders.push(format!(
249 "{}:{line}: a string literal spanning {span} lines",
250 path.display()
251 ));
252 }
253 }
254 }
255
256 const GLUE: [&str; 3] = [
262 "{}\\n\\n{block}\\n",
263 "{HOOK_TYPES_LINE}\\n\\nrepos:\\n{block}\\n",
264 concat!(
265 "Authorization: Bearer {jwt}\\nAccept: application/vnd.github+json\\n",
266 "X-GitHub-Api-Version: 2022-11-28\\n"
267 ),
268 ];
269
270 fn multiline_literals(text: &str) -> Vec<(usize, usize)> {
278 let bytes = text.as_bytes();
279 let mut spans = Vec::new();
280 let mut line = 1;
281 let mut i = 0;
282 while i < bytes.len() {
283 match bytes[i] {
284 b'\n' => {
285 line += 1;
286 i += 1;
287 }
288 b'/' if bytes.get(i + 1) == Some(&b'/') => {
289 while i < bytes.len() && bytes[i] != b'\n' {
290 i += 1;
291 }
292 }
293 b'r' if matches!(bytes.get(i + 1), Some(&b'#' | &b'"')) => {
294 let hashes = bytes[i + 1..]
295 .iter()
296 .take_while(|byte| **byte == b'#')
297 .count();
298 if bytes.get(i + 1 + hashes) != Some(&b'"') {
299 i += 1;
300 continue;
301 }
302 let body = i + hashes + 2;
303 let close = format!("\"{}", "#".repeat(hashes));
304 let end = text[body..]
305 .find(&close)
306 .map_or(bytes.len(), |at| body + at);
307 let physical = text[i..end].matches('\n').count();
308 if physical >= 2 {
309 spans.push((line, physical + 1));
310 }
311 line += physical;
312 i = (end + close.len()).min(bytes.len());
313 }
314 b'"' => {
315 let mut j = i + 1;
316 while j < bytes.len() && bytes[j] != b'"' {
317 j += if bytes[j] == b'\\' { 2 } else { 1 };
318 }
319 let segment = &text[i + 1..j.min(bytes.len())];
320 let physical = segment.matches('\n').count();
321 let decoded = physical + segment.matches("\\n").count();
322 if physical >= 2 || (decoded >= 2 && !GLUE.contains(&segment)) {
329 spans.push((line, decoded + 1));
330 }
331 line += physical;
332 i = j + 1;
333 }
334 _ => i += 1,
335 }
336 }
337 spans
338 }
339
340 #[test]
341 fn the_artifact_list_is_stable_and_complete() {
342 let listed = artifacts();
343 let total: usize = PAYLOAD_ROOTS
344 .iter()
345 .map(|root| root_files(root).expect("a declared root resolves").len())
346 .sum();
347 assert_eq!(listed.len(), total);
348 assert!(
349 listed.iter().any(|(path, _)| path == "versions.toml"),
350 "the single-file root must appear as itself"
351 );
352 }
353}