diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2baa839..4d37d07 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,8 @@ on: push: branches: - main + tags: + - 'v*' pull_request: branches: - main @@ -36,23 +38,20 @@ jobs: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GHCR_PAT || secrets.GITHUB_TOKEN }} - - - name: Generate Docker tags + - name: Extract Docker metadata id: meta - run: | - IMAGE="${{ env.REGISTRY }}/${{ steps.image.outputs.name }}" - SHA_SHORT=$(echo ${{ github.sha }} | cut -c1-7) - - if [ "${{ github.event_name }}" = "pull_request" ]; then - # PR builds: tag as pr- - TAGS="${IMAGE}:pr-${{ github.event.pull_request.number }},${IMAGE}:sha-${SHA_SHORT}" - else - # Push to main: tag as both 'main' and 'latest' - TAGS="${IMAGE}:main,${IMAGE}:latest,${IMAGE}:sha-${SHA_SHORT}" - fi - - echo "tags=${TAGS}" >> $GITHUB_OUTPUT - echo "primary_tag=$(echo ${TAGS} | cut -d',' -f1 | cut -d':' -f2)" >> $GITHUB_OUTPUT + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=semver,pattern={{major}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + type=sha,format=short - name: Build and push Docker image uses: docker/build-push-action@v5 @@ -62,11 +61,17 @@ jobs: platforms: linux/amd64 push: true tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - name: Image summary run: | - echo "### Docker Image Built 🐳" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Tags:** ${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY + { + echo "### Docker Image Built 🐳" + echo + echo "**Tags:**" + while IFS= read -r tag; do + printf -- "- %s\n" "$tag" + done <<< "${{ steps.meta.outputs.tags }}" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/Makefile b/Makefile index ddabc6e..3d08ac9 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,9 @@ -.PHONY: help setup lint format typecheck pre-commit build push clean +.PHONY: help setup lint format typecheck pre-commit build push clean release IMAGE_NAME := ghcr.io/eopf-explorer/data-pipeline -TAG := v0 +TAG ?= v0 +MESSAGE ?= +FLAGS ?= help: ## Show this help message @echo "🚀 EOPF GeoZarr Data Pipeline (Slim Branch)" @@ -43,6 +45,10 @@ push: ## Push Docker image to registry docker push $(IMAGE_NAME):$(TAG) docker push $(IMAGE_NAME):latest +release: ## Cut and push a git tag (set TAG=vX[.Y[.Z]], optional MESSAGE="...", FLAGS="--force") + @if [ "$(origin TAG)" = "default" ]; then echo "TAG is required, e.g. make release TAG=v0"; exit 1; fi + @if [ -n "$(MESSAGE)" ]; then scripts/tag_release.sh $(FLAGS) "$(TAG)" "$(MESSAGE)"; else scripts/tag_release.sh $(FLAGS) "$(TAG)"; fi + clean: ## Clean generated files and caches @echo "Cleaning generated files..." find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true diff --git a/scripts/tag_release.sh b/scripts/tag_release.sh new file mode 100755 index 0000000..5a34ad6 --- /dev/null +++ b/scripts/tag_release.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' >&2 +Usage: tag_release.sh [-f|--force] [message] + + Annotated tag to create (semantic version, e.g. v1.2.0) + [message] Optional tag message (defaults to "Release ") + +Options: + -f, --force Move an existing tag locally and on origin (force-with-lease) + -h, --help Show this help and exit +EOF +} + +die() { + printf 'Error: %s\n' "$1" >&2 + exit 1 +} + +FORCE=0 +TAG="" + +while [[ $# -gt 0 ]]; do + case "$1" in + -f|--force) + FORCE=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + break + ;; + -*) + die "unknown option: $1" + ;; + *) + TAG="$1" + shift + break + ;; + esac +done + +[[ -n "$TAG" ]] || { usage; exit 1; } + +MESSAGE=${*:-"Release ${TAG}"} + +[[ "$TAG" =~ ^v[0-9]+(\.[0-9]+)*$ ]] || die "tag must follow semantic style (e.g. v1.2.0)." + +if [[ -n $(git status --porcelain) ]]; then + die "working tree has uncommitted changes." +fi + +CURRENT_BRANCH=$(git symbolic-ref --short HEAD) +[[ "$CURRENT_BRANCH" == "main" ]] || die "releases must be created from main (current: $CURRENT_BRANCH)." + +REMOTE=origin +git config --get remote."$REMOTE".url >/dev/null 2>&1 || die "remote '$REMOTE' is not configured." + +git fetch "$REMOTE" main --tags --quiet >/dev/null 2>&1 || die "failed to fetch $REMOTE/main." +[[ $(git rev-parse HEAD) == $(git rev-parse "$REMOTE/main") ]] || die "local main is out of sync with $REMOTE/main." + +if git rev-parse "$TAG" >/dev/null 2>&1 && [[ $FORCE -eq 0 ]]; then + die "tag '$TAG' already exists locally. Use --force to move it." +fi + +if git ls-remote --tags "$REMOTE" "$TAG" | grep -q "refs/tags/$TAG$" && [[ $FORCE -eq 0 ]]; then + die "tag '$TAG' already exists on $REMOTE. Use --force to replace it." +fi + +if [[ $FORCE -eq 1 ]]; then + git tag -fa "$TAG" -m "$MESSAGE" + git push "$REMOTE" "$TAG" --force-with-lease + echo "Moved tag '$TAG' to $(git rev-parse --short HEAD) and pushed with force-with-lease." +else + git tag -a "$TAG" -m "$MESSAGE" + git push "$REMOTE" "$TAG" + echo "Created tag '$TAG' at $(git rev-parse --short HEAD) and pushed to $REMOTE." +fi diff --git a/workflows/README.md b/workflows/README.md index faae5e6..5443b74 100644 --- a/workflows/README.md +++ b/workflows/README.md @@ -99,6 +99,21 @@ geozarr-jflnj Failed 10h ## Configuration +### Docker Image Versions + +CI now publishes images to `ghcr.io/eopf-explorer/data-pipeline` with the following tags: + +- `main`, `latest`, and `sha-` whenever a change lands on `main`. +- `vX.Y.Z`, `vX.Y`, `vX`, and `sha-` from annotated semantic tags (for example `v1.2.0`). +- `pr-` and `sha-` for every pull-request build. + +**Releasing a new tag** +1. Merge to `main`, then run `make release TAG=v1.2.0 [MESSAGE="Release v1.2.0"]` from a clean repo. The helper ensures `main` is clean and in sync before tagging. +2. Need to reuse an existing tag? Add `FLAGS="--force"`. +3. After GitHub Actions completes, production keeps running the new `latest` image. To pin a specific tag: + - **Staging first (preferred):** set `pipeline_image_version` in `workflows/overlays/staging/kustomization.yaml` to the tag (for example `v1.2.0`) and run `kubectl apply -k workflows/overlays/staging`. If it passes validation, make the same edit in `workflows/overlays/production/kustomization.yaml` and apply it. + - **Ship now:** set the tag directly in `workflows/overlays/production/kustomization.yaml` and apply. Staging already tracks the latest `main`; bump it too if you want staging and production on the same image. + ### S3 Storage - **Endpoint**: `https://s3.de.io.cloud.ovh.net` (OVH Frankfurt) diff --git a/workflows/base/workflowtemplate.yaml b/workflows/base/workflowtemplate.yaml index 66c2d54..9a8d69d 100644 --- a/workflows/base/workflowtemplate.yaml +++ b/workflows/base/workflowtemplate.yaml @@ -31,7 +31,7 @@ spec: - name: s3_output_prefix value: tests-output - name: pipeline_image_version - value: feature-align-data-model + value: latest # Optional conversion parameter overrides (empty = use collection defaults) - name: override_groups value: ""