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