diff --git a/.github/workflows/baked-tools-check.yml b/.github/workflows/baked-tools-check.yml index 258e798..139a741 100644 --- a/.github/workflows/baked-tools-check.yml +++ b/.github/workflows/baked-tools-check.yml @@ -28,6 +28,10 @@ jobs: name: Validate baked tools in image runs-on: ubuntu-latest timeout-minutes: 20 + # #323: lockstep-heal needs contents:write to push the sync commit to the PR head + permissions: + contents: write + checks: read steps: - name: Checkout builder repository @@ -37,6 +41,10 @@ jobs: fetch-depth: 1 - name: Manifest <-> Dockerfile lockstep gate + id: gate + # #323: let the heal step run on mismatch; a hard-fail step would stop the job here. + # The 'Fail if still red' step below re-asserts the red when healing is impossible. + continue-on-error: true run: | python3 - << 'GATE' import json, re, sys @@ -81,6 +89,100 @@ jobs: print('Lockstep OK: all Dockerfile pins match baked-tools.json') GATE + # #323 lockstep-heal: when the ONLY failures are 'Dockerfile pin X != baked-tools.json Y' + # (Dockerfile leads — dependabot FROM bumps, manual @getpaseo bumps) and the PR did + # NOT hand-edit baked-tools.json, sync baked-tools.json to the Dockerfile pins and + # push to the PR head. This run then fails intentionally ('HEALED — rerun verifies'); + # the pushed commit re-triggers the checks and auto-merge proceeds on the new SHA. + # Never heals the reverse direction (baked-tools.json leading) — that needs a human. + - name: Checkout PR head (for heal) + id: healsrc + if: steps.gate.outcome == 'failure' + uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.ref }} + path: healsrc + fetch-depth: 1 + + - name: Heal lockstep drift (Dockerfile pin leads) + id: heal + if: steps.gate.outcome == 'failure' + run: | + set -euo pipefail + if [ "${{ github.event_name }}" != "pull_request" ] \ + || [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then + echo "not healable (fork PR or non-PR context) — leaving red for a human" + exit 0 + fi + HEAD_BRANCH="${{ github.event.pull_request.head.ref }}" + if gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \ + --paginate --jq '.[].filename' 2>/dev/null | grep -qx 'managed-tools/baked-tools.json'; then + echo "PR touches baked-tools.json — heal disabled (human edit in flight)" + exit 0 + fi + cd healsrc + set +e + python3 - <<'HEAL' + import json, re, sys + df = open('Dockerfile.dockerfile').read() + path = 'managed-tools/baked-tools.json' + data = json.load(open(path)) + manifest = {t['name']: t for t in data['tools']} + pins = { + 'docker': (r'FROM docker:([0-9][\w.]*)-dind', None), + 'dockerd': (r'FROM docker:([0-9][\w.]*)-dind', None), + 'paseo': (r'@getpaseo/cli@([0-9][\w.]*)', r'@getpaseo/server@([0-9][\w.]*)'), + 'paseo-relay': (r'ARG PASEO_RELAY_VERSION=([0-9][\w.]*)', None), + 'code-server': (r'ARG CODE_SERVER_VERSION=([0-9][\w.]*)', None), + } + mismatch = re.compile(r'^(\S+): (?:secondary )?Dockerfile pin ([0-9][\w.]*) != baked-tools\.json ([0-9][\w.]*)$') + healed = [] + only_mismatches = True + # recompute failures exactly like the gate, but capture them for classification + for tool, (rx, rx2) in pins.items(): + if tool not in manifest: + only_mismatches = False; continue + want = manifest[tool]['currentVersion'] + got = re.search(rx, df) + if not got: + only_mismatches = False; continue + if got.group(1) != want: + healed.append((tool, got.group(1), want)) + if rx2: + got2 = re.search(rx2, df) + if got2 and got2.group(1) != want: + pass # same tool, already captured above + if not only_mismatches or not healed: + print('no healable drift (non-mismatch failures or clean) — leaving red') + sys.exit(1) + for tool, new, old in healed: + manifest[tool]['currentVersion'] = new + print(f'healed {tool}: {old} -> {new} (Dockerfile leads)') + json.dump(data, open(path, 'w'), indent=2) + open(path, 'a').write('\n') + sys.exit(42) + HEAL + rc=$? + set -e + if [ "$rc" -ne 42 ]; then + echo "heal: nothing pushed (rc=$rc)" + exit 0 + fi + git config user.name "lockstep-heal[bot]" + git config user.email "action@github.com" + git add managed-tools/baked-tools.json + git commit -q -m "chore(baked-tools): lockstep heal — sync baked-tools.json to Dockerfile pins (automated, #323)" + git push origin "HEAD:refs/heads/$HEAD_BRANCH" + echo "pushed=true" >> "$GITHUB_OUTPUT" + echo "HEALED — sync commit pushed; this run fails intentionally so nothing merges on the pre-heal SHA." + exit 1 + + - name: Fail if still red and not healed + if: steps.gate.outcome == 'failure' && steps.heal.outputs.pushed != 'true' + run: | + echo "Lockstep gate failed and healing did not apply — see gate output above." + exit 1 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4