From 43875de6b4165669cfc235d370ca333f66118d32 Mon Sep 17 00:00:00 2001 From: Donavan Fritz Date: Wed, 6 May 2026 08:07:33 -0500 Subject: [PATCH] initial: action/cascade-from @v1 --- README.md | 61 ++++++++++++++++++++++++++++++++++ action.yaml | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 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..f11b3ae --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# action/cascade-from + +Composite Gitea Action that bumps a `FROM :` line in a target +repo's Dockerfile, then commits and pushes. The target repo's CI fires on +that push, rebuilding against the new base. + +The only consumer today is `fritzlab/base` triggering a rebuild of +`fritzlab/runner` whenever the base image changes. This action is the +generalization of that pattern. + +## Usage + +```yaml +- uses: actions/checkout@v4 +- uses: https://code.fritzlab.net/action/image-build@v1 + with: + image: code.fritzlab.net/fritzlab/base +- uses: https://code.fritzlab.net/action/image-push@v1 + with: + image: code.fritzlab.net/fritzlab/base + token: ${{ secrets.CI_BOT_TOKEN }} + org: fritzlab + name: base +- uses: https://code.fritzlab.net/action/cascade-from@v1 + with: + target-repo: fritzlab/runner + image: code.fritzlab.net/fritzlab/base + token: ${{ secrets.CI_BOT_TOKEN }} +``` + +## Inputs + +| Name | Required | Default | Description | +|---|---|---|---| +| `target-repo` | yes | — | Target repo to edit (e.g. `fritzlab/runner`). | +| `image` | yes | — | Image to look for in the FROM line. | +| `tag` | no | `github.run_number` | New tag to write into FROM. | +| `file` | no | `Dockerfile` | File inside target-repo to edit. | +| `token` | yes | — | `CI_BOT_TOKEN` with write to target-repo. | +| `host` | no | `code.fritzlab.net` | Gitea host without protocol. | +| `message` | no | `bump to #` | Commit message override. | + +## Behavior + +1. Shallow-clone target-repo to a temp dir. +2. `sed -i "s|^FROM :.*|FROM :|" `. +3. Verify sed actually matched a line — fail if not (catches typos in image + name). +4. If no diff (target already on this tag): exit 0 silently. +5. Otherwise commit + push to `main` with rebase-on-conflict retry up to + 3 times. + +## Notes + +- Only matches lines starting with `FROM :` (anchored to start). Multi-stage + Dockerfiles with a non-anchored `FROM :tag AS stage` will be missed — + add the AS-aware pattern as a future enhancement if needed. +- Pushing to target-repo triggers its CI, which produces a new image of its + own. There is no end-to-end orchestration: the upstream repo's CI completes + the moment cascade-from pushes, regardless of whether the downstream build + succeeds. diff --git a/action.yaml b/action.yaml new file mode 100644 index 0000000..6134542 --- /dev/null +++ b/action.yaml @@ -0,0 +1,96 @@ +name: Cascade FROM bump +description: | + Bump a `FROM :` line in a target repo's Dockerfile to trigger that + repo's CI. Used when a base image rebuild needs to cascade rebuilds in + downstream image repos. +inputs: + target-repo: + description: Target repo to edit (e.g. fritzlab/runner) + required: true + image: + description: Image referenced in the target file's FROM line (e.g. code.fritzlab.net/fritzlab/base) + required: true + tag: + description: New tag. Defaults to github.run_number when empty. + required: false + default: '' + file: + description: File path inside target-repo to edit + required: false + default: Dockerfile + token: + description: CI_BOT_TOKEN with write access to target-repo + required: true + host: + description: Gitea host (without protocol) + required: false + default: code.fritzlab.net + message: + description: Commit message override. Defaults to "bump to #". + required: false + default: '' +runs: + using: composite + steps: + - name: Clone, edit, push + shell: bash + env: + TARGET_REPO: ${{ inputs.target-repo }} + IMAGE: ${{ inputs.image }} + TAG_INPUT: ${{ inputs.tag }} + FILE: ${{ inputs.file }} + TOKEN: ${{ inputs.token }} + HOST: ${{ inputs.host }} + MESSAGE: ${{ inputs.message }} + RUN_NUMBER: ${{ github.run_number }} + run: | + set -euo pipefail + + TAG="${TAG_INPUT:-$RUN_NUMBER}" + NAME="$(basename "$IMAGE")" + MSG="${MESSAGE:-bump ${NAME} to #${TAG}}" + + WORK="$(mktemp -d)" + trap 'rm -rf "$WORK"' EXIT + + git clone --depth 1 "https://ci-bot:${TOKEN}@${HOST}/${TARGET_REPO}.git" "$WORK" + cd "$WORK" + git config user.name ci-bot + git config user.email ci-bot@fritzlab.net + + if [ ! -f "$FILE" ]; then + echo "FATAL: ${FILE} not found in ${TARGET_REPO}" + exit 1 + fi + + # Escape forward slashes for sed pattern + ESC_IMAGE="${IMAGE//\//\\/}" + sed -i "s|^FROM ${IMAGE}:.*|FROM ${IMAGE}:${TAG}|" "$FILE" + + if ! grep -q "^FROM ${IMAGE}:${TAG}\b" "$FILE"; then + echo "FATAL: sed did not match a FROM line for ${IMAGE} in ${FILE}" + grep "^FROM" "$FILE" || true + exit 1 + fi + + if git diff --quiet; then + echo "${TARGET_REPO} already on ${NAME}:${TAG}, skipping" + exit 0 + fi + + git add "$FILE" + git commit -m "$MSG" + + ATTEMPTS=0 + until git push origin main; do + ATTEMPTS=$((ATTEMPTS + 1)) + if [ "$ATTEMPTS" -ge 3 ]; then + echo "FATAL: push to ${TARGET_REPO} failed after ${ATTEMPTS} attempts" + exit 1 + fi + echo "push rejected, attempt ${ATTEMPTS}; rebasing and retrying" + git pull --rebase origin main + sleep $((ATTEMPTS * 2)) + done + + echo "cascaded ${NAME}:${TAG} → ${TARGET_REPO}"