#!/usr/bin/env bash source "$(dirname -- "$0")/common.sh" # Also enforce this inside the script, independently of the workflow expression. [[ "${CI_REF:-}" == refs/heads/main ]] || { echo 'Branch is not main; no publication'; exit 0; } [[ -f "$ARTIFACTS/verified-commit" && "$(cat "$ARTIFACTS/verified-commit")" == "$CI_COMMIT" ]] || { echo 'This commit has not passed all verification steps' >&2; exit 1; } : "${REGISTRY_USER:?Missing REGISTRY_USER secret}" : "${REGISTRY_TOKEN:?Missing REGISTRY_TOKEN secret}" : "${GITEA_SERVER_URL:?Missing Gitea server URL}" : "${CI_REPOSITORY:?Missing repository}" require_platform exec > >(tee "$ARTIFACTS/publish.log") 2>&1 REGISTRY=${GITEA_SERVER_URL#http://} REGISTRY=${REGISTRY#https://} REGISTRY=${REGISTRY%/} [[ "$REGISTRY" != */* ]] || { echo 'Registry must be hosted at the Gitea server root'; exit 1; } OWNER=${CI_REPOSITORY%%/*} OWNER=${OWNER,,} [[ "$OWNER" =~ ^[a-z0-9][a-z0-9_.-]*$ ]] || { echo 'Invalid registry namespace'; exit 1; } # Gitea 1.25 ignores workflow concurrency. All publications on the sole runner # share this lock, outside checkout directories and across workflow attempts. lock_dir=${CI_PUBLISH_LOCK_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/agiledancers-ci} mkdir -p "$lock_dir" exec 9>"$lock_dir/publish.lock" flock -w 600 9 # Preserve the selected daemon and isolate credentials from the developer's config. isolate_docker_config trap 'rm -rf -- "$DOCKER_CONFIG"' EXIT trap 'exit 143' TERM trap 'exit 130' INT printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY" --username "$REGISTRY_USER" --password-stdin ensure_commit_image() { local role=$1 source=$2 repository="$REGISTRY/$OWNER/agiledancers-$1" ref error ref="$repository:$CI_COMMIT" error=$(mktemp) if docker manifest inspect --insecure "$ref" > /dev/null 2>"$error"; then echo "Preserving existing $ref" elif grep -Eqi 'manifest unknown|no such manifest' "$error"; then docker tag "$source" "$ref" docker push "$ref" else # Authentication/network/server errors are never treated as missing tags. cat "$error" >&2 rm -f "$error" return 1 fi rm -f "$error" docker pull --platform "$TARGET_PLATFORM" "$ref" [[ "$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$ref")" == "$CI_COMMIT" ]] || { echo "Existing $role image has an unexpected source revision" >&2; return 1; } docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$ref" \ > "$ARTIFACTS/$role-digests.txt" grep -Fq "$repository@sha256:" "$ARTIFACTS/$role-digests.txt" || { echo 'Missing registry digest'; return 1; } } ensure_commit_image backend "$BACKEND_IMAGE" ensure_commit_image frontend "$FRONTEND_IMAGE" # Both immutable images are now pull-verified. Only current main may move aliases. head=$(python3 ci/main-head.py) if [[ "$head" != "$CI_COMMIT" ]]; then echo 'A newer main commit exists; leaving moving tags unchanged.' exit 0 fi for role in backend frontend; do repository="$REGISTRY/$OWNER/agiledancers-$role" # Recheck under the publication lock before each moving-tag write. [[ "$(python3 ci/main-head.py)" == "$CI_COMMIT" ]] || { echo 'Main advanced during publication; leaving remaining moving tags unchanged.' exit 0 } docker tag "$repository:$CI_COMMIT" "$repository:main" docker push "$repository:main" docker pull --platform "$TARGET_PLATFORM" "$repository:main" done