Skip to content

PR Maintenance

PR Maintenance #7498

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