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
240 changes: 240 additions & 0 deletions .github/workflows/check-versions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
name: check-versions

on:
schedule:
# Run every Sunday at midnight UTC
- cron: '0 0 * * 0'
workflow_dispatch:

jobs:
check-versions:
name: Check for new upstream versions
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 https://github.com/actions/checkout/releases/tag/v6.0.2
with:
fetch-depth: 0

- name: Check and create PRs for new versions
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail

# Configure git
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

UPSTREAM_CONFIG="scripts/upstream-versions.json"
COMPOSE_FILE="docker-compose.yaml"

if [[ ! -f "$UPSTREAM_CONFIG" ]]; then
echo "ERROR: Upstream config not found: $UPSTREAM_CONFIG"
exit 1
fi

if [[ ! -f "$COMPOSE_FILE" ]]; then
echo "ERROR: Compose file not found: $COMPOSE_FILE"
exit 1
fi

PACKAGES=$(jq -r 'keys[]' "$UPSTREAM_CONFIG")

for PACKAGE in $PACKAGES; do
echo "=========================================="
echo "Checking package: $PACKAGE"
echo "=========================================="

COMPOSE_VAR=$(jq -r --arg pkg "$PACKAGE" '.[$pkg].compose_var' "$UPSTREAM_CONFIG")
GHCR_IMAGE=$(jq -r --arg pkg "$PACKAGE" '.[$pkg].ghcr_image // ""' "$UPSTREAM_CONFIG")
DOCKER_IMAGE=$(jq -r --arg pkg "$PACKAGE" '.[$pkg].docker_image // ""' "$UPSTREAM_CONFIG")
TAG_PREFIX=$(jq -r --arg pkg "$PACKAGE" '.[$pkg].tag_prefix // ""' "$UPSTREAM_CONFIG")
TAG_REGEX=$(jq -r --arg pkg "$PACKAGE" '.[$pkg].tag_regex // ""' "$UPSTREAM_CONFIG")

if [[ "$COMPOSE_VAR" == "null" || -z "$COMPOSE_VAR" ]]; then
echo " WARNING: compose_var not set for $PACKAGE, skipping"
continue
fi

if [[ -z "$GHCR_IMAGE" && -z "$DOCKER_IMAGE" ]]; then
echo " WARNING: No image source defined for $PACKAGE, skipping"
continue
fi

if [[ -z "$TAG_REGEX" ]]; then
TAG_REGEX='^[0-9]+(\.[0-9]+){2,3}$'
fi

if [[ -n "$GHCR_IMAGE" ]]; then
echo " Using GHCR image: $GHCR_IMAGE"
GHCR_ORG="${GHCR_IMAGE%%/*}"
GHCR_PKG="${GHCR_IMAGE#*/}"

UPSTREAM_TAG=$(gh api "/orgs/${GHCR_ORG}/packages/container/${GHCR_PKG}/versions" --paginate \
--jq '.[].metadata.container.tags[]' 2>/dev/null \
| grep -E "$TAG_REGEX" \
| sort -V | tail -1 || echo "")

if [[ -z "$UPSTREAM_TAG" ]]; then
echo " WARNING: Could not fetch tags from GHCR for $GHCR_IMAGE, skipping"
continue
fi

SOURCE_REF="ghcr.io/${GHCR_IMAGE}:${UPSTREAM_TAG}"
else
echo " Using Docker Hub image: $DOCKER_IMAGE"

UPSTREAM_TAG=$(python3 - "$DOCKER_IMAGE" "$TAG_REGEX" <<'PY' || echo ""
import json
import re
import sys
import urllib.request

image = sys.argv[1]
regex = re.compile(sys.argv[2])

page = 1
page_size = 100
found = []

while True:
url = f"https://hub.docker.com/v2/repositories/{image}/tags?page={page}&page_size={page_size}"
with urllib.request.urlopen(url) as resp:
data = json.loads(resp.read().decode("utf-8"))

for item in data.get("results", []):
tag = item.get("name", "")
if regex.match(tag):
found.append(tag)

if not data.get("next"):
break
page += 1

if not found:
sys.exit(1)

def normalize(tag):
if tag.startswith("v"):
tag = tag[1:]
parts = re.split(r"[.-]", tag)
key = []
for part in parts:
if part.isdigit():
key.append(int(part))
else:
key.append(part)
return key

found.sort(key=normalize)
print(found[-1])
PY
)

if [[ -z "$UPSTREAM_TAG" ]]; then
echo " WARNING: Could not fetch tags from Docker Hub for $DOCKER_IMAGE, skipping"
continue
fi

SOURCE_REF="docker.io/${DOCKER_IMAGE}:${UPSTREAM_TAG}"
fi

if [[ -n "$TAG_PREFIX" && "$UPSTREAM_TAG" == "$TAG_PREFIX"* ]]; then
UPSTREAM_VERSION="${UPSTREAM_TAG#${TAG_PREFIX}}"
else
UPSTREAM_VERSION="$UPSTREAM_TAG"
fi

echo " Upstream tag: $UPSTREAM_TAG"
echo " Upstream version: $UPSTREAM_VERSION"

LOCAL_VERSION=$(python3 - "$COMPOSE_FILE" "$COMPOSE_VAR" <<'PY' || echo ""
import re
import sys

compose_file = sys.argv[1]
compose_var = sys.argv[2]

pattern = re.compile(r"\$\{" + re.escape(compose_var) + r":-([^}]+)\}")
with open(compose_file, "r") as fh:
data = fh.read()

match = pattern.search(data)
if not match:
sys.exit(1)

print(match.group(1))
PY
)

if [[ -z "$LOCAL_VERSION" ]]; then
echo " WARNING: Could not find ${COMPOSE_VAR} default in $COMPOSE_FILE, skipping"
continue
fi

echo " Local version: $LOCAL_VERSION"

if [[ "$UPSTREAM_VERSION" == "$LOCAL_VERSION" ]]; then
echo " Already up to date"
continue
fi

echo " New version available: $UPSTREAM_VERSION"

# Verify image exists before updating
echo " Verifying image: $SOURCE_REF"
if ! docker manifest inspect "$SOURCE_REF" > /dev/null 2>&1; then
echo " WARNING: Image not found or not accessible: $SOURCE_REF, skipping"
continue
fi

BRANCH_NAME="chore/${PACKAGE}-${UPSTREAM_VERSION}"
EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --json number --jq '.[0].number' 2>/dev/null || echo "")
if [[ -n "$EXISTING_PR" ]]; then
echo " PR #$EXISTING_PR already exists for this version, skipping"
continue
fi

echo " Updating $COMPOSE_FILE..."
if ! ./scripts/add-version.sh "$PACKAGE" "$UPSTREAM_VERSION"; then
echo " WARNING: Failed to update $COMPOSE_FILE for $PACKAGE, skipping"
git checkout -- "$COMPOSE_FILE" || true
continue
fi

git checkout -b "$BRANCH_NAME"
git add "$COMPOSE_FILE"
git commit -m "chore: ${PACKAGE} ${UPSTREAM_VERSION}"

echo " Pushing branch..."
git push -u origin "$BRANCH_NAME"
Comment on lines +202 to +213
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Branch creation happens on a dirty working tree without resetting to main first.

git checkout -b "$BRANCH_NAME" (Line 201) creates the branch from whatever the current HEAD is. After the first package update, the working tree has uncommitted changes from add-version.sh. While git add and git commit follow, if a previous iteration's git checkout main (Line 225) fails or is skipped, subsequent packages would branch off a non-main commit, stacking unrelated changes.

Consider adding an explicit git checkout main and git reset --hard origin/main before creating each new branch to ensure a clean starting point.

Proposed fix
+            git checkout main
+            git pull origin main
+
             git checkout -b "$BRANCH_NAME"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ! ./scripts/add-version.sh "$PACKAGE" "$UPSTREAM_VERSION"; then
echo " WARNING: Failed to update $COMPOSE_FILE for $PACKAGE, skipping"
git checkout -- "$COMPOSE_FILE" || true
continue
fi
git checkout -b "$BRANCH_NAME"
git add "$COMPOSE_FILE"
git commit -m "chore: ${PACKAGE} ${UPSTREAM_VERSION}"
echo " Pushing branch..."
git push -u origin "$BRANCH_NAME"
if ! ./scripts/add-version.sh "$PACKAGE" "$UPSTREAM_VERSION"; then
echo " WARNING: Failed to update $COMPOSE_FILE for $PACKAGE, skipping"
git checkout -- "$COMPOSE_FILE" || true
continue
fi
git checkout main
git pull origin main
git checkout -b "$BRANCH_NAME"
git add "$COMPOSE_FILE"
git commit -m "chore: ${PACKAGE} ${UPSTREAM_VERSION}"
echo " Pushing branch..."
git push -u origin "$BRANCH_NAME"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/check-versions.yml around lines 195 - 206, The script is
creating feature branches from the current (possibly dirty) HEAD via git
checkout -b "$BRANCH_NAME" after running ./scripts/add-version.sh, which can
cause branches to stack unintended changes; before creating each branch (before
git checkout -b "$BRANCH_NAME") ensure you explicitly return to a clean main by
running git checkout main and then git fetch origin && git reset --hard
origin/main (or equivalent) so the new branch starts from the remote/main tip;
apply this change around the code that calls ./scripts/add-version.sh, git add
"$COMPOSE_FILE", git commit -m "chore: ${PACKAGE} ${UPSTREAM_VERSION}", and git
push -u origin "$BRANCH_NAME" to guarantee BRANCH_NAME is created cleanly and
unrelated COMPOSE_FILE changes are not carried forward.


