Skip to content

Commit f3161fd

Browse files
feat: add automated Helm chart updates
Signed-off-by: Tripura Repalle <tripura@blinklabs.io>
1 parent 5b02f8f commit f3161fd

4 files changed

Lines changed: 479 additions & 0 deletions

File tree

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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. Mirrors the check-versions workflow used in
6+
# blinklabs-io/cardano-up-packages.
7+
8+
on:
9+
schedule:
10+
# Run every Sunday at midnight UTC
11+
- cron: '0 0 * * 0'
12+
workflow_dispatch:
13+
14+
jobs:
15+
check-image-versions:
16+
name: Check for new upstream image versions
17+
runs-on: ubuntu-latest
18+
permissions:
19+
contents: write
20+
pull-requests: write
21+
steps:
22+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 https://github.com/actions/checkout/releases/tag/v7.0.0
23+
with:
24+
fetch-depth: 0
25+
26+
- name: Check and create PRs for new versions
27+
env:
28+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29+
run: |
30+
set -euo pipefail
31+
32+
# ── Git identity ───────────────────────────────────────────────────
33+
git config user.name "github-actions[bot]"
34+
git config user.email "github-actions[bot]@users.noreply.github.com"
35+
36+
UPSTREAM_CONFIG="scripts/upstream-versions.json"
37+
38+
# Iterate over every chart defined in the config
39+
CHARTS=$(jq -r 'keys[]' "$UPSTREAM_CONFIG")
40+
41+
for CHART in $CHARTS; do
42+
echo "=========================================="
43+
echo "Checking chart: $CHART"
44+
echo "=========================================="
45+
46+
# ── Read config fields ─────────────────────────────────────────
47+
REPO=$(jq -r --arg c "$CHART" '.[$c].repo' "$UPSTREAM_CONFIG")
48+
TAG_PATTERN=$(jq -r --arg c "$CHART" '.[$c].tag_pattern' "$UPSTREAM_CONFIG")
49+
VALUES_PREFIX=$(jq -r --arg c "$CHART" '.[$c].values_tag_prefix' "$UPSTREAM_CONFIG")
50+
APP_PREFIX=$(jq -r --arg c "$CHART" '.[$c].app_version_prefix' "$UPSTREAM_CONFIG")
51+
DOCKER_IMAGE=$(jq -r --arg c "$CHART" '.[$c].docker_image // ""' "$UPSTREAM_CONFIG")
52+
GHCR_IMAGE=$(jq -r --arg c "$CHART" '.[$c].ghcr_image // ""' "$UPSTREAM_CONFIG")
53+
54+
echo " Upstream repo : $REPO"
55+
echo " Tag pattern : '${TAG_PATTERN}'"
56+
echo " values_tag_prefix : '${VALUES_PREFIX}'"
57+
echo " app_version_prefix: '${APP_PREFIX}'"
58+
59+
# ── Determine upstream version ─────────────────────────────────
60+
61+
LATEST_TAG=""
62+
UPSTREAM_VERSION=""
63+
64+
if [[ -n "$GHCR_IMAGE" ]]; then
65+
# Use GHCR as source of truth (e.g. hsd – no predictable GH release tag)
66+
echo " Using GHCR image as version source: $GHCR_IMAGE"
67+
GHCR_ORG="${GHCR_IMAGE%%/*}"
68+
GHCR_PKG="${GHCR_IMAGE#*/}"
69+
70+
UPSTREAM_VERSION=$(gh api "/orgs/${GHCR_ORG}/packages/container/${GHCR_PKG}/versions" \
71+
--paginate \
72+
--jq '.[].metadata.container.tags[]' 2>/dev/null \
73+
| grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \
74+
| sort -V | tail -1 || echo "")
75+
76+
if [[ -z "$UPSTREAM_VERSION" ]]; then
77+
echo " WARNING: Could not fetch versions from GHCR for $GHCR_IMAGE, skipping"
78+
continue
79+
fi
80+
echo " Latest GHCR version: $UPSTREAM_VERSION"
81+
82+
else
83+
# Use GitHub releases
84+
LATEST_TAG=$(gh api "repos/${REPO}/releases/latest" --jq '.tag_name' 2>/dev/null || echo "")
85+
86+
if [[ -z "$LATEST_TAG" ]]; then
87+
echo " WARNING: Could not fetch latest release for $REPO, skipping"
88+
continue
89+
fi
90+
echo " Latest upstream tag: $LATEST_TAG"
91+
92+
# Strip the tag_pattern prefix to derive the plain version number
93+
if [[ -n "$TAG_PATTERN" ]]; then
94+
UPSTREAM_VERSION="${LATEST_TAG#$TAG_PATTERN}"
95+
else
96+
UPSTREAM_VERSION="$LATEST_TAG"
97+
fi
98+
99+
# If docker_image is configured, look up the Docker Hub tag that
100+
# corresponds to this release (handles short GH tags like v2.11 vs
101+
# full image tags like v2.11.0, matching cardano-up-packages logic).
102+
if [[ -n "$DOCKER_IMAGE" ]]; then
103+
echo " Looking up Docker Hub tags for: $DOCKER_IMAGE"
104+
DOCKER_TAG=$(curl -s \
105+
"https://hub.docker.com/v2/repositories/${DOCKER_IMAGE}/tags?page_size=100" \
106+
| jq -r --arg prefix "${TAG_PATTERN}${UPSTREAM_VERSION}" \
107+
'.results[].name | select(startswith($prefix))' \
108+
| sort -V | tail -1 || echo "")
109+
110+
if [[ -n "$DOCKER_TAG" ]]; then
111+
UPSTREAM_VERSION="${DOCKER_TAG#$TAG_PATTERN}"
112+
echo " Found Docker Hub tag: $DOCKER_TAG -> version $UPSTREAM_VERSION"
113+
else
114+
echo " WARNING: No matching Docker Hub tag for ${TAG_PATTERN}${UPSTREAM_VERSION}, using GitHub tag"
115+
fi
116+
fi
117+
fi
118+
119+
echo " Upstream version: $UPSTREAM_VERSION"
120+
121+
# ── Derive the values-tag and app-version strings ──────────────
122+
NEW_VALUES_TAG="${VALUES_PREFIX}${UPSTREAM_VERSION}"
123+
NEW_APP_VERSION="${APP_PREFIX}${UPSTREAM_VERSION}"
124+
125+
# ── Read current image tag from values.yaml ────────────────────
126+
VALUES_FILE="charts/${CHART}/values.yaml"
127+
if [[ ! -f "$VALUES_FILE" ]]; then
128+
echo " WARNING: values.yaml not found for $CHART, skipping"
129+
continue
130+
fi
131+
132+
CURRENT_TAG=$(grep '^ tag:' "$VALUES_FILE" | head -1 | awk '{print $2}' | tr -d '"')
133+
echo " Current values.yaml tag: $CURRENT_TAG"
134+
135+
# ── Skip if already up to date ─────────────────────────────────
136+
if [[ "$CURRENT_TAG" == "$NEW_VALUES_TAG" ]]; then
137+
echo " Already up to date"
138+
continue
139+
fi
140+
141+
echo " Update available: $CURRENT_TAG -> $NEW_VALUES_TAG"
142+
143+
BRANCH_NAME="chore/${CHART}-${UPSTREAM_VERSION}"
144+
145+
# Skip if an open PR already exists for this update.
146+
EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --state open --json number --jq '.[0].number')
147+
if [[ -n "$EXISTING_PR" ]]; then
148+
echo " PR #$EXISTING_PR already open for $BRANCH_NAME, skipping"
149+
continue
150+
fi
151+
152+
# ── Apply the update ───────────────────────────────────────────
153+
echo " Applying update..."
154+
./scripts/update-chart-version.sh "$CHART" "$NEW_VALUES_TAG" "$NEW_APP_VERSION"
155+
156+
# ── Commit and push ────────────────────────────────────────────
157+
git checkout -b "$BRANCH_NAME"
158+
git add "charts/${CHART}/values.yaml" "charts/${CHART}/Chart.yaml"
159+
git commit -m "chore: update ${CHART} image to ${NEW_VALUES_TAG}"
160+
161+
# Remove a stale remote branch left over from a previous failed run
162+
# (only safe to do when no open PR was found above).
163+
if git ls-remote --exit-code --heads origin "$BRANCH_NAME" >/dev/null 2>&1; then
164+
echo " Stale remote branch found with no open PR, deleting"
165+
git push origin --delete "$BRANCH_NAME"
166+
fi
167+
168+
git push -u origin "$BRANCH_NAME"
169+
170+
# ── Open PR ────────────────────────────────────────────────────
171+
if [[ -n "$LATEST_TAG" ]]; then
172+
PR_BODY="## Summary
173+
- Updates \`${CHART}\` image tag from \`${CURRENT_TAG}\` to \`${NEW_VALUES_TAG}\`
174+
- Upstream release: https://github.com/${REPO}/releases/tag/${LATEST_TAG}
175+
176+
## Changes
177+
- \`charts/${CHART}/values.yaml\`: \`image.tag\` → \`${NEW_VALUES_TAG}\`
178+
- \`charts/${CHART}/Chart.yaml\`: \`appVersion\` → \`${NEW_APP_VERSION}\`, chart \`version\` patch bumped
179+
180+
## Test plan
181+
- [ ] CI lint passes
182+
- [ ] Manual smoke-test if needed
183+
184+
🤖 Generated automatically by check-image-versions workflow"
185+
else
186+
PR_BODY="## Summary
187+
- Updates \`${CHART}\` image tag from \`${CURRENT_TAG}\` to \`${NEW_VALUES_TAG}\`
188+
- Source: ghcr.io/${GHCR_IMAGE}:${UPSTREAM_VERSION}
189+
190+
## Changes
191+
- \`charts/${CHART}/values.yaml\`: \`image.tag\` → \`${NEW_VALUES_TAG}\`
192+
- \`charts/${CHART}/Chart.yaml\`: \`appVersion\` → \`${NEW_APP_VERSION}\`, chart \`version\` patch bumped
193+
194+
## Test plan
195+
- [ ] CI lint passes
196+
- [ ] Manual smoke-test if needed
197+
198+
🤖 Generated automatically by check-image-versions workflow"
199+
fi
200+
201+
PR_CREATED=false
202+
if PR_OUTPUT=$(gh pr create \
203+
--title "chore: update ${CHART} image to ${NEW_VALUES_TAG}" \
204+
--body "$PR_BODY" \
205+
--base main \
206+
--head "$BRANCH_NAME" 2>&1); then
207+
echo "$PR_OUTPUT"
208+
PR_CREATED=true
209+
else
210+
if echo "$PR_OUTPUT" | grep -qi 'already exists'; then
211+
echo " PR already exists for $BRANCH_NAME, skipping create"
212+
else
213+
echo "$PR_OUTPUT"
214+
exit 1
215+
fi
216+
fi
217+
218+
# Return to main for the next chart
219+
git checkout main
220+
221+
if [[ "$PR_CREATED" == "true" ]]; then
222+
echo " PR created successfully!"
223+
fi
224+
done
225+
226+
echo ""
227+
echo "=========================================="
228+
echo "Image version check complete"
229+
echo "=========================================="

README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,72 @@ 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+
which mirrors the approach used in
91+
[blinklabs-io/cardano-up-packages](https://github.com/blinklabs-io/cardano-up-packages).
92+
93+
### How it works
94+
95+
1. The workflow runs **every Sunday at 00:00 UTC** (and can be triggered
96+
manually via `workflow_dispatch`).
97+
2. It reads [`scripts/upstream-versions.json`](scripts/upstream-versions.json),
98+
which maps each manageable chart to its upstream GitHub repository (and
99+
optional Docker Hub / GHCR source).
100+
3. For every chart it fetches the latest published release tag, derives the
101+
new image tag and `appVersion`, then compares against the value currently
102+
stored in `charts/<name>/values.yaml`.
103+
4. When a newer version is detected it calls
104+
[`scripts/update-chart-version.sh`](scripts/update-chart-version.sh), which:
105+
- Updates `image.tag` in `values.yaml`.
106+
- Updates `appVersion` in `Chart.yaml`.
107+
- Bumps the patch segment of `version` in `Chart.yaml`.
108+
5. The changes are committed to a branch named
109+
`chore/<chart-name>-<version>` and a pull-request is opened automatically.
110+
6. If the chart is already on the latest version, or a PR for that version
111+
already exists, no action is taken.
112+
113+
### Adding a new chart to auto-updates
114+
115+
Edit [`scripts/upstream-versions.json`](scripts/upstream-versions.json) and
116+
add an entry following the schema below:
117+
118+
```jsonc
119+
"my-chart": {
120+
// GitHub org/repo whose releases drive the update
121+
"repo": "org/repo",
122+
123+
// Prefix to strip from the GitHub release tag to get the plain version.
124+
// E.g. "v" turns "v1.2.3" into "1.2.3". Use "" to keep the tag as-is.
125+
"tag_pattern": "v",
126+
127+
// Prefix written in front of the version when updating values.yaml image.tag.
128+
// E.g. "v" produces tag: v1.2.3; "" produces tag: 1.2.3.
129+
"values_tag_prefix": "",
130+
131+
// Prefix written in front of the version when updating Chart.yaml appVersion.
132+
"app_version_prefix": "",
133+
134+
// (Optional) Docker Hub image to resolve full semver when the GitHub release
135+
// tag is shorter (e.g. v2.11 -> v2.11.0).
136+
// "docker_image": "org/image",
137+
138+
// (Optional) Use GHCR as the version source instead of GitHub releases.
139+
// Useful when the upstream project does not publish GitHub releases.
140+
// "ghcr_image": "org/image"
141+
}
142+
```
143+
144+
### Manually triggering
145+
146+
Navigate to **Actions → check-image-versions → Run workflow** in the GitHub UI.
147+
148+
### Scripts
149+
150+
| Script | Purpose |
151+
|--------|---------|
152+
| [`scripts/upstream-versions.json`](scripts/upstream-versions.json) | Config mapping each chart to its upstream source |
153+
| [`scripts/update-chart-version.sh`](scripts/update-chart-version.sh) | Updates `values.yaml`, `Chart.yaml` appVersion, and bumps chart version patch |

scripts/update-chart-version.sh

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env bash
2+
# Updates a Helm chart's image tag, appVersion, and chart version patch number.
3+
# Mirrors the add-version.sh pattern from blinklabs-io/cardano-up-packages.
4+
#
5+
# Usage:
6+
# ./scripts/update-chart-version.sh <chart-name> <values-tag> <app-version>
7+
#
8+
# Arguments:
9+
# chart-name - Directory name under charts/ (e.g. dingo, cardano-node)
10+
# values-tag - Full image tag string to write into values.yaml (e.g. 0.52.0, v1.4.0)
11+
# app-version - Full appVersion string to write into Chart.yaml (e.g. 0.52.0, v1.4.0)
12+
#
13+
# What this script does:
14+
# 1. Replaces the top-level image.tag in charts/<name>/values.yaml
15+
# 2. Replaces appVersion in charts/<name>/Chart.yaml
16+
# 3. Bumps the patch segment of version in charts/<name>/Chart.yaml
17+
18+
set -euo pipefail
19+
20+
if [[ $# -ne 3 ]]; then
21+
echo "Usage: $0 <chart-name> <values-tag> <app-version>"
22+
echo "Example: $0 dingo 0.52.0 0.52.0"
23+
exit 1
24+
fi
25+
26+
CHART_NAME="$1"
27+
NEW_VALUES_TAG="$2"
28+
NEW_APP_VERSION="$3"
29+
30+
CHART_DIR="charts/${CHART_NAME}"
31+
VALUES_FILE="${CHART_DIR}/values.yaml"
32+
CHART_FILE="${CHART_DIR}/Chart.yaml"
33+
34+
if [[ ! -d "$CHART_DIR" ]]; then
35+
echo "ERROR: Chart directory not found: $CHART_DIR"
36+
exit 1
37+
fi
38+
39+
# ── 1. Derive current and next chart version ──────────────────────────────────
40+
# Strip any surrounding quotes from the version field
41+
CURRENT_CHART_VERSION=$(grep '^version:' "$CHART_FILE" | head -1 | awk '{print $2}' | tr -d '"')
42+
43+
# Split on '.' and bump the patch (third) segment
44+
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_CHART_VERSION"
45+
NEW_CHART_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
46+
47+
echo "Updating chart: ${CHART_NAME}"
48+
echo " values.yaml image.tag : ${NEW_VALUES_TAG}"
49+
echo " Chart.yaml appVersion : ${NEW_APP_VERSION}"
50+
echo " Chart.yaml version : ${CURRENT_CHART_VERSION} -> ${NEW_CHART_VERSION}"
51+
echo ""
52+
53+
# Portable sed: BSD sed (macOS) needs -i '' while GNU sed (Linux/CI) needs -i.
54+
# Using SED_INPLACE makes the script work in both environments.
55+
if sed --version 2>/dev/null | grep -q GNU; then
56+
SED_INPLACE=(sed -i)
57+
else
58+
SED_INPLACE=(sed -i '')
59+
fi
60+
61+
# ── 2. Update values.yaml ─────────────────────────────────────────────────────
62+
# Replace the first (and only) top-level ` tag:` line in the file.
63+
# All target charts have exactly one two-space-indented `tag:` key, which belongs
64+
# to the primary image block. Nested image blocks use deeper indentation.
65+
"${SED_INPLACE[@]}" "s|^ tag:.*| tag: ${NEW_VALUES_TAG}|" "$VALUES_FILE"
66+
67+
# ── 3. Update Chart.yaml ──────────────────────────────────────────────────────
68+
# appVersion may be unquoted or quoted; replace the whole line either way.
69+
"${SED_INPLACE[@]}" "s|^appVersion:.*|appVersion: ${NEW_APP_VERSION}|" "$CHART_FILE"
70+
71+
# Bump the chart version.
72+
"${SED_INPLACE[@]}" "s|^version:.*|version: ${NEW_CHART_VERSION}|" "$CHART_FILE"
73+
74+
echo "Updated ${VALUES_FILE}:"
75+
grep ' tag:' "$VALUES_FILE" | head -1
76+
echo ""
77+
echo "Updated ${CHART_FILE}:"
78+
grep -E '^(version|appVersion):' "$CHART_FILE"
79+
echo ""
80+
echo "Please review the diff before committing:"
81+
git diff -- "$VALUES_FILE" "$CHART_FILE" || true

0 commit comments

Comments
 (0)