logmv 0.7.1

Logged atomic file move and trash with an append-only JSON-Lines audit trail
Documentation
// Regression guard: every GitHub Actions `uses:` reference across every
// workflow file must be pinned to a full 40-hex commit SHA, never a tag or
// branch name. This is a supply-chain invariant, not a snapshot of specific
// SHAs: it stays meaningful as pins are bumped over time.

#[test]
fn every_workflow_action_is_pinned_to_a_full_commit_sha() {
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
    let workflows_dir = std::path::Path::new(manifest_dir).join(".github/workflows");

    let mut files_scanned = 0usize;
    let mut uses_lines_checked = 0usize;

    let entries = std::fs::read_dir(&workflows_dir)
        .unwrap_or_else(|e| panic!("failed to read {}: {e}", workflows_dir.display()));

    for entry in entries {
        let entry = entry.unwrap_or_else(|e| panic!("failed to read directory entry: {e}"));
        let path = entry.path();
        let ext = path.extension().and_then(|e| e.to_str());
        if ext != Some("yml") && ext != Some("yaml") {
            continue;
        }

        files_scanned += 1;
        let content = std::fs::read_to_string(&path)
            .unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));

        for (line_num, line) in content.lines().enumerate() {
            if !line.contains("uses:") {
                continue;
            }
            uses_lines_checked += 1;

            // Trim any trailing `#` comment before isolating the ref, since a
            // version comment (e.g. `# stable branch tip @ 2026-06-30`) can
            // itself contain an `@` that is not part of the pin.
            let before_comment = line.split('#').next().unwrap_or(line);
            let after_at = before_comment.rsplit_once('@').unwrap_or_else(|| {
                panic!(
                    "{}:{}: `uses:` line has no `@` ref separator: {line:?}",
                    path.display(),
                    line_num + 1
                )
            });
            let ref_str = after_at.1.trim();

            let is_40_hex = ref_str.len() == 40 && ref_str.chars().all(|c| c.is_ascii_hexdigit());

            assert!(
                is_40_hex,
                "{}:{}: action ref {:?} is not a full 40-hex commit SHA (line: {:?})",
                path.display(),
                line_num + 1,
                ref_str,
                line
            );
        }
    }

    assert!(
        files_scanned >= 1,
        "no workflow files found under {}; glob is broken",
        workflows_dir.display()
    );
    assert!(
        uses_lines_checked >= 1,
        "no `uses:` lines found across scanned workflow files; scan is broken"
    );
}