Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,17 @@
},
{
// Group the four tools into one PR and auto-merge once CI passes.
// platformAutomerge:false => Renovate performs the merge itself only
// after it observes the branch tests are green, so CI gating needs no
// branch-protection rule. (Optional hardening: enable branch protection
// requiring the CI checks and set platformAutomerge:true for native
// GitHub auto-merge.)
//
// Relies on Renovate's default platformAutomerge:true — GitHub's native
// auto-merge merges on green with no second Renovate run. The previous
// explicit platformAutomerge:false is what broke this: only a Renovate
// run could merge, and every run found a newer claude-code, force-pushed
// the branch and ended before CI finished (#121 sat green for 3.5 weeks).
//
// Requires: repo setting "Allow auto-merge", and a ruleset on master
// requiring the "CI complete" check (.github/workflows/ci.yml). Without
// that required check nothing blocks the PR, GitHub never offers native
// auto-merge, and Renovate silently falls back to the broken path.
matchPackageNames: [
'rtk-ai/rtk',
'umputun/ralphex',
Expand All @@ -42,7 +48,18 @@
],
groupName: 'devcontainer agent tools',
automerge: true,
platformAutomerge: false,

// These bumps merge unreviewed and publish straight to ghcr.io, so let a
// release soak before adopting it. internalChecksFilter defaults to
// 'strict', so a too-young version is simply not offered yet — the group
// PR carries whichever tools are currently eligible.
minimumReleaseAge: '3 days',
},
{
// ...except claude-code, which is tracked at latest deliberately.
// Later packageRules win, so this clears the soak period above.
matchPackageNames: ['@anthropic-ai/claude-code'],
minimumReleaseAge: null,
},
],
}
58 changes: 53 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ name: CI
on:
pull_request:
branches: [master]
paths:
- '**/Dockerfile'
- '**/*.sh'
- '.github/workflows/**'
- '.hadolint.yaml'
# Deliberately unfiltered: "CI complete" below is the required status check
# for master, and a required check that never runs blocks a PR forever -- a
# docs-only PR would deadlock. Image builds are still skipped per-image via
# detect-changes; the always-on jobs are lint-only and take seconds.

concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
Expand Down Expand Up @@ -194,3 +193,52 @@ jobs:
IMAGE_TAG: ${{ matrix.image }}:test
VERIFY_CMD: ${{ matrix.verify }}
run: docker run --rm --entrypoint sh "$IMAGE_TAG" -c "$VERIFY_CMD"

# -- Single required status check for branch protection ------------------
# Aggregates every job above so master needs exactly one required check.
# Passes when each job succeeded or was legitimately skipped (path-filtered
# builds), and fails if any job failed or was cancelled.
#
# Any job added to this workflow must also be added to `needs` below: this is
# the only required check on master and Renovate auto-merges on it, so an
# unwatched job would go red while the gate stayed green. The first step
# enforces that rather than trusting anyone to remember.
ci-complete:
name: CI complete
# always(), not !cancelled(): GitHub counts a *skipped* required check as
# passing, so !cancelled() would let a cancelled run report a green gate.
if: always()
needs: [lint-dockerfiles, lint-workflows, detect-changes, build-and-verify]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Verify every job is watched
env:
WATCHED: ${{ toJSON(needs) }}
run: |
missing=""
for job in $(yq '.jobs | keys | .[]' .github/workflows/ci.yml); do
[ "$job" = "ci-complete" ] && continue
jq -e --arg j "$job" 'has($j)' <<< "$WATCHED" > /dev/null || missing="$missing $job"
done
if [ -n "$missing" ]; then
echo "::error::Jobs missing from ci-complete.needs:$missing"
exit 1
fi

- name: Check results of all CI jobs
env:
WATCHED: ${{ toJSON(needs) }}
run: |
jq -r 'to_entries[] | "\(.key): \(.value.result)"' <<< "$WATCHED"
bad=$(jq -r '
to_entries[]
| select(.value.result != "success" and .value.result != "skipped")
| "\(.key) (\(.value.result))"' <<< "$WATCHED")
if [ -n "$bad" ]; then
echo "::error::CI jobs did not pass: $(echo "$bad" | tr '\n' ' ')"
exit 1
fi
echo "All CI jobs passed or were skipped."