echo " Creating PR..."
PR_BODY="## Summary
- Updates ${PACKAGE} to version ${UPSTREAM_VERSION}
- Source: ${SOURCE_REF}

## Test plan
- [ ] CI validation passes
- [ ] Manual testing if needed

🤖 Generated automatically by check-versions workflow"

gh pr create \
--title "chore: ${PACKAGE} ${UPSTREAM_VERSION}" \
--body "$PR_BODY" \
--base main \
--head "$BRANCH_NAME"

git checkout main

echo " PR created successfully!"
done

echo ""
echo "=========================================="
echo "Version check complete"
echo "=========================================="
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.PHONY: check-versions validate-versions add-version

check-versions:
./scripts/check-versions.sh

validate-versions:
./scripts/validate-versions.sh

add-version:
@test -n "$(PACKAGE)" && test -n "$(VERSION)" || (echo "Usage: make add-version PACKAGE=name VERSION=1.2.3"; exit 1)
./scripts/add-version.sh "$(PACKAGE)" "$(VERSION)"
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,31 @@ To start just the `cardano-wallet` service, which is part of the `wallet` profil
```bash
docker compose --profile wallet up
```

## Version Maintenance

To check for upstream image updates and refresh default versions in the compose file, run:

```bash
./scripts/check-versions.sh
```

To validate that all configured compose variables exist with defaults:

```bash
./scripts/validate-versions.sh
```

To update a specific service version:

```bash
./scripts/add-version.sh cardano-node 10.6.2
```

You can also use the Makefile targets:

```bash
make check-versions
make validate-versions
make add-version PACKAGE=cardano-node VERSION=10.6.2
```
90 changes: 90 additions & 0 deletions scripts/add-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Updates the docker-compose default version for a package.
# Usage: ./scripts/add-version.sh <package-name> <new-version>
# Example: ./scripts/add-version.sh cardano-node 10.6.2

set -euo pipefail

if [[ $# -ne 2 ]]; then
echo "Usage: $0 <package-name> <new-version>"
echo "Example: $0 cardano-node 10.6.2"
exit 1
fi

PACKAGE_NAME="$1"
NEW_VERSION="$2"
UPSTREAM_CONFIG="scripts/upstream-versions.json"
COMPOSE_FILE="docker-compose.yaml"

if [[ ! -f "$UPSTREAM_CONFIG" ]]; then
echo "ERROR: Upstream config not found: $UPSTREAM_CONFIG"
exit 1
fi

if [[ ! -f "$COMPOSE_FILE" ]]; then
echo "ERROR: Compose file not found: $COMPOSE_FILE"
exit 1
fi

COMPOSE_VAR=$(jq -r --arg pkg "$PACKAGE_NAME" '.[$pkg].compose_var // ""' "$UPSTREAM_CONFIG")
if [[ -z "$COMPOSE_VAR" || "$COMPOSE_VAR" == "null" ]]; then
echo "ERROR: compose_var not set for $PACKAGE_NAME"
exit 1
fi

OLD_VERSION=$(python3 - "$COMPOSE_FILE" "$COMPOSE_VAR" <<'PY' || echo ""
import re
import sys

compose_file = sys.argv[1]
compose_var = sys.argv[2]

pattern = re.compile(r"\$\{" + re.escape(compose_var) + r":-([^}]+)\}")
with open(compose_file, "r") as fh:
data = fh.read()

match = pattern.search(data)
if not match:
sys.exit(1)

print(match.group(1))
PY
)

if [[ -z "$OLD_VERSION" ]]; then
echo "ERROR: Could not find ${COMPOSE_VAR} default in $COMPOSE_FILE"
exit 1
fi

if [[ "$OLD_VERSION" == "$NEW_VERSION" ]]; then
echo "No change: ${COMPOSE_VAR} already set to ${NEW_VERSION}"
exit 0
fi

if ! python3 - "$COMPOSE_FILE" "$COMPOSE_VAR" "$NEW_VERSION" <<'PY'
import re
import sys

compose_file = sys.argv[1]
compose_var = sys.argv[2]
new_version = sys.argv[3]

pattern = re.compile(r"(\$\{" + re.escape(compose_var) + r":-)([^}]+)(\})")
with open(compose_file, "r") as fh:
data = fh.read()

if not pattern.search(data):
print("compose var not found", file=sys.stderr)
sys.exit(1)

updated = pattern.sub(r"\g<1>" + new_version + r"\g<3>", data, count=1)

with open(compose_file, "w") as fh:
fh.write(updated)
PY
then
echo "ERROR: Failed to update $COMPOSE_FILE for $PACKAGE_NAME"
exit 1
fi

echo "Updated ${COMPOSE_VAR}: ${OLD_VERSION} -> ${NEW_VERSION}"
Loading