Compare commits

...
Author SHA1 Message Date
Henry ParkandClaude Sonnet 4.6 dcd8bc3697 fix(ci): secrets can't be used in step if conditions [skip-regression-check]
GitHub Actions step-level `if:` doesn't have access to `secrets` context.
Replace `if: secrets.X != ''` with `continue-on-error: true` and let
the Set token step handle the fallback.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 16:41:31 -07:00
Henry ParkandClaude Sonnet 4.6 b125832787 fix(ci): remove Sonnet reference from prompt — model is Haiku [skip-regression-check]
The orchestrator model is Haiku but the prompt said "Launch 4 parallel
Sonnet agents". Haiku can't spawn Sonnet subagents. Changed to just
"4 parallel agents" so it matches the configured model.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 16:15:28 -07:00
Henry ParkandClaude Sonnet 4.6 294449065b fix(ci): fetch tags for staging-tested + fix issue counter [skip-regression-check]
1. Add fetch-tags: true to checkout — staging-tested is a tag and
   without it every run behaves like a first run (no incremental batching).

2. Only increment ISSUES_CREATED when gh issue create succeeds.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 16:00:17 -07:00
Henry ParkandClaude Sonnet 4.6 8a3f0c0870 fix(ci): 20-min polling + timeout-minutes on gate job [skip-regression-check]
- Set polling timeout to 20 min (down from 30)
- Add timeout-minutes: 25 on gate job for hard kill on cancel
- Revert 1s sleep loop to sleep $INTERVAL (1s loop doesn't help
  with cancellation — GitHub only cancels between steps)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 15:55:32 -07:00
Henry ParkandClaude Sonnet 4.6 6685f2ca9d fix(ci): gate long-running tests by target branch, not label [skip-regression-check]
Replace label-based gating with branch-based: run full suite unless
PR targets staging. Fixes timing bug where gh pr create --label adds
the label after the opened event, causing gated jobs to never run.

