Skip to content

Repair failed Baseline #6

Repair failed Baseline

Repair failed Baseline #6

name: Repair failed Baseline
on:
workflow_run:
workflows: ["Baseline verification"]
types: [completed]
permissions:
actions: read
contents: read
pull-requests: read
concurrency:
group: repair-baseline-${{ github.event.workflow_run.head_branch || github.event.workflow_run.id }}
cancel-in-progress: false
jobs:
qualify:
if: >-
github.event.workflow_run.conclusion == 'failure' &&
(github.event.workflow_run.event == 'pull_request' ||
github.event.workflow_run.event == 'workflow_dispatch')
runs-on: ubuntu-latest
outputs:
eligible: ${{ steps.gate.outputs.eligible }}
pr_number: ${{ steps.gate.outputs.pr_number }}
head_sha: ${{ steps.gate.outputs.head_sha }}
head_ref: ${{ steps.gate.outputs.head_ref }}
reason: ${{ steps.gate.outputs.reason }}
steps:
- name: Qualify trusted low-risk repair
id: gate
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
script: |
const run = context.payload.workflow_run;
const { owner, repo } = context.repo;
const deny = (reason) => {
core.setOutput('eligible', 'false');
core.setOutput('reason', reason);
core.notice(`Automatic repair skipped: ${reason}`);
};
const resolvePullRequest = async () => {
const linked = run.pull_requests?.[0];
if (run.event === 'pull_request' && linked) {
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: linked.number,
});
return { pr, verifiedHead: linked.head.sha };
}
if (run.event !== 'workflow_dispatch' || !run.head_branch) {
return null;
}
const candidates = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
base: 'main',
head: `${owner}:${run.head_branch}`,
per_page: 100,
});
const matches = candidates.filter(
(pr) =>
pr.head.repo?.full_name === `${owner}/${repo}` &&
pr.head.ref === run.head_branch &&
pr.head.sha === run.head_sha,
);
return matches.length === 1
? { pr: matches[0], verifiedHead: run.head_sha }
: null;
};
const resolved = await resolvePullRequest();
if (!resolved) return deny('no exact linked pull request');
const { pr, verifiedHead } = resolved;
core.setOutput('pr_number', String(pr.number));
core.setOutput('head_sha', verifiedHead);
core.setOutput('head_ref', pr.head.ref);
const trusted = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
if (pr.state !== 'open' || pr.draft) return deny('PR is closed or draft');
if (pr.head.repo?.full_name !== `${owner}/${repo}`) return deny('fork PR');
if (!pr.head.ref.startsWith('agent/')) return deny('branch is not an agent branch');
if (!trusted.has(pr.author_association)) return deny('author is not trusted');
if (pr.head.sha !== verifiedHead) return deny('failed run is not for the current head');
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: pr.number,
per_page: 100,
});
const protectedPatterns = [
/^\.github\//,
/(^|\/)migrations?\//i,
/(^|\/)(fly|docker|compose|terraform|infra|deploy)/i,
/(^|\/)(auth|security|crypto|secrets?|permissions?)(\/|\.|-)/i,
/(^|\/)(legal|privacy|governance|release)(\/|\.|-)/i,
/package-lock\.json$/,
/(^|\/)package\.json$/,
/reports\//,
/\.env/i,
];
const protectedFile = files.find(({ filename }) =>
protectedPatterns.some((pattern) => pattern.test(filename)),
);
if (protectedFile) return deny(`protected file changed: ${protectedFile.filename}`);
const commits = await github.paginate(github.rest.pulls.listCommits, {
owner,
repo,
pull_number: pr.number,
per_page: 100,
});
const repairs = commits.filter(({ commit }) =>
commit.message.startsWith('Auto-fix Baseline failure'),
).length;
if (repairs >= 2) return deny('two automatic repair attempts already used');
core.setOutput('eligible', 'true');
core.setOutput('reason', 'trusted low-risk PR');
repair:
needs: qualify
if: needs.qualify.outputs.eligible == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
actions: read
contents: read
outputs:
repair_result: ${{ steps.codex.outputs.final-message }}
steps:
- name: Require OpenAI API key
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: test -n "$OPENAI_API_KEY"
- name: Check out exact failed head without credentials
uses: actions/checkout@v7
with:
ref: ${{ needs.qualify.outputs.head_sha }}
fetch-depth: 0
persist-credentials: false
- name: Capture failed Baseline logs
env:
GH_TOKEN: ${{ github.token }}
RUN_ID: ${{ github.event.workflow_run.id }}
run: |
mkdir -p .ross-autofix
gh run view "$RUN_ID" --log-failed > .ross-autofix/baseline-failure.log
test -s .ross-autofix/baseline-failure.log
- name: Prepare bounded repair instructions
env:
PR_NUMBER: ${{ needs.qualify.outputs.pr_number }}
HEAD_SHA: ${{ needs.qualify.outputs.head_sha }}
run: |
cat > .ross-autofix/repair-prompt.md <<'PROMPT'
Diagnose the failed ROSS Baseline using .ross-autofix/baseline-failure.log and the checked-out repository. Return the smallest correct unified git patch that resolves the concrete failure.
You are operating in deliberately bounded automatic-repair mode:
- Do not modify .github/, migrations, deployment or infrastructure files, authentication, security, cryptography, secrets, permissions, legal/privacy/governance/release files, reports, package.json, or package-lock.json.
- Do not weaken, skip, delete, or broadly disable tests, audits, lint rules, validation, authorization, privacy controls, or release controls.
- Do not add dependencies, change public APIs, alter database schemas, or make architectural refactors.
- Prefer a narrow implementation fix. A narrow test expectation correction is allowed only when the log proves the implementation is correct and the expectation is stale.
- Do not modify the working tree, commit, or push.
- If a safe bounded repair is unavailable, return status "unsafe" or "no-fix" with an empty patch.
- For status "fix", return a complete unified patch produced against the exact checked-out HEAD, beginning with "diff --git". Do not wrap the patch in Markdown fences.
PROMPT
printf '\nPR: %s\nExact failed head: %s\n' "$PR_NUMBER" "$HEAD_SHA" >> .ross-autofix/repair-prompt.md
- name: Produce read-only structured repair
id: codex
uses: openai/codex-action@dd78cb653811af44014baa08fe954e28d32c1bf9
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
prompt-file: .ross-autofix/repair-prompt.md
permission-profile: ":read-only"
safety-strategy: drop-sudo
allow-bot-users: github-actions[bot]
output-schema: |
{
"type": "object",
"additionalProperties": false,
"properties": {
"status": {
"type": "string",
"enum": ["fix", "unsafe", "no-fix"]
},
"reason": {
"type": "string"
},
"patch": {
"type": "string"
}
},
"required": ["status", "reason", "patch"]
}
commit:
needs: [qualify, repair]
if: needs.repair.result == 'success' && needs.repair.outputs.repair_result != ''
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: read
steps:
- name: Check out current PR branch in a clean runner
uses: actions/checkout@v7
with:
ref: ${{ needs.qualify.outputs.head_ref }}
fetch-depth: 0
- name: Confirm exact head is unchanged
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
env:
EXPECTED_HEAD: ${{ needs.qualify.outputs.head_sha }}
PR_NUMBER: ${{ needs.qualify.outputs.pr_number }}
with:
script: |
const { owner, repo } = context.repo;
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: Number(process.env.PR_NUMBER),
});
if (pr.state !== 'open' || pr.head.sha !== process.env.EXPECTED_HEAD) {
core.setFailed('PR head changed before automatic repair commit.');
}
- name: Parse structured repair in the clean runner
id: parse
env:
REPAIR_RESULT: ${{ needs.repair.outputs.repair_result }}
run: |
python - <<'PY'
import json
import os
from pathlib import Path
result = json.loads(os.environ["REPAIR_RESULT"])
status = result.get("status")
reason = str(result.get("reason", ""))
patch = str(result.get("patch", ""))
apply = status == "fix"
if apply:
if not patch.startswith("diff --git "):
raise SystemExit("Structured repair did not contain a unified git patch")
if "\x00" in patch:
raise SystemExit("Structured repair contains a NUL byte")
encoded = patch.encode("utf-8")
if len(encoded) > 200_000:
raise SystemExit("Automatic repair patch exceeds 200 KB")
Path("/tmp/ross-autofix.patch").write_bytes(encoded)
elif patch.strip():
raise SystemExit("Non-fix structured result must have an empty patch")
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
output.write(f"apply={'true' if apply else 'false'}\n")
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
summary.write(f"## Automatic repair decision\n\n- Status: `{status}`\n- Reason: {reason}\n")
PY
- name: Apply and validate bounded patch
if: steps.parse.outputs.apply == 'true'
run: |
set -euo pipefail
git apply --check --whitespace=error-all /tmp/ross-autofix.patch
git apply --index --whitespace=error-all /tmp/ross-autofix.patch
mapfile -t changed < <(git diff --cached --name-only)
test "${#changed[@]}" -gt 0
test "${#changed[@]}" -le 8
test -z "$(git diff --cached --diff-filter=D --name-only)"
test -z "$(git diff --cached --diff-filter=RCTU --name-only)"
for path in "${changed[@]}"; do
case "$path" in
backend/src/*|backend/tests/*|frontend/src/*|website/src/*|website/tests/*|tests/*|scripts/*) ;;
*) echo "Unsafe automatic-repair path: $path" >&2; exit 1 ;;
esac
if [[ "$path" =~ (^|/)(auth|security|crypto|secret|permission|legal|privacy|governance|release|deploy|migration) ]]; then
echo "Protected automatic-repair path: $path" >&2
exit 1
fi
done
if git diff --cached --numstat | awk '$1 == "-" || $2 == "-" { found=1 } END { exit !found }'; then
echo "Binary automatic repairs are not permitted." >&2
exit 1
fi
total_lines="$(git diff --cached --numstat | awk '{ total += $1 + $2 } END { print total + 0 }')"
test "$total_lines" -le 800
if git diff --cached --summary | grep -Eq 'mode change|create mode 100755|create mode 120000|create mode 160000|delete mode'; then
echo "File-mode, executable, symlink, submodule, or deletion changes are not permitted." >&2
exit 1
fi
- name: Commit and push bounded repair
if: steps.parse.outputs.apply == 'true'
env:
HEAD_REF: ${{ needs.qualify.outputs.head_ref }}
FAILED_RUN: ${{ github.event.workflow_run.id }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -m "Auto-fix Baseline failure from run ${FAILED_RUN}"
git push origin "HEAD:${HEAD_REF}"