Repository navigation
fix(ci): untrack scraper staging files and bypass peter-evans - #51
Conversation
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
PR Summary by QodoRestore scraper PR creation with transient proposal files
AI Description
Diagram
High-Level Assessment
Files changed (3)
|
Code Review by QodoResolved findings 1.
|
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
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.jsonandproposed-cves.jsonwere 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 "Deleteproposed-bugs.jsonbefore 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
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 passeduv run pytest -q testing/test_workflow_security.py→ 10 passedgit status --ignoredconfirms proposed-bugs.json and proposed-cves.json now appear under "Ignored files" after thegit rm --cachedEnd-to-end verification in CI via
workflow_dispatchon this branch, three cases:git diff --cached --quietPostgreSQL 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:
3a29a91combined scout and commit into one step. Local repro worked, CI did not. Useful debugging commit but not the fix.e6ac3fduntrack proposed-*.json and add to .gitignore. Switchgit addtogit add -f. This was the real fix; scout+commit now writes a real diff.cbbabf5bypass 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 runsgit 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 withgh pr create.c416c3afetch 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.115021dbase the first-push branch on HEAD, not origin/main.actions/checkoutwithfetch-depth=1only fetches the dispatched ref, soorigin/maindoesn't exist as a local ref on the runner. The first dispatch failed withfatal: 'origin/main' is not a commituntil this commit landed.dbcef9fskip commit + push when proposals match origin. Without this, on a subsequent dispatch where the scout produces identical output,git commitexits 1 with "nothing to commit" and--force-with-leaseis never reached, marking the step failed even though there is genuinely nothing to do. Guarded bygit diff --cached --quiet.Why bypass peter-evans
peter-evans/create-pull-requestis designed around uncommitted changes that the action itself commits viaadd-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 createis simpler, more transparent (every command is explicit in the workflow file), and easier to debug.Force-push safety
git push --force-with-leaseis used for both workflows. On the first push the remote ref is absent and the lease is vacuous. On later runs, commitc416c3arefreshes 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 openand 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 editthe 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.ymlfrom main will open a clean PR withproposed-bugs.jsoncontaining the 20 accumulated proposals. Curate from there intodata/known_bugs.json.