- Developer PRs (-> staging): fast tests only
- Promotion PRs (-> main): full suite
- Chained PRs (-> staging-promote/*): full suite
- Push to main: full suite
- workflow_call: fast tests only

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 15:44:49 -07:00
Henry ParkandClaude Sonnet 4.6 fb8ac7455e fix(ci): handle "No issues found" comment + restore version-check [skip-regression-check]
1. Handle Claude's "No issues found" comment as a clean pass instead
   of treating empty BODY as blocking. Checks for this before looking
   for "Found N issues" comments.

2. Restore version-check to run on all PRs (not just staging-promotion).
   It's 4-8 seconds and catches missing version bumps early.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 15:33:15 -07:00
Henry ParkandClaude Sonnet 4.6 b5b9934df0 fix(ci): Haiku model, 30-min timeout, HAS_BLOCKING injection fix [skip-regression-check]
- Switch orchestrator to claude-haiku-4-5-20251001 (cheaper per review)
- Increase polling timeout from 20 to 30 minutes for larger diffs
- Move HAS_BLOCKING from ${{ }} interpolation to env var (same
  injection fix applied to FORCE_RUN and SKIP_GATE earlier)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 15:23:52 -07:00
Henry ParkandClaude Sonnet 4.6 0e4d50454b fix(ci): remove --delete-branch — it closes chained PRs [skip-regression-check]
Verified in ironclaw-ci-test: when a promotion branch is deleted on
merge, GitHub CLOSES (not retargets) any chained PRs using it as
their base. This silently breaks the promotion chain.

Remove --delete-branch from gh pr merge. Stale promotion branches
will be cleaned up separately.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 15:04:49 -07:00
Henry ParkandClaude Sonnet 4.6 ab37518008 fix(ci): address review round 2 — injection, cancellable polling, concurrency [skip-regression-check]
1. Script injection: use env vars for workflow_dispatch inputs instead
   of ${{ inputs.* }} interpolation in run blocks.

2. Cancellable polling: sleep in 1s increments so GitHub can cancel
   between iterations instead of blocking on a single long sleep.

3. Remove workflow_dispatch from claude-review — fails without PR
   context. Re-trigger by removing/re-adding staging-promotion label.

4. Add concurrency group to claude-review to prevent duplicate
   reviews consuming API credits.

5. Fix dead code in test.yml roll-up (redundant variable init).

6. Add comment explaining --delete-branch safety (GitHub auto-retargets
   chained PRs, verified in ironclaw-ci-test).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 14:58:15 -07:00
Henry ParkandClaude Sonnet 4.6 e55b160a2e fix(ci): add labeled trigger — gh pr create adds label after opened event [skip-regression-check]
gh pr create --label adds the label AFTER the PR is created, so the
opened event fires without the label. The if condition checking for
staging-promotion label fails. Adding labeled trigger type fixes this.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 14:50:51 -07:00
Henry ParkandClaude Sonnet 4.6 fdfc5fbccc fix(ci): add workflow_dispatch to claude-review for manual re-trigger [skip-regression-check]
Allows manually re-triggering a Claude review without closing and
reopening the PR. Addresses review feedback from @zmanian.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 14:44:27 -07:00
Henry ParkandClaude Sonnet 4.6 402c71d642 fix(ci): address Copilot review — workflow_call, branch collision, merge failure [skip-regression-check]
1. Add workflow_call trigger to test.yml and e2e.yml so staging-ci
   can invoke them as reusable workflows.

2. Add github.run_id suffix to promotion branch name to prevent
   collision on workflow reruns.

3. Merge step now fails hard instead of swallowing errors. If merge
   fails, the step fails and the PR stays open.

4. update-tag intentionally advances regardless of gate outcome —
   this is by design so the next batch covers only new commits.
   Blocked batches stay as open PRs with review comments.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 14:41:35 -07:00
Henry ParkandClaude Sonnet 4.6 831484abab fix(ci): poll check-run status + split merge step [skip-regression-check]
1. Replace 15-min sleep with polling: check Claude Code Review
   check-run status every 30s with 20-min timeout. Exits as soon
   as job completes. Process-findings then reads the last comment.

2. Split merge into its own step conditioned on
   steps.evaluate.outputs.passed == 'true'. Prevents accidental
   merge if gate exit logic is refactored.

3. Add checks:read permission for check-runs API.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 14:38:32 -07:00
Henry ParkandClaude Sonnet 4.6 ecc026843c refactor(ci): gate long-running checks to promotion PRs + push to main [skip-regression-check]
Skip Docker Build, WASM WIT, Windows Build, Telegram tests, and
version-check on developer PRs (branch→staging). These only run on
promotion PRs (staging→main, labeled staging-promotion) and push
to main.

Developer PRs only run unit tests for fast feedback. Full integration
suite runs at promotion time as the quality gate.

Roll-up job updated to accept skipped as passing for gated jobs.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 14:27:32 -07:00
Henry ParkandClaude Sonnet 4.6 87674420c5 fix(ci): address Copilot review — fail-closed gate + subshell fix [skip-regression-check]
1. Gate fails closed: when no Claude review comment is found (API key
   missing, workflow failure, etc.), gate now blocks instead of
   passing. Use skip_claude_gate to override.

2. Process substitution: use `< <(...)` instead of pipe so the while
   loop runs in the current shell. ISSUES_CREATED counter and
   HAS_BLOCKING flag now propagate correctly. Removes /tmp file hack.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 14:06:37 -07:00
Henry ParkandClaude Sonnet 4.6 c474972a49 feat(ci): chained promotion PRs with multi-agent Claude review [skip-regression-check]
Staging CI workflow with batched promotion PRs:
- Creates staging-promote/<sha> branches per batch
- Chains PRs onto previous promotion branch (incremental diffs)
- Claude Code reviews only the incremental changes per batch
- Blocked PRs stay open as records of findings
- staging-tested tag advances regardless of gate outcome
- Runs every 60 min on cron + manual dispatch

Multi-agent Claude review (Sonnet orchestrator + Haiku agents):
- 4 parallel Sonnet review agents (security, architecture, bugs, performance)
- Haiku agents for severity/confidence scoring
- [SEVERITY:CONFIDENCE] output format
- Severity/confidence matrix for issue creation and gate blocking:
  CRITICAL: always create issue, block if confidence >=80
  HIGH: create issue if confidence >=50
  MEDIUM/LOW: create issue if confidence >=80

Gate waits 15 min for Claude review then processes comments.
Validated end-to-end in nearai/ironclaw-ci-test.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 13:55:46 -07:00
4 changed files with 603 additions and 7 deletions
+99
View File
@@ -0,0 +1,99 @@
name: Claude Code Review
on:
pull_request:
types: [opened, labeled]
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
concurrency:
group: claude-review-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
jobs:
review:
name: Claude Code Review
if: contains(github.event.pull_request.labels.*.name, 'staging-promotion')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Run Claude Code review
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: "--max-turns 50 --model claude-haiku-4-5-20251001 --allowedTools 'Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh search:*),Bash(git blame:*),Bash(git log:*),Bash(git diff:*)'"
prompt: |
Code review this pull request. Follow these steps precisely:
1. Use a Haiku agent to find relevant CLAUDE.md files: the root CLAUDE.md
and any CLAUDE.md files in directories whose files this PR modifies.
2. Use a Haiku agent to summarize the PR change (use `gh pr diff`).
3. Launch 4 parallel agents to review the change independently. Each agent should
read the PR diff with `gh pr diff` and the full source files for changed
code, then return a list of issues found:
Agent 1 — Security & Safety
Check for: command injection, path traversal, SSRF, XSS, auth bypass,
secrets in logs, .unwrap()/.expect() in production code (not tests),
race conditions, TOCTOU, unsafe blocks, panics in async, unbounded allocations.
Agent 2 — Architecture & Patterns
Check for: extensible design (traits/enums over nested conditionals),
clean abstractions, proper error types (thiserror), CLAUDE.md compliance,
type-driven design over stringly-typed code, DRY violations.
Agent 3 — Bug Scan
Shallow diff-only scan for obvious bugs: logic errors, off-by-one,
missing error handling, division by zero, incorrect return values.
Ignore nitpicks and likely false positives. Do NOT read extra context
beyond the diff — focus only on the changes.
Agent 4 — Performance & Production
Check for: blocking in async, N+1 queries, unbounded loops, missing
timeouts, resource leaks (file handles, connections), large allocations
in hot paths.
4. For each issue found, launch a parallel Haiku agent to:
a. Assign a severity:
- CRITICAL: security vulns, panics in prod (.unwrap/.expect), data exfiltration, race conditions
- HIGH: logic bugs, missing error handling, breaking API/schema changes
- MEDIUM: missing tests, unnecessary complexity, performance issues
- LOW: documentation gaps, naming suggestions
b. Score confidence 0-100 (give this rubric verbatim):
0: False positive, doesn't stand up to scrutiny, or pre-existing issue.
25: Might be real, but may be false positive. Stylistic issues not in CLAUDE.md.
50: Real issue but nitpick or rare in practice. Not very important.
75: Verified real issue, will be hit in practice. Directly impacts functionality
or explicitly mentioned in CLAUDE.md.
100: Certain, confirmed, will happen frequently. Evidence directly confirms.
5. Post a single comment on the PR using `gh pr comment` with this format.
If no issues were found, post "No issues found." instead:
### Code review
Found N issues:
1. [SEVERITY:CONFIDENCE] <brief description>
<permalink to file:line using full SHA, eg https://github.com/owner/repo/blob/abc123def/src/file.rs#L10-L15>
Example: [CRITICAL:92] `.unwrap()` can panic in production when config is missing
You MUST use the full git SHA in links (not HEAD or branch name).
Provide 1 line of context before and after each linked range.
Notes:
- Use `gh` for all GitHub interactions, not web fetch
- Do NOT check build signal or attempt to build/test the code
- Ignore pre-existing issues not introduced by this PR
- Ignore issues a linter/compiler would catch (formatting, imports, types)
+1
View File
@@ -1,5 +1,6 @@
name: E2E Tests
on:
workflow_call:
schedule:
- cron: "0 6 * * 1" # Weekly Monday 6 AM UTC
workflow_dispatch:
+473
View File
@@ -0,0 +1,473 @@
name: Staging CI (Batched)
on:
schedule:
- cron: "0 * * * *" # Every 60 minutes
workflow_dispatch:
inputs:
force:
description: "Force run even if no new commits"
type: boolean
default: false
skip_claude_gate:
description: "Skip Claude review gate (bypass blocking findings)"
type: boolean
default: false
permissions:
contents: write
issues: write
pull-requests: write
checks: read
concurrency:
group: staging-ci
cancel-in-progress: false # Let running suites finish
jobs:
# ── Check for new commits ──────────────────────────────────────
check-changes:
name: Check for new commits
runs-on: ubuntu-latest
outputs:
has_changes: ${{ steps.check.outputs.has_changes }}
current_head: ${{ steps.check.outputs.current_head }}
diff_range: ${{ steps.check.outputs.diff_range }}
steps:
- uses: actions/checkout@v6
with:
ref: staging
fetch-depth: 0
fetch-tags: true
- name: Check for changes since last tested
id: check
env:
FORCE_RUN: ${{ inputs.force }}
run: |
CURRENT_HEAD=$(git rev-parse HEAD)
echo "current_head=${CURRENT_HEAD}" >> "$GITHUB_OUTPUT"
if git rev-parse staging-tested >/dev/null 2>&1; then
LAST_TESTED=$(git rev-parse staging-tested)
else
LAST_TESTED=""
fi
DIFF_RANGE=""
if [ -n "$LAST_TESTED" ] && [ "$LAST_TESTED" = "$CURRENT_HEAD" ]; then
echo "No new commits since last tested (${CURRENT_HEAD})"
HAS_CHANGES=false
else
HAS_CHANGES=true
if [ -n "$LAST_TESTED" ]; then
COMMIT_COUNT=$(git rev-list --count "${LAST_TESTED}..HEAD")
echo "Found ${COMMIT_COUNT} new commit(s) since last tested"
DIFF_RANGE="${LAST_TESTED}..${CURRENT_HEAD}"
else
git fetch origin main
MERGE_BASE=$(git merge-base origin/main HEAD)
echo "First run -- reviewing from merge-base ${MERGE_BASE}"
DIFF_RANGE="${MERGE_BASE}..${CURRENT_HEAD}"
fi
fi
# Force override from workflow_dispatch
if [ "$FORCE_RUN" = "true" ]; then
echo "Force run requested"
HAS_CHANGES=true
if [ -z "$DIFF_RANGE" ]; then
DIFF_RANGE="${CURRENT_HEAD}..${CURRENT_HEAD}"
fi
fi
echo "has_changes=${HAS_CHANGES}" >> "$GITHUB_OUTPUT"
echo "diff_range=${DIFF_RANGE}" >> "$GITHUB_OUTPUT"
# ── Run full test suite ──────────────────────────────────────────
tests:
name: Test Suite
needs: check-changes
if: needs.check-changes.outputs.has_changes == 'true'
uses: ./.github/workflows/test.yml
# ── Run E2E browser tests ────────────────────────────────────────
e2e:
name: E2E Browser Tests
needs: check-changes
if: needs.check-changes.outputs.has_changes == 'true'
uses: ./.github/workflows/e2e.yml
# ── Create promotion PR (triggers claude-review.yml on the PR) ──
create-promotion-pr:
name: Create Promotion PR
needs: check-changes
if: needs.check-changes.outputs.has_changes == 'true'
runs-on: ubuntu-latest
outputs:
pr_number: ${{ steps.create-pr.outputs.pr_number }}
promotion_branch: ${{ steps.branch.outputs.branch }}
steps:
- uses: actions/checkout@v6
with:
ref: staging
fetch-depth: 0
- name: Generate GitHub App token
id: app-token
continue-on-error: true
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.GH_RELEASES_MANAGER_APP_ID }}
private-key: ${{ secrets.GH_RELEASES_MANAGER_APP_PRIVATE_KEY }}
- name: Set token
id: token
run: |
if [ -n "${{ steps.app-token.outputs.token }}" ]; then
echo "token=${{ steps.app-token.outputs.token }}" >> "$GITHUB_OUTPUT"
else
echo "token=${{ github.token }}" >> "$GITHUB_OUTPUT"
fi
- name: Check if staging is ahead of main
id: ahead-check
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
run: |
git fetch origin main
AHEAD=$(git rev-list --count origin/main..origin/staging)
echo "commits_ahead=${AHEAD}" >> "$GITHUB_OUTPUT"
if [ "$AHEAD" -eq 0 ]; then
echo "Staging is not ahead of main. Nothing to promote."
else
echo "Staging is ${AHEAD} commits ahead of main."
fi
- name: Create promotion branch
id: branch
if: steps.ahead-check.outputs.commits_ahead != '0'
run: |
SHORT_SHA=$(echo "${{ needs.check-changes.outputs.current_head }}" | cut -c1-8)
BRANCH="staging-promote/${SHORT_SHA}-${{ github.run_id }}"
git checkout -b "$BRANCH"
git push origin "$BRANCH"
echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"
echo "Created promotion branch: ${BRANCH}"
- name: Find base branch
id: find-base
if: steps.ahead-check.outputs.commits_ahead != '0'
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
run: |
# Find the newest open promotion PR with a staging-promote/* head branch
LATEST=$(gh pr list --label staging-promotion --state open \
--json headRefName,createdAt \
--jq '[.[] | select(.headRefName | startswith("staging-promote/"))] | sort_by(.createdAt) | last | .headRefName // empty')
if [ -n "$LATEST" ]; then
echo "base=${LATEST}" >> "$GITHUB_OUTPUT"
echo "Chaining onto existing promotion branch: ${LATEST}"
else
echo "base=main" >> "$GITHUB_OUTPUT"
echo "No existing promotion PR — targeting main"
fi
- name: Create promotion PR
id: create-pr
if: steps.ahead-check.outputs.commits_ahead != '0'
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
run: |
RANGE="${{ needs.check-changes.outputs.diff_range }}"
TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M UTC")
BRANCH="${{ steps.branch.outputs.branch }}"
BASE="${{ steps.find-base.outputs.base }}"
PR_URL=$(gh pr create \
--base "$BASE" \
--head "$BRANCH" \
--title "chore: promote staging to main (${TIMESTAMP})" \
--body "## Auto-promotion from staging CI
**Batch range:** \`${RANGE}\`
**Promotion branch:** \`${BRANCH}\`
**Base:** \`${BASE}\`
**Triggered by:** Staging CI batch at ${TIMESTAMP}
Waiting for gates:
- Tests: pending
- E2E: pending
- Claude Code review: pending (will post comments on this PR)
---
*Auto-created by staging-ci workflow*" \
--label "staging-promotion")
PR_NUM=$(echo "$PR_URL" | grep -oE '[0-9]+$')
echo "pr_number=${PR_NUM}" >> "$GITHUB_OUTPUT"
echo "Created promotion PR #${PR_NUM}"
# ── Gate: wait for review, process findings, merge or block ─────
gate:
name: Staging Gate
needs: [check-changes, tests, e2e, create-promotion-pr]
if: >
always() &&
needs.check-changes.outputs.has_changes == 'true' &&
needs.tests.result == 'success' &&
needs.e2e.result == 'success' &&
needs.create-promotion-pr.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 25
outputs:
gate_passed: ${{ steps.evaluate.outputs.passed }}
steps:
- uses: actions/checkout@v6
with:
ref: staging
fetch-depth: 1
- name: Generate GitHub App token
id: app-token
continue-on-error: true
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.GH_RELEASES_MANAGER_APP_ID }}
private-key: ${{ secrets.GH_RELEASES_MANAGER_APP_PRIVATE_KEY }}
- name: Set token
id: token
run: |
if [ -n "${{ steps.app-token.outputs.token }}" ]; then
echo "token=${{ steps.app-token.outputs.token }}" >> "$GITHUB_OUTPUT"
else
echo "token=${{ github.token }}" >> "$GITHUB_OUTPUT"
fi
- name: Wait for Claude review job
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
PR_NUMBER: ${{ needs.create-promotion-pr.outputs.pr_number }}
REPO: ${{ github.repository }}
run: |
if [ -z "$PR_NUMBER" ]; then
echo "No PR number — skipping wait"
exit 0
fi
PR_SHA=$(gh pr view "$PR_NUMBER" --json headRefOid --jq '.headRefOid' || echo "")
if [ -z "$PR_SHA" ]; then
echo "::warning::Could not get PR head SHA"
exit 0
fi
echo "Polling for Claude Code Review job on PR #${PR_NUMBER} (SHA: ${PR_SHA})..."
TIMEOUT=1200 # 20 minutes
ELAPSED=0
INTERVAL=30
while [ "$ELAPSED" -lt "$TIMEOUT" ]; do
STATUS=$(gh api "repos/${REPO}/commits/${PR_SHA}/check-runs" \
--jq '[.check_runs[] | select(.name == "Claude Code Review") | .conclusion // .status] | first // "pending"' 2>/dev/null || echo "pending")
if [ "$STATUS" = "success" ] || [ "$STATUS" = "failure" ] || [ "$STATUS" = "cancelled" ]; then
echo "Claude review job completed with status: ${STATUS} (${ELAPSED}s)"
exit 0
fi
echo "Claude review status: ${STATUS} (${ELAPSED}s elapsed)"
sleep "$INTERVAL"
ELAPSED=$((ELAPSED + INTERVAL))
done
echo "::warning::Claude review job not completed after ${TIMEOUT}s"
- name: Process Claude review comments and create issues
id: process-findings
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
PR_NUMBER: ${{ needs.create-promotion-pr.outputs.pr_number }}
REPO: ${{ github.repository }}
run: |
HAS_BLOCKING=false
ISSUES_CREATED=0
if [ -z "$PR_NUMBER" ]; then
echo "No PR — skipping finding processing"
echo "has_blocking=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Check for "No issues found" first (clean pass)
NO_ISSUES=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
--jq '[.[] | select(.user.login == "claude[bot]") | select(.body | test("No issues found"))] | length' 2>/dev/null || echo "0")
if [ "$NO_ISSUES" -gt 0 ]; then
echo "Claude review found no issues — gate passes"
echo "has_blocking=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Get the last Claude comment that contains findings
JQ_FILTER='[.[] | select(.user.login == "claude[bot]") | select(.body | test("Found [0-9]+ issue"))] | last'
BODY=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
--jq "${JQ_FILTER} | .body // empty" 2>/dev/null || echo "")
COMMENT_URL=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
--jq "${JQ_FILTER} | .html_url // empty" 2>/dev/null || echo "")
if [ -z "$BODY" ]; then
echo "::warning::No Claude review comment found for PR #${PR_NUMBER} — treating as blocking"
echo "has_blocking=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Parse [SEVERITY:CONFIDENCE] tags from each numbered finding
# Matrix: CRITICAL always→issue, ≥80→block. HIGH ≥50→issue. MEDIUM ≥80→issue. LOW ≥80→issue.
# Use process substitution so variables propagate to parent shell
while read -r line; do
TAG=$(echo "$line" | grep -oE '^\[(CRITICAL|HIGH|MEDIUM|LOW):[0-9]+\]')
SEVERITY=$(echo "$TAG" | sed 's/\[\(.*\):\(.*\)\]/\1/')
CONFIDENCE=$(echo "$TAG" | sed 's/\[\(.*\):\(.*\)\]/\2/')
DESC=$(echo "$line" | sed "s/\[${SEVERITY}:${CONFIDENCE}\] *//" | head -1)
echo "Found: [${SEVERITY}:${CONFIDENCE}] ${DESC}"
# Check if blocking (CRITICAL ≥80)
if [ "$SEVERITY" = "CRITICAL" ] && [ "$CONFIDENCE" -ge 80 ]; then
HAS_BLOCKING=true
fi
# Determine if this should create an issue
CREATE_ISSUE=false
case "$SEVERITY" in
CRITICAL) CREATE_ISSUE=true ;;
HIGH) [ "$CONFIDENCE" -ge 50 ] && CREATE_ISSUE=true ;;
MEDIUM) [ "$CONFIDENCE" -ge 80 ] && CREATE_ISSUE=true ;;
LOW) [ "$CONFIDENCE" -ge 80 ] && CREATE_ISSUE=true ;;
esac
if [ "$CREATE_ISSUE" = "true" ]; then
case "$SEVERITY" in
CRITICAL) LABELS="bug,risk: high,staging-ci-review" ;;
HIGH) LABELS="bug,risk: medium,staging-ci-review" ;;
MEDIUM) LABELS="risk: medium,staging-ci-review" ;;
LOW) LABELS="risk: low,staging-ci-review" ;;
esac
TITLE=$(echo "$DESC" | cut -c1-80)
{
echo "## [${SEVERITY}:${CONFIDENCE}] Issue Found by Staging CI Review"
echo ""
echo "**Severity:** ${SEVERITY}"
echo "**Confidence:** ${CONFIDENCE}/100"
echo "**PR comment:** ${COMMENT_URL}"
echo ""
echo "### Description"
echo "$DESC"
echo ""
echo "---"
echo "*Auto-created by staging-ci Claude Code review*"
} > /tmp/issue-body.md
if gh issue create \
--title "[${SEVERITY}] ${TITLE}" \
--body-file /tmp/issue-body.md \
--label "${LABELS}"; then
ISSUES_CREATED=$((ISSUES_CREATED + 1))
else
echo "::warning::Failed to create issue for ${SEVERITY} finding"
fi
fi
done < <(echo "$BODY" | grep -oE '\[(CRITICAL|HIGH|MEDIUM|LOW):[0-9]+\].*')
echo "Created ${ISSUES_CREATED} issues"
echo "has_blocking=${HAS_BLOCKING}" >> "$GITHUB_OUTPUT"
- name: Evaluate gate
id: evaluate
env:
PR_NUMBER: ${{ needs.create-promotion-pr.outputs.pr_number }}
SKIP_GATE: ${{ inputs.skip_claude_gate }}
HAS_BLOCKING: ${{ steps.process-findings.outputs.has_blocking }}
run: |
SKIP_INPUT="$SKIP_GATE"
if [ "$HAS_BLOCKING" = "true" ]; then
echo "::warning::Claude review found blocking issues (CRITICAL ≥80 confidence)"
if [ "$SKIP_INPUT" = "true" ]; then
echo "::warning::Gate overridden by skip_claude_gate workflow input"
echo "passed=true" >> "$GITHUB_OUTPUT"
else
echo "::error::Blocking promotion due to CRITICAL findings (≥80 confidence)"
echo "::error::PR #${PR_NUMBER} left open with review comments"
echo "passed=false" >> "$GITHUB_OUTPUT"
exit 1
fi
else
echo "No blocking findings. Gate passed."
echo "passed=true" >> "$GITHUB_OUTPUT"
fi
- name: Merge promotion PR
id: merge
if: steps.evaluate.outputs.passed == 'true'
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
PR_NUMBER: ${{ needs.create-promotion-pr.outputs.pr_number }}
run: |
if [ -n "$PR_NUMBER" ]; then
echo "Merging promotion PR #${PR_NUMBER}"
# Do NOT use --delete-branch: deleting a promotion branch closes
# any chained PRs that use it as their base (verified in ironclaw-ci-test).
# Stale promotion branches are cleaned up separately.
gh pr merge "$PR_NUMBER" --merge
echo "merged=true" >> "$GITHUB_OUTPUT"
fi
# ── Update tested tag (always, so next batch covers only new commits) ──
update-tag:
name: Update staging-tested tag
needs: [check-changes, tests, e2e, create-promotion-pr, gate]
if: >
always() &&
needs.check-changes.outputs.has_changes == 'true' &&
needs.tests.result == 'success' &&
needs.e2e.result == 'success' &&
needs.create-promotion-pr.result == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: staging
fetch-depth: 1
- name: Update staging-tested tag
run: |
git tag -f staging-tested "${{ needs.check-changes.outputs.current_head }}"
git push origin staging-tested --force
echo "Updated staging-tested tag to ${{ needs.check-changes.outputs.current_head }}"
# ── Report ───────────────────────────────────────────────────────
report:
name: Staging CI Summary
needs: [check-changes, tests, e2e, create-promotion-pr, gate, update-tag]
if: always() && needs.check-changes.outputs.has_changes == 'true'
runs-on: ubuntu-latest
steps:
- name: Summary
run: |
echo "## Staging CI Batch Results" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| Check | Result |" >> "$GITHUB_STEP_SUMMARY"
echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY"
echo "| Tests | ${{ needs.tests.result }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| E2E | ${{ needs.e2e.result }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| Promotion PR | ${{ needs.create-promotion-pr.result }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| Gate | ${{ needs.gate.result }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| Tag Updated | ${{ needs.update-tag.result }} |" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Range: ${{ needs.check-changes.outputs.diff_range }}" >> "$GITHUB_STEP_SUMMARY"
PR_NUM="${{ needs.create-promotion-pr.outputs.pr_number }}"
if [ -n "$PR_NUM" ]; then
echo "Promotion PR: #${PR_NUM}" >> "$GITHUB_STEP_SUMMARY"
fi
+30 -7
View File
@@ -1,5 +1,6 @@
name: Run Tests
on:
workflow_call:
pull_request:
push:
branches:
@@ -38,6 +39,9 @@ jobs:
telegram-tests:
name: Telegram Channel Tests
if: >
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.base_ref != 'staging')
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -50,6 +54,9 @@ jobs:
windows-build:
name: Windows Build (${{ matrix.name }})
if: >
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.base_ref != 'staging')
runs-on: windows-latest
strategy:
fail-fast: false
@@ -74,6 +81,9 @@ jobs:
wasm-wit-compat:
name: WASM WIT Compatibility
if: >
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.base_ref != 'staging')
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -94,6 +104,9 @@ jobs:
docker-build:
name: Docker Build
if: >
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.base_ref != 'staging')
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -123,12 +136,22 @@ jobs:
needs: [tests, telegram-tests, wasm-wit-compat, docker-build, windows-build, version-check]
steps:
- run: |
if [[ "${{ needs.tests.result }}" != "success" || "${{ needs.telegram-tests.result }}" != "success" || "${{ needs.wasm-wit-compat.result }}" != "success" || "${{ needs.docker-build.result }}" != "success" || "${{ needs.windows-build.result }}" != "success" ]]; then
echo "One or more jobs failed"
exit 1
fi
# version-check only runs on PRs, so skip/success are both acceptable
if [[ "${{ needs.version-check.result }}" == "failure" ]]; then
echo "Version bump check failed"
# Unit tests must always pass
if [[ "${{ needs.tests.result }}" != "success" ]]; then
echo "Unit tests failed"
exit 1
fi
# Gated jobs: must pass on promotion PRs / push, skipped on developer PRs
for job in telegram-tests wasm-wit-compat docker-build windows-build version-check; do
case "$job" in
telegram-tests) result="${{ needs.telegram-tests.result }}" ;;
wasm-wit-compat) result="${{ needs.wasm-wit-compat.result }}" ;;
docker-build) result="${{ needs.docker-build.result }}" ;;
windows-build) result="${{ needs.windows-build.result }}" ;;
version-check) result="${{ needs.version-check.result }}" ;;
esac
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "$job failed"
exit 1
fi
done