Skip to content

Commit 5f0b497

Browse files
Merge remote-tracking branch 'origin/main' into feat-handshake-node-helm-chart
2 parents b11c4b5 + 8b13cc4 commit 5f0b497

4 files changed

Lines changed: 520 additions & 0 deletions

File tree

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
name: check-image-versions
2+
3+
# Automatically checks whether any upstream container image has received a new
4+
# release, and opens a pull-request to update the matching Helm chart when one
5+
# is found. Container-registry tags are the sole source of truth: repository
6+
# release tags are never consulted, because they can diverge from the tags that
7+
# are actually published to Docker Hub or GHCR.
8+
9+
on:
10+
schedule:
11+
# Run every Sunday at midnight UTC
12+
- cron: '0 0 * * 0'
13+
workflow_dispatch:
14+
15+
jobs:
16+
check-image-versions:
17+
name: Check for new upstream image versions
18+
runs-on: ubuntu-latest
19+
permissions:
20+
contents: write
21+
pull-requests: write
22+
packages: read
23+
steps:
24+
# Use AUTO_PR_TOKEN (a PAT or GitHub-App token) when available so that
25+
# PRs opened by this workflow trigger the repository's pull_request
26+
# workflows (chart lint, conventional-commit checks, etc.). Falls back
27+
# to GITHUB_TOKEN if the secret is not configured, but PRs created with
28+
# GITHUB_TOKEN will NOT trigger downstream workflows.
29+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
30+
with:
31+
fetch-depth: 0
32+
token: ${{ secrets.AUTO_PR_TOKEN || secrets.GITHUB_TOKEN }}
33+
34+
- name: Check and create PRs for new versions
35+
env:
36+
GH_TOKEN: ${{ secrets.AUTO_PR_TOKEN || secrets.GITHUB_TOKEN }}
37+
run: |
38+
set -euo pipefail
39+
40+
# ── Git identity ───────────────────────────────────────────────────
41+
git config user.name "github-actions[bot]"
42+
git config user.email "github-actions[bot]@users.noreply.github.com"
43+
44+
UPSTREAM_CONFIG="scripts/upstream-versions.json"
45+
46+
# Iterate over every chart defined in the config
47+
CHARTS=$(jq -r 'keys[]' "$UPSTREAM_CONFIG")
48+
49+
for CHART in $CHARTS; do
50+
echo "=========================================="
51+
echo "Checking chart: $CHART"
52+
echo "=========================================="
53+
54+
# ── Read config fields ─────────────────────────────────────────
55+
REGISTRY=$(jq -r --arg c "$CHART" '.[$c].registry' "$UPSTREAM_CONFIG")
56+
IMAGE=$(jq -r --arg c "$CHART" '.[$c].image' "$UPSTREAM_CONFIG")
57+
TAG_REGEX=$(jq -r --arg c "$CHART" '.[$c].tag_regex' "$UPSTREAM_CONFIG")
58+
TAG_PATTERN=$(jq -r --arg c "$CHART" '.[$c].tag_pattern // ""' "$UPSTREAM_CONFIG")
59+
APP_PREFIX=$(jq -r --arg c "$CHART" '.[$c].app_version_prefix // ""' "$UPSTREAM_CONFIG")
60+
61+
echo " Registry : $REGISTRY"
62+
echo " Image : $IMAGE"
63+
echo " Tag regex : $TAG_REGEX"
64+
65+
# ── Fetch candidate tags from the container registry ───────────
66+
case "$REGISTRY" in
67+
ghcr)
68+
# GHCR container package. The path segment before the last '/'
69+
# is the org; the remainder is the package name (which may
70+
# contain '/' for nested packages, though none of ours do).
71+
GHCR_ORG="${IMAGE%%/*}"
72+
GHCR_PKG="${IMAGE#*/}"
73+
RAW_TAGS=$(gh api "/orgs/${GHCR_ORG}/packages/container/${GHCR_PKG}/versions" \
74+
--paginate \
75+
--jq '.[].metadata.container.tags[]' 2>/dev/null || echo "")
76+
;;
77+
dockerhub)
78+
# Docker Hub v2 tags API. Paginate through the `next` link so
79+
# we do not miss releases that are older than the first 100.
80+
RAW_TAGS=""
81+
URL="https://hub.docker.com/v2/repositories/${IMAGE}/tags?page_size=100"
82+
# Bound the pagination loop so a broken `next` link cannot spin
83+
# forever.
84+
for _ in $(seq 1 20); do
85+
[[ -z "$URL" || "$URL" == "null" ]] && break
86+
RESP=$(curl -sS "$URL" || echo '{}')
87+
PAGE=$(echo "$RESP" | jq -r '.results[]?.name' 2>/dev/null || echo "")
88+
if [[ -n "$PAGE" ]]; then
89+
RAW_TAGS="${RAW_TAGS}${PAGE}"$'\n'
90+
fi
91+
URL=$(echo "$RESP" | jq -r '.next // empty' 2>/dev/null || echo "")
92+
done
93+
;;
94+
*)
95+
echo " WARNING: unsupported registry '$REGISTRY' for $CHART, skipping"
96+
continue
97+
;;
98+
esac
99+
100+
if [[ -z "${RAW_TAGS// /}" ]]; then
101+
echo " WARNING: no tags returned from $REGISTRY for $IMAGE, skipping"
102+
continue
103+
fi
104+
105+
NEW_VALUES_TAG=$(printf '%s\n' "$RAW_TAGS" \
106+
| grep -E "$TAG_REGEX" \
107+
| sort -V \
108+
| tail -1 || echo "")
109+
110+
if [[ -z "$NEW_VALUES_TAG" ]]; then
111+
echo " WARNING: no tag on $IMAGE matched $TAG_REGEX, skipping"
112+
continue
113+
fi
114+
echo " Latest registry tag: $NEW_VALUES_TAG"
115+
116+
# ── Derive the appVersion string ───────────────────────────────
117+
# Strip the shared image-tag prefix (e.g. "v") to obtain the
118+
# numeric core, then prepend the desired appVersion prefix.
119+
CORE_VERSION="${NEW_VALUES_TAG#$TAG_PATTERN}"
120+
NEW_APP_VERSION="${APP_PREFIX}${CORE_VERSION}"
121+
122+
# ── Read current image tag from values.yaml ────────────────────
123+
VALUES_FILE="charts/${CHART}/values.yaml"
124+
if [[ ! -f "$VALUES_FILE" ]]; then
125+
echo " WARNING: values.yaml not found for $CHART, skipping"
126+
continue
127+
fi
128+
129+
CURRENT_TAG=$(grep '^ tag:' "$VALUES_FILE" | head -1 | awk '{print $2}' | tr -d '"' | tr -d "'")
130+
echo " Current values.yaml tag: $CURRENT_TAG"
131+
132+
# ── Skip if already up to date ─────────────────────────────────
133+
if [[ "$CURRENT_TAG" == "$NEW_VALUES_TAG" ]]; then
134+
echo " Already up to date"
135+
continue
136+
fi
137+
138+
# ── Downgrade guard ────────────────────────────────────────────
139+
# sort -V handles semver-ish ordering including trailing packaging
140+
# revisions ("11.0.1-1" > "11.0.1") and 4-segment tags. Skip if
141+
# the newest registry tag is not strictly greater than the current
142+
# values.yaml tag.
143+
HIGHEST=$(printf '%s\n%s\n' "$CURRENT_TAG" "$NEW_VALUES_TAG" | sort -V | tail -1)
144+
if [[ "$HIGHEST" != "$NEW_VALUES_TAG" ]]; then
145+
echo " Resolved tag $NEW_VALUES_TAG is not newer than current $CURRENT_TAG, skipping"
146+
continue
147+
fi
148+
149+
echo " Update available: $CURRENT_TAG -> $NEW_VALUES_TAG"
150+
151+
BRANCH_NAME="chore/${CHART}-${CORE_VERSION}"
152+
153+
# Skip if an open PR already exists for this update.
154+
EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --state open --json number --jq '.[0]?.number // empty')
155+
if [[ -n "$EXISTING_PR" ]]; then
156+
echo " PR #$EXISTING_PR already open for $BRANCH_NAME, skipping"
157+
continue
158+
fi
159+
160+
# ── Apply the update ───────────────────────────────────────────
161+
echo " Applying update..."
162+
./scripts/update-chart-version.sh "$CHART" "$NEW_VALUES_TAG" "$NEW_APP_VERSION"
163+
164+
# ── Commit and push ────────────────────────────────────────────
165+
git checkout -b "$BRANCH_NAME"
166+
git add "charts/${CHART}/values.yaml" "charts/${CHART}/Chart.yaml"
167+
git commit -m "chore: update ${CHART} image to ${NEW_VALUES_TAG}"
168+
169+
# Remove a stale remote branch left over from a previous failed run
170+
# (only safe to do when no open PR was found above).
171+
if git ls-remote --exit-code --heads origin "$BRANCH_NAME" >/dev/null 2>&1; then
172+
echo " Stale remote branch found with no open PR, deleting"
173+
git push origin --delete "$BRANCH_NAME"
174+
fi
175+
176+
git push -u origin "$BRANCH_NAME"
177+
178+
# ── Open PR ────────────────────────────────────────────────────
179+
# printf is used instead of a multi-line string so that each body
180+
# line starts at column 0, preventing GitHub's CommonMark renderer
181+
# from treating the indented content as a code block.
182+
case "$REGISTRY" in
183+
ghcr) IMAGE_REF="ghcr.io/${IMAGE}:${NEW_VALUES_TAG}" ;;
184+
dockerhub) IMAGE_REF="docker.io/${IMAGE}:${NEW_VALUES_TAG}" ;;
185+
*) IMAGE_REF="${IMAGE}:${NEW_VALUES_TAG}" ;;
186+
esac
187+
188+
PR_BODY=$(printf '%s\n' \
189+
"## Summary" \
190+
"- Updates \`${CHART}\` image tag from \`${CURRENT_TAG}\` to \`${NEW_VALUES_TAG}\`" \
191+
"- Source: \`${IMAGE_REF}\`" \
192+
"" \
193+
"## Changes" \
194+
"- \`charts/${CHART}/values.yaml\`: \`image.tag\` → \`${NEW_VALUES_TAG}\`" \
195+
"- \`charts/${CHART}/Chart.yaml\`: \`appVersion\` → \`${NEW_APP_VERSION}\`, chart \`version\` patch bumped" \
196+
"" \
197+
"## Test plan" \
198+
"- [ ] CI lint passes" \
199+
"- [ ] Manual smoke-test if needed" \
200+
"" \
201+
"🤖 Generated automatically by check-image-versions workflow")
202+
203+
PR_CREATED=false
204+
if PR_OUTPUT=$(gh pr create \
205+
--title "chore: update ${CHART} image to ${NEW_VALUES_TAG}" \
206+
--body "$PR_BODY" \
207+
--base main \
208+
--head "$BRANCH_NAME" 2>&1); then
209+
echo "$PR_OUTPUT"
210+
PR_CREATED=true
211+
else
212+
if echo "$PR_OUTPUT" | grep -qi 'already exists'; then
213+
echo " PR already exists for $BRANCH_NAME, skipping create"
214+
else
215+
echo "$PR_OUTPUT"
216+
exit 1
217+
fi
218+
fi
219+
220+
# Return to main for the next chart
221+
git checkout main
222+
223+
if [[ "$PR_CREATED" == "true" ]]; then
224+
echo " PR created successfully!"
225+
fi
226+
done
227+
228+
echo ""
229+
echo "=========================================="
230+
echo "Image version check complete"
231+
echo "=========================================="

README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,95 @@ helm pull oci://ghcr.io/blinklabs-io/helm-charts/charts/dingo \
8282
--version 0.0.6 \
8383
--untar
8484
```
85+
86+
## Automatic image-version updates
87+
88+
Charts are kept in sync with their upstream container images via the
89+
[`check-image-versions`](.github/workflows/check-image-versions.yml) workflow.
90+
Container-registry tags (Docker Hub or GHCR) are the sole source of truth:
91+
GitHub repository release tags are **not** consulted, because release tags can
92+
diverge from tags that are actually published to the registry (packaging
93+
revisions, mirror re-tags, missing builds, etc.).
94+
95+
### How it works
96+
97+
1. The workflow runs **every Sunday at 00:00 UTC** (and can be triggered
98+
manually via `workflow_dispatch`).
99+
2. It reads [`scripts/upstream-versions.json`](scripts/upstream-versions.json),
100+
which maps each manageable chart to the container image (GHCR or Docker
101+
Hub) that should drive updates.
102+
3. For every chart it lists tags from the configured registry, keeps those
103+
that fully match `tag_regex`, sorts them with `sort -V`, and picks the
104+
highest.
105+
4. The resolved tag is compared against `image.tag` in
106+
`charts/<name>/values.yaml`. The update is applied only when the resolved
107+
tag is **strictly greater** than the current one (downgrades and equals
108+
are rejected).
109+
5. When an update is applied,
110+
[`scripts/update-chart-version.sh`](scripts/update-chart-version.sh):
111+
- Updates `image.tag` in `values.yaml` (double-quoted to preserve string
112+
type for YAML).
113+
- Updates `appVersion` in `Chart.yaml` (double-quoted for the same reason).
114+
- Bumps the patch segment of `version` in `Chart.yaml`.
115+
6. The changes are committed to a branch named
116+
`chore/<chart-name>-<core-version>` and a pull-request is opened.
117+
118+
### Adding a new chart to auto-updates
119+
120+
Edit [`scripts/upstream-versions.json`](scripts/upstream-versions.json) and
121+
add an entry following the schema below:
122+
123+
```jsonc
124+
"my-chart": {
125+
// Container registry that publishes the image. Only "ghcr" and "dockerhub"
126+
// are supported.
127+
"registry": "ghcr",
128+
129+
// Path segment after the registry host: "<org>/<name>".
130+
// E.g. "blinklabs-io/adder" (GHCR) or "cardanosolutions/kupo" (Docker Hub).
131+
"image": "org/name",
132+
133+
// POSIX extended regex that a candidate tag must fully match. Use
134+
// this to exclude floating tags like "latest", release-candidate tags,
135+
// and anything with an unexpected shape.
136+
// Simple semver : "^[0-9]+\\.[0-9]+\\.[0-9]+$"
137+
// Semver with v-prefix : "^v[0-9]+\\.[0-9]+\\.[0-9]+$"
138+
// Semver + packaging rev : "^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9]+)?$"
139+
// Four-segment : "^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$"
140+
"tag_regex": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
141+
142+
// Leading substring that is part of the image tag itself but should be
143+
// stripped before applying the appVersion prefix below. Use "v" when the
144+
// image tag looks like "v1.2.3" and appVersion needs a plain "1.2.3".
145+
"tag_pattern": "",
146+
147+
// Prefix prepended to the (post-strip) version when writing appVersion in
148+
// Chart.yaml. Use "v" to produce "v1.2.3", "" for a bare "1.2.3".
149+
"app_version_prefix": ""
150+
}
151+
```
152+
153+
The value written to `values.yaml`'s `image.tag` is always the raw registry
154+
tag (verbatim, so that `image.repository:image.tag` is exactly what will be
155+
pulled). Only `appVersion` is transformed via `tag_pattern` + `app_version_prefix`.
156+
157+
### CI on generated pull requests
158+
159+
Pull requests opened using the default `GITHUB_TOKEN` **do not trigger** the
160+
repository's `pull_request` workflows (chart lint, conventional-commit checks,
161+
etc.). To ensure automated update PRs are CI-checked before merge, configure
162+
a repository secret named `AUTO_PR_TOKEN` containing a PAT or GitHub App
163+
installation token with `contents: write` and `pull-requests: write`. The
164+
workflow uses `AUTO_PR_TOKEN` when present and falls back to `GITHUB_TOKEN`
165+
otherwise.
166+
167+
### Manually triggering
168+
169+
Navigate to **Actions → check-image-versions → Run workflow** in the GitHub UI.
170+
171+
### Scripts
172+
173+
| Script | Purpose |
174+
|--------|---------|
175+
| [`scripts/upstream-versions.json`](scripts/upstream-versions.json) | Config mapping each chart to its upstream source |
176+
| [`scripts/update-chart-version.sh`](scripts/update-chart-version.sh) | Updates `values.yaml`, `Chart.yaml` appVersion, and bumps chart version patch |

0 commit comments

Comments
 (0)