fix(ci): use PR-based commits instead of direct push to main (#123)

Refactors all commit steps in crawl-and-publish.yml to create a branch and open a PR with auto-merge instead of pushing directly to the protected main branch. Removes continue-on-error band-aids. Also reverts the ruleset bypass that was added mistakenly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Juan Manuel Servera committed May 19, 2026 at 19:08 UTC 293cdd475cf474a1ef68753f81b6b593f65a1152
3 files changed +85 -47
.github/workflows/crawl-and-publish.yml
+73 -44
@@ -14,6 +14,7 @@ on:
14 permissions:
15 actions: read
16 contents: write
17 + pull-requests: write
18
19 concurrency:
20 group: weekly-crawl
@@ -116,35 +117,41 @@ jobs:
117 path: data/cache/
118 if-no-files-found: warn
119
119 - - name: Commit crawl data
120 - continue-on-error: true
120 + - name: Commit crawl data via PR
121 env:
122 DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
123 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
124 run: |
125 set -euo pipefail
126 git config user.name "github-actions[bot]"
127 git config user.email "github-actions[bot]@users.noreply.github.com"
128 # Save crawl output before syncing with remote
128 - cp -r data/raw /tmp/crawl-raw
129 - cp -r data/snapshots /tmp/crawl-snapshots
129 + cp -r data/raw crawl-raw-backup
130 + cp -r data/snapshots crawl-snapshots-backup
131 git fetch origin "$DEFAULT_BRANCH"
132 git checkout -B "$DEFAULT_BRANCH" "origin/$DEFAULT_BRANCH"
133 # Restore crawl output on top of synced branch
133 - cp -r /tmp/crawl-raw/* data/raw/ 2>/dev/null || true
134 - cp -r /tmp/crawl-snapshots/* data/snapshots/ 2>/dev/null || true
134 + cp -r crawl-raw-backup/* data/raw/ 2>/dev/null || true
135 + cp -r crawl-snapshots-backup/* data/snapshots/ 2>/dev/null || true
136 + rm -rf crawl-raw-backup crawl-snapshots-backup
137 if git diff --quiet -- data/raw data/snapshots; then
136 - echo "No changes to data/raw or data/snapshots. Skipping counter increment."
138 + echo "No changes to data/raw or data/snapshots. Skipping."
139 exit 0
140 fi
141 COUNTER=$(cat .squad/run-counter.txt 2>/dev/null || echo 0)
142 COUNTER=$((COUNTER + 1))
143 printf '%s\n' "$COUNTER" > .squad/run-counter.txt
144 + WEEK=$(date +%Y-W%V)
145 + BRANCH="data/weekly-crawl-${WEEK}-${GITHUB_RUN_ID}"
146 + git checkout -b "$BRANCH"
147 git add data/raw/ data/snapshots/ .squad/run-counter.txt
143 - git diff --cached --quiet || git commit -m "data: weekly crawl $(date +%Y-W%V)"
144 - git push origin "HEAD:$DEFAULT_BRANCH" || {
145 - echo "Push failed after syncing with $DEFAULT_BRANCH."
146 - exit 1
147 - }
148 + git diff --cached --quiet && exit 0
149 + git commit -m "data: weekly crawl $WEEK"
150 + git push origin "$BRANCH"
151 + gh pr create --base "$DEFAULT_BRANCH" --head "$BRANCH" \
152 + --title "data: weekly crawl $WEEK" \
153 + --body "Automated weekly crawl data commit from run #${GITHUB_RUN_ID}."
154 + gh pr merge "$BRANCH" --squash --auto --delete-branch
155
156 analyze:
157 needs: crawl
@@ -152,6 +159,7 @@ jobs:
159 permissions:
160 actions: read
161 contents: write
162 + pull-requests: write
163 outputs:
164 week: ${{ steps.analysis-context.outputs.week }}
165 summary_file: ${{ steps.analysis-context.outputs.output_file }}
@@ -307,31 +315,37 @@ jobs:
315 --current-datetime "$CURRENT_DATETIME" \
316 --source "$ANALYSIS_SOURCE"
317
310 - - name: Commit analysis
311 - continue-on-error: true
318 + - name: Commit analysis via PR
319 env:
320 DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
321 WEEK: ${{ steps.analysis-context.outputs.week }}
322 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
323 run: |
324 + set -euo pipefail
325 git config user.name "github-actions[bot]"
326 git config user.email "github-actions[bot]@users.noreply.github.com"
327 if ! git status --short -- data/analyzed data/metrics | grep -q .; then
328 echo "No analyzed data or token usage changes to commit."
329 exit 0
330 fi
322 - cp -r data/analyzed /tmp/analyzed-data
323 - cp -r data/metrics /tmp/metrics-data
331 + cp -r data/analyzed analyzed-data-backup
332 + cp -r data/metrics metrics-data-backup
333 git fetch origin "$DEFAULT_BRANCH"
334 git checkout -B "$DEFAULT_BRANCH" "origin/$DEFAULT_BRANCH"
335 mkdir -p data/analyzed data/metrics
327 - cp -r /tmp/analyzed-data/* data/analyzed/ 2>/dev/null || true
328 - cp -r /tmp/metrics-data/* data/metrics/ 2>/dev/null || true
336 + cp -r analyzed-data-backup/* data/analyzed/ 2>/dev/null || true
337 + cp -r metrics-data-backup/* data/metrics/ 2>/dev/null || true
338 + rm -rf analyzed-data-backup metrics-data-backup
339 + BRANCH="data/analysis-${WEEK}-${GITHUB_RUN_ID}"
340 + git checkout -b "$BRANCH"
341 git add data/analyzed/ data/metrics/
330 - git diff --cached --quiet || git commit -m "analysis: weekly summary $WEEK"
331 - git push origin "HEAD:$DEFAULT_BRANCH" || {
332 - echo "Push failed after syncing with $DEFAULT_BRANCH."
333 - exit 1
334 - }
342 + git diff --cached --quiet && exit 0
343 + git commit -m "analysis: weekly summary $WEEK"
344 + git push origin "$BRANCH"
345 + gh pr create --base "$DEFAULT_BRANCH" --head "$BRANCH" \
346 + --title "analysis: weekly summary $WEEK" \
347 + --body "Automated analysis commit from run #${GITHUB_RUN_ID}."
348 + gh pr merge "$BRANCH" --squash --auto --delete-branch
349
350 - name: Upload analyzed data
351 uses: actions/upload-artifact@v4
@@ -346,6 +360,7 @@ jobs:
360 permissions:
361 actions: read
362 contents: write
363 + pull-requests: write
364 outputs:
365 page_path: ${{ steps.generate-content.outputs.page_path }}
366
@@ -387,33 +402,39 @@ jobs:
402 - name: Generate rollups
403 run: python3 scripts/generate_rollups.py
404
390 - - name: Commit generated content
391 - continue-on-error: true
405 + - name: Commit generated content via PR
406 env:
407 DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
408 WEEK: ${{ needs.analyze.outputs.week }}
409 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
410 run: |
411 + set -euo pipefail
412 git config user.name "github-actions[bot]"
413 git config user.email "github-actions[bot]@users.noreply.github.com"
414 if ! git status --short -- content/weekly content/monthly content/yearly | grep -q .; then
415 echo "No generated content changes to commit."
416 exit 0
417 fi
402 - cp -r content/weekly /tmp/content-weekly 2>/dev/null || true
403 - cp -r content/monthly /tmp/content-monthly 2>/dev/null || true
404 - cp -r content/yearly /tmp/content-yearly 2>/dev/null || true
418 + cp -r content/weekly content-weekly-backup 2>/dev/null || true
419 + cp -r content/monthly content-monthly-backup 2>/dev/null || true
420 + cp -r content/yearly content-yearly-backup 2>/dev/null || true
421 git fetch origin "$DEFAULT_BRANCH"
422 git checkout -B "$DEFAULT_BRANCH" "origin/$DEFAULT_BRANCH"
423 mkdir -p content/weekly content/monthly content/yearly
408 - cp -r /tmp/content-weekly/* content/weekly/ 2>/dev/null || true
409 - cp -r /tmp/content-monthly/* content/monthly/ 2>/dev/null || true
410 - cp -r /tmp/content-yearly/* content/yearly/ 2>/dev/null || true
424 + cp -r content-weekly-backup/* content/weekly/ 2>/dev/null || true
425 + cp -r content-monthly-backup/* content/monthly/ 2>/dev/null || true
426 + cp -r content-yearly-backup/* content/yearly/ 2>/dev/null || true
427 + rm -rf content-weekly-backup content-monthly-backup content-yearly-backup
428 + BRANCH="data/content-${WEEK}-${GITHUB_RUN_ID}"
429 + git checkout -b "$BRANCH"
430 git add content/weekly/ content/monthly/ content/yearly/
412 - git diff --cached --quiet || git commit -m "content: weekly page $WEEK"
413 - git push origin "HEAD:$DEFAULT_BRANCH" || {
414 - echo "Push failed after syncing with $DEFAULT_BRANCH."
415 - exit 1
416 - }
431 + git diff --cached --quiet && exit 0
432 + git commit -m "content: weekly page $WEEK"
433 + git push origin "$BRANCH"
434 + gh pr create --base "$DEFAULT_BRANCH" --head "$BRANCH" \
435 + --title "content: weekly page $WEEK" \
436 + --body "Automated content generation from run #${GITHUB_RUN_ID}."
437 + gh pr merge "$BRANCH" --squash --auto --delete-branch
438
439 - name: Upload generated content artifact
440 uses: actions/upload-artifact@v4
@@ -596,6 +617,7 @@ jobs:
617 runs-on: ubuntu-latest
618 permissions:
619 contents: write
620 + pull-requests: write
621
622 steps:
623 - uses: actions/checkout@v4
@@ -621,6 +643,7 @@ jobs:
643 DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
644 COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GH_TOKEN }}
645 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
646 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
647 run: |
648 set -euo pipefail
649 git config user.name "github-actions[bot]"
@@ -659,15 +682,21 @@ jobs:
682 exit 0
683 fi
684
662 - cp -r .squad /tmp/squad-state
663 - cp -r data/metrics /tmp/reskill-metrics
685 + cp -r .squad squad-state-backup
686 + cp -r data/metrics reskill-metrics-backup
687 git fetch origin "$DEFAULT_BRANCH"
688 git checkout -B "$DEFAULT_BRANCH" "origin/$DEFAULT_BRANCH"
666 - cp -r /tmp/squad-state/* .squad/ 2>/dev/null || true
689 + cp -r squad-state-backup/* .squad/ 2>/dev/null || true
690 mkdir -p data/metrics
668 - cp -r /tmp/reskill-metrics/* data/metrics/ 2>/dev/null || true
691 + cp -r reskill-metrics-backup/* data/metrics/ 2>/dev/null || true
692 + rm -rf squad-state-backup reskill-metrics-backup
693 + BRANCH="data/reskill-${WEEK}-${GITHUB_RUN_ID}"
694 + git checkout -b "$BRANCH"
695 git add .squad/ data/metrics/
670 - git diff --cached --quiet || git commit -m "chore: reskill state update"
671 - git push origin "HEAD:$DEFAULT_BRANCH" || {
672 - echo "Warning: Failed to push .squad updates to $DEFAULT_BRANCH, but continuing."
673 - }
696 + git diff --cached --quiet && exit 0
697 + git commit -m "chore: reskill state update"
698 + git push origin "$BRANCH"
699 + gh pr create --base "$DEFAULT_BRANCH" --head "$BRANCH" \
700 + --title "chore: reskill state update $WEEK" \
701 + --body "Automated reskill commit from run #${GITHUB_RUN_ID}."
702 + gh pr merge "$BRANCH" --squash --auto --delete-branch
.squad/agents/leela/history.md
+9
@@ -150,3 +150,12 @@
150 - **Workflow change:** Milestone-based versioning adopted per user directive. PRDs → issues → milestones → docs/processed/
151 - **Dependencies respected:** TechCrunch (v0.8) follows topic-channels foundation (v0.6); cost optimization (v0.9) follows cost visibility (v0.5)
152 - **Label convention:** All issues carry `squad` + `squad:{agent}` labels for routing
153 +
154 +### 2026-05-19T18:05:10+02:00 — CI Workflow: PR-based commits, ruleset bypass reverted
155 +
156 +- **Ruleset fix:** Removed RepositoryRole:5 bypass actor from the `main` ruleset (id 16532660). Branch protection must never be bypassed.
157 +- **Workflow refactor:** All commit steps in `crawl-and-publish.yml` now create a timestamped branch, open a PR via `gh pr create`, and auto-merge with `--squash --auto` instead of pushing directly to main.
158 +- **Steps renamed:** "Commit crawl data" → "Commit crawl data via PR", "Commit analysis" → "Commit analysis via PR", "Commit generated content" → "Commit generated content via PR", reskill step also converted.
159 +- **No more `continue-on-error: true`** on commit steps — they succeed properly now via the PR path.
160 +- **Tests updated:** Adjusted step name references in `tests/test_pipeline.py` to match new naming.
161 +- **Decision recorded:** `.squad/decisions/inbox/leela-no-ruleset-bypass.md`
tests/test_pipeline.py
+3 -3
@@ -155,11 +155,11 @@ class WorkflowConfigTests(unittest.TestCase):
155 crawl_job = workflow["jobs"]["crawl"]
156 commit_step = None
157 for step in crawl_job["steps"]:
158 - if step.get("name") == "Commit crawl data":
158 + if step.get("name") == "Commit crawl data via PR":
159 commit_step = step
160 break
161
162 - self.assertIsNotNone(commit_step, "Commit crawl data step not found")
162 + self.assertIsNotNone(commit_step, "Commit crawl data via PR step not found")
163 run_script = commit_step["run"]
164 self.assertIn("COUNTER=$(cat .squad/run-counter.txt", run_script)
165 self.assertIn("COUNTER=$((COUNTER + 1))", run_script)
@@ -227,7 +227,7 @@ class WorkflowConfigTests(unittest.TestCase):
227 self.assertIsNotNone(generate_rollups_step)
228 self.assertEqual(generate_rollups_step["run"], "python3 scripts/generate_rollups.py")
229
230 - commit_step = next((s for s in generate_job["steps"] if s.get("name") == "Commit generated content"), None)
230 + commit_step = next((s for s in generate_job["steps"] if s.get("name") == "Commit generated content via PR"), None)
231 self.assertIsNotNone(commit_step)
232 commit_run = commit_step["run"]
233 self.assertIn("content/weekly", commit_run)