Canonical shell-only composite actions: deploy-pages, docker-publish, forgejo-release
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
8b0ef6d709
4 changed files with 301 additions and 0 deletions
84
README.md
Normal file
84
README.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# ci-actions
|
||||||
|
|
||||||
|
Canonical, self-contained CI building blocks shared across the repos on this
|
||||||
|
Forgejo instance. Every action is a **composite action implemented in plain
|
||||||
|
shell** — none of them fetch or run a third-party marketplace action, so the
|
||||||
|
runner only ever executes code from this repo.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The publishing steps across the repos were copy-pasted variants of two things:
|
||||||
|
copying a built site into the `/pages` mount, and building + pushing a Docker
|
||||||
|
image to the Forgejo registry (plus the occasional release upload). These
|
||||||
|
actions are the single, vetted implementation of each.
|
||||||
|
|
||||||
|
## How repos reference these
|
||||||
|
|
||||||
|
Because this Forgejo instance is served under a `/git/` sub-path,
|
||||||
|
`DEFAULT_ACTIONS_URL` points at `code.forgejo.org` (so bare `actions/checkout`
|
||||||
|
keeps working). To pull an action from *this* instance, reference it by the
|
||||||
|
runner-internal URL:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: http://forgejo:3000/andras/ci-actions/docker-publish@main
|
||||||
|
```
|
||||||
|
|
||||||
|
The runner (which can reach `forgejo:3000`) fetches it; nothing external is
|
||||||
|
involved. Pin `@main` for instant propagation, or a tag for stability.
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
### `deploy-pages`
|
||||||
|
rsync a built directory into `/pages/<target>`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: http://forgejo:3000/andras/ci-actions/deploy-pages@main
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
with:
|
||||||
|
source: dist/ # contents are synced
|
||||||
|
target: photos # -> /pages/photos
|
||||||
|
# delete: "true" # default; set "false" to keep extra files
|
||||||
|
```
|
||||||
|
|
||||||
|
### `docker-publish`
|
||||||
|
Build and push an image to the Forgejo container registry. Computes tags from
|
||||||
|
the git ref: `sha-<short>` always, `latest` on the default branch, and
|
||||||
|
`vX.Y.Z` / `X.Y.Z` / `X.Y` / `X` on semver tags.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: http://forgejo:3000/andras/ci-actions/docker-publish@main
|
||||||
|
with:
|
||||||
|
context: ./backend # default "."
|
||||||
|
# dockerfile: path/Dockerfile
|
||||||
|
# image-suffix: -server # -> <host>/<owner>/<repo>-server
|
||||||
|
# build-args: |
|
||||||
|
# BASE_HREF=/towers/
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
token: ${{ secrets.FORGEJO_PACKAGE_TOKEN }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Single-architecture build (no buildx multi-arch / registry buildcache).
|
||||||
|
- An idempotent `docker` CLI install runs if the runner image lacks it.
|
||||||
|
|
||||||
|
### `forgejo-release`
|
||||||
|
Create a release via the Forgejo API and upload assets.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Moving "latest" prerelease on default-branch pushes:
|
||||||
|
- uses: http://forgejo:3000/andras/ci-actions/forgejo-release@main
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.FORGEJO_PACKAGE_TOKEN }}
|
||||||
|
files: dist/release
|
||||||
|
tag: latest
|
||||||
|
prerelease: "true"
|
||||||
|
override: "true"
|
||||||
|
|
||||||
|
# Stable release on a tag push (tag defaults to the pushed ref):
|
||||||
|
- uses: http://forgejo:3000/andras/ci-actions/forgejo-release@main
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.FORGEJO_PACKAGE_TOKEN }}
|
||||||
|
files: dist/release
|
||||||
|
```
|
||||||
33
deploy-pages/action.yml
Normal file
33
deploy-pages/action.yml
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
name: Deploy to pages mount
|
||||||
|
description: >-
|
||||||
|
rsync a built directory into the host /pages mount that nginx serves.
|
||||||
|
Pure shell — depends on no marketplace actions.
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
source:
|
||||||
|
description: Source directory whose *contents* are synced (a trailing slash is added automatically).
|
||||||
|
required: true
|
||||||
|
target:
|
||||||
|
description: Subdirectory under /pages to deploy into, e.g. "photos" -> /pages/photos.
|
||||||
|
required: true
|
||||||
|
delete:
|
||||||
|
description: Delete files in the target that are absent from the source ("true"/"false").
|
||||||
|
default: "true"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: rsync to /pages
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
SOURCE: ${{ inputs.source }}
|
||||||
|
TARGET: ${{ inputs.target }}
|
||||||
|
DELETE: ${{ inputs.delete }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
command -v rsync >/dev/null 2>&1 || { apt-get update && apt-get install -y --no-install-recommends rsync; }
|
||||||
|
del=""
|
||||||
|
[ "$DELETE" = "true" ] && del="--delete"
|
||||||
|
# Trailing slash on the source => copy its contents, not the dir itself.
|
||||||
|
src="${SOURCE%/}/"
|
||||||
|
rsync -a $del --mkpath "$src" "/pages/${TARGET}/"
|
||||||
106
docker-publish/action.yml
Normal file
106
docker-publish/action.yml
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
name: Build & publish Docker image
|
||||||
|
description: >-
|
||||||
|
Build a Docker image and push it to this Forgejo instance's container
|
||||||
|
registry. Reimplements docker/login-action + docker/metadata-action +
|
||||||
|
docker/build-push-action in plain shell using the docker CLI — no
|
||||||
|
marketplace actions are fetched or run.
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
context:
|
||||||
|
description: Docker build context.
|
||||||
|
default: "."
|
||||||
|
dockerfile:
|
||||||
|
description: Path to the Dockerfile (relative to the workspace). Empty = <context>/Dockerfile.
|
||||||
|
default: ""
|
||||||
|
image-suffix:
|
||||||
|
description: Appended to the derived image name <host>/<owner>/<repo>, e.g. "-server".
|
||||||
|
default: ""
|
||||||
|
build-args:
|
||||||
|
description: Newline-separated KEY=VALUE pairs passed as --build-arg.
|
||||||
|
default: ""
|
||||||
|
push:
|
||||||
|
description: Whether to log in and push ("true"/"false"). Set false for pull-request builds.
|
||||||
|
default: "true"
|
||||||
|
token:
|
||||||
|
description: Registry password/token (e.g. secrets.FORGEJO_PACKAGE_TOKEN).
|
||||||
|
required: true
|
||||||
|
registry-host:
|
||||||
|
description: Override the registry host. Defaults to github.server_url (vars.CONTAINER_REGISTRY_HOST is honoured if set in the env).
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Build and publish
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
INPUT_CONTEXT: ${{ inputs.context }}
|
||||||
|
INPUT_DOCKERFILE: ${{ inputs.dockerfile }}
|
||||||
|
INPUT_IMAGE_SUFFIX: ${{ inputs.image-suffix }}
|
||||||
|
INPUT_BUILD_ARGS: ${{ inputs.build-args }}
|
||||||
|
INPUT_PUSH: ${{ inputs.push }}
|
||||||
|
INPUT_TOKEN: ${{ inputs.token }}
|
||||||
|
INPUT_REGISTRY_HOST: ${{ inputs.registry-host }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
OWNER: ${{ github.repository_owner }}
|
||||||
|
SERVER_URL: ${{ github.server_url }}
|
||||||
|
SHA: ${{ github.sha }}
|
||||||
|
REF: ${{ github.ref }}
|
||||||
|
REF_TYPE: ${{ github.ref_type }}
|
||||||
|
REF_NAME: ${{ github.ref_name }}
|
||||||
|
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- Ensure the docker CLI exists (idempotent; the node:* runner image may lack it) ---
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
ARCH="$(uname -m)"
|
||||||
|
curl -fsSL --retry 3 --retry-connrefused \
|
||||||
|
"https://download.docker.com/linux/static/stable/${ARCH}/docker-27.5.1.tgz" \
|
||||||
|
| tar xz --strip-components=1 -C /usr/local/bin docker/docker
|
||||||
|
fi
|
||||||
|
docker --version
|
||||||
|
|
||||||
|
# --- Resolve the registry host (canonical form of the block copy-pasted across repos) ---
|
||||||
|
host="${INPUT_REGISTRY_HOST:-${CONTAINER_REGISTRY_HOST:-$SERVER_URL}}"
|
||||||
|
host="${host#https://}"; host="${host#http://}"; host="${host%/}"
|
||||||
|
# The job's docker daemon reaches the registry via the host-published port, not the
|
||||||
|
# internal service name; this is the same remap the old inline scripts performed.
|
||||||
|
[ "$host" = "forgejo:3000" ] && host="127.0.0.1:13000"
|
||||||
|
repo="$(printf '%s' "$REPO" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
image="${host}/${repo}${INPUT_IMAGE_SUFFIX}"
|
||||||
|
echo "Target image: ${image}"
|
||||||
|
|
||||||
|
# --- Compute tags from the git ref (replaces docker/metadata-action) ---
|
||||||
|
tags=( -t "${image}:sha-$(printf '%s' "$SHA" | cut -c1-12)" )
|
||||||
|
if [ "$REF" = "refs/heads/${DEFAULT_BRANCH}" ]; then
|
||||||
|
tags+=( -t "${image}:latest" )
|
||||||
|
fi
|
||||||
|
if [ "$REF_TYPE" = "tag" ]; then
|
||||||
|
tags+=( -t "${image}:${REF_NAME}" )
|
||||||
|
if printf '%s' "$REF_NAME" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||||
|
v="${REF_NAME#v}"
|
||||||
|
tags+=( -t "${image}:${v}" -t "${image}:${v%.*}" -t "${image}:${v%%.*}" )
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Assemble extra build flags ---
|
||||||
|
extra=()
|
||||||
|
[ -n "${INPUT_DOCKERFILE}" ] && extra+=( -f "${INPUT_DOCKERFILE}" )
|
||||||
|
while IFS= read -r a; do
|
||||||
|
[ -n "$a" ] && extra+=( --build-arg "$a" )
|
||||||
|
done <<< "${INPUT_BUILD_ARGS}"
|
||||||
|
|
||||||
|
# --- Build ---
|
||||||
|
docker build "${extra[@]}" \
|
||||||
|
--label "org.opencontainers.image.source=${SERVER_URL}/${REPO}" \
|
||||||
|
--label "org.opencontainers.image.revision=${SHA}" \
|
||||||
|
--label "org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
|
||||||
|
"${tags[@]}" \
|
||||||
|
"${INPUT_CONTEXT}"
|
||||||
|
|
||||||
|
# --- Push (skipped on pull requests by passing push: false) ---
|
||||||
|
if [ "${INPUT_PUSH}" = "true" ]; then
|
||||||
|
printf '%s' "${INPUT_TOKEN}" | docker login "$host" -u "$OWNER" --password-stdin
|
||||||
|
docker push --all-tags "$image"
|
||||||
|
fi
|
||||||
78
forgejo-release/action.yml
Normal file
78
forgejo-release/action.yml
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
name: Publish a Forgejo release
|
||||||
|
description: >-
|
||||||
|
Create (or replace) a release on this Forgejo instance and upload assets,
|
||||||
|
using the Forgejo API via curl. Reimplements forgejo-release / the
|
||||||
|
hand-rolled curl uploads — no marketplace actions.
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
token:
|
||||||
|
description: API token with write access (e.g. secrets.FORGEJO_PACKAGE_TOKEN or the workflow token).
|
||||||
|
required: true
|
||||||
|
files:
|
||||||
|
description: A directory (all regular files within are uploaded) or a glob of asset files.
|
||||||
|
required: true
|
||||||
|
tag:
|
||||||
|
description: Release tag. Defaults to the pushed ref name (github.ref_name).
|
||||||
|
default: ""
|
||||||
|
draft:
|
||||||
|
description: Create the release as a draft ("true"/"false").
|
||||||
|
default: "false"
|
||||||
|
prerelease:
|
||||||
|
description: Mark the release as a prerelease ("true"/"false").
|
||||||
|
default: "false"
|
||||||
|
override:
|
||||||
|
description: >-
|
||||||
|
Delete any existing release (and its tag) with the same tag first, then recreate it
|
||||||
|
at the current commit. Use for moving tags such as "latest".
|
||||||
|
default: "false"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Create release and upload assets
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ inputs.token }}
|
||||||
|
FILES: ${{ inputs.files }}
|
||||||
|
IN_TAG: ${{ inputs.tag }}
|
||||||
|
DRAFT: ${{ inputs.draft }}
|
||||||
|
PRERELEASE: ${{ inputs.prerelease }}
|
||||||
|
OVERRIDE: ${{ inputs.override }}
|
||||||
|
SERVER_URL: ${{ github.server_url }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
REF_NAME: ${{ github.ref_name }}
|
||||||
|
SHA: ${{ github.sha }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
command -v jq >/dev/null 2>&1 || { apt-get update && apt-get install -y --no-install-recommends jq curl; }
|
||||||
|
|
||||||
|
tag="${IN_TAG:-$REF_NAME}"
|
||||||
|
api="${SERVER_URL}/api/v1/repos/${REPO}"
|
||||||
|
auth=(-H "Authorization: token ${TOKEN}")
|
||||||
|
|
||||||
|
# Override: remove an existing release+tag so a moving tag (e.g. latest) is recreated cleanly.
|
||||||
|
if [ "$OVERRIDE" = "true" ]; then
|
||||||
|
rid="$(curl -fsS "${auth[@]}" "${api}/releases/tags/${tag}" 2>/dev/null | jq -r '.id // empty' || true)"
|
||||||
|
[ -n "$rid" ] && curl -fsS -X DELETE "${auth[@]}" "${api}/releases/${rid}" >/dev/null 2>&1 || true
|
||||||
|
curl -fsS -X DELETE "${auth[@]}" "${api}/tags/${tag}" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create the release. target_commitish pins a not-yet-existing tag to this commit.
|
||||||
|
payload="$(jq -nc --arg t "$tag" --arg c "$SHA" \
|
||||||
|
--argjson d "$DRAFT" --argjson p "$PRERELEASE" \
|
||||||
|
'{tag_name:$t, name:$t, target_commitish:$c, draft:$d, prerelease:$p}')"
|
||||||
|
rid="$(curl -fsS -X POST "${auth[@]}" -H "Content-Type: application/json" \
|
||||||
|
"${api}/releases" -d "$payload" | jq -r '.id')"
|
||||||
|
echo "Created release ${tag} (id ${rid})"
|
||||||
|
|
||||||
|
# Collect assets: a directory expands to its contents, otherwise treat the input as a glob.
|
||||||
|
shopt -s nullglob
|
||||||
|
if [ -d "$FILES" ]; then set -- "$FILES"/*; else set -- $FILES; fi
|
||||||
|
for f in "$@"; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
name="$(basename "$f")"
|
||||||
|
curl -fsS -X POST "${auth[@]}" \
|
||||||
|
"${api}/releases/${rid}/assets?name=${name}" \
|
||||||
|
-F "attachment=@${f};type=application/octet-stream" >/dev/null
|
||||||
|
echo "uploaded ${name}"
|
||||||
|
done
|
||||||
Loading…
Add table
Add a link
Reference in a new issue