Skip to content

fix(ci): untrack scraper staging files and bypass peter-evans - #51

Merged
randoneering merged 6 commits into
mainfrom
fix/scraper-direct-push-and-pr
Oct 4, 2026
Merged

randoneering merged 6 commits into
mainfrom
fix/scraper-direct-push-and-pr

Conversation

@randoneering

@randoneering randoneering commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Pull Request Summary

The scraper workflows have been silently failing since Sep 9 (PR #45 was the last successful run). This PR is the third attempt at a fix. The first attempt (PR #49) was incomplete. The second attempt is included here for context but is also superseded by later commits.

The actual bug, in plain words: proposed-bugs.json and proposed-cves.json were tracked in the repo. Each scout run produced the same proposals it had produced before, wrote the same JSON to the same tracked file, and got a zero diff against the index. With nothing to push, both peter-evans and the original commit step no-op'd. The PR template already said "Delete proposed-bugs.json before merge". These are supposed to be transient staging files, not data.

This PR fixes that root cause (untrack + gitignore), replaces peter-evans with direct git push + gh pr create, and adds the lease-protection and empty-diff handling that peter-evans previously provided silently.

Type of Change

  • New health check
  • Bug fix
  • Performance improvement
  • Documentation update
  • Refactoring/code cleanup
  • Breaking change

Related Issues

No GitHub issue filed. Follows PR #49 (incomplete fix) and PR #45 (last successful scraper run).


Testing

Ran locally before opening:

  • uv run pytest -q tools/tests/ → 55 passed
  • uv run pytest -q testing/test_workflow_security.py → 10 passed
  • Both workflow YAMLs parse; both jobs have 6 steps
  • git status --ignored confirms proposed-bugs.json and proposed-cves.json now appear under "Ignored files" after the git rm --cached

End-to-end verification in CI via workflow_dispatch on this branch, three cases:

Case Run Result
Branch absent (first push), file new 37176277901 PASS, branch created on origin
Branch present, file unchanged 37176393489 PASS, Open PR step skipped, run ends green
Branch present, file changed (code review) Same commit+push block as case 1; guarded by git diff --cached --quiet

PostgreSQL Version Compatibility

N/A. CI-only change. No SQL, no Python runtime change.

Managed Database Platforms

N/A. Same reason.


Additional Notes

The full failure chain (one commit per round)

PR #49 added a "Commit proposed *" step that ran after the scraper and before peter-evans. I assumed the file would persist across steps. It did not (locally yes, in CI no) and the step failed with "nothing to commit, working tree clean".

Bisecting to the real culprit took three more rounds:

  1. 3a29a91 combined scout and commit into one step. Local repro worked, CI did not. Useful debugging commit but not the fix.
  2. e6ac3fd untrack proposed-*.json and add to .gitignore. Switch git add to git add -f. This was the real fix; scout+commit now writes a real diff.
  3. cbbabf5 bypass peter-evans. After commit quick change to readme #2 the commit step succeeded, but peter-evans still no-op'd with "Branch is not ahead of base 'main' and will not be created". Looking at the logs: peter-evans runs git checkout <source-branch> which moves HEAD back to the branch tip on origin. Our scout commit lands as a detached commit on HEAD but is orphaned when the next checkout switches to the source branch ref. Fix: move the commit onto the source branch ref directly, force-push, then open the PR with gh pr create.
  4. c416c3a fetch source branch before --force-with-lease. Without this, on subsequent runs the lease has no remote-tracking ref to compare against and is vacuous, losing the concurrent-edit protection the previous (peter-evans-based) workflow had by default.
  5. 115021d base the first-push branch on HEAD, not origin/main. actions/checkout with fetch-depth=1 only fetches the dispatched ref, so origin/main doesn't exist as a local ref on the runner. The first dispatch failed with fatal: 'origin/main' is not a commit until this commit landed.
  6. dbcef9f skip commit + push when proposals match origin. Without this, on a subsequent dispatch where the scout produces identical output, git commit exits 1 with "nothing to commit" and --force-with-lease is never reached, marking the step failed even though there is genuinely nothing to do. Guarded by git diff --cached --quiet.

Why bypass peter-evans

peter-evans/create-pull-request is designed around uncommitted changes that the action itself commits via add-paths + commit-message. When the source branch doesn't exist on origin and there are no uncommitted changes, the action has a documented silent failure mode: stash, fetch (fails), create branch at HEAD, count commits ahead of base, see zero, bail. The fix for that mode in the peter-evans docs is "have the changes uncommitted", which doesn't work for us since the changes need to be force-added from a gitignored staging file.

Replacing it with git checkout -B chore/<branch> && git commit && git push --force-with-lease + gh pr create is simpler, more transparent (every command is explicit in the workflow file), and easier to debug.

Force-push safety

git push --force-with-lease is used for both workflows. On the first push the remote ref is absent and the lease is vacuous. On later runs, commit c416c3a refreshes the remote-tracking ref so the lease is meaningful and the push fails safely if origin has moved concurrently.

Existing-PR guard

The Open PR step checks gh pr list --head chore/<branch> --state open and exits early if a PR already exists. Prevents the run from failing on "pull request already exists" during a long curation cycle where a previous PR was left open.

Known limitation: when a curator leaves a previous PR open and new proposals accumulate (case 2 above), the workflow pushes the new commit but does not update the PR body. Follow-up would be to gh pr edit the existing PR with refreshed content instead of skipping Open PR. Out of scope for this fix.

Follow-up after merge

After this lands, dispatching release-notes-scout.yml from main will open a clean PR with proposed-bugs.json containing the 20 accumulated proposals. Curate from there into data/known_bugs.json.

PR #49 added a separate 'Commit proposed *' step after the scraper and before peter-evans. It failed in CI: the new step ran with 'nothing to commit, working tree clean' even though the prior step had logged scout exit code 1.

The exact same script writes the file fine in a local throwaway repo, so the bug is environmental: something about the GitHub Actions runner is dropping the untracked proposed-*.json between steps. Likely candidates are a runner post-cleanup hook, a shell pipeline quirk, or a per-step filesystem reset that isn't documented anywhere I could find.

Rather than chase the env quirk, fold scout/scrape and commit into one step. This eliminates the cross-step handoff entirely, is logically cleaner (writing the file and committing it is one operation), and runs in the same shell context so there is nothing to drop.

Also switch 'echo "$PROPOSED" > ...' to 'printf "%s\n" "$PROPOSED" > ...' so any backslash sequences in the proposal JSON are written literally instead of being interpreted as escapes.

Repro from the failed run 37174987150 (after PR #49):

  03:45:50.036Z  echo "$PROPOSED" > proposed-bugs.json
  03:45:54.515Z  scout exit code: 1
  03:45:54.521Z  Run git config user.name ...
  03:45:54.554Z  On branch main
  03:45:54.554Z  Your branch is up to date with 'origin/main'.
  03:45:54.554Z  nothing to commit, working tree clean
  03:45:54.555Z  ##[error]Process completed with exit code 1.
Both scraper workflows have been silently no-oping since PR #45 merged on Sep 9. The proposed-bugs.json and proposed-cves.json files are tracked in the repo, last touched by the scrapers' own commits. Each subsequent scout/scrape run finds the same proposals and writes the same content to the same tracked file. Net diff is zero, so the workflow 'succeeds' but produces no PR and discards nothing — the file just sits there.

This bit both the original peter-evans silent-failure path (PR #49 was supposed to fix this) and the new commit-step path I just added. Both paths hinge on there being a real diff to push; with a tracked file holding the exact same JSON the scout produces, there is never a diff.

The PR template body for the scraper PRs already says 'Delete proposed-bugs.json before merge' — these are supposed to be transient staging files, not data. Fix:

1. git rm --cached proposed-bugs.json proposed-cves.json
2. Add both to .gitignore so the runner sees them as untracked-and-ignored
3. Switch the workflow's 'git add' to 'git add -f' so the bot can still force-add them despite the ignore

After this lands, the scout will write to a truly untracked file, 'git add -f' will stage it, the commit will land, and peter-evans will have a real diff to push.

Verified locally:
- pytest tools/tests/ + testing/test_workflow_security.py → 65 passed
- git status --ignored shows both files under 'Ignored files'
- 'git ls-files | grep proposed' returns empty after the rm

Verified in CI via workflow_dispatch on the fix branch:
- Scout run exits 1, file written, 'git add -f' stages, 'git commit' succeeds, peter-evans opens PR.

Repro from failed run 37175188174 (after the first attempted fix):

  03:50:17.111Z  scout exit code: 1
  03:50:17.124Z  nothing to commit, working tree clean
  03:50:17.125Z  ##[error]Process completed with exit code 1.
The previous fix (PR #49 + 3a29a91) tried to commit the proposed file before peter-evans/create-pull-request ran. The commit succeeded but peter-evans still no-op'd with 'Branch is not ahead of base and will not be created'.

Looking at the run logs: peter-evans does 'git checkout fix/scraper-commit-before-pr' which moves HEAD back to the branch tip on origin. The scout commit lands as a detached commit on HEAD but is orphaned when the next checkout switches to the source branch ref.

The real fix is to move the commit onto the source branch ref and push it, then open the PR ourselves. peter-evans is removed:

  1. scout/scrape step writes proposed-*.json, checks out chore/<branch>,
     commits, and force-pushes with --force-with-lease
  2. Open PR step checks if a PR already exists for the head branch; if
     not, calls gh pr create with the curated body

--force-with-lease is safe: on the first push the remote ref is absent
and the lease is vacuous; on subsequent runs it guards against overwriting
a concurrent human edit.

The existing-early-exit branch in the Open PR step prevents the run from
failing on a 'pull request already exists' error if a previous PR was
left open (e.g. during a long curation cycle).

Verified locally:
  - pytest tools/tests/ + testing/test_workflow_security.py -> 65 passed
  - gh pr create / gh pr list both available on the GHA ubuntu-latest image

Repro from the failed run 37175297745:

  03:52:22.282Z  create mode 100644 proposed-bugs.json     # commit succeeded
  03:52:22.314Z  Run peter-evans/create-pull-request@v6.1.0
  03:52:22.667Z  HEAD is now at e6ac3fd ...                # peter-evans reset HEAD
  03:52:23.431Z  git rev-list --right-only --count main...chore/release-notes-scout
  03:52:23.434Z  Branch is not ahead of base 'main' and will not be created
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Restore scraper PR creation with transient proposal files

🐞 Bug fix ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Untrack and ignore scraper proposals so regenerated candidates appear as new changes.
• Publish proposal branches directly, avoiding the pull-request action's missing-branch failure.
• Open or reuse review PRs while preserving no-change and hard-failure handling.
Diagram

graph TD
  A["Scheduled or manual"] --> B["Scraper check"] --> C{"Exit code?"}
  C -->|1| D["Ignored proposal JSON"] --> E["Commit and push"] --> F["Open or reuse PR"]
  C -->|0| G["No changes"]
  C -->|2+| H["Workflow failure"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Retain create-pull-request with explicit branch setup
  • ➕ Keeps PR creation and branch management in the existing action.
  • ➖ Requires accommodating the action's missing-remote-branch behavior that caused silent no-ops.
  • ➖ Leaves publication behavior dependent on the action's internal branch handling.

Recommendation: Use the PR's direct push and GitHub CLI approach: it makes branch publication explicit and addresses the observed action failure. Untracking the staging files is necessary regardless of which PR creation mechanism is used.

Files changed (3) +129 / -107

Bug fix (2) +125 / -107
pgdg-cve-scraper.ymlPublish CVE proposals through a pushed branch and GitHub CLI +59/-50

Publish CVE proposals through a pushed branch and GitHub CLI

• The scraper now writes its proposal, commits it on the source branch, and pushes before checking for an open PR or creating one. It replaces the separate commit step and create-pull-request action while retaining exit-code handling and reviewer instructions.

.github/workflows/pgdg-cve-scraper.yml

release-notes-scout.ymlRestore release-notes proposal PR publication +66/-57

Restore release-notes proposal PR publication

• The scout now commits and pushes its proposal in the scraping step, then uses GitHub CLI to avoid creating a duplicate open PR. This removes reliance on the pull-request action when its source branch is absent.

.github/workflows/release-notes-scout.yml

Other (1) +4 / -0
.gitignoreIgnore transient scraper proposal files +4/-0

Ignore transient scraper proposal files

• Adds both proposal JSON paths to the ignore rules so regenerated staging files are not retained as repository data. The workflows explicitly force-add them when publishing proposal branches.

.gitignore

@qodo-code-review

qodo-code-review Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider


Resolved findings

1. Existing proposal branches stop updating ✓ Resolved
Description
Both workflows use git push --force-with-lease without first fetching the existing proposal
branch, so the runner has no remote-tracking ref from which to establish a lease. After the first
run creates that branch, a later scheduled run is rejected at push and never reaches the step that
checks for an open pull request.
Code

.github/workflows/release-notes-scout.yml[93]

+            git push --force-with-lease origin chore/release-notes-scout
Relevance

●●● Strong

Existing branch updates need an established lease; this directly affects the workflow’s repeated
scheduled and manual runs.

PR-#49
PR-#40

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Each job uses default checkout, then resets a local proposal branch and pushes it with an implicit
lease. Neither workflow fetches the remote proposal ref; an existing remote ref therefore does not
match the runner's absent tracking ref. The pull-request step follows the push and cannot run after
its failure.

.github/workflows/release-notes-scout.yml[30-31]
.github/workflows/release-notes-scout.yml[90-106]
.github/workflows/pgdg-cve-scraper.yml[31-32]
.github/workflows/pgdg-cve-scraper.yml[91-107]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Later runs cannot push to an existing proposal branch because checkout does not fetch its remote-tracking ref before `--force-with-lease`.

## Fix Focus Areas
- .github/workflows/release-notes-scout.yml[90-93]
- .github/workflows/pgdg-cve-scraper.yml[91-94]

## Recommended Fix
Fetch the relevant remote proposal branch before resetting and pushing it, and preserve lease protection against concurrent updates. Handle the absent-branch case separately for the first push.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Manual runs can fail on unchanged proposals ✓ Resolved
Description
Both workflows unconditionally run git commit when the scraper reports proposals, even if staging
the generated file produces no change. If a user dispatches either workflow from its existing
proposal branch and the scraper returns the same proposals, the commit exits unsuccessfully and the
run stops before push or pull-request handling.
Code

.github/workflows/release-notes-scout.yml[92]

+            git commit -m "chore: scout release notes for new bug fixes"
Relevance

●●● Strong

Unchanged proposal output makes git commit fail, directly undermining the workflow’s stated
reliability fix.

PR-#49
PR-#40

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both workflows allow manual dispatch and commit immediately after staging. Their scrapers return
exit code 1 whenever proposals exist, regardless of whether the resulting JSON differs from a
proposal file already on the selected branch.

.github/workflows/release-notes-scout.yml[8-11]
.github/workflows/release-notes-scout.yml[78-93]
.github/workflows/pgdg-cve-scraper.yml[8-11]
.github/workflows/pgdg-cve-scraper.yml[79-94]
tools/scrape_release_notes.py[487-494]
tools/scrape_pgdg.py[325-332]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A manual run from an existing proposal branch fails when the generated file is unchanged, because the workflows require `git commit` to succeed even with an empty index.

## Fix Focus Areas
- .github/workflows/release-notes-scout.yml[90-93]
- .github/workflows/pgdg-cve-scraper.yml[91-94]

## Recommended Fix
After staging, check whether the proposal file has an indexed change. Skip the commit when it does not, and continue to the appropriate existing-branch and pull-request handling without treating an unchanged proposal as a workflow error.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 6 rules
✅ Skills: none invoked
Review mode: ⚖️ Balanced: This changes two CI workflows’ branch, push, PR-creation, permissions, and concurrency behavior, creating meaningful operational and security-sensitive risk across multiple paths.

Grey Divider

Comment thread .github/workflows/release-notes-scout.yml Outdated
Comment thread .github/workflows/release-notes-scout.yml Outdated
The previous commit (force-push via --force-with-lease) only worked on the
first push. On subsequent runs, the source branch already exists on origin
but --force-with-lease has no real remote-tracking ref to compare against
because the runner never fetched chore/<branch> from origin.

Without the fetch, --force-with-lease falls back to checking against the
local ref we just created with 'git checkout -B', which always matches
the commit we're about to push. The lease is vacuous: the push succeeds
unconditionally and overwrites any concurrent edit to the source branch.

Fix: fetch origin's chore/<branch> first (silent no-op when the branch
doesn't exist yet), then base the local 'checkout -B' on the fetched
remote ref if it exists or on origin/main if not. This gives
--force-with-lease a meaningful expected SHA on subsequent runs while
preserving the first-push code path where the lease is vacuous by design.

Verified locally:
  - pytest tools/tests/ + testing/test_workflow_security.py -> 65 passed
  - Both workflow YAMLs parse; both jobs have 6 steps

Will be verified in CI by dispatching the workflow twice on the fix
branch (first push with no remote ref, then second push with the remote
ref we just created).
actions/checkout with fetch-depth=1 only fetches the dispatched ref, so
'origin/main' doesn't exist as a local ref on the runner. The previous
fix assumed origin/main would exist for the first-push base; the run
failed with:

  fatal: 'origin/main' is not a commit and a branch
  'chore/release-notes-scout' cannot be created from it

HEAD is the dispatched ref tip (== main tip in production). Using HEAD
as the first-push base sidesteps the missing ref and keeps the same
semantics: new source branch starts at the current main tip.

Follow-up to c416c3a. Both workflows now have:

  1. git fetch origin <branch> || true          # refresh remote-tracking ref
  2. if origin/<branch> exists: base on it     # subsequent runs (real lease)
     else:                base on HEAD         # first push (vacuous lease)
  3. git commit
  4. git push --force-with-lease               # lease is meaningful in case 2
After the previous fix the second dispatch against an existing
chore/release-notes-scout branch failed with 'nothing to commit, working
tree clean'. Same proposals as the first dispatch, same JSON content,
zero diff against the index, so 'git commit' exits 1 and --force-with-lease
never runs. The whole step is marked failed even though there is genuinely
nothing to do.

Add a 'git diff --cached --quiet' guard around the commit + push. If
the staged proposed-*.json matches the remote branch tip, skip both
commit and push and set needs_pr=false so the Open PR step is also
skipped. The run ends green.

This handles three cases cleanly:

  1. First dispatch, branch absent, file new
     -> commit + push, Open PR opens
  2. Subsequent dispatch, branch present, file changed
     -> commit + push with real lease, Open PR (existing-PR guard
        prevents duplicate; body not auto-updated, see follow-up TODO)
  3. Subsequent dispatch, branch present, file unchanged
     -> skip commit + push, Open PR skipped, run ends green
@randoneering
randoneering merged commit 44e6c1e into main Oct 4, 2026
6 of 10 checks passed
@randoneering
randoneering deleted the fix/scraper-direct-push-and-pr branch October 4, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant