name: Deploy Image (kustomize image-pin in apps repo) description: | Pin an image tag in fritzlab/apps via `kustomize edit set image`, validate the rendered manifests, and push to apps-repo main. Retries on push conflict. inputs: image: description: Full image name without tag (must match an entry in the target kustomization.yaml `images:` block) required: true tag: description: Tag to pin. Defaults to github.run_number when empty. required: false default: '' path: description: Path inside fritzlab/apps to the manifests dir (e.g. sjc001/infra/chrony/manifests) required: true token: description: CI_BOT_TOKEN with write access to fritzlab/apps required: true apps-repo: description: Apps repo URL (without protocol) required: false default: code.fritzlab.net/fritzlab/apps message: description: Commit message. Defaults to "deploy #". required: false default: '' runs: using: composite steps: - name: Pin image and push shell: bash env: IMAGE: ${{ inputs.image }} TAG_INPUT: ${{ inputs.tag }} PATH_IN_REPO: ${{ inputs.path }} TOKEN: ${{ inputs.token }} APPS_REPO: ${{ inputs.apps-repo }} MESSAGE: ${{ inputs.message }} RUN_NUMBER: ${{ github.run_number }} run: | set -euo pipefail TAG="${TAG_INPUT:-$RUN_NUMBER}" NAME="$(basename "$IMAGE")" MSG="${MESSAGE:-deploy ${NAME} #${TAG}}" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT git clone --depth 1 "https://ci-bot:${TOKEN}@${APPS_REPO}.git" "$WORK" cd "$WORK" git config user.name ci-bot git config user.email ci-bot@fritzlab.net cd "$PATH_IN_REPO" kustomize edit set image "${IMAGE}=${IMAGE}:${TAG}" # Validate the kustomization renders cleanly before we push. if ! kustomize build . > /dev/null; then echo "FATAL: kustomize build failed after image pin" git --no-pager diff exit 1 fi if git -C "$WORK" diff --quiet; then echo "apps repo already on ${NAME}:${TAG}, skipping" exit 0 fi # Detect an open PR already targeting the same image:tag in the same # kustomization.yaml. If one is open, defer to it so the review gate # is the actual control path — not a race with the direct push. GITEA_HOST="${APPS_REPO%%/*}" GITEA_REPO_PATH="${APPS_REPO#*/}" GITEA_API="https://${GITEA_HOST}/api/v1" KUSTOMIZATION_FILE="${PATH_IN_REPO}/kustomization.yaml" BLOCKING_PR="" PR_NUMS=$(curl -sf -H "Authorization: token ${TOKEN}" \ "${GITEA_API}/repos/${GITEA_REPO_PATH}/pulls?state=open&limit=50" \ | jq -r '.[].number' 2>/dev/null || true) for pr_num in $PR_NUMS; do FILES_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \ "${GITEA_API}/repos/${GITEA_REPO_PATH}/pulls/${pr_num}/files" || echo "[]") if echo "$FILES_JSON" | jq -e \ --arg f "$KUSTOMIZATION_FILE" \ --arg t "$TAG" \ 'any(.[]; .filename == $f and (.patch // "" | contains($t)))' \ > /dev/null 2>&1; then BLOCKING_PR="$pr_num" break fi done if [ -n "$BLOCKING_PR" ]; then echo "PR #${BLOCKING_PR} is open and already targets ${NAME}:${TAG} in ${KUSTOMIZATION_FILE}; deferring to PR" exit 0 fi git -C "$WORK" add "${PATH_IN_REPO}/kustomization.yaml" git -C "$WORK" commit -m "$MSG" # Push with rebase-on-conflict retry: concurrent CI from another image # repo could have pushed since our clone. ATTEMPTS=0 until git -C "$WORK" push origin main; do ATTEMPTS=$((ATTEMPTS + 1)) if [ "$ATTEMPTS" -ge 3 ]; then echo "FATAL: push to apps repo failed after ${ATTEMPTS} attempts" exit 1 fi echo "push rejected, attempt ${ATTEMPTS}; rebasing and retrying" git -C "$WORK" pull --rebase origin main sleep $((ATTEMPTS * 2)) done echo "deployed ${NAME}:${TAG}"