gobby-code 1.0.0

Fast Rust CLI for Gobby's code index — AST-aware search, symbol navigation, and dependency graph
Documentation
fn count_run_step(workflow: &str, command: &str) -> usize {
    workflow
        .lines()
        .filter(|line| line.trim() == format!("run: {command}"))
        .count()
}

const RELEASE_WORKFLOWS: [(&str, &str); 5] = [
    (
        "gcode",
        include_str!("../../../.github/workflows/release-gcode.yml"),
    ),
    (
        "ghook",
        include_str!("../../../.github/workflows/release-ghook.yml"),
    ),
    (
        "gloc",
        include_str!("../../../.github/workflows/release-gloc.yml"),
    ),
    (
        "gsqz",
        include_str!("../../../.github/workflows/release-gsqz.yml"),
    ),
    (
        "gwiki",
        include_str!("../../../.github/workflows/release-gwiki.yml"),
    ),
];

const SOFTPROPS_ACTION_GH_RELEASE_SHA: &str = "3bb12739c298aeb8a4eeaf626c5b8d85266b0e65";
const TAIKI_INSTALL_ACTION_SHA: &str = "f5b277aa8941a90c16bc1cd6ab9363e0502b7d31";
const ACTIONS_CHECKOUT_SHA: &str = "34e114876b0b11c390a56381ad16ebd13914f8d5";
const DTOLNAY_RUST_TOOLCHAIN_SHA: &str = "29eef336d9b2848a0b548edc03f92a220660cdb8";
const ACTIONS_CACHE_SHA: &str = "0057852bfaa89a56745cba8c7296529d2fc39830";
const ACTIONS_UPLOAD_ARTIFACT_SHA: &str = "ea165f8d65b6e75b540449e92b4886f43607fa02";

fn release_upload_marker(workflow: &str) -> Option<usize> {
    workflow
        .find("softprops/action-gh-release")
        .or_else(|| workflow.find("gh release create"))
}

#[test]
fn release_workflows_have_one_default_and_one_no_default_check() {
    let cases = [
        (
            include_str!("../../../.github/workflows/release-gcode.yml"),
            "gobby-code",
        ),
        (
            include_str!("../../../.github/workflows/release-gsqz.yml"),
            "gobby-squeeze",
        ),
        (
            include_str!("../../../.github/workflows/release-ghook.yml"),
            "gobby-hooks",
        ),
    ];

    for (workflow, package) in cases {
        assert_eq!(
            count_run_step(
                workflow,
                &format!("cargo clippy -p {package} -- -D warnings")
            ),
            1,
            "{package} default clippy step count"
        );
        assert_eq!(
            count_run_step(
                workflow,
                &format!("cargo clippy -p {package} --no-default-features -- -D warnings")
            ),
            1,
            "{package} no-default clippy step count"
        );
        assert_eq!(
            count_run_step(
                workflow,
                &format!("cargo nextest run --profile ci -p {package}")
            ),
            1,
            "{package} default test step count"
        );
        assert_eq!(
            count_run_step(workflow, &format!("cargo test --doc -p {package}")),
            1,
            "{package} default doctest step count"
        );
        assert_eq!(
            count_run_step(
                workflow,
                &format!("cargo nextest run --profile ci -p {package} --no-default-features")
            ),
            1,
            "{package} no-default test step count"
        );
        assert_eq!(
            count_run_step(
                workflow,
                &format!("cargo test --doc -p {package} --no-default-features")
            ),
            1,
            "{package} no-default doctest step count"
        );
    }
}

#[test]
fn release_workflows_pin_github_release_action_by_sha() {
    for (tool, workflow) in RELEASE_WORKFLOWS {
        let release_ref = format!("softprops/action-gh-release@{SOFTPROPS_ACTION_GH_RELEASE_SHA}");
        let release_uses = workflow
            .lines()
            .map(str::trim)
            .filter_map(|line| line.strip_prefix("uses: "))
            .filter(|uses| uses.starts_with("softprops/action-gh-release@"))
            .collect::<Vec<_>>();
        if !release_uses.is_empty() {
            assert!(
                release_uses
                    .iter()
                    .all(|uses| *uses == release_ref.as_str()),
                "release-{tool}.yml should pin softprops/action-gh-release by SHA"
            );
        }
    }
}

#[test]
fn ci_workflow_pins_taiki_install_actions_by_sha() {
    let workflow = include_str!("../../../.github/workflows/ci.yml");

    assert_eq!(
        workflow.matches("taiki-e/install-action@nextest").count(),
        0
    );
    assert_eq!(
        workflow
            .matches("taiki-e/install-action@cargo-llvm-cov")
            .count(),
        0
    );
    assert_eq!(
        workflow
            .matches(&format!(
                "taiki-e/install-action@{TAIKI_INSTALL_ACTION_SHA}"
            ))
            .count(),
        3
    );
    assert!(workflow.contains("tool: nextest"));
    assert!(workflow.contains("tool: cargo-llvm-cov"));
}

#[test]
fn ci_workflow_pins_core_actions_by_sha() {
    let workflow = include_str!("../../../.github/workflows/ci.yml");

    assert_eq!(
        workflow
            .matches(&format!("actions/checkout@{ACTIONS_CHECKOUT_SHA}"))
            .count(),
        2
    );
    assert_eq!(workflow.matches("actions/checkout@v4").count(), 0);
    assert_eq!(workflow.matches("persist-credentials: false").count(), 2);

    assert_eq!(
        workflow
            .matches(&format!(
                "dtolnay/rust-toolchain@{DTOLNAY_RUST_TOOLCHAIN_SHA}"
            ))
            .count(),
        2
    );
    assert_eq!(workflow.matches("dtolnay/rust-toolchain@stable").count(), 0);
    assert_eq!(workflow.matches("toolchain: stable").count(), 2);

    assert_eq!(
        workflow
            .matches(&format!("actions/cache@{ACTIONS_CACHE_SHA}"))
            .count(),
        2
    );
    assert_eq!(workflow.matches("actions/cache@v4").count(), 0);

    assert_eq!(
        workflow
            .matches(&format!(
                "actions/upload-artifact@{ACTIONS_UPLOAD_ARTIFACT_SHA}"
            ))
            .count(),
        1
    );
    assert_eq!(workflow.matches("actions/upload-artifact@v4").count(), 0);
}

#[test]
fn release_gwiki_uses_github_cli_release_creation() {
    let workflow = include_str!("../../../.github/workflows/release-gwiki.yml");

    assert!(
        !workflow.contains("softprops/action-gh-release"),
        "release-gwiki.yml should use the GitHub CLI instead of softprops"
    );
    assert!(
        workflow.contains("GH_TOKEN: ${{ github.token }}"),
        "release-gwiki.yml should authenticate gh with github.token"
    );
    assert!(
        workflow
            .contains(r#"gh release create "$tag" "${assets[@]}" --generate-notes --verify-tag"#),
        "release-gwiki.yml should create the release with generated notes and tag verification"
    );
}

#[test]
fn release_gsqz_uses_github_cli_release_creation() {
    let workflow = include_str!("../../../.github/workflows/release-gsqz.yml");

    assert!(
        !workflow.contains("softprops/action-gh-release"),
        "release-gsqz.yml should use the GitHub CLI instead of softprops"
    );
    assert!(
        workflow.contains("GITHUB_RELEASE_TOKEN: ${{ github.token }}"),
        "release-gsqz.yml should pass github.token to the explicit gh auth flow"
    );
    assert!(
        workflow.contains("gh auth login --with-token"),
        "release-gsqz.yml should authenticate gh explicitly"
    );
    assert!(
        workflow
            .contains(r#"gh release create "$tag" "${assets[@]}" --generate-notes --verify-tag"#),
        "release-gsqz.yml should create the release with generated notes and tag verification"
    );
}

#[test]
fn release_gloc_uses_github_cli_release_creation_and_upload() {
    let workflow = include_str!("../../../.github/workflows/release-gloc.yml");

    assert!(
        !workflow.contains("softprops/action-gh-release"),
        "release-gloc.yml should use the GitHub CLI instead of softprops"
    );
    assert!(
        workflow.contains("GH_TOKEN: ${{ github.token }}"),
        "release-gloc.yml should authenticate gh with github.token"
    );
    assert!(
        workflow.contains(r#"gh release create "$tag" --generate-notes --verify-tag"#),
        "release-gloc.yml should create generated release notes with tag verification"
    );
    assert!(
        workflow.contains(r#"gh release upload "$tag" "${assets[@]}""#),
        "release-gloc.yml should upload all generated gloc assets"
    );
}

#[test]
fn release_gwiki_validates_dependencies_without_python_helper() {
    let workflow = include_str!("../../../.github/workflows/release-gwiki.yml");

    assert!(
        !workflow.contains("python3"),
        "release-gwiki.yml should validate dependencies inline without python3"
    );
    assert!(
        !workflow.contains(".github/scripts/verify-gwiki-deps.py"),
        "release-gwiki.yml should not reference the removed dependency helper"
    );
}

#[test]
fn release_workflows_generate_and_upload_archive_checksums() {
    for (tool, workflow) in RELEASE_WORKFLOWS {
        let checksum_step = workflow
            .find("name: Generate SHA-256 checksums")
            .unwrap_or_else(|| panic!("release-{tool}.yml missing checksum generation step"));
        let release_upload = release_upload_marker(workflow)
            .unwrap_or_else(|| panic!("release-{tool}.yml missing GitHub release upload"));

        assert!(
            checksum_step < release_upload,
            "release-{tool}.yml should generate checksums before release upload"
        );

        let checksum_body = &workflow[checksum_step..release_upload];
        assert!(
            checksum_body.contains(&format!("{tool}-*.tar.gz")),
            "release-{tool}.yml should generate tar.gz checksums"
        );
        assert!(
            checksum_body.contains(&format!("{tool}-*.zip")),
            "release-{tool}.yml should generate zip checksums"
        );
        assert!(
            checksum_body.contains(r#"sha256sum "$asset" > "$asset.sha256""#),
            "release-{tool}.yml should write per-asset checksum files"
        );
        assert!(
            checksum_body.contains("shell: bash"),
            "release-{tool}.yml should run checksum generation with bash"
        );

        if matches!(tool, "gwiki" | "gsqz") {
            assert!(
                workflow[checksum_step..].contains(&format!(
                    "assets=({tool}-*.tar.gz {tool}-*.zip {tool}-*.sha256)"
                )),
                "release-{tool}.yml should add checksum files to the gh release asset array"
            );
        } else if tool == "gloc" {
            assert!(
                workflow[checksum_step..]
                    .contains("assets=(gloc-*.tar.gz gloc-*.zip gloc-*.sha256)"),
                "release-gloc.yml should add checksum files to the gh release asset array"
            );
            assert!(
                workflow[release_upload..].contains(r#"gh release upload "$tag" "${assets[@]}""#),
                "release-gloc.yml should upload checksum files with gh release upload"
            );
        } else {
            let release_body = &workflow[release_upload..];
            assert!(
                release_body.contains(&format!("{tool}-*.sha256")),
                "release-{tool}.yml should upload checksum files"
            );
        }
    }
}