From b6867553739772085d8460db9f52e2fbbffc6a45 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Thu, 17 Sep 2026 23:41:55 -0400 Subject: [PATCH 1/2] Stamp the image version and gate the chart version bumps The Docker build context carries no .git, so the Makefile's `git describe` never resolved and every published image reported itself as "dev". The version is now resolved on the runner and handed to the build as an argument. The image tags were assembled by a github-script step that stripped and rejoined the metadata action's output, which produced :latest and :latest-v0.5.0. docker/metadata-action emits them directly instead: master publishes :latest, a release tag publishes :X.Y.Z, :X.Y and :latest. The same scheme is mirrored in build-multiarch.sh, which also never passed the version through. Release notes come from GitHub's own generator rather than a commit dump. scripts/check-version.sh runs on pull requests and on tags. A published chart version is immutable in practice: republishing one overwrites its .tgz and drops the previous release from the repository index entirely. The check refuses a change that did not move the versions it affects, and refuses a tag whose appVersion disagrees with it. Pushes to master and to feature branches are not gated. The .gitpod.yml stub is unused and goes with it. --- .github/workflows/build.yml | 68 ++++++++++----- .github/workflows/release.yaml | 4 + .gitpod.yml | 5 -- .goreleaser.yaml | 8 +- Dockerfile | 5 +- build-multiarch.sh | 37 +++++++-- scripts/check-version.sh | 148 +++++++++++++++++++++++++++++++++ 7 files changed, 237 insertions(+), 38 deletions(-) delete mode 100644 .gitpod.yml create mode 100755 scripts/check-version.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 965a67c..baf8d4c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,8 +37,33 @@ jobs: - name: Test run: make test + VersionCheck: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # The check compares against the base branch or the previous tag. + fetch-depth: 0 + + # Gated on a pull request and on a tag, the two events that can publish + # something carrying a version. Pushes to master and to feature branches + # fall through and the job passes. + # + # The condition sits on the step rather than the job: a skipped job also + # skips everything that needs it, which would take the whole build with it. + - name: Check chart and app versions + if: github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/') + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + ./scripts/check-version.sh pr "origin/${{ github.base_ref }}" + else + ./scripts/check-version.sh tag "${{ github.ref_name }}" + fi + Build: - needs: Test + needs: [Test, VersionCheck] runs-on: ubuntu-latest permissions: contents: read @@ -47,6 +72,10 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + # The Makefile derives the version from `git describe`, which needs + # the tags and the history behind them. + fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -64,28 +93,27 @@ jobs: # Extract metadata (tags, labels) for Docker # https://github.com/docker/metadata-action + # + # master -> :latest + # v0.5.0 -> :0.5.0, :0.5 and :latest (the default `latest=auto` flavor + # moves `latest` onto every semver tag) + # PR -> :pr-N, built but not pushed - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ secrets.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=ref,event=pr + type=raw,value=latest,enable={{is_default_branch}} - - uses: actions/github-script@v6 - id: tags - with: - script: | - tags = `${{ join(steps.meta.outputs.tags, ',') }}` - result = [] - tags.split("\n").forEach(function (item) { - short_tag = item.trim().split(":")[1]; - if (short_tag != "latest") { - result.push(short_tag); - } - }) - return result.join("-"); - result-encoding: string - - name: Get result - run: echo "${{steps.set-result.outputs.result}}" + # The build context has no .git, so `git describe` has to run here and be + # handed to the image as a build argument. + - name: Resolve the binary version + id: version + run: echo "version=$(git describe --tags --always --dirty)" >> "$GITHUB_OUTPUT" # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action @@ -95,10 +123,10 @@ jobs: context: . file: Dockerfile platforms: linux/amd64,linux/arm64 + build-args: | + VERSION=${{ steps.version.outputs.version }} push: ${{ github.event_name != 'pull_request' || github.event.inputs.push == 'true' }} - tags: | - ${{ secrets.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest - ${{ secrets.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-${{ steps.tags.outputs.result }} + tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} # - name: Docker Hub Description diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index fdb8431..8d57cce 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -20,6 +20,10 @@ jobs: with: go-version-file: go.mod + # Before any artifact is produced: the tag has to agree with the chart. + - name: Check chart and app versions + run: ./scripts/check-version.sh tag "${{ github.ref_name }}" + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 96cfeb6..0000000 --- a/.gitpod.yml +++ /dev/null @@ -1,5 +0,0 @@ -tasks: - - init: | - echo 'TODO: build project' - command: | - git pull \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml index dfae5d7..e6e7728 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -39,11 +39,9 @@ checksum: name_template: "checksums.txt" changelog: - sort: asc - filters: - exclude: - - "^docs:" - - "^test:" + # Let GitHub generate the notes from the merged pull requests, the same list + # the release page offers, instead of a flat commit dump. + use: github-native release: github: diff --git a/Dockerfile b/Dockerfile index 9837215..73ee39a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,10 @@ FROM docker.io/golang:1.26 AS builder RUN apt-get update && apt-get install -y make && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY go.mod main.go Makefile ./ -RUN make build +# The build context has no .git, so the Makefile's git describe finds nothing; +# the version has to be passed in. +ARG VERSION=dev +RUN make build VERSION=$VERSION FROM alpine:3.23 RUN apk --no-cache add ca-certificates diff --git a/build-multiarch.sh b/build-multiarch.sh index e54f67a..d0d1086 100755 --- a/build-multiarch.sh +++ b/build-multiarch.sh @@ -15,15 +15,38 @@ buildah login --username $DOCKER_USERNAME --password $DOCKER_PASSWORD $DOCKER_RE podman run --rm --events-backend=file --cgroup-manager=cgroupfs --privileged docker://multiarch/qemu-user-static --reset -p yes -VERSION="latest" +IMAGE="byjg/static-httpserver" + +# The version stamped into the binary. The build context has no .git, so the +# Makefile's git describe finds nothing and it has to be passed in, the same way +# the GitHub workflow does it. +VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo dev) + +# Image tags follow the same scheme as the workflow: sitting on a release tag +# publishes :X.Y.Z, :X.Y and :latest, anything else publishes :latest alone. +RELEASE_TAG=$(git describe --exact-match --tags 2>/dev/null || true) +if [ -n "$RELEASE_TAG" ]; then + SEMVER=${RELEASE_TAG#v} + TAGS=("$SEMVER" "${SEMVER%.*}" "latest") +else + TAGS=("latest") +fi + +echo "Building $IMAGE version $VERSION, tags: ${TAGS[*]}" + +MANIFEST="$IMAGE:${TAGS[0]}" -buildah manifest create byjg/static-httpserver:$VERSION +# A manifest left behind by an earlier run makes create fail. +buildah manifest rm "$MANIFEST" 2>/dev/null || true +buildah manifest create "$MANIFEST" -buildah bud --arch arm64 --os linux --iidfile /tmp/iid-arm64 -f Dockerfile -t byjg/static-httpserver:$VERSION-arm64 . -buildah bud --arch amd64 --os linux --iidfile /tmp/iid-amd64 -f Dockerfile -t byjg/static-httpserver:$VERSION-amd64 . +buildah bud --arch arm64 --os linux --build-arg VERSION="$VERSION" --iidfile /tmp/iid-arm64 -f Dockerfile -t "$IMAGE:${TAGS[0]}-arm64" . +buildah bud --arch amd64 --os linux --build-arg VERSION="$VERSION" --iidfile /tmp/iid-amd64 -f Dockerfile -t "$IMAGE:${TAGS[0]}-amd64" . -buildah manifest add byjg/static-httpserver:$VERSION --arch arm64 --os linux --variant v8 $(cat /tmp/iid-arm64) -buildah manifest add byjg/static-httpserver:$VERSION --arch amd64 --os linux --os=linux $(cat /tmp/iid-amd64) +buildah manifest add "$MANIFEST" --arch arm64 --os linux --variant v8 "$(cat /tmp/iid-arm64)" +buildah manifest add "$MANIFEST" --arch amd64 --os linux "$(cat /tmp/iid-amd64)" -buildah manifest push --all --format v2s2 byjg/static-httpserver:$VERSION docker://byjg/static-httpserver:$VERSION +for tag in "${TAGS[@]}"; do + buildah manifest push --all --format v2s2 "$MANIFEST" "docker://$IMAGE:$tag" +done diff --git a/scripts/check-version.sh b/scripts/check-version.sh new file mode 100755 index 0000000..000648a --- /dev/null +++ b/scripts/check-version.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# +# Refuses to build anything publishable when a change was made without the +# matching version bump. +# +# Two failures this guards against: +# - a chart version that did not move, which makes the published .tgz +# overwrite the previous one and drop it from the repo index entirely; +# - an appVersion that disagrees with the release tag, which points the chart +# at an image tag that was never built. +# +set -euo pipefail + +CHART_FILE="helm/static-httpserver/Chart.yaml" + +# Everything that ends up inside the image. Touching it has to move appVersion. +APP_PATHS="main.go go.mod Makefile Dockerfile html/" +# The chart itself. Touching it has to move the chart version. Chart.yaml is +# excluded: bumping either version edits it, and that must not count as a chart +# change or an appVersion bump would demand a chart bump alongside it. +CHART_PATHS="helm/" + +usage() { + cat <<'EOF' +Usage: + scripts/check-version.sh pr compare against the PR base branch + scripts/check-version.sh tag compare against the previous tag + +Examples: + scripts/check-version.sh pr origin/master + scripts/check-version.sh tag v0.6.0 +EOF +} + +# field_at +field_at() { + local ref="$1" field="$2" content + if [ -z "$ref" ]; then + content=$(cat "$CHART_FILE") + else + content=$(git show "$ref:$CHART_FILE") + fi + printf '%s\n' "$content" | + sed -nE "s/^${field}:[[:space:]]*\"?([^\"[:space:]]+)\"?.*/\1/p" | + head -1 +} + +# changed_under +changed_under() { + local files="$1" prefix + for prefix in $2; do + if printf '%s\n' "$files" | grep -qE "^${prefix}"; then + return 0 + fi + done + return 1 +} + +STATUS=0 +fail() { + echo "FAIL: $*" >&2 + STATUS=1 +} + +MODE="${1:-}" +REF="${2:-}" +if [ -z "$MODE" ] || [ -z "$REF" ]; then + usage + exit 1 +fi + +case "$MODE" in + pr) + CHANGED=$(git diff --name-only "$REF...HEAD") + echo "Files changed against $REF:" + printf '%s\n' "$CHANGED" | sed 's/^/ /' + echo + + CHART_CHANGES=$(printf '%s\n' "$CHANGED" | grep -E "^${CHART_PATHS}" | grep -vxF "$CHART_FILE" || true) + APP_CHANGED=no + changed_under "$CHANGED" "$APP_PATHS" && APP_CHANGED=yes + + # An application change moves appVersion, appVersion lives in the chart, and + # the chart is republished on every merge to master. So it lands as a new + # .tgz and needs a chart version of its own, exactly as a template change + # does: whenever either side moves, the chart version moves with it. + if [ -n "$CHART_CHANGES" ] || [ "$APP_CHANGED" = yes ]; then + OLD=$(field_at "$REF" version) + NEW=$(field_at "" version) + if [ "$OLD" = "$NEW" ]; then + if [ -n "$CHART_CHANGES" ]; then + WHY="the chart changed" + else + WHY="the application changed, which republishes the chart" + fi + fail "$WHY, but the chart version is still $NEW. Bump 'version' in $CHART_FILE." + else + echo "OK: chart version $OLD -> $NEW" + fi + else + echo "SKIP: neither the chart nor the application changed, chart version not required to move" + fi + + if [ "$APP_CHANGED" = yes ]; then + OLD=$(field_at "$REF" appVersion) + NEW=$(field_at "" appVersion) + if [ "$OLD" = "$NEW" ]; then + fail "the application changed but appVersion is still $NEW. Bump 'appVersion' in $CHART_FILE." + else + echo "OK: appVersion $OLD -> $NEW" + fi + else + echo "SKIP: application untouched, appVersion not required to move" + fi + ;; + + tag) + WANT="${REF#v}" + + APP=$(field_at "" appVersion) + if [ "$APP" != "$WANT" ]; then + fail "tag $REF expects appVersion $WANT, but $CHART_FILE says $APP." + else + echo "OK: appVersion $APP matches tag $REF" + fi + + # The tag is already checked out, so the previous tag is the one before it. + PREV=$(git describe --abbrev=0 --tags HEAD^ 2>/dev/null || true) + if [ -z "$PREV" ]; then + echo "SKIP: no previous tag to compare the chart version against" + else + OLD=$(field_at "$PREV" version) + NEW=$(field_at "" version) + if [ "$OLD" = "$NEW" ]; then + fail "chart version is still $NEW since $PREV. A published chart version is immutable: bump 'version' in $CHART_FILE or the new .tgz replaces the old one and $OLD disappears from the repo index." + else + echo "OK: chart version $OLD ($PREV) -> $NEW" + fi + fi + ;; + + *) + usage + exit 1 + ;; +esac + +exit "$STATUS" From 2f3fa3561d4c2dd13f3ec9fc4aa79e097bcab669 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Thu, 17 Sep 2026 23:45:48 -0400 Subject: [PATCH 2/2] Bump the chart to 0.3.1 and the app to 0.5.1 The version gate requires both to move when the application changes, and appVersion has to name a real release instead of "latest" so it can match the tag that publishes it. --- helm/static-httpserver/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helm/static-httpserver/Chart.yaml b/helm/static-httpserver/Chart.yaml index c3230a4..84594ca 100644 --- a/helm/static-httpserver/Chart.yaml +++ b/helm/static-httpserver/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.3.0 +version: 0.3.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "latest" +appVersion: "0.5.1"