Skip to content

feat: add changelog PR coverage test and backfill missing entries - #17

Merged
tunahorse merged 2 commits into
mainfrom
changelog-pr-coverage-test
May 1, 2026
Merged

tunahorse merged 2 commits into
mainfrom
changelog-pr-coverage-test

Conversation

@tunahorse

@tunahorse tunahorse commented Apr 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

How it works

  1. Extracts all #N PR references from CHANGELOG.md
  2. Extracts all #N PR references from git log --all (covers both merge and squash-merged PRs)
  3. Fails with a clear message listing any missing PR IDs

Verification

  • cargo test --test changelog_test passes
  • just check passes (all 66 tests, fmt, clippy, docs)
  • Manually verified: removing a PR reference from the changelog causes the test to fail with the expected message

Summary by CodeRabbit

  • Documentation

    • Updated changelog documenting Exa as a search/provider option, hygiene-check validation chain, and library target availability for integration testing.
  • Tests

    • Added integration test suite covering app-layer fan-out, provider registry, and CLI parsing.
    • Added validation test ensuring changelog entries stay synchronized with merged PRs.
  • Bug Fixes

    • Configuration rejects empty or whitespace-only provider API keys.
  • Chores

    • CI workflow updated to declare repository permissions and supply token for validation.

Adds a test asserting every merged PR number appears in CHANGELOG.md.
Backfills missing entries for PRs #1, #2, #10, #12.
@coderabbitai

coderabbitai Bot commented Apr 29, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Updates the Unreleased section of CHANGELOG.md; adds a new test tests/changelog_test.rs that verifies changelog PR IDs against merged GitHub PRs via the REST API; and adjusts CI workflow permissions and GITHUB_TOKEN exposure for the verification step.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Added ### Added, ### Changed, and ### Fixed entries: Exa selection as search provider, hygiene checks (file-size limits, TODO/FIXME), integration tests, hygiene-check validation chain reference, exposed library target for tests, HARNESS gate note, and provider API key rejection behavior.
Changelog Validation Test
tests/changelog_test.rs
New test that parses #<number> IDs from CHANGELOG.md, queries the GitHub REST API for merged PR numbers (paginated), computes missing IDs, and fails CI if changelog lacks merged PRs. Includes unit test for ID parsing and CI-safe behavior when GitHub calls fail.
CI Workflow
.github/workflows/validate-agents.yml
Added explicit permissions (read: contents, pull-requests) and exports GITHUB_TOKEN into the environment for the “Verify canonical command works” (just check) step.

Sequence Diagram(s)

sequenceDiagram
  participant CI as CI runner
  participant Test as changelog test
  participant GH as GitHub REST API
  participant Repo as Repository (CHANGELOG.md)

  CI->>Test: run tests (including changelog_test)
  Test->>Repo: read CHANGELOG.md
  Test->>GH: GET /pulls?state=closed&base=main (paginated)
  GH-->>Test: merged PR pages (numbers)
  Test->>Test: extract IDs from CHANGELOG.md, compute difference
  alt missing IDs found
    Test-->>CI: fail with missing list
  else all present or no merged PRs
    Test-->>CI: pass (or skip if GH unreachable in non-CI)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐇 I hopped through changelogs, numbers in paw,
Counting each #123 to make CI roar,
I queried the clouds, fetched PRs from afar,
Now entries align — hip-hop hooray! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add changelog PR coverage test and backfill missing entries' clearly and concisely summarizes the main changes: adding a new test and backfilling changelog entries.
Description check ✅ Passed The PR description covers the main changes, how the solution works, and verification steps performed, addressing the core requirements of the template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch changelog-pr-coverage-test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/changelog_test.rs (1)

9-17: Make missing-ID assertion output deterministic.

HashSet iteration order is unstable, so failure output order can vary. Sort missing before asserting to keep CI diffs predictable.

Proposed fix
-    let missing: Vec<_> = git_pr_ids
+    let mut missing: Vec<_> = git_pr_ids
         .iter()
         .filter(|id| !changelog_ids.contains(id))
-        .collect();
+        .copied()
+        .collect();
+    missing.sort_unstable();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/changelog_test.rs` around lines 9 - 17, The failure message can vary
because HashSet iteration order is unstable; collect the missing IDs into a Vec
(the existing missing variable), sort it deterministically (e.g., sort() or
sort_unstable()), and then assert on the sorted missing vector so the
"CHANGELOG.md is missing PR IDs: {:?}" output is stable; update the logic around
git_pr_ids, changelog_ids, and missing to sort missing before the assert.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Line 31: Replace the typo "TODO/FFIXME" in the changelog entry with the
correct marker "TODO/FIXME" so the user-facing release notes read: "Hygiene
checks for file-size limits and tech-debt markers (TODO/FIXME) in the canonical
`just check` gate and CI workflow. (`#10`)"; locate the string "TODO/FFIXME" in
CHANGELOG.md and update it to "TODO/FIXME".

In `@tests/changelog_test.rs`:
- Around line 25-32: The function extract_git_merged_pr_ids should only consider
PR IDs from merge commits and must validate the git command succeeded; change
the git invocation to include only merges (e.g., use "git log --merges --all
--format=%s") and after .output() assert output.status.success(), bubbling
stderr on failure, then pass output.stdout through regex_find_pr_ids; apply the
same changes to the analogous function handling lines ~34-55 so both use
"--merges" and check output.status.success() (include stderr in the error
message) to avoid false positives and silent failures.

---

Nitpick comments:
In `@tests/changelog_test.rs`:
- Around line 9-17: The failure message can vary because HashSet iteration order
is unstable; collect the missing IDs into a Vec (the existing missing variable),
sort it deterministically (e.g., sort() or sort_unstable()), and then assert on
the sorted missing vector so the "CHANGELOG.md is missing PR IDs: {:?}" output
is stable; update the logic around git_pr_ids, changelog_ids, and missing to
sort missing before the assert.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 53464e30-e778-4977-8b43-b50ea6e6893f

📥 Commits

Reviewing files that changed from the base of the PR and between a21326a and 9ae29a4.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • tests/changelog_test.rs

Comment thread CHANGELOG.md Outdated
Comment thread tests/changelog_test.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/changelog_test.rs`:
- Around line 36-77: Replace the network-based logic inside
extract_github_merged_pr_ids() with a git-history-first approach: run "git log
--all --format=%s" (spawn a command instead of calling github_api_request) and
parse each subject line for PR number patterns (e.g., "Merge pull request `#123`",
"Merge branch '...'", and squash/merge suffixes like "… (`#123`)") to collect u32
PR IDs into the HashSet; only if this set is empty should you fall back to the
existing GitHub API loop (the code that constructs api_url and calls
github_api_request) as a last-resort path, and remove reliance on exported
GITHUB_TOKEN/other env vars for the primary path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a05e5312-7d58-48f9-991f-10530d45e7f7

📥 Commits

Reviewing files that changed from the base of the PR and between 9ae29a4 and 70e2dc3.

📒 Files selected for processing (3)
  • .github/workflows/validate-agents.yml
  • CHANGELOG.md
  • tests/changelog_test.rs

Comment thread tests/changelog_test.rs
Comment on lines +36 to +77
fn extract_github_merged_pr_ids() -> Result<HashSet<u32>, String> {
let repo = env::var("GITHUB_REPOSITORY")
.unwrap_or_else(|_| "alchemiststudiosDOTai/sophon".to_string());
let api_url =
env::var("GITHUB_API_URL").unwrap_or_else(|_| "https://api.github.com".to_string());
let mut ids = HashSet::new();

for page in 1.. {
let url =
format!("{api_url}/repos/{repo}/pulls?state=closed&base=main&per_page=100&page={page}");
let output = github_api_request(&url)?;
let pulls: Vec<serde_json::Value> = serde_json::from_slice(&output)
.map_err(|error| format!("failed to parse GitHub API response: {error}"))?;

if pulls.is_empty() {
break;
}

for pull in &pulls {
if pull.get("merged_at").is_none_or(serde_json::Value::is_null) {
continue;
}
let number = pull
.get("number")
.and_then(serde_json::Value::as_u64)
.ok_or_else(|| "GitHub API response is missing a numeric PR number".to_string())?;
let id = u32::try_from(number)
.map_err(|error| format!("PR number {number} does not fit in u32: {error}"))?;
ids.insert(id);
}

if pulls.len() < 100 {
break;
}
}

if ids.is_empty() {
return Err("GitHub API returned no merged PR IDs".to_string());
}

Ok(ids)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify current test is API-coupled and workflow exports token for it.
rg -n 'extract_github_merged_pr_ids|github_api_request|api.github.com|GITHUB_TOKEN|GH_TOKEN' tests/changelog_test.rs
rg -n 'Verify canonical command works|GITHUB_TOKEN' .github/workflows/validate-agents.yml

# After applying a git-first refactor, expected:
# - tests/changelog_test.rs no longer depends on api.github.com/github_api_request
# - workflow can drop the GITHUB_TOKEN env export for just check

Repository: alchemiststudiosDOTai/sophon

Length of output: 569


Replace GitHub API calls with git history parsing to eliminate flaky CI dependency

The current extract_github_merged_pr_ids() function (lines 36–77) relies on GitHub API calls, which can fail during rate-limit outages or API unavailability—blocking merges for infrastructure reasons unrelated to code quality. Additionally, this approach requires exporting GITHUB_TOKEN to the workflow's broader just check step.

Use git log --all --format=%s parsing instead, which captures both merge commits and squash commits via commit message patterns. Keep the GitHub API only as a true fallback if strictly required.

Proposed approach (git-first extraction)
-    let merged_pr_ids = extract_github_merged_pr_ids().unwrap_or_else(|error| {
+    let merged_pr_ids = extract_git_merged_pr_ids().unwrap_or_else(|error| {
         if is_ci() {
-            panic!("failed to fetch merged PR IDs from GitHub: {error}");
+            panic!("failed to extract merged PR IDs from git history: {error}");
         }
-        eprintln!("skipping changelog merged-PR coverage outside CI: {error}");
+        eprintln!("skipping changelog merged-PR coverage outside CI: {error}");
         HashSet::new()
     });
@@
-fn extract_github_merged_pr_ids() -> Result<HashSet<u32>, String> {
-    let repo = env::var("GITHUB_REPOSITORY")
-        .unwrap_or_else(|_| "alchemiststudiosDOTai/sophon".to_string());
-    let api_url =
-        env::var("GITHUB_API_URL").unwrap_or_else(|_| "https://api.github.com".to_string());
-    let mut ids = HashSet::new();
-
-    for page in 1.. {
-        let url =
-            format!("{api_url}/repos/{repo}/pulls?state=closed&base=main&per_page=100&page={page}");
-        let output = github_api_request(&url)?;
-        let pulls: Vec<serde_json::Value> = serde_json::from_slice(&output)
-            .map_err(|error| format!("failed to parse GitHub API response: {error}"))?;
-
-        if pulls.is_empty() {
-            break;
-        }
-
-        for pull in &pulls {
-            if pull.get("merged_at").is_none_or(serde_json::Value::is_null) {
-                continue;
-            }
-            let number = pull
-                .get("number")
-                .and_then(serde_json::Value::as_u64)
-                .ok_or_else(|| "GitHub API response is missing a numeric PR number".to_string())?;
-            let id = u32::try_from(number)
-                .map_err(|error| format!("PR number {number} does not fit in u32: {error}"))?;
-            ids.insert(id);
-        }
-
-        if pulls.len() < 100 {
-            break;
-        }
-    }
-
-    if ids.is_empty() {
-        return Err("GitHub API returned no merged PR IDs".to_string());
-    }
-
-    Ok(ids)
+fn extract_git_merged_pr_ids() -> Result<HashSet<u32>, String> {
+    let output = Command::new("git")
+        .args(["log", "--all", "--format=%s"])
+        .output()
+        .map_err(|error| format!("failed to run git log: {error}"))?;
+
+    if !output.status.success() {
+        return Err(format!(
+            "git log failed with status {}: {}",
+            output.status,
+            String::from_utf8_lossy(&output.stderr)
+        ));
+    }
+
+    let mut ids = HashSet::new();
+    for subject in String::from_utf8_lossy(&output.stdout).lines() {
+        if let Some(id) = parse_merged_pr_id(subject) {
+            ids.insert(id);
+        }
+    }
+
+    if ids.is_empty() {
+        return Err("git history returned no merged PR IDs".to_string());
+    }
+    Ok(ids)
 }
+
+fn parse_merged_pr_id(subject: &str) -> Option<u32> {
+    if let Some(rest) = subject.strip_prefix("Merge pull request #") {
+        let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
+        return digits.parse::<u32>().ok();
+    }
+    if let Some(start) = subject.rfind("(#") {
+        if subject.ends_with(')') {
+            let digits = &subject[start + 2..subject.len() - 1];
+            if !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit()) {
+                return digits.parse::<u32>().ok();
+            }
+        }
+    }
+    None
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/changelog_test.rs` around lines 36 - 77, Replace the network-based
logic inside extract_github_merged_pr_ids() with a git-history-first approach:
run "git log --all --format=%s" (spawn a command instead of calling
github_api_request) and parse each subject line for PR number patterns (e.g.,
"Merge pull request `#123`", "Merge branch '...'", and squash/merge suffixes like
"… (`#123`)") to collect u32 PR IDs into the HashSet; only if this set is empty
should you fall back to the existing GitHub API loop (the code that constructs
api_url and calls github_api_request) as a last-resort path, and remove reliance
on exported GITHUB_TOKEN/other env vars for the primary path.

@tunahorse
tunahorse merged commit 28d31dd into main May 1, 2026
2 checks passed
larock22 pushed a commit that referenced this pull request Jun 17, 2026
…ge-test

feat: add changelog PR coverage test and backfill missing entries
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants