Skip to content

fix(changelog-site): retry the GitHub tag fetch and fail instead of degrading - #645

Open
balajinvda wants to merge 1 commit into
mainfrom
fix/changelog-github-tag-fetch
Open

fix(changelog-site): retry the GitHub tag fetch and fail instead of degrading#645
balajinvda wants to merge 1 commit into
mainfrom
fix/changelog-github-tag-fetch

Conversation

@balajinvda

@balajinvda balajinvda commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Why

The published changelog flapped: services appeared and disappeared between half-hourly rebuilds, and always all of them together.

fetchGitHubTags runs git ls-remote and git fetch against GitHub with no retry, and the caller only warned on failure:

// Degrade gracefully: an unreachable mirror must not fail the build.
fmt.Fprintf(os.Stderr, "changelog-site: github tags unavailable (%v)\n", err)

So one transient network failure dropped the entire GitHub tag set, and the site republished with every GitHub-only release missing, overwriting a good deployment with a worse one. The next successful run restored it. Nothing reported an error, because the job exits zero either way.

That "degrade gracefully" is wrong for a publisher. Serving a stale-but-complete site is strictly better than serving a fresh-but-half-empty one, and the failure is invisible in the output.

What changed

  • Retry both network calls three times with a short backoff, so a blip does not decide the contents of the site.
  • An unavailable GitHub tag set is now fatal. Nothing is written, so Pages keeps serving the last good deployment. --allow-missing-github-tags opts back into the old behaviour deliberately.

Testing

Run against a full GitLab clone plus the GitHub mirror, both paths:

path result
GitHub reachable 37 services, exit 0, {gitlab: 1058, both: 36, github: 960}
GitHub unreachable exits non-zero, writes no output at all

Spot-checked the services that were reported flapping:

container-cache:  0.26.4 both, 0.27.0 github, 0.28.0 github
http-invocation:  0.8.8 github, 0.8.9 github, 0.9.0 github

Notes

Two things found while testing, neither fixed here:

  1. Running the tool twice against the same checkout mislabels origins as both. The first run fetches GitHub tags into the clone, so the second run's git tag -l baseline already contains them. CI is unaffected because it clones fresh, but it makes local iteration misleading.
  2. Every Java service is absent from the changelog. The generator reads tools/ci/subproject-validations.yaml from the frozen GitLab umbrella, which lists no Java subprojects because they were added after the freeze. That needs a decision about where the service list should live and is not addressable in this repo alone.

tools/changelog-site/changelog-site is a compiled binary committed by accident in #554. Left untouched here rather than committing a rebuilt one.

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability when discovering and fetching release tags by retrying temporary failures.
    • Builds now clearly fail when release tag information cannot be retrieved, preventing incomplete releases by default.
  • New Features

    • Added an option to continue publishing without GitHub-only releases when tag information is unavailable.

…egrading

The published changelog flapped: services appeared and disappeared between
half-hourly rebuilds, all of them together.

fetchGitHubTags runs git ls-remote and git fetch against GitHub with no
retry, and the caller only warned on failure. One transient network failure
therefore dropped the entire GitHub tag set, and the site republished with
every GitHub-only release missing -- overwriting a good deployment with a
worse one. The next successful run restored it. Nothing reported an error,
because the job exits zero either way.

Retry both network calls three times with a short backoff so a blip does not
decide the contents of the site, and make an unavailable GitHub tag set fatal
so a degraded build is never published and Pages keeps serving the last good
deployment. --allow-missing-github-tags opts back into publishing without
them.

Verified locally against a full GitLab clone plus the GitHub mirror:
  happy path   37 services, exit 0, github-origin releases present
  failure path exits non-zero, writes no output at all

Co-authored-by: Balaji Ganesan <bganesan@nvidia.com>
@balajinvda
balajinvda requested a review from a team as a code owner August 3, 2026 23:14
@balajinvda
balajinvda requested a review from apartha-nv August 3, 2026 23:14
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The changelog site retries GitHub tag discovery and fetching up to three times with incremental backoff. GitHub tag failures now stop the build by default. The --allow-missing-github-tags flag enables publishing without GitHub-only releases.

Changes

GitHub tag retrieval

Layer / File(s) Summary
GitHub tag retrieval retries
tools/changelog-site/main.go
GitHub ls-remote and fetch commands retry up to three times with incremental backoff. Final errors are scrubbed.
Missing-tag failure policy
tools/changelog-site/main.go
The --allow-missing-github-tags option permits publishing when GitHub tag retrieval fails. Without the option, the build aborts.

Estimated code review effort: 3 (Moderate) | ~15–30 minutes

Suggested reviewers: apartha-nv

Sequence Diagram(s)

sequenceDiagram
  participant ChangelogSite
  participant GitCommands
  participant GitHub
  ChangelogSite->>GitCommands: Run tag discovery or fetch
  GitCommands->>GitHub: Retrieve GitHub tags
  GitHub-->>GitCommands: Return tags or error
  GitCommands-->>ChangelogSite: Return result
  ChangelogSite->>GitCommands: Retry failed command up to three times
  ChangelogSite-->>ChangelogSite: Abort by default or publish with --allow-missing-github-tags
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title uses the required Conventional Commits format and accurately describes the GitHub tag retry and failure behavior change.
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.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/changelog-github-tag-fetch

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tools/changelog-site/main.go`:
- Around line 453-474: Update the local git command runner around run to use
exec.CommandContext with a per-attempt timeout, canceling each context after the
attempt completes. Set GIT_TERMINAL_PROMPT=0 on every command so credential
prompts cannot block, while preserving the existing retry, stderr capture, and
error-reporting behavior.
- Around line 453-474: Update fetchGitHubTags to fetch GitHub tag refs into a
dedicated, non-canonical namespace rather than refs/tags/*, preventing stale
refs from contaminating the checkout. Update buildReleases to read GitHub tags
explicitly from that isolated namespace while leaving gitlabTags sourced only
from canonical tags; preserve --allow-missing-github-tags behavior without
promoting stale GitHub refs.
- Around line 453-474: Extend TestFetchGitHubTagsLabelsOrigin with focused
coverage for retry recovery, retry exhaustion, token scrubbing, failure modes,
output preservation, and --allow-missing-github-tags using an injectable or fake
Git runner. Update the GitHub tag synchronization flow around the run helper and
its refspec so deleted remote tags are pruned or isolated before releases are
built, and add a two-run test verifying deletion is reflected.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: df16f37a-8ec8-407c-a04b-b417ca46bee8

📥 Commits

Reviewing files that changed from the base of the PR and between 3d76cd5 and e51b8c0.

📒 Files selected for processing (1)
  • tools/changelog-site/main.go

Comment on lines +453 to +474
// Both calls below cross the network to GitHub. A single transient failure
// used to drop the entire GitHub tag set, and because the caller only
// warned, the site republished with every GitHub-only release missing --
// services appeared and disappeared between half-hourly rebuilds. Retry
// with a short backoff so a blip does not decide the contents of the site.
run := func(args ...string) (string, error) {
cmd := exec.Command("git", append([]string{"-C", repoDir}, args...)...)
var stdout, stderr strings.Builder
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("git %s: %w: %s", args[0], err, scrub(stderr.String()))
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
if attempt > 0 {
time.Sleep(time.Duration(attempt) * 2 * time.Second)
}
cmd := exec.Command("git", append([]string{"-C", repoDir}, args...)...)
var stdout, stderr strings.Builder
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
lastErr = fmt.Errorf("git %s: %w: %s", args[0], err, scrub(stderr.String()))
continue
}
return stdout.String(), nil
}
return stdout.String(), nil
return "", lastErr

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Expect: network-facing git commands use CommandContext with a bounded timeout.
rg -n -C 4 \
  'exec\.Command(Context)?|context\.WithTimeout|GIT_TERMINAL_PROMPT|time\.Sleep' \
  tools/changelog-site --glob '*.go'

Repository: NVIDIA/nvcf

Length of output: 2907


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- imports and retry helper ---'
sed -n '1,45p;430,490p' tools/changelog-site/main.go

printf '%s\n' '--- call sites and related Git operations ---'
sed -n '370,425p;490,560p' tools/changelog-site/main.go

printf '%s\n' '--- focused tests ---'
sed -n '1,180p' tools/changelog-site/main_test.go

Repository: NVIDIA/nvcf

Length of output: 15303


Bound each Git attempt with a deadline.

Because fetchGitHubTags runs network operations with exec.Command, git ls-remote or git fetch can block on network I/O or credential handling. Use exec.CommandContext with a per-attempt timeout and set GIT_TERMINAL_PROMPT=0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/changelog-site/main.go` around lines 453 - 474, Update the local git
command runner around run to use exec.CommandContext with a per-attempt timeout,
canceling each context after the attempt completes. Set GIT_TERMINAL_PROMPT=0 on
every command so credential prompts cannot block, while preserving the existing
retry, stderr capture, and error-reporting behavior.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

git init -q "$tmp/umbrella"
git -C "$tmp/umbrella" config user.name review
git -C "$tmp/umbrella" config user.email review@example.invalid
printf 'initial\n' > "$tmp/umbrella/file"
git -C "$tmp/umbrella" add file
git -C "$tmp/umbrella" commit -qm initial
git -C "$tmp/umbrella" tag svc/v1.0.0

git clone -q "$tmp/umbrella" "$tmp/github"
git -C "$tmp/github" tag svc/v2.0.0

git -C "$tmp/umbrella" fetch --quiet "$tmp/github" '+refs/tags/*:refs/tags/*'
git -C "$tmp/umbrella" tag -l

Repository: NVIDIA/nvcf

Length of output: 171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- target file outline ---'
ast-grep outline tools/changelog-site/main.go --match 'func $_($_) $_ { $$$ }' --view summary || true

printf '%s\n' '--- fetch and tag-discovery code ---'
sed -n '430,570p' tools/changelog-site/main.go

printf '%s\n' '--- related symbols and call sites ---'
rg -n -C 4 'allow-missing-github-tags|refs/tags|gitlabTags|githubTags|fetch' tools/changelog-site

Repository: NVIDIA/nvcf

Length of output: 18044


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- release discovery and tag consumption ---'
rg -n -C 8 'func buildReleases|buildReleases\(|git\(\*repo|git\([^)]*"tag"|tag -l|originFor' tools/changelog-site/main.go

printf '%s\n' '--- relevant tests and CI invocation ---'
sed -n '1,180p' tools/changelog-site/main_test.go
rg -n -C 5 'changelog-site|allow-missing-github-tags|--repo|repo:' .github tools ci 2>/dev/null || true

printf '%s\n' '--- concise file map ---'
git ls-files tools/changelog-site

Repository: NVIDIA/nvcf

Length of output: 33186


Isolate GitHub tag refs from the canonical tag namespace.

fetchGitHubTags writes to refs/tags/*, while buildReleases scans that namespace. On a reused checkout, stale GitHub-only tags enter gitlabTags before the fetch and can be labeled both. With --allow-missing-github-tags, they can be published as gitlab.

Use an isolated ref namespace and consume it explicitly. --atomic does not prevent cross-run contamination.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/changelog-site/main.go` around lines 453 - 474, Update fetchGitHubTags
to fetch GitHub tag refs into a dedicated, non-canonical namespace rather than
refs/tags/*, preventing stale refs from contaminating the checkout. Update
buildReleases to read GitHub tags explicitly from that isolated namespace while
leaving gitlabTags sourced only from canonical tags; preserve
--allow-missing-github-tags behavior without promoting stale GitHub refs.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 5 \
  'fetchGitHubTags|allow-missing-github-tags|github tags unavailable|changelog\.json|index\.html' \
  tools/changelog-site --glob '*_test.go'

Repository: NVIDIA/nvcf

Length of output: 1885


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- test file outline ---'
ast-grep outline tools/changelog-site/main_test.go --lang go
printf '%s\n' '--- production symbols and relevant call sites ---'
ast-grep outline tools/changelog-site/main.go --lang go
printf '%s\n' '--- focused production sections ---'
sed -n '400,570p' tools/changelog-site/main.go
printf '%s\n' '--- focused test sections ---'
sed -n '1,220p' tools/changelog-site/main_test.go
printf '%s\n' '--- repository diff summary ---'
git diff --stat

Repository: NVIDIA/nvcf

Length of output: 17047


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- remaining main flow ---'
sed -n '505,675p' tools/changelog-site/main.go
printf '%s\n' '--- release scanning implementation ---'
sed -n '245,345p' tools/changelog-site/main.go
printf '%s\n' '--- test inventory and package metadata ---'
rg -n '^(func Test|func Benchmark)|^module |^go ' tools/changelog-site --glob '*_test.go' --glob 'go.mod'
printf '%s\n' '--- relevant references outside the test file ---'
rg -n -C 3 'allow-missing-github-tags|fetchGitHubTags|github tags unavailable|os\.WriteFile|MkdirAll|RemoveAll|index\.html|changelog\.json' tools/changelog-site

Repository: NVIDIA/nvcf

Length of output: 22124


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("tools/changelog-site/main.go")
s = p.read_text()
for needle in [
    'func fetchGitHubTags',
    'func main()',
    'git(*repo, "tag", "-l")',
    'buildReleases(',
    'os.WriteFile',
    'os.MkdirAll',
]:
    print(f"{needle}: {s.count(needle)} occurrence(s)")
PY

Repository: NVIDIA/nvcf

Length of output: 348


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Exercise the Git refspec used by fetchGitHubTags in isolated temporary
# repositories. This does not execute repository code or alter the checkout.
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

remote="$tmp/remote.git"
work="$tmp/work"
checkout="$tmp/checkout"

git init --bare -q "$remote"
git init -q -b main "$work"
git -C "$work" -c user.email=t@t -c user.name=t commit --allow-empty -q -m base
git -C "$work" tag svc/v1.0.0
git -C "$work" push -q "$remote" main 'refs/tags/*'

git clone -q "$remote" "$checkout"
git -C "$checkout" fetch --quiet "$remote" '+refs/tags/*:refs/tags/*'
printf '%s\n' 'tags after first fetch:'
git -C "$checkout" tag --list 'svc/*'

# Remove the remote tag, then repeat the exact non-pruning refspec.
git -C "$work" push -q "$remote" ':refs/tags/svc/v1.0.0'
git -C "$checkout" fetch --quiet "$remote" '+refs/tags/*:refs/tags/*'
printf '%s\n' 'tags after second fetch without --prune:'
git -C "$checkout" tag --list 'svc/*'

printf '%s\n' 'remote tags after deletion:'
git --git-dir="$remote" tag --list 'svc/*'

Repository: NVIDIA/nvcf

Length of output: 331


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- fatal and output helpers ---'
sed -n '675,710p' tools/changelog-site/main.go
printf '%s\n' '--- full tracked test list for this tool ---'
git ls-files tools/changelog-site '*_test.go'

Repository: NVIDIA/nvcf

Length of output: 50367


Add focused tests and synchronize reused GitHub tag refs.

TestFetchGitHubTagsLabelsOrigin covers only success. Add tests for retry recovery, exhaustion, token scrubbing, failure modes, output preservation, and --allow-missing-github-tags using a fake or injectable Git runner. The current refspec does not prune deleted tags. Add a two-run deletion test and prune or isolate GitHub tag refs before building releases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/changelog-site/main.go` around lines 453 - 474, Extend
TestFetchGitHubTagsLabelsOrigin with focused coverage for retry recovery, retry
exhaustion, token scrubbing, failure modes, output preservation, and
--allow-missing-github-tags using an injectable or fake Git runner. Update the
GitHub tag synchronization flow around the run helper and its refspec so deleted
remote tags are pruned or isolated before releases are built, and add a two-run
test verifying deletion is reflected.

Source: Coding guidelines

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.

3 participants