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