PR Maintenance #7498
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR Maintenance | |
| on: | |
| pull_request: | |
| types: [opened, edited, synchronize, reopened, ready_for_review] | |
| schedule: | |
| - cron: "*/30 * * * *" | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: Select maintenance job(s). | |
| type: choice | |
| options: | |
| - status | |
| - auto-merge | |
| - update-branch | |
| - eligibility | |
| - both | |
| default: both | |
| pr_number: | |
| description: "PR number (required for eligibility or update-branch)" | |
| required: false | |
| enable_auto_merge: | |
| description: "Enable auto-merge if eligible (eligibility mode only)" | |
| required: false | |
| default: "false" | |
| concurrency: | |
| group: >- | |
| ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}-${{ (github.event_name == 'pull_request' && github.event.action) || (github.event_name == 'workflow_dispatch' && inputs.mode) || (github.event_name == 'schedule' && 'schedule') || 'dispatch' }}-${{ (github.event_name == 'workflow_dispatch' && inputs.pr_number) || 'pr' }} | |
| cancel-in-progress: true | |
| permissions: | |
| actions: read | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| update-branch: | |
| # Skip edited events to avoid branch updates on metadata-only changes. | |
| if: >- | |
| !contains(fromJSON('["1","true","yes","on","TRUE","YES","ON","True","Yes","On"]'), vars.AE_AUTOMATION_GLOBAL_DISABLE) | |
| && ( | |
| (github.event_name == 'pull_request' && github.event.action != 'edited' && github.event.pull_request.head.repo.fork == false) | |
| || (github.event_name == 'workflow_dispatch' && inputs.mode == 'update-branch') | |
| ) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| steps: | |
| - name: Update branch if behind | |
| uses: actions/github-script@v7 | |
| with: | |
| # Use the scoped workflow token. If AE_AUTO_UPDATE_TOKEN is configured | |
| # but expired, expression fallback cannot recover after authentication. | |
| github-token: ${{ github.token }} | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const inputNumber = context.payload.inputs?.pr_number ?? core.getInput('pr_number'); | |
| const parsedInput = inputNumber ? Number(inputNumber) : null; | |
| const prNumber = context.payload.pull_request?.number ?? parsedInput; | |
| if (!Number.isFinite(prNumber)) { | |
| core.notice('No PR number provided; skipping.'); | |
| return; | |
| } | |
| const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); | |
| const maxAttempts = 3; | |
| const waitMs = 10000; | |
| let mergeableState = 'unknown'; | |
| let pr = null; | |
| const marker = '<!-- AE-PR-AUTO-UPDATE -->'; | |
| const upsertComment = async (content) => { | |
| const body = [marker, content].join('\n\n'); | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner, | |
| repo, | |
| issue_number: prNumber, | |
| per_page: 100 | |
| }); | |
| const existing = comments.find((comment) => | |
| typeof comment.body === 'string' && comment.body.includes(marker) | |
| ); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner, | |
| repo, | |
| comment_id: existing.id, | |
| body | |
| }); | |
| return; | |
| } | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: prNumber, | |
| body | |
| }); | |
| }; | |
| const safeComment = async (content, contextLabel) => { | |
| try { | |
| await upsertComment(content); | |
| } catch (error) { | |
| const message = error?.message ?? String(error); | |
| core.warning(`Failed to post update comment (${contextLabel}) on PR #${prNumber}: ${message}`); | |
| } | |
| }; | |
| for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { | |
| const { data } = await github.rest.pulls.get({ | |
| owner, | |
| repo, | |
| pull_number: prNumber | |
| }); | |
| pr = data; | |
| if (pr.draft) { | |
| core.notice('PR is draft; skipping update.'); | |
| return; | |
| } | |
| mergeableState = pr.mergeable_state || 'unknown'; | |
| if (mergeableState !== 'unknown') { | |
| break; | |
| } | |
| if (attempt < maxAttempts) { | |
| core.info(`Attempt ${attempt}/${maxAttempts}: mergeable_state still unknown, waiting ${waitMs / 1000}s before retry...`); | |
| await sleep(waitMs); | |
| } | |
| } | |
| if (mergeableState === 'unknown') { | |
| core.warning(`PR #${prNumber} mergeable_state remains unknown after ${maxAttempts} attempts; skipping auto-update.`); | |
| return; | |
| } | |
| if (mergeableState !== 'behind') { | |
| core.notice(`PR #${prNumber} mergeable_state=${mergeableState}; update not required.`); | |
| return; | |
| } | |
| try { | |
| await github.rest.pulls.updateBranch({ | |
| owner, | |
| repo, | |
| pull_number: prNumber | |
| }); | |
| } catch (error) { | |
| const message = error?.message ?? String(error); | |
| const status = error?.status ?? error?.response?.status; | |
| const looksLikeConflict = status === 409 || /conflict/i.test(message); | |
| if (looksLikeConflict) { | |
| const content = [ | |
| '### Auto Update Branch', | |
| `PR #${prNumber} could not be auto-updated due to conflicts.`, | |
| `Details: ${message}`, | |
| 'Please resolve conflicts manually.' | |
| ].join('\n'); | |
| await safeComment(content, 'conflict'); | |
| core.notice(`Auto-update skipped for PR #${prNumber} due to conflicts.`); | |
| return; | |
| } | |
| core.setFailed(`Failed to update branch for PR #${prNumber}: ${message}`); | |
| return; | |
| } | |
| const content = [ | |
| '### Auto Update Branch', | |
| `PR #${prNumber} was behind base; triggered branch update.`, | |
| 'If conflicts remain, manual resolution is required.' | |
| ].join('\n'); | |
| await safeComment(content, 'updated'); | |
| label: | |
| if: >- | |
| !contains(fromJSON('["1","true","yes","on","TRUE","YES","ON","True","Yes","On"]'), vars.AE_AUTOMATION_GLOBAL_DISABLE) | |
| && github.event_name == 'pull_request' | |
| && !github.event.pull_request.head.repo.fork | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { owner, repo, number } = context.issue; | |
| const pr = context.payload.pull_request; | |
| if (!pr) return; | |
| const toAdd = new Set(); | |
| // trace:<id> | |
| const m = (pr.title||'').match(/trace:([A-Za-z0-9_.:-]+)/); | |
| if (m) toAdd.add(`trace:${m[1]}`); | |
| // detailed | |
| if ((pr.title||'').includes('[detailed]') || (pr.body||'').includes('[detailed]')) toAdd.add('pr-summary:detailed'); | |
| // coverage | |
| if ((pr.title||'').includes('[enforce-coverage]') || (pr.body||'').includes('[enforce-coverage]')) toAdd.add('enforce-coverage'); | |
| const m2 = (pr.title||'').match(/\[cov=(\d{2,3})\]/) || (pr.body||'').match(/\[cov=(\d{2,3})\]/); | |
| if (m2) toAdd.add(`coverage:${m2[1]}`); | |
| // language | |
| const m3 = (pr.title||'').match(/\[lang=(ja|en)\]/i) || (pr.body||'').match(/\[lang=(ja|en)\]/i); | |
| if (m3) toAdd.add(`lang:${m3[1].toLowerCase()}`); | |
| if (toAdd.size) { | |
| await github.rest.issues.addLabels({ owner, repo, issue_number: number, labels: Array.from(toAdd) }); | |
| } | |
| summarize: | |
| if: >- | |
| !contains(fromJSON('["1","true","yes","on","TRUE","YES","ON","True","Yes","On"]'), vars.AE_AUTOMATION_GLOBAL_DISABLE) | |
| && github.event_name == 'pull_request' | |
| && !github.event.pull_request.head.repo.fork | |
| && github.event.action != 'edited' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Determine summary language | |
| id: lang | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const labels = (context.payload.pull_request?.labels || []).map(l=>l.name); | |
| const ja = labels.includes('lang:ja'); | |
| core.setOutput('lang', ja ? 'ja' : 'en'); | |
| - name: Determine summary mode | |
| id: mode | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const labels = (context.payload.pull_request?.labels || []).map(l=>l.name); | |
| const detailed = labels.includes('pr-summary:detailed'); | |
| core.setOutput('mode', detailed ? 'detailed' : 'digest'); | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node + pnpm | |
| uses: ./.github/actions/setup-node-pnpm | |
| with: | |
| node-version: '20' | |
| - name: Resolve verify-lite run for current head SHA | |
| id: verify-lite-run | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const headSha = context.payload.pull_request?.head?.sha; | |
| if (!headSha) { | |
| core.notice('No pull_request head SHA found; skipping verify-lite artifact lookup.'); | |
| core.setOutput('run_id', ''); | |
| return; | |
| } | |
| const { data } = await github.rest.actions.listWorkflowRuns({ | |
| owner, | |
| repo, | |
| workflow_id: 'verify-lite.yml', | |
| event: 'pull_request', | |
| head_sha: headSha, | |
| status: 'completed', | |
| per_page: 1, | |
| }); | |
| const run = (data.workflow_runs || []).find((candidate) => | |
| candidate.head_sha === headSha | |
| && candidate.conclusion === 'success' | |
| ); | |
| if (!run) { | |
| core.notice(`No successful verify-lite run found for ${headSha}.`); | |
| core.setOutput('run_id', ''); | |
| return; | |
| } | |
| core.notice(`Using verify-lite run ${run.id} for ${headSha}.`); | |
| core.setOutput('run_id', String(run.id)); | |
| - name: Download verify-lite report artifact | |
| if: ${{ steps.verify-lite-run.outputs.run_id != '' }} | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: verify-lite-report | |
| path: artifacts/downloaded/verify-lite-report | |
| github-token: ${{ github.token }} | |
| repository: ${{ github.repository }} | |
| run-id: ${{ steps.verify-lite-run.outputs.run_id }} | |
| - name: Aggregate artifacts if present | |
| run: | | |
| if npm run -s | grep -q "artifacts:aggregate"; then npm run artifacts:aggregate || true; fi | |
| - name: Generate Markdown summary | |
| id: gen | |
| env: | |
| SUMMARY_MODE: ${{ steps.mode.outputs.mode }} | |
| SUMMARY_LANG: ${{ steps.lang.outputs.lang }} | |
| run: | | |
| if [ -f scripts/summary/render-pr-summary.mjs ]; then node scripts/summary/render-pr-summary.mjs; else printf "No renderer; keeping inline\n"; fi | |
| if [ -f artifacts/summary/PR_SUMMARY.md ]; then | |
| exit 0 | |
| fi | |
| node <<'JS' | |
| const fs=require("fs"); | |
| function r(p){ try { return JSON.parse(fs.readFileSync(p,"utf-8")); } catch { return undefined; } } | |
| function normalizeFormalStatus(status) { | |
| if (status === "ok") return "pass"; | |
| if (status === "failed") return "fail"; | |
| return status; | |
| } | |
| function summarizeFormalPresence(summary) { | |
| const presentMap = summary?.present && typeof summary.present === "object" | |
| ? summary.present | |
| : (summary?.info?.present && typeof summary.info.present === "object" ? summary.info.present : null); | |
| if (!presentMap) return ""; | |
| const entries = Object.entries(presentMap); | |
| if (!entries.length) return ""; | |
| const presentKeys = entries.filter(([, value]) => value).map(([key]) => key); | |
| return `present ${presentKeys.length}/${entries.length}${presentKeys.length ? ` (${presentKeys.join(", ")})` : ""}`; | |
| } | |
| function formalResultFromSummary(summary) { | |
| if (!summary || typeof summary !== "object") return ""; | |
| if (typeof summary.result === "string" && summary.result.trim()) return summary.result.trim(); | |
| if (typeof summary.status === "string" && summary.status.trim()) return normalizeFormalStatus(summary.status.trim()); | |
| return summarizeFormalPresence(summary); | |
| } | |
| function selectFormalSummary(c) { | |
| const candidates = [ | |
| c.formal, | |
| r("artifacts/formal/formal-summary-v2.json"), | |
| r("artifacts/downloaded/verify-lite-report/artifacts/formal/formal-summary-v2.json"), | |
| r("artifacts/formal/formal-summary-v1.json"), | |
| r("artifacts/downloaded/verify-lite-report/artifacts/formal/formal-summary-v1.json"), | |
| r("artifacts/hermetic-reports/formal/summary.json"), | |
| r("artifacts/downloaded/verify-lite-report/artifacts/hermetic-reports/formal/summary.json"), | |
| r("formal/summary.json"), | |
| ]; | |
| for (const candidate of candidates) { | |
| if (formalResultFromSummary(candidate)) return candidate; | |
| } | |
| return {}; | |
| } | |
| const c = r("artifacts/summary/combined.json") || {}; | |
| const adapters=(c.adapters||[]).map(a=>` - ${a.adapter||a.name}: ${a.summary} (${a.status})`).join("\n"); | |
| const formalObj = selectFormalSummary(c); | |
| const formal = formalResultFromSummary(formalObj) || "n/a"; | |
| const replay = c.replay || r("artifacts/domain/replay.summary.json") || {}; | |
| const props = c.properties ? (Array.isArray(c.properties) ? c.properties : [c.properties]) : (r("artifacts/properties/summary.json") ? [r("artifacts/properties/summary.json")] : []); | |
| const assurance = r("artifacts/assurance/assurance-summary.json") || {}; | |
| const assuranceSummary = assurance.summary && typeof assurance.summary === "object" ? assurance.summary : null; | |
| const assuranceWarnings = Array.isArray(assurance.warnings) | |
| ? [...new Set(assurance.warnings.map((warning) => warning?.code).filter(Boolean))] | |
| : []; | |
| const traceIds = new Set(); | |
| for (const a of c.adapters||[]) if (a?.traceId) traceIds.add(a.traceId); | |
| if (formalObj?.traceId) traceIds.add(formalObj.traceId); | |
| if (replay?.traceId) traceIds.add(replay.traceId); | |
| for (const p of props) if (p?.traceId) traceIds.add(p.traceId); | |
| const replayLine = replay.totalEvents!==undefined ? `Replay: ${replay.totalEvents} events, ${(replay.violatedInvariants||[]).length} violations` : "Replay: n/a"; | |
| const assuranceLine = assuranceSummary | |
| ? `Assurance: satisfied=${assuranceSummary.satisfiedClaims ?? "n/a"}/${assuranceSummary.claimCount ?? "n/a"}, warningClaims=${assuranceSummary.warningClaims ?? "n/a"}, warnings=${assuranceSummary.warningCount ?? "n/a"}` | |
| : null; | |
| const assuranceWarningsLine = assuranceSummary | |
| ? `Assurance warning codes: ${assuranceWarnings.length ? assuranceWarnings.join(", ") : "none"}` | |
| : null; | |
| const assuranceBlock = assuranceLine | |
| ? `- ${assuranceLine}\n${assuranceWarningsLine ? `- ${assuranceWarningsLine}\n` : ""}` | |
| : ""; | |
| const md = `## Quality Summary\n${assuranceBlock}- Adapters:\n${adapters}\n- Formal: ${formal}\n- ${replayLine}\n- Trace IDs: ${Array.from(traceIds).join(", ")}`; | |
| fs.mkdirSync("artifacts/summary",{recursive:true}); | |
| fs.writeFileSync("artifacts/summary/PR_SUMMARY.md", md); | |
| console.log(md); | |
| JS | |
| - name: Build harness health summary | |
| if: ${{ always() }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} | |
| SUMMARY_MODE: ${{ steps.mode.outputs.mode }} | |
| run: | | |
| if [ ! -f artifacts/summary/PR_SUMMARY.md ]; then | |
| mkdir -p artifacts/summary | |
| printf '%s\n' '## Quality Summary' '- n/a' > artifacts/summary/PR_SUMMARY.md | |
| fi | |
| node scripts/ci/build-harness-health.mjs \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --pr "${{ github.event.pull_request.number }}" \ | |
| --workflow "${GITHUB_WORKFLOW}" \ | |
| --run-id "${GITHUB_RUN_ID}" \ | |
| --commit-sha "${GITHUB_SHA}" \ | |
| --mode "${SUMMARY_MODE}" \ | |
| --output-json artifacts/ci/harness-health.json \ | |
| --output-md artifacts/ci/harness-health.md | |
| if [ -f artifacts/ci/harness-health.md ]; then | |
| printf '\n\n' >> artifacts/summary/PR_SUMMARY.md | |
| cat artifacts/ci/harness-health.md >> artifacts/summary/PR_SUMMARY.md | |
| fi | |
| - name: Install dependencies for Change Package scripts | |
| run: | | |
| # Exception: automation-generated diffs can arrive before `pnpm-lock.yaml` refresh; review when upstream lockfile updates are mandatory. | |
| pnpm install --frozen-lockfile || pnpm install --no-frozen-lockfile | |
| - name: Generate Change Package | |
| env: | |
| SUMMARY_MODE: ${{ steps.mode.outputs.mode }} | |
| run: | | |
| args=( | |
| --policy policy/risk-policy.yml \ | |
| --mode "${SUMMARY_MODE}" \ | |
| --output-json artifacts/change-package/change-package.json \ | |
| --output-md artifacts/change-package/change-package.md | |
| ) | |
| if [ "${AE_CHANGE_PACKAGE_DUAL_WRITE:-0}" = "1" ]; then | |
| args+=(--dual-write) | |
| fi | |
| if [ -f artifacts/assurance/claim-evidence-manifest.json ]; then | |
| args+=(--claim-evidence-manifest artifacts/assurance/claim-evidence-manifest.json) | |
| fi | |
| if [ -f artifacts/ci/policy-decision-js-v1.json ]; then | |
| args+=(--policy-decision artifacts/ci/policy-decision-js-v1.json) | |
| fi | |
| if [ -f artifacts/assurance/assurance-summary.json ]; then | |
| args+=(--assurance-summary artifacts/assurance/assurance-summary.json) | |
| fi | |
| if [ -f artifacts/assurance/claim-level-summary.json ]; then | |
| args+=(--claim-level-summary artifacts/assurance/claim-level-summary.json) | |
| fi | |
| if [ -f artifacts/release/post-deploy-verify.json ]; then | |
| args+=(--post-deploy-verify artifacts/release/post-deploy-verify.json) | |
| fi | |
| node scripts/change-package/generate.mjs "${args[@]}" | |
| - name: Validate Change Package | |
| run: | | |
| node scripts/change-package/validate.mjs \ | |
| --file artifacts/change-package/change-package.json \ | |
| --schema schema/change-package.schema.json \ | |
| --output-json artifacts/change-package/change-package-validation.json \ | |
| --output-md artifacts/change-package/change-package-validation.md | |
| if [ -f artifacts/change-package/change-package-v2.json ]; then | |
| v2_args=( | |
| --file artifacts/change-package/change-package-v2.json | |
| --schema schema/change-package-v2.schema.json | |
| --output-json artifacts/change-package/change-package-v2-validation.json | |
| --output-md artifacts/change-package/change-package-v2-validation.md | |
| ) | |
| if [ "${AE_CHANGE_PACKAGE_V2_STRICT:-0}" = "1" ]; then | |
| v2_args+=(--strict) | |
| fi | |
| if [ -f artifacts/ci/policy-decision-js-v1.json ]; then | |
| v2_args+=(--policy-decision artifacts/ci/policy-decision-js-v1.json) | |
| fi | |
| node scripts/change-package/validate.mjs "${v2_args[@]}" | |
| fi | |
| - name: Build hook feedback (report-only) | |
| if: ${{ always() }} | |
| shell: bash | |
| run: | | |
| verify_lite_summary="artifacts/downloaded/verify-lite-report/artifacts/verify-lite/verify-lite-run-summary.json" | |
| assurance_summary="artifacts/downloaded/verify-lite-report/artifacts/assurance/assurance-summary.json" | |
| if [ ! -f "$verify_lite_summary" ]; then | |
| printf '%s\n' 'Verify Lite report artifact not available for this head SHA; skipped hook-feedback generation.' >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| fi | |
| args=( | |
| --verify-lite-summary "$verify_lite_summary" | |
| --harness-health artifacts/ci/harness-health.json | |
| --change-package artifacts/change-package/change-package.json | |
| ) | |
| if [ -f artifacts/context-pack/context-pack-suggestions.json ]; then | |
| args+=(--context-pack-suggestions artifacts/context-pack/context-pack-suggestions.json) | |
| fi | |
| if [ -f "$assurance_summary" ]; then | |
| args+=(--assurance-summary "$assurance_summary") | |
| fi | |
| if [ -f artifacts/e2e/ui-e2e-summary.json ]; then | |
| args+=(--ui-e2e-summary artifacts/e2e/ui-e2e-summary.json) | |
| fi | |
| pnpm -s run hook-feedback:build "${args[@]}" | |
| - name: Validate Plan Artifact | |
| if: ${{ always() }} | |
| continue-on-error: true | |
| run: | | |
| if [ -f artifacts/plan/plan-artifact.json ]; then | |
| node scripts/plan-artifact/validate.mjs \ | |
| --file artifacts/plan/plan-artifact.json \ | |
| --schema schema/plan-artifact.schema.json \ | |
| --output-json artifacts/plan/plan-artifact-validation.json \ | |
| --output-md artifacts/plan/plan-artifact-validation.md | |
| else | |
| printf '%s\n' 'Plan Artifact not present in this PR branch; skipping append.' >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Append Change Package, Plan Artifact, Hook Feedback, and Quality Scorecard to PR summary | |
| if: ${{ always() }} | |
| run: | | |
| if [ -f artifacts/change-package/change-package.md ]; then | |
| printf '\n\n' >> artifacts/summary/PR_SUMMARY.md | |
| cat artifacts/change-package/change-package.md >> artifacts/summary/PR_SUMMARY.md | |
| fi | |
| if [ -f artifacts/change-package/change-package-validation.md ]; then | |
| printf '\n\n' >> artifacts/summary/PR_SUMMARY.md | |
| cat artifacts/change-package/change-package-validation.md >> artifacts/summary/PR_SUMMARY.md | |
| fi | |
| if [ -f artifacts/change-package/change-package-v2.md ]; then | |
| printf '\n\n' >> artifacts/summary/PR_SUMMARY.md | |
| cat artifacts/change-package/change-package-v2.md >> artifacts/summary/PR_SUMMARY.md | |
| fi | |
| if [ -f artifacts/change-package/change-package-v2-validation.md ]; then | |
| printf '\n\n' >> artifacts/summary/PR_SUMMARY.md | |
| cat artifacts/change-package/change-package-v2-validation.md >> artifacts/summary/PR_SUMMARY.md | |
| fi | |
| if [ -f artifacts/plan/plan-artifact.md ]; then | |
| printf '\n\n' >> artifacts/summary/PR_SUMMARY.md | |
| cat artifacts/plan/plan-artifact.md >> artifacts/summary/PR_SUMMARY.md | |
| fi | |
| if [ -f artifacts/plan/plan-artifact-validation.md ]; then | |
| printf '\n\n' >> artifacts/summary/PR_SUMMARY.md | |
| cat artifacts/plan/plan-artifact-validation.md >> artifacts/summary/PR_SUMMARY.md | |
| fi | |
| if [ -f artifacts/agents/hook-feedback.md ]; then | |
| printf '\n\n' >> artifacts/summary/PR_SUMMARY.md | |
| cat artifacts/agents/hook-feedback.md >> artifacts/summary/PR_SUMMARY.md | |
| fi | |
| if [ -f artifacts/downloaded/verify-lite-report/artifacts/quality/quality-scorecard.md ]; then | |
| printf '\n\n' >> artifacts/summary/PR_SUMMARY.md | |
| cat artifacts/downloaded/verify-lite-report/artifacts/quality/quality-scorecard.md >> artifacts/summary/PR_SUMMARY.md | |
| fi | |
| if [ -f artifacts/downloaded/verify-lite-report/artifacts/assurance/claim-evidence-manifest.md ]; then | |
| printf '\n\n' >> artifacts/summary/PR_SUMMARY.md | |
| cat artifacts/downloaded/verify-lite-report/artifacts/assurance/claim-evidence-manifest.md >> artifacts/summary/PR_SUMMARY.md | |
| fi | |
| - name: Upload harness health artifact | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: harness-health-pr-${{ github.event.pull_request.number }} | |
| path: | | |
| artifacts/ci/harness-health.json | |
| artifacts/ci/harness-health.md | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: Upload change-package and plan-artifact artifacts | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: change-package-pr-${{ github.event.pull_request.number }} | |
| path: | | |
| artifacts/change-package/change-package.json | |
| artifacts/change-package/change-package.md | |
| artifacts/change-package/change-package-validation.json | |
| artifacts/change-package/change-package-validation.md | |
| artifacts/change-package/change-package-v2.json | |
| artifacts/change-package/change-package-v2.md | |
| artifacts/change-package/change-package-v2-validation.json | |
| artifacts/change-package/change-package-v2-validation.md | |
| artifacts/plan/plan-artifact.json | |
| artifacts/plan/plan-artifact.md | |
| artifacts/plan/plan-artifact-validation.json | |
| artifacts/plan/plan-artifact-validation.md | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: Upload hook-feedback artifact | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: hook-feedback-pr-${{ github.event.pull_request.number }} | |
| path: | | |
| artifacts/agents/hook-feedback.json | |
| artifacts/agents/hook-feedback.md | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: Post or update PR comment | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const body = fs.readFileSync('artifacts/summary/PR_SUMMARY.md','utf-8'); | |
| const header = '<!-- AE-PR-SUMMARY -->\n' + body; | |
| const { owner, repo, number } = context.issue; | |
| const comments = await github.rest.issues.listComments({ owner, repo, issue_number: number, per_page: 100 }); | |
| const mine = comments.data.find(c => c.body && c.body.startsWith('<!-- AE-PR-SUMMARY -->')); | |
| if (mine) { | |
| await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body: header }); | |
| } else { | |
| await github.rest.issues.createComment({ owner, repo, issue_number: number, body: header }); | |
| } | |
| - name: Generate progress summary | |
| run: node scripts/progress/aggregate-progress.mjs | |
| - name: Load previous progress summary | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const marker = '<!-- AE-PR-PROGRESS -->'; | |
| const { owner, repo, number } = context.issue; | |
| const comments = await github.rest.issues.listComments({ owner, repo, issue_number: number, per_page: 100 }); | |
| const existing = comments.data.find(c => typeof c.body === 'string' && c.body.includes(marker)); | |
| if (!existing) return; | |
| const match = existing.body.match(/<!-- AE-PR-PROGRESS-JSON ([\s\S]*?) -->/); | |
| if (!match) return; | |
| try { | |
| const parsed = JSON.parse(match[1]); | |
| fs.mkdirSync('artifacts/progress', { recursive: true }); | |
| fs.writeFileSync('artifacts/progress/summary.prev.json', JSON.stringify(parsed, null, 2)); | |
| } catch (error) { | |
| core.warning(`Failed to parse previous progress summary: ${error?.message ?? error}`); | |
| } | |
| - name: Render progress summary | |
| env: | |
| PROGRESS_SUMMARY_PREVIOUS: artifacts/progress/summary.prev.json | |
| run: node scripts/progress/render-progress-summary.mjs | |
| - name: Post or update progress comment | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const marker = '<!-- AE-PR-PROGRESS -->'; | |
| const summary = JSON.parse(fs.readFileSync('artifacts/progress/summary.json','utf-8')); | |
| const body = fs.readFileSync('artifacts/progress/PR_PROGRESS.md','utf-8'); | |
| const payload = `${marker}\n\n${body}\n\n<!-- AE-PR-PROGRESS-JSON ${JSON.stringify(summary)} -->`; | |
| const { owner, repo, number } = context.issue; | |
| const comments = await github.rest.issues.listComments({ owner, repo, issue_number: number, per_page: 100 }); | |
| const existing = comments.data.find(c => c.body && c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body: payload }); | |
| } else { | |
| await github.rest.issues.createComment({ owner, repo, issue_number: number, body: payload }); | |
| } | |
| - name: Write job summary | |
| run: | | |
| cat artifacts/summary/PR_SUMMARY.md >> "$GITHUB_STEP_SUMMARY" | |
| if [ -f artifacts/progress/PR_PROGRESS.md ]; then | |
| printf "\n\n" >> "$GITHUB_STEP_SUMMARY" | |
| cat artifacts/progress/PR_PROGRESS.md >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| post-status: | |
| if: >- | |
| !contains(fromJSON('["1","true","yes","on","TRUE","YES","ON","True","Yes","On"]'), vars.AE_AUTOMATION_GLOBAL_DISABLE) | |
| && ( | |
| (github.event_name == 'schedule') | |
| || (github.event_name == 'workflow_dispatch' && (inputs.mode == 'status' || inputs.mode == 'both')) | |
| ) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node + pnpm | |
| uses: ./.github/actions/setup-node-pnpm | |
| with: | |
| node-version: "20" | |
| - name: Resolve automation config | |
| env: | |
| AE_AUTOMATION_PROFILE: ${{ vars.AE_AUTOMATION_PROFILE || '' }} | |
| AE_COPILOT_AUTO_FIX: ${{ vars.AE_COPILOT_AUTO_FIX || '' }} | |
| AE_COPILOT_AUTO_FIX_SCOPE: ${{ vars.AE_COPILOT_AUTO_FIX_SCOPE || '' }} | |
| AE_COPILOT_AUTO_FIX_LABEL: ${{ vars.AE_COPILOT_AUTO_FIX_LABEL || '' }} | |
| AE_AUTO_MERGE: ${{ vars.AE_AUTO_MERGE || '' }} | |
| AE_AUTO_MERGE_MODE: ${{ vars.AE_AUTO_MERGE_MODE || '' }} | |
| AE_AUTO_MERGE_LABEL: ${{ vars.AE_AUTO_MERGE_LABEL || '' }} | |
| AE_AUTO_MERGE_REQUIRE_RISK_LOW: ${{ vars.AE_AUTO_MERGE_REQUIRE_RISK_LOW || '' }} | |
| AE_AUTO_MERGE_REQUIRE_CHANGE_PACKAGE: ${{ vars.AE_AUTO_MERGE_REQUIRE_CHANGE_PACKAGE || '' }} | |
| AE_AUTO_MERGE_CHANGE_PACKAGE_ALLOW_WARN: ${{ vars.AE_AUTO_MERGE_CHANGE_PACKAGE_ALLOW_WARN || '' }} | |
| AE_GH_THROTTLE_MS: ${{ vars.AE_GH_THROTTLE_MS || '' }} | |
| AE_GH_RETRY_MAX_ATTEMPTS: ${{ vars.AE_GH_RETRY_MAX_ATTEMPTS || '' }} | |
| AE_GH_RETRY_INITIAL_DELAY_MS: ${{ vars.AE_GH_RETRY_INITIAL_DELAY_MS || '' }} | |
| AE_GH_RETRY_MAX_DELAY_MS: ${{ vars.AE_GH_RETRY_MAX_DELAY_MS || '' }} | |
| AE_GH_RETRY_MULTIPLIER: ${{ vars.AE_GH_RETRY_MULTIPLIER || '' }} | |
| AE_GH_RETRY_JITTER_MS: ${{ vars.AE_GH_RETRY_JITTER_MS || '' }} | |
| AE_GH_RETRY_DEBUG: ${{ vars.AE_GH_RETRY_DEBUG || '' }} | |
| COPILOT_REVIEW_WAIT_MINUTES: ${{ vars.COPILOT_REVIEW_WAIT_MINUTES || '' }} | |
| COPILOT_REVIEW_MAX_ATTEMPTS: ${{ vars.COPILOT_REVIEW_MAX_ATTEMPTS || '' }} | |
| run: | | |
| node scripts/ci/lib/automation-config.mjs --format github-env >> "$GITHUB_ENV" | |
| node scripts/ci/lib/automation-config.mjs --format summary >> "$GITHUB_STEP_SUMMARY" | |
| - name: Post CI status summary | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} | |
| run: node scripts/ci/pr-ci-status-comment.mjs | |
| enable-auto-merge: | |
| if: >- | |
| !contains(fromJSON('["1","true","yes","on","TRUE","YES","ON","True","Yes","On"]'), vars.AE_AUTOMATION_GLOBAL_DISABLE) | |
| && ( | |
| (github.event_name == 'pull_request' && github.event.action != 'edited' && github.event.pull_request.head.repo.fork == false) | |
| || (github.event_name == 'schedule') | |
| || (github.event_name == 'workflow_dispatch' && (inputs.mode == 'auto-merge' || inputs.mode == 'both')) | |
| ) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node + pnpm | |
| uses: ./.github/actions/setup-node-pnpm | |
| with: | |
| node-version: "20" | |
| - name: Resolve automation config | |
| env: | |
| AE_AUTOMATION_PROFILE: ${{ vars.AE_AUTOMATION_PROFILE || '' }} | |
| AE_COPILOT_AUTO_FIX: ${{ vars.AE_COPILOT_AUTO_FIX || '' }} | |
| AE_COPILOT_AUTO_FIX_SCOPE: ${{ vars.AE_COPILOT_AUTO_FIX_SCOPE || '' }} | |
| AE_COPILOT_AUTO_FIX_LABEL: ${{ vars.AE_COPILOT_AUTO_FIX_LABEL || '' }} | |
| AE_AUTO_MERGE: ${{ vars.AE_AUTO_MERGE || '' }} | |
| AE_AUTO_MERGE_MODE: ${{ vars.AE_AUTO_MERGE_MODE || '' }} | |
| AE_AUTO_MERGE_LABEL: ${{ vars.AE_AUTO_MERGE_LABEL || '' }} | |
| AE_AUTO_MERGE_REQUIRE_RISK_LOW: ${{ vars.AE_AUTO_MERGE_REQUIRE_RISK_LOW || '' }} | |
| AE_AUTO_MERGE_REQUIRE_CHANGE_PACKAGE: ${{ vars.AE_AUTO_MERGE_REQUIRE_CHANGE_PACKAGE || '' }} | |
| AE_AUTO_MERGE_CHANGE_PACKAGE_ALLOW_WARN: ${{ vars.AE_AUTO_MERGE_CHANGE_PACKAGE_ALLOW_WARN || '' }} | |
| AE_GH_THROTTLE_MS: ${{ vars.AE_GH_THROTTLE_MS || '' }} | |
| AE_GH_RETRY_MAX_ATTEMPTS: ${{ vars.AE_GH_RETRY_MAX_ATTEMPTS || '' }} | |
| AE_GH_RETRY_INITIAL_DELAY_MS: ${{ vars.AE_GH_RETRY_INITIAL_DELAY_MS || '' }} | |
| AE_GH_RETRY_MAX_DELAY_MS: ${{ vars.AE_GH_RETRY_MAX_DELAY_MS || '' }} | |
| AE_GH_RETRY_MULTIPLIER: ${{ vars.AE_GH_RETRY_MULTIPLIER || '' }} | |
| AE_GH_RETRY_JITTER_MS: ${{ vars.AE_GH_RETRY_JITTER_MS || '' }} | |
| AE_GH_RETRY_DEBUG: ${{ vars.AE_GH_RETRY_DEBUG || '' }} | |
| COPILOT_REVIEW_WAIT_MINUTES: ${{ vars.COPILOT_REVIEW_WAIT_MINUTES || '' }} | |
| COPILOT_REVIEW_MAX_ATTEMPTS: ${{ vars.COPILOT_REVIEW_MAX_ATTEMPTS || '' }} | |
| run: | | |
| node scripts/ci/lib/automation-config.mjs --format github-env >> "$GITHUB_ENV" | |
| node scripts/ci/lib/automation-config.mjs --format summary >> "$GITHUB_STEP_SUMMARY" | |
| - name: Enable auto-merge when eligible | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} | |
| PR_NUMBER: ${{ github.event.pull_request.number || '' }} | |
| run: node scripts/ci/auto-merge-enabler.mjs | |
| check-auto-merge: | |
| if: >- | |
| (github.event_name == 'workflow_dispatch') | |
| && (inputs.mode == 'eligibility') | |
| && (inputs.pr_number != '') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node + pnpm | |
| uses: ./.github/actions/setup-node-pnpm | |
| with: | |
| node-version: "20" | |
| - name: Resolve automation config | |
| env: | |
| AE_AUTOMATION_PROFILE: ${{ vars.AE_AUTOMATION_PROFILE || '' }} | |
| AE_COPILOT_AUTO_FIX: ${{ vars.AE_COPILOT_AUTO_FIX || '' }} | |
| AE_COPILOT_AUTO_FIX_SCOPE: ${{ vars.AE_COPILOT_AUTO_FIX_SCOPE || '' }} | |
| AE_COPILOT_AUTO_FIX_LABEL: ${{ vars.AE_COPILOT_AUTO_FIX_LABEL || '' }} | |
| AE_AUTO_MERGE: ${{ vars.AE_AUTO_MERGE || '' }} | |
| AE_AUTO_MERGE_MODE: ${{ vars.AE_AUTO_MERGE_MODE || '' }} | |
| AE_AUTO_MERGE_LABEL: ${{ vars.AE_AUTO_MERGE_LABEL || '' }} | |
| AE_AUTO_MERGE_REQUIRE_RISK_LOW: ${{ vars.AE_AUTO_MERGE_REQUIRE_RISK_LOW || '' }} | |
| AE_AUTO_MERGE_REQUIRE_CHANGE_PACKAGE: ${{ vars.AE_AUTO_MERGE_REQUIRE_CHANGE_PACKAGE || '' }} | |
| AE_AUTO_MERGE_CHANGE_PACKAGE_ALLOW_WARN: ${{ vars.AE_AUTO_MERGE_CHANGE_PACKAGE_ALLOW_WARN || '' }} | |
| AE_GH_THROTTLE_MS: ${{ vars.AE_GH_THROTTLE_MS || '' }} | |
| AE_GH_RETRY_MAX_ATTEMPTS: ${{ vars.AE_GH_RETRY_MAX_ATTEMPTS || '' }} | |
| AE_GH_RETRY_INITIAL_DELAY_MS: ${{ vars.AE_GH_RETRY_INITIAL_DELAY_MS || '' }} | |
| AE_GH_RETRY_MAX_DELAY_MS: ${{ vars.AE_GH_RETRY_MAX_DELAY_MS || '' }} | |
| AE_GH_RETRY_MULTIPLIER: ${{ vars.AE_GH_RETRY_MULTIPLIER || '' }} | |
| AE_GH_RETRY_JITTER_MS: ${{ vars.AE_GH_RETRY_JITTER_MS || '' }} | |
| AE_GH_RETRY_DEBUG: ${{ vars.AE_GH_RETRY_DEBUG || '' }} | |
| COPILOT_REVIEW_WAIT_MINUTES: ${{ vars.COPILOT_REVIEW_WAIT_MINUTES || '' }} | |
| COPILOT_REVIEW_MAX_ATTEMPTS: ${{ vars.COPILOT_REVIEW_MAX_ATTEMPTS || '' }} | |
| run: | | |
| node scripts/ci/lib/automation-config.mjs --format github-env >> "$GITHUB_ENV" | |
| node scripts/ci/lib/automation-config.mjs --format summary >> "$GITHUB_STEP_SUMMARY" | |
| - name: Evaluate auto-merge eligibility | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} | |
| PR_NUMBER: ${{ inputs.pr_number }} | |
| ENABLE_AUTO_MERGE: ${{ inputs.enable_auto_merge }} | |
| run: node scripts/ci/auto-merge-eligible.mjs |