#!/usr/bin/env bash # Publish a CodeCraft release named vYYYYMM-NNNNN (year, month, then a counter that # starts at 00001 every month): # 1. pick the next number from the existing tags of this month, # 2. rename "## [Unreleased]" in CHANGELOG.md and the "unreleased" entry in # web/src/whatsnew.ts to the version, update the README badge, commit, # 3. tag and push branch and tag, # 4. build the multi-arch image (amd64 + arm64) with the version embedded and push # :vYYYYMM-NNNNN (and :latest), # 5. create the Gitea release with the changelog section as notes. # # Usage: scripts/release.sh [vYYYYMM-NNNNN] [--dry-run] [--no-latest] [--skip-image] # (without a version the next number of the current month is used) # # Environment: # REGISTRY image name without tag (default git.familie-zink.org/zink/codecraft) # GITEA_URL Gitea base URL (default https://git.familie-zink.org) # GITEA_REPO owner/repo (default zink/codecraft) # GITEA_TOKEN API token; if empty, the git credential helper is asked # PLATFORMS buildx platforms (default linux/amd64,linux/arm64) # BUILDER buildx builder name (default codecraft-multi, created if missing) # # Requires: git, docker with buildx (logged in to the registry), curl, jq. # Building for a foreign architecture needs QEMU/binfmt: # docker run --privileged --rm tonistiigi/binfmt --install all set -euo pipefail REGISTRY="${REGISTRY:-git.familie-zink.org/zink/codecraft}" GITEA_URL="${GITEA_URL:-https://git.familie-zink.org}" GITEA_REPO="${GITEA_REPO:-zink/codecraft}" PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}" BUILDER="${BUILDER:-codecraft-multi}" tag="" dry=0 latest=1 image=1 for arg in "$@"; do case "$arg" in --dry-run) dry=1 ;; --no-latest) latest=0 ;; --skip-image) image=0 ;; -h|--help) sed -n '2,26p' "$0"; exit 0 ;; -*) echo "unknown option $arg" >&2; exit 2 ;; *) tag="$arg" ;; esac done cd "$(git rev-parse --show-toplevel)" step() { printf '\n==> %s\n' "$*"; } run() { if (( dry )); then echo "[dry-run] $*"; else "$@"; fi; } fail() { echo "error: $*" >&2; exit 1; } step "Checking the working tree" for cmd in git docker curl jq; do command -v "$cmd" >/dev/null || fail "$cmd is not installed"; done [[ -z "$(git status --porcelain --untracked-files=no)" ]] || fail "uncommitted changes – commit or stash them first" run git fetch --tags --quiet origin if [[ -z "$tag" ]]; then month=$(date +%Y%m) last=$(git tag -l "v$month-*" | sed -n "s/^v$month-\([0-9]\{5\}\)$/\1/p" | sort -n | tail -1) tag=$(printf 'v%s-%05d' "$month" $((10#${last:-0} + 1))) fi [[ "$tag" =~ ^v[0-9]{6}-[0-9]{5}$ ]] || fail "version must look like vYYYYMM-NNNNN, got $tag" git rev-parse -q --verify "refs/tags/$tag" >/dev/null && fail "tag $tag already exists" version=${tag#v} today=$(date +%Y-%m-%d) echo "release $tag" # Release notes: the [Unreleased] section of the changelog (without its heading). notes=$(awk '/^## \[Unreleased\]/ { found=1; next } found && /^## \[/ { exit } found { print }' CHANGELOG.md) [[ -n "${notes//[[:space:]]/}" ]] || fail "CHANGELOG.md has no (or an empty) \"## [Unreleased]\" section" notes+=$'\n\n'"Image: \`$REGISTRY:$tag\` ($(tr ',' ' ' <<<"$PLATFORMS"))." step "Naming the release in CHANGELOG.md, whatsnew.ts and README.md" if (( dry )); then echo "[dry-run] ## [Unreleased] -> ## [$tag] – $today; whatsnew unreleased -> \"$version\"; README badge" else sed -i "s/^## \[Unreleased\].*/## [$tag] – $today/" CHANGELOG.md grep -q '^ unreleased: {' web/src/whatsnew.ts && sed -i "s/^ unreleased: {/ \"$version\": {/" web/src/whatsnew.ts # shields.io escapes "-" as "--". sed -i -E "s|(alt=\"Version )[^\"]*(\" src=\"https://img.shields.io/badge/version-)[^?]*-([0-9a-f]{6}\?)|\1$tag\2${tag//-/--}-\3|" README.md git add CHANGELOG.md web/src/whatsnew.ts README.md git commit -q -m "Release $tag" fi step "Tagging $tag and pushing" branch=$(git rev-parse --abbrev-ref HEAD) run git tag -a "$tag" -m "CodeCraft $tag" run git push origin "$branch" "$tag" if (( image )); then step "Building and pushing $REGISTRY:$tag for $PLATFORMS" docker buildx inspect "$BUILDER" >/dev/null 2>&1 || run docker buildx create --name "$BUILDER" --driver docker-container >/dev/null tags=(-t "$REGISTRY:$tag") (( latest )) && tags+=(-t "$REGISTRY:latest") run docker buildx build --builder "$BUILDER" --platform "$PLATFORMS" \ --build-arg GIT_COMMIT="$(git rev-parse --short HEAD)" --build-arg CODECRAFT_VERSION="$version" \ "${tags[@]}" --push . fi step "Creating the Gitea release $tag" token="${GITEA_TOKEN:-}" user="" if [[ -z "$token" ]]; then host=${GITEA_URL#*://} creds=$(printf 'protocol=%s\nhost=%s\n\n' "${GITEA_URL%%://*}" "${host%%/*}" | git credential fill 2>/dev/null || true) user=$(sed -n 's/^username=//p' <<<"$creds") token=$(sed -n 's/^password=//p' <<<"$creds") fi [[ -n "$token" ]] || fail "no Gitea credentials – set GITEA_TOKEN" auth=(-H "Authorization: token $token") [[ -n "$user" ]] && auth=(-u "$user:$token") body=$(jq -n --arg tag "$tag" --arg name "CodeCraft $tag" --arg body "$notes" \ '{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}') if (( dry )); then echo "[dry-run] POST $GITEA_URL/api/v1/repos/$GITEA_REPO/releases" else url=$(curl -fsS "${auth[@]}" -H 'Content-Type: application/json' -X POST \ "$GITEA_URL/api/v1/repos/$GITEA_REPO/releases" -d "$body" | jq -r .html_url) echo "published: $url" fi step "Done" echo "Update a deployment: set CODECRAFT_VERSION=$tag in .env, then docker compose pull && docker compose up -d"