Skip to content

Commit ac98d98

Browse files
davidamaceyclaude
andcommitted
chore(ci): add Renovate auto-updates and weekly image audit
Sets up automated maintenance for all 59 Docker Compose services across the repository. Renovate Bot (.github/renovate.json): - Scans all docker-compose.yml files weekly for pinned image updates - Auto-merges minor/patch bumps (redis, valkey, minor infra updates) - Groups postgres updates into a single PR for coordinated migration - Requires manual review for major version bumps and infra images (traefik, caddy) where breaking changes are more likely - Schedule: every Monday, America/New_York timezone PR validation (.github/workflows/ci.yml): - Runs on every PR that touches a docker-compose.yml - Executes `docker compose config --quiet` on changed files only - Catches YAML errors and schema violations before merge - Also runs full validation on push to main Weekly image audit (.github/workflows/weekly-audit.yml): - Runs every Monday at 6 AM UTC (also triggerable manually) - Installs `crane` (google/go-containerregistry) for registry checks - Resolves ${VAR:-default} references to their defaults before checking - Uses `crane manifest` to verify each image exists without pulling - Opens a GitHub issue listing unavailable images when failures found - Auto-closes the issue when all images become healthy again To activate Renovate: install the free Renovate GitHub App at https://github.com/apps/renovate and grant it access to this repo. The renovate.json config activates immediately after installation. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1dd48c4 commit ac98d98

File tree

3 files changed

+307
-0
lines changed

3 files changed

+307
-0
lines changed

.github/renovate.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
3+
"extends": ["config:recommended"],
4+
5+
"timezone": "America/New_York",
6+
"schedule": ["on the first day of the week"],
7+
8+
"labels": ["dependencies", "docker"],
9+
"prConcurrentLimit": 10,
10+
"prHourlyLimit": 3,
11+
12+
"packageRules": [
13+
{
14+
"description": "Auto-merge minor and patch Docker image updates without review",
15+
"matchDatasources": ["docker"],
16+
"matchUpdateTypes": ["minor", "patch", "digest"],
17+
"automerge": true,
18+
"automergeType": "pr",
19+
"automergeStrategy": "squash"
20+
},
21+
{
22+
"description": "Major Docker version bumps require manual review — breaking changes possible",
23+
"matchDatasources": ["docker"],
24+
"matchUpdateTypes": ["major"],
25+
"automerge": false,
26+
"reviewers": ["davidamacey"]
27+
},
28+
{
29+
"description": "Group all postgres updates into one PR",
30+
"matchDatasources": ["docker"],
31+
"matchPackageNames": ["postgres", "docker.io/library/postgres", "pgvector/pgvector"],
32+
"groupName": "PostgreSQL",
33+
"automerge": false
34+
},
35+
{
36+
"description": "Group all redis/valkey updates into one PR",
37+
"matchDatasources": ["docker"],
38+
"matchPackageNames": ["redis", "docker.io/library/redis", "valkey/valkey"],
39+
"groupName": "Redis / Valkey",
40+
"automerge": true
41+
},
42+
{
43+
"description": "Pin infrastructure images (traefik, caddy) — higher blast radius",
44+
"matchDatasources": ["docker"],
45+
"matchPackageNames": ["traefik", "caddy"],
46+
"automerge": false,
47+
"reviewers": ["davidamacey"]
48+
}
49+
]
50+
}

.github/workflows/ci.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# ===========================================================================
2+
# CI — Validate Docker Compose files on every PR
3+
#
4+
# Runs `docker compose config --quiet` on every service whose compose file
5+
# was added or modified in the PR. Catches YAML errors, bad env variable
6+
# references, and schema violations before they reach main.
7+
# ===========================================================================
8+
9+
name: Validate compose files
10+
11+
on:
12+
pull_request:
13+
paths:
14+
- "services/**/docker-compose.yml"
15+
- ".github/workflows/ci.yml"
16+
push:
17+
branches: [main]
18+
paths:
19+
- "services/**/docker-compose.yml"
20+
21+
jobs:
22+
validate:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
27+
- name: Find changed or all compose files
28+
id: find
29+
run: |
30+
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
31+
# On PRs: only validate changed compose files
32+
FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD \
33+
| grep "docker-compose.yml" || true)
34+
else
35+
# On push to main: validate everything
36+
FILES=$(find services -name "docker-compose.yml" | sort)
37+
fi
38+
echo "files<<EOF" >> "$GITHUB_OUTPUT"
39+
echo "$FILES" >> "$GITHUB_OUTPUT"
40+
echo "EOF" >> "$GITHUB_OUTPUT"
41+
echo "Found $(echo "$FILES" | grep -c . || echo 0) file(s) to validate"
42+
43+
- name: Validate compose files
44+
if: steps.find.outputs.files != ''
45+
run: |
46+
FAILED=0
47+
while IFS= read -r file; do
48+
[[ -z "$file" ]] && continue
49+
dir="$(dirname "$file")"
50+
echo -n " Validating $file ... "
51+
# Use an empty env so missing vars default to blank (expected for templates)
52+
if docker compose -f "$file" config --quiet 2>/dev/null; then
53+
echo "OK"
54+
else
55+
echo "FAILED"
56+
docker compose -f "$file" config 2>&1 | grep -v "variable is not set" || true
57+
FAILED=$((FAILED + 1))
58+
fi
59+
done <<< "${{ steps.find.outputs.files }}"
60+
61+
if [[ $FAILED -gt 0 ]]; then
62+
echo ""
63+
echo "ERROR: $FAILED compose file(s) failed validation."
64+
exit 1
65+
fi
66+
echo ""
67+
echo "All compose files are valid."

.github/workflows/weekly-audit.yml

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# ===========================================================================
2+
# Weekly image audit — check all Docker images are still available
3+
#
4+
# Runs every Monday at 6 AM UTC. For each image referenced in any
5+
# docker-compose.yml, uses `crane manifest` (no pull required) to verify
6+
# the image exists on its registry. Creates a GitHub issue listing any
7+
# unavailable images so you can update the compose file before the next
8+
# deployment.
9+
#
10+
# Also handles ${VAR:-default} interpolation so env-var image references
11+
# (like ${IMMICH_VERSION:-release}) are resolved to their defaults before
12+
# checking.
13+
#
14+
# To run manually: Actions → Weekly image audit → Run workflow
15+
# ===========================================================================
16+
17+
name: Weekly image audit
18+
19+
on:
20+
schedule:
21+
- cron: "0 6 * * 1" # Every Monday at 6 AM UTC
22+
workflow_dispatch: # Allow manual trigger from Actions tab
23+
24+
permissions:
25+
issues: write
26+
contents: read
27+
28+
jobs:
29+
audit:
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@v4
33+
34+
- name: Install crane
35+
run: |
36+
VERSION=$(curl -fsSL https://api.github.com/repos/google/go-containerregistry/releases/latest \
37+
| grep '"tag_name"' | cut -d'"' -f4)
38+
curl -fsSL "https://github.com/google/go-containerregistry/releases/download/${VERSION}/go-containerregistry_Linux_x86_64.tar.gz" \
39+
| tar xz crane
40+
sudo mv crane /usr/local/bin/crane
41+
crane version
42+
43+
- name: Extract and resolve image references
44+
id: images
45+
run: |
46+
python3 - <<'PYEOF'
47+
import re, subprocess, sys
48+
from pathlib import Path
49+
50+
# Collect all image lines from compose files
51+
raw_images = set()
52+
for f in Path("services").rglob("docker-compose.yml"):
53+
for line in f.read_text().splitlines():
54+
m = re.match(r'^\s+image:\s+(.+)$', line)
55+
if m:
56+
raw_images.add(m.group(1).strip())
57+
58+
# Resolve ${VAR:-default} → default, skip ${VAR} (no default)
59+
resolved = set()
60+
for img in raw_images:
61+
img = re.sub(r'\$\{[^}]+:-([^}]+)\}', r'\1', img) # replace ${VAR:-default} with default
62+
if '${' in img:
63+
continue # still has unresolvable var — skip
64+
resolved.add(img)
65+
66+
# Write to file for next step
67+
with open("/tmp/images.txt", "w") as fh:
68+
fh.write("\n".join(sorted(resolved)))
69+
print(f"Found {len(resolved)} resolvable image references")
70+
PYEOF
71+
72+
- name: Check image availability
73+
id: check
74+
run: |
75+
FAILED=()
76+
OK=0
77+
SKIPPED=0
78+
79+
while IFS= read -r image; do
80+
[[ -z "$image" ]] && continue
81+
82+
# crane needs explicit registry for short names (docker.io)
83+
echo -n " Checking $image ... "
84+
if crane manifest "$image" &>/dev/null; then
85+
echo "OK"
86+
OK=$((OK + 1))
87+
else
88+
echo "NOT FOUND"
89+
FAILED+=("$image")
90+
fi
91+
done < /tmp/images.txt
92+
93+
echo ""
94+
echo "Results: $OK OK, ${#FAILED[@]} failed"
95+
96+
# Write failed list to output
97+
if [[ ${#FAILED[@]} -gt 0 ]]; then
98+
printf '%s\n' "${FAILED[@]}" > /tmp/failed_images.txt
99+
echo "has_failures=true" >> "$GITHUB_OUTPUT"
100+
else
101+
echo "has_failures=false" >> "$GITHUB_OUTPUT"
102+
fi
103+
104+
- name: Close any existing audit issue (images all healthy)
105+
if: steps.check.outputs.has_failures == 'false'
106+
uses: actions/github-script@v7
107+
with:
108+
script: |
109+
const issues = await github.rest.issues.listForRepo({
110+
owner: context.repo.owner,
111+
repo: context.repo.repo,
112+
labels: 'broken-image',
113+
state: 'open'
114+
});
115+
for (const issue of issues.data) {
116+
await github.rest.issues.update({
117+
owner: context.repo.owner,
118+
repo: context.repo.repo,
119+
issue_number: issue.number,
120+
state: 'closed',
121+
body: issue.body + '\n\n---\n_Automatically closed — all images are now available._'
122+
});
123+
console.log(`Closed issue #${issue.number}`);
124+
}
125+
console.log('All images healthy — no issues to open.');
126+
127+
- name: Create or update issue for unavailable images
128+
if: steps.check.outputs.has_failures == 'true'
129+
uses: actions/github-script@v7
130+
with:
131+
script: |
132+
const fs = require('fs');
133+
const failed = fs.readFileSync('/tmp/failed_images.txt', 'utf8').trim().split('\n');
134+
const date = new Date().toISOString().split('T')[0];
135+
136+
const body = [
137+
`## Unavailable Docker images — ${date}`,
138+
'',
139+
'The weekly image audit found the following images that could not be resolved.',
140+
'These images may have been renamed, deleted, or moved to a private registry.',
141+
'Update the affected `docker-compose.yml` files before next deployment.',
142+
'',
143+
'| Image | Action needed |',
144+
'|-------|---------------|',
145+
...failed.map(img => `| \`${img}\` | Update or replace image reference |`),
146+
'',
147+
'**To re-run this audit:** Actions → Weekly image audit → Run workflow',
148+
'',
149+
`_Audit run: ${new Date().toISOString()}_`
150+
].join('\n');
151+
152+
// Check for existing open audit issue
153+
const existing = await github.rest.issues.listForRepo({
154+
owner: context.repo.owner,
155+
repo: context.repo.repo,
156+
labels: 'broken-image',
157+
state: 'open'
158+
});
159+
160+
if (existing.data.length > 0) {
161+
// Update existing issue
162+
await github.rest.issues.update({
163+
owner: context.repo.owner,
164+
repo: context.repo.repo,
165+
issue_number: existing.data[0].number,
166+
body: body
167+
});
168+
console.log(`Updated issue #${existing.data[0].number}`);
169+
} else {
170+
// Ensure label exists
171+
try {
172+
await github.rest.issues.createLabel({
173+
owner: context.repo.owner,
174+
repo: context.repo.repo,
175+
name: 'broken-image',
176+
color: 'e11d48',
177+
description: 'Docker image no longer available in registry'
178+
});
179+
} catch (e) { /* label may already exist */ }
180+
181+
// Create new issue
182+
const issue = await github.rest.issues.create({
183+
owner: context.repo.owner,
184+
repo: context.repo.repo,
185+
title: `[Image Audit] ${failed.length} unavailable image(s) — ${date}`,
186+
body: body,
187+
labels: ['broken-image']
188+
});
189+
console.log(`Created issue #${issue.data.number}`);
190+
}

0 commit comments

Comments
 (0)