From 511259e8301c21f4c63372c6e31807e8ceb41db0 Mon Sep 17 00:00:00 2001 From: Donavan Fritz Date: Wed, 6 May 2026 08:07:23 -0500 Subject: [PATCH] initial: action/image-deploy @v1 --- README.md | 60 ++++++++++++++++++++++++++++++++++++ action.yaml | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 README.md create mode 100644 action.yaml diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ded8ab --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# action/image-deploy + +Composite Gitea Action that pins an image tag in `fritzlab/apps` via +`kustomize edit set image`, validates the rendered manifests, and pushes +to apps-repo `main`. Retries on push conflict. + +This is the standard "deploy" step for image producers (chrony, profiles, +runner). After `image-build` + `image-push`, this writes the new tag into +the GitOps target so ArgoCD can sync it. + +## Usage + +```yaml +- uses: actions/checkout@v4 +- uses: https://code.fritzlab.net/action/image-build@v1 + with: + image: code.fritzlab.net/fritzlab/chrony + smoke-test: docker run --rm --entrypoint /usr/sbin/chronyd $IMAGE -v +- uses: https://code.fritzlab.net/action/image-push@v1 + with: + image: code.fritzlab.net/fritzlab/chrony + token: ${{ secrets.CI_BOT_TOKEN }} + org: fritzlab + name: chrony +- uses: https://code.fritzlab.net/action/image-deploy@v1 + with: + image: code.fritzlab.net/fritzlab/chrony + path: sjc001/infra/chrony/manifests + token: ${{ secrets.CI_BOT_TOKEN }} +``` + +## Inputs + +| Name | Required | Default | Description | +|---|---|---|---| +| `image` | yes | — | Full image name without tag. Must match an entry already in the target `kustomization.yaml` `images:` block. | +| `tag` | no | `github.run_number` | Tag to pin. | +| `path` | yes | — | Path inside `fritzlab/apps` to the manifests dir (e.g. `sjc001/infra/chrony/manifests`). | +| `token` | yes | — | `CI_BOT_TOKEN` with write access to `fritzlab/apps`. | +| `apps-repo` | no | `code.fritzlab.net/fritzlab/apps` | Apps repo URL without protocol. | +| `message` | no | `deploy #` | Commit message override. | + +## Behavior + +1. Shallow-clone `fritzlab/apps` to a temp dir. +2. `cd ` and run `kustomize edit set image =:`. +3. Run `kustomize build .` to validate the manifests still render. **Fails the + workflow if validation breaks** — apps repo is left untouched. +4. If no diff (apps repo already on this tag): exit 0 silently. +5. Otherwise commit + push to `main`. On push rejection (concurrent CI race), + `git pull --rebase` and retry up to 3 times with linear backoff. + +## Notes + +- The image entry must already exist in `kustomization.yaml`. This action only + updates the tag; it does not add or remove image entries. To switch image + registry paths, edit `kustomization.yaml` by hand once, commit, then let CI + resume. +- `git diff --quiet` skip means re-running the same CI run (or an earlier run) + is idempotent. diff --git a/action.yaml b/action.yaml new file mode 100644 index 0000000..5fdf517 --- /dev/null +++ b/action.yaml @@ -0,0 +1,87 @@ +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 + + 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}"