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 COMPATIBILITY: &str = include_str!("../compatibility.toml");
52
53pub static GUIDANCE: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/guidance");
56
57pub static LICENSE: &str = include_str!("../LICENSE");
59
60pub static LICENSE_MIT: &str = include_str!("../LICENSE-MIT");
62
63pub static LICENSE_CC_BY: &str = include_str!("../LICENSE-CC-BY-4.0");
65
66pub const SENTINEL: &str = "TODO(release-kit)";
69
70pub(crate) fn walk<'a>(dir: &Dir<'a>) -> Vec<(String, &'a [u8])> {
73 let mut out = Vec::new();
74 for file in dir.files() {
75 out.push((file.path().to_string_lossy().into_owned(), file.contents()));
76 }
77 for sub in dir.dirs() {
78 out.extend(walk(sub));
79 }
80 out.sort_by(|a, b| a.0.cmp(&b.0));
81 out
82}
83
84#[must_use]
88pub fn root_files(root: &str) -> Option<Vec<(String, &'static [u8])>> {
89 let dir = match root {
90 "method" => &METHOD,
91 "bindings" => &BINDINGS,
92 "runbooks" => &RUNBOOKS,
93 "forges" => &FORGES,
94 "snippets" => &SNIPPETS,
95 "blocks" => &BLOCKS,
96 "setup" => &SETUP,
97 "skills" => &SKILLS,
98 "skill-shared" => &SKILL_SHARED,
99 "guidance" => &GUIDANCE,
100 "versions.toml" => return Some(vec![(root.to_owned(), VERSIONS.as_bytes())]),
101 "compatibility.toml" => return Some(vec![(root.to_owned(), COMPATIBILITY.as_bytes())]),
102 _ => return None,
103 };
104 Some(
105 walk(dir)
106 .into_iter()
107 .map(|(path, bytes)| (format!("{root}/{path}"), bytes))
108 .collect(),
109 )
110}
111
112#[must_use]
118pub fn artifacts() -> Vec<(String, &'static [u8])> {
119 PAYLOAD_ROOTS
120 .iter()
121 .filter_map(|root| root_files(root))
122 .flatten()
123 .collect()
124}
125
126#[cfg(test)]
127mod tests {
128 use super::{PAYLOAD_ROOTS, artifacts, root_files};
129
130 #[test]
136 fn the_inventory_and_the_embed_declare_the_same_roots() {
137 let source = include_str!("embedded.rs");
138 let mut embedded: Vec<String> = source
139 .lines()
140 .filter_map(|line| {
141 let (_, rest) = line.split_once("include_dir!(\"$CARGO_MANIFEST_DIR/")?;
142 let (root, _) = rest.split_once('"')?;
143 Some(root.to_owned())
144 })
145 .collect();
146 embedded.extend(source.lines().filter_map(|line| {
147 let (_, rest) = line.split_once("include_str!(\"../")?;
148 let (name, _) = rest.split_once('"')?;
149 (!name.starts_with("LICENSE")).then(|| name.to_owned())
150 }));
151 embedded.sort();
152 let mut declared: Vec<String> = PAYLOAD_ROOTS.iter().map(ToString::to_string).collect();
153 declared.sort();
154 assert_eq!(
155 embedded, declared,
156 "src/embedded.rs and src/payload_roots.rs disagree on the payload roots"
157 );
158 }
159
160 #[test]
161 fn every_declared_root_serves_at_least_one_file() {
162 for root in PAYLOAD_ROOTS {
163 let files = root_files(root).expect("a declared root resolves");
164 assert!(!files.is_empty(), "{root}: the root carries no file");
165 for (path, _) in &files {
166 assert!(
167 path == root || path.starts_with(&format!("{root}/")),
168 "{path}: an artifact path must carry its root"
169 );
170 }
171 }
172 assert!(root_files("no-such-root").is_none());
173 }
174
175 #[test]
180 fn every_block_is_authored_with_one_final_newline() {
181 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("blocks");
182 for file in super::BLOCKS.files() {
183 let name = file.path().to_string_lossy().into_owned();
184 let disk = std::fs::read(root.join(&name)).expect("an embedded block exists on disk");
185 assert_eq!(disk, file.contents(), "{name}: embed and disk disagree");
186 let text = std::str::from_utf8(file.contents()).expect("a block is UTF-8");
187 assert!(text.ends_with('\n'), "{name}: a block ends in a newline");
188 assert!(
189 !text.ends_with("\n\n"),
190 "{name}: a block ends in exactly one newline"
191 );
192 }
193 }
194
195 #[test]
206 fn no_artifact_body_lives_as_a_source_literal() {
207 let needles = [
208 "## Releases",
209 "Installed by rk setup step branch-reminder",
210 "This project works in worktrees:",
211 "Branches are worked in the main checkout",
212 "stages: [commit-msg]",
213 "ROUTING_BLOCK",
214 "ROUTING_WORKTREE_LINE",
215 "ROUTING_BRANCHES_LINE",
216 "HOOKS_BLOCK",
217 "WORKTREE_GUARD_ENTRY",
218 "HOOK_BODY",
219 "use flake",
220 "rk self-depend sync --apply",
221 "release-kit.packages.",
222 "inputs.nixpkgs.follows",
223 ];
224 let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
225 let mut offenders = Vec::new();
226 scan(&src, &needles, &mut offenders);
227 assert!(
228 offenders.is_empty(),
229 "an artifact body belongs under blocks/, not in the sources: {offenders:?}"
230 );
231 }
232
233 fn scan(dir: &std::path::Path, needles: &[&str], offenders: &mut Vec<String>) {
234 for entry in std::fs::read_dir(dir).expect("the source tree is readable") {
235 let entry = entry.expect("a directory entry is readable");
236 let path = entry.path();
237 if path.is_dir() {
238 scan(&path, needles, offenders);
239 continue;
240 }
241 if path.extension().is_none_or(|ext| ext != "rs") {
242 continue;
243 }
244 let text = std::fs::read_to_string(&path).expect("a source file is UTF-8");
245 let production = text.split("#[cfg(test)]").next().unwrap_or("");
246 for (index, line) in production.lines().enumerate() {
247 if line.trim_start().starts_with("//") {
248 continue;
249 }
250 for needle in needles {
251 if line.contains(needle) {
252 offenders.push(format!("{}:{}: {needle}", path.display(), index + 1));
253 }
254 }
255 }
256 for (line, span) in multiline_literals(production) {
257 offenders.push(format!(
258 "{}:{line}: a string literal spanning {span} lines",
259 path.display()
260 ));
261 }
262 }
263 }
264
265 const GLUE: [&str; 3] = [
271 "{}\\n\\n{block}\\n",
272 "{HOOK_TYPES_LINE}\\n\\nrepos:\\n{block}\\n",
273 concat!(
274 "Authorization: Bearer {jwt}\\nAccept: application/vnd.github+json\\n",
275 "X-GitHub-Api-Version: 2022-11-28\\n"
276 ),
277 ];
278
279 fn multiline_literals(text: &str) -> Vec<(usize, usize)> {
287 let bytes = text.as_bytes();
288 let mut spans = Vec::new();
289 let mut line = 1;
290 let mut i = 0;
291 while i < bytes.len() {
292 match bytes[i] {
293 b'\n' => {
294 line += 1;
295 i += 1;
296 }
297 b'/' if bytes.get(i + 1) == Some(&b'/') => {
298 while i < bytes.len() && bytes[i] != b'\n' {
299 i += 1;
300 }
301 }
302 b'r' if matches!(bytes.get(i + 1), Some(&b'#' | &b'"')) => {
303 let hashes = bytes[i + 1..]
304 .iter()
305 .take_while(|byte| **byte == b'#')
306 .count();
307 if bytes.get(i + 1 + hashes) != Some(&b'"') {
308 i += 1;
309 continue;
310 }
311 let body = i + hashes + 2;
312 let close = format!("\"{}", "#".repeat(hashes));
313 let end = text[body..]
314 .find(&close)
315 .map_or(bytes.len(), |at| body + at);
316 let physical = text[i..end].matches('\n').count();
317 if physical >= 2 {
318 spans.push((line, physical + 1));
319 }
320 line += physical;
321 i = (end + close.len()).min(bytes.len());
322 }
323 b'"' => {
324 let mut j = i + 1;
325 while j < bytes.len() && bytes[j] != b'"' {
326 j += if bytes[j] == b'\\' { 2 } else { 1 };
327 }
328 let segment = &text[i + 1..j.min(bytes.len())];
329 let physical = segment.matches('\n').count();
330 let decoded = physical + segment.matches("\\n").count();
331 if physical >= 2 || (decoded >= 2 && !GLUE.contains(&segment)) {
338 spans.push((line, decoded + 1));
339 }
340 line += physical;
341 i = j + 1;
342 }
343 _ => i += 1,
344 }
345 }
346 spans
347 }
348
349 #[test]
350 fn the_artifact_list_is_stable_and_complete() {
351 let listed = artifacts();
352 let total: usize = PAYLOAD_ROOTS
353 .iter()
354 .map(|root| root_files(root).expect("a declared root resolves").len())
355 .sum();
356 assert_eq!(listed.len(), total);
357 assert!(
358 listed.iter().any(|(path, _)| path == "versions.toml"),
359 "the single-file root must appear as itself"
360 );
361 }
362}