Adjust build pipeline
This commit is contained in:
+229
-187
@@ -3,12 +3,17 @@ name: Build image
|
||||
# Reusable workflow. Builds a Docker image from the caller repo and pushes
|
||||
# it to the Gitea container registry at `gitea.alexandru.macocian.me`.
|
||||
#
|
||||
# Companion to `deploy-stack.yml`: the source repos (`amacocian/<name>`)
|
||||
# produce images here; the deployment repos (`Charlie/<stack>`) consume
|
||||
# them via the `image:` field in their compose files and call
|
||||
# `deploy-stack.yml` to roll out.
|
||||
# Architecture mirrors `deploy-stack.yml`: the runner is stateless and
|
||||
# every operation happens on the target build host as `sys-gitea-runner`
|
||||
# (LDAP, member of `charlie-deploy`) over a short-lived step-ca-issued
|
||||
# SSH user cert. The runner container itself does not have a docker
|
||||
# socket — `docker build` / `docker push` run on the build host's daemon.
|
||||
#
|
||||
# Caller pattern (from a source repo, e.g. amacocian/newsletter2atom):
|
||||
# Companion to `deploy-stack.yml`: source repos produce images here;
|
||||
# deployment repos (`Charlie/<stack>`) consume them via the `image:`
|
||||
# field in their compose files and call `deploy-stack.yml` to roll out.
|
||||
#
|
||||
# Caller pattern (from a source repo):
|
||||
#
|
||||
# on:
|
||||
# push:
|
||||
@@ -16,7 +21,9 @@ name: Build image
|
||||
# workflow_dispatch:
|
||||
# jobs:
|
||||
# build:
|
||||
# uses: https://sys-gitea-runner:${{ secrets.RUNNER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/workflows/build-image.yml@main
|
||||
# uses: https://<caller-user>:${{ secrets.CALLER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/workflows/build-image.yml@main
|
||||
# with:
|
||||
# host: melina
|
||||
# secrets: inherit
|
||||
#
|
||||
# Tags pushed on every successful build:
|
||||
@@ -27,21 +34,39 @@ name: Build image
|
||||
# via the `extra-tags` input — one per line. Empty lines / `#` comments
|
||||
# are ignored.
|
||||
#
|
||||
# Required caller secrets — both `sys-gitea-runner` PATs. Set once at the
|
||||
# owning user/org level (Settings → Actions → Secrets) so every source
|
||||
# repo under that namespace inherits them via `secrets: inherit`:
|
||||
# Required caller secrets — both belong to the *caller's* Gitea user, NOT
|
||||
# `sys-gitea-runner`. Set once at the owning user/org level (Settings →
|
||||
# Actions → Secrets) so every source repo under that namespace inherits
|
||||
# them via `secrets: inherit`:
|
||||
#
|
||||
# RUNNER_REPO_PAT `read:repository` -- fetch this workflow file
|
||||
# + clone the caller source
|
||||
# RUNNER_REGISTRY_PAT `write:package` -- docker login + push to the
|
||||
# Gitea container registry
|
||||
# CALLER_REPO_PAT `read:repository` -- fetch this workflow file,
|
||||
# clone the caller source on
|
||||
# the build host
|
||||
# CALLER_REGISTRY_PAT `write:package` -- docker login + push on the
|
||||
# build host
|
||||
#
|
||||
# Kept as two distinct PATs so the high-privilege package-write token is
|
||||
# never used for git over HTTPS (and vice versa).
|
||||
# The caller user must be a Read collaborator on `Charlie/project-charlie`
|
||||
# (or a Read member of the `Charlie` org) so its PAT can fetch this
|
||||
# workflow file via the absolute-URL `uses:` reference.
|
||||
#
|
||||
# Two distinct PATs so the high-privilege package-write token is never
|
||||
# used for git over HTTPS (and vice versa).
|
||||
#
|
||||
# `sys-gitea-runner` is still used as the SSH identity into the build
|
||||
# host (short-lived step-ca user cert) — that's a separate identity
|
||||
# layer, unrelated to these PATs.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
host:
|
||||
description: |
|
||||
Hostname this build runs on. The runner SSHes in here as
|
||||
`sys-gitea-runner` and invokes `docker build` / `docker push`
|
||||
against the host's docker daemon. Any host with docker + the
|
||||
standard `charlie-deploy` setup qualifies.
|
||||
required: true
|
||||
type: string
|
||||
image:
|
||||
description: |
|
||||
Fully-qualified image name without tag, e.g.
|
||||
@@ -65,8 +90,7 @@ on:
|
||||
build-args:
|
||||
description: |
|
||||
Multi-line `KEY=VALUE` list passed as `--build-arg`. Empty lines
|
||||
and `#` comments are ignored. Values containing `=` are preserved
|
||||
as-is after the first `=`.
|
||||
and `#` comments are ignored.
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
@@ -91,46 +115,58 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
registry-user:
|
||||
description: |
|
||||
Username used for `docker login`. Must own (or be a collaborator
|
||||
with `write:package` on) the target image namespace, and must
|
||||
match the user the `CALLER_REGISTRY_PAT` PAT belongs to.
|
||||
Defaults to the caller repo's owner (`github.repository_owner`),
|
||||
which is correct for personal-namespace builds.
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
secrets:
|
||||
RUNNER_REPO_PAT:
|
||||
CALLER_REPO_PAT:
|
||||
description: |
|
||||
`sys-gitea-runner` PAT with `read:repository`. Used in the
|
||||
absolute-URL `uses:` form to fetch this workflow file and to
|
||||
clone the caller's source inside the job. Same name and
|
||||
contract as the secret consumed by `deploy-stack.yml`.
|
||||
Caller-owned PAT with `read:repository`. Used for:
|
||||
- the absolute-URL `uses:` reference fetching this workflow
|
||||
+ its sibling composite actions from `Charlie/project-charlie`
|
||||
(caller user must be a Read collaborator there);
|
||||
- cloning the caller's source on the build host.
|
||||
required: true
|
||||
RUNNER_REGISTRY_PAT:
|
||||
CALLER_REGISTRY_PAT:
|
||||
description: |
|
||||
`sys-gitea-runner` PAT with `write:package`. Used by
|
||||
`docker login` against `gitea.alexandru.macocian.me`. Kept
|
||||
separate from `RUNNER_REPO_PAT` so the package-write PAT is
|
||||
Caller-owned PAT with `write:package` on the target image
|
||||
namespace. Used by `docker login` on the build host. Kept
|
||||
separate from `CALLER_REPO_PAT` so the package-write PAT is
|
||||
never exposed during git clone (and vice versa).
|
||||
required: true
|
||||
outputs:
|
||||
image:
|
||||
description: Full image reference without tag (e.g. `gitea.../amacocian/foo`).
|
||||
description: Full image reference without tag (e.g. `gitea.../foo/bar`).
|
||||
value: ${{ jobs.build.outputs.image }}
|
||||
timestamp-tag:
|
||||
description: The UTC timestamp tag pushed for this build.
|
||||
value: ${{ jobs.build.outputs.timestamp-tag }}
|
||||
digest:
|
||||
description: The image digest reported by `docker push` (sha256:...).
|
||||
value: ${{ jobs.build.outputs.digest }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# `self-hosted` is the only label requirement. Routing to the
|
||||
# build host happens via SSH inside the job, not via Gitea's runner
|
||||
# picker. Sidesteps the reusable-workflow `runs-on` matching bug
|
||||
# (Gitea #32348). Same model as `deploy-stack.yml`.
|
||||
runs-on: self-hosted
|
||||
outputs:
|
||||
image: ${{ steps.resolve.outputs.image }}
|
||||
timestamp-tag: ${{ steps.resolve.outputs.timestamp-tag }}
|
||||
digest: ${{ steps.push.outputs.digest }}
|
||||
steps:
|
||||
- name: Resolve image, ref, and timestamp
|
||||
- name: Resolve image, ref, timestamp, workdir
|
||||
id: resolve
|
||||
shell: bash
|
||||
env:
|
||||
IMAGE_INPUT: ${{ inputs.image }}
|
||||
REF_INPUT: ${{ inputs.ref }}
|
||||
REGISTRY_USER_INPUT: ${{ inputs.registry-user }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
image="$IMAGE_INPUT"
|
||||
@@ -145,169 +181,175 @@ jobs:
|
||||
if [ -z "$ref" ]; then
|
||||
ref="${GITHUB_REF_NAME:-main}"
|
||||
fi
|
||||
# UTC ISO-ish, filename-/tag-safe: 2026-05-22T095126Z
|
||||
registry_user="$REGISTRY_USER_INPUT"
|
||||
if [ -z "$registry_user" ]; then
|
||||
registry_user="${GITHUB_REPOSITORY_OWNER}"
|
||||
fi
|
||||
# UTC ISO-ish, filename-/tag-safe: 2026-05-22T130927Z
|
||||
ts="$(date -u +%Y-%m-%dT%H%M%SZ)"
|
||||
echo "image=$image" >> "$GITHUB_OUTPUT"
|
||||
echo "ref=$ref" >> "$GITHUB_OUTPUT"
|
||||
echo "timestamp-tag=$ts" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::building ${image}:${ts} (+ latest) from ref ${ref}"
|
||||
# Per-run scratch dir on the build host. /tmp is fine — the
|
||||
# tree is wiped at job end and only needs to survive the SSH
|
||||
# session. GITHUB_RUN_ID is unique per workflow run on this
|
||||
# Gitea instance, so parallel builds of the same repo to the
|
||||
# same host don't collide.
|
||||
workdir="/tmp/charlie-build-${GITHUB_RUN_ID:-manual-$$}"
|
||||
echo "image=$image" >> "$GITHUB_OUTPUT"
|
||||
echo "ref=$ref" >> "$GITHUB_OUTPUT"
|
||||
echo "registry-user=$registry_user" >> "$GITHUB_OUTPUT"
|
||||
echo "timestamp-tag=$ts" >> "$GITHUB_OUTPUT"
|
||||
echo "workdir=$workdir" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::building ${image}:${ts} (+ latest) from ref ${ref} on ${{ inputs.host }} as ${registry_user}"
|
||||
|
||||
- name: Check out source
|
||||
shell: bash
|
||||
env:
|
||||
REF: ${{ steps.resolve.outputs.ref }}
|
||||
REPO: ${{ github.repository }}
|
||||
REPO_PAT: ${{ secrets.RUNNER_REPO_PAT }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Manual clone — sidesteps the question of which `actions/checkout`
|
||||
# mirror this Gitea instance proxies. Same one-shot auth pattern
|
||||
# as deploy-stack.yml's auto-provisioning step: PAT lives only in
|
||||
# an `http.extraheader` for this single clone, never persisted to
|
||||
# `.git/config`.
|
||||
rm -rf src
|
||||
auth=$(printf '%s' "sys-gitea-runner:${REPO_PAT}" | base64 -w0)
|
||||
git -c "http.extraheader=Authorization: Basic ${auth}" \
|
||||
clone --depth=1 --branch "${REF}" \
|
||||
"https://gitea.alexandru.macocian.me/${REPO}.git" src
|
||||
- name: Open SSH session
|
||||
uses: https://${{ github.repository_owner }}:${{ secrets.CALLER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/actions/charlie-ssh-open@main
|
||||
with:
|
||||
host: ${{ inputs.host }}
|
||||
|
||||
- name: Provision build workdir + clone caller source
|
||||
uses: https://${{ github.repository_owner }}:${{ secrets.CALLER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/actions/charlie-ssh-target@main
|
||||
with:
|
||||
# Auth model mirrors deploy-stack.yml's auto-provisioning step:
|
||||
# the PAT is passed as a one-shot `http.extraHeader` for the
|
||||
# clone only. It is NOT written to `.git/config`, so nothing
|
||||
# persists on the host after this job. Gitea masks
|
||||
# `${{ secrets.* }}` in workflow logs; the base64-encoded value
|
||||
# is briefly visible in the host's process list while git runs.
|
||||
# Acceptable on the intranet-only homelab.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
workdir="${{ steps.resolve.outputs.workdir }}"
|
||||
ref="${{ steps.resolve.outputs.ref }}"
|
||||
repo="${{ github.repository }}"
|
||||
# Defensive: a leftover dir from a crashed prior run would
|
||||
# make `git clone` refuse. Re-running the workflow with the
|
||||
# same RUN_ID is impossible (Gitea allocates per run) but
|
||||
# being explicit costs nothing.
|
||||
rm -rf "$workdir"
|
||||
mkdir -p "$(dirname "$workdir")"
|
||||
auth=$(printf '%s' '${{ github.repository_owner }}:${{ secrets.CALLER_REPO_PAT }}' | base64 -w0)
|
||||
git -c "http.extraheader=Authorization: Basic ${auth}" \
|
||||
clone --depth=1 --branch "$ref" \
|
||||
"https://gitea.alexandru.macocian.me/${repo}.git" \
|
||||
"$workdir"
|
||||
|
||||
- name: Log in to Gitea container registry
|
||||
shell: bash
|
||||
env:
|
||||
REGISTRY_PAT: ${{ secrets.RUNNER_REGISTRY_PAT }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# `docker login --password-stdin` keeps the PAT out of the
|
||||
# process list. Gitea masks `${{ secrets.* }}` in logs.
|
||||
printf '%s' "$REGISTRY_PAT" | \
|
||||
docker login gitea.alexandru.macocian.me \
|
||||
--username sys-gitea-runner \
|
||||
--password-stdin
|
||||
|
||||
- name: Assemble tag and build-arg lists
|
||||
id: args
|
||||
shell: bash
|
||||
env:
|
||||
IMAGE: ${{ steps.resolve.outputs.image }}
|
||||
TS_TAG: ${{ steps.resolve.outputs.timestamp-tag }}
|
||||
EXTRA_TAGS: ${{ inputs.extra-tags }}
|
||||
BUILD_ARGS: ${{ inputs.build-args }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Tag list: always latest + timestamp. Dedupe so callers that
|
||||
# pass `latest` in extra-tags don't get a noisy double-push.
|
||||
declare -A seen=()
|
||||
tag_args=()
|
||||
add_tag() {
|
||||
local t="$1"
|
||||
[ -z "$t" ] && return 0
|
||||
if [ -z "${seen[$t]+x}" ]; then
|
||||
seen[$t]=1
|
||||
tag_args+=(-t "${IMAGE}:${t}")
|
||||
fi
|
||||
}
|
||||
add_tag "latest"
|
||||
add_tag "$TS_TAG"
|
||||
while IFS= read -r raw; do
|
||||
line="${raw%%#*}"
|
||||
line="$(printf '%s' "$line" | awk '{$1=$1;print}')"
|
||||
[ -z "$line" ] && continue
|
||||
add_tag "$line"
|
||||
done <<< "$EXTRA_TAGS"
|
||||
|
||||
# Build-args: one --build-arg per non-empty / non-comment line.
|
||||
ba_args=()
|
||||
while IFS= read -r raw; do
|
||||
line="${raw%%#*}"
|
||||
line="$(printf '%s' "$line" | awk '{$1=$1;print}')"
|
||||
[ -z "$line" ] && continue
|
||||
case "$line" in
|
||||
*=*) ba_args+=(--build-arg "$line") ;;
|
||||
*) echo "::error::invalid build-arg (missing '='): $raw"; exit 1 ;;
|
||||
esac
|
||||
done <<< "$BUILD_ARGS"
|
||||
|
||||
# Serialise both arrays into single-line, shell-quoted strings
|
||||
# so the next step can `eval set --` them safely. printf %q
|
||||
# handles spaces, quotes, glob chars, etc.
|
||||
printf '%s\n' "$(printf '%q ' "${tag_args[@]}")" > /tmp/charlie-build-tag-args
|
||||
printf '%s\n' "$(printf '%q ' "${ba_args[@]:-}")" > /tmp/charlie-build-ba-args
|
||||
|
||||
{
|
||||
echo "tag-list<<EOF"
|
||||
for t in "${!seen[@]}"; do echo " ${IMAGE}:${t}"; done
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
uses: https://${{ github.repository_owner }}:${{ secrets.CALLER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/actions/charlie-ssh-target@main
|
||||
with:
|
||||
# The PAT is interpolated into the command string here. Gitea
|
||||
# masks `${{ secrets.* }}` in workflow logs, but the literal
|
||||
# value is briefly visible in the host's process list while
|
||||
# `docker login` runs. Acceptable on the intranet-only homelab;
|
||||
# revisit if the host is ever exposed to untrusted local users.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
printf '%s' '${{ secrets.CALLER_REGISTRY_PAT }}' | \
|
||||
docker login gitea.alexandru.macocian.me \
|
||||
--username '${{ steps.resolve.outputs.registry-user }}' \
|
||||
--password-stdin
|
||||
|
||||
- name: Build image
|
||||
shell: bash
|
||||
env:
|
||||
CONTEXT: ${{ inputs.context }}
|
||||
DOCKERFILE: ${{ inputs.dockerfile }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag_args=$(cat /tmp/charlie-build-tag-args)
|
||||
ba_args=$(cat /tmp/charlie-build-ba-args)
|
||||
eval "set -- $tag_args $ba_args"
|
||||
# `--pull` keeps the base image fresh on long-lived runners.
|
||||
# No buildx / multi-arch by default — every host in the fleet
|
||||
# is linux/amd64. Add a `platforms` input + `docker buildx`
|
||||
# branch here if/when that changes.
|
||||
# Dockerfile path is interpreted relative to the build context,
|
||||
# matching the `inputs.dockerfile` doc string.
|
||||
cd src
|
||||
docker build \
|
||||
--pull \
|
||||
-f "${CONTEXT%/}/${DOCKERFILE}" \
|
||||
"$@" \
|
||||
"${CONTEXT}"
|
||||
uses: https://${{ github.repository_owner }}:${{ secrets.CALLER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/actions/charlie-ssh-target@main
|
||||
with:
|
||||
workdir: ${{ steps.resolve.outputs.workdir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
image="${{ steps.resolve.outputs.image }}"
|
||||
ts="${{ steps.resolve.outputs.timestamp-tag }}"
|
||||
context="${{ inputs.context }}"
|
||||
dockerfile="${{ inputs.dockerfile }}"
|
||||
|
||||
# Tag list: always latest + timestamp. Dedupe so callers that
|
||||
# pass `latest` in extra-tags don't get a noisy double-push.
|
||||
declare -A seen=()
|
||||
tag_args=()
|
||||
add_tag() {
|
||||
local t="$1"
|
||||
[ -z "$t" ] && return 0
|
||||
if [ -z "${seen[$t]+x}" ]; then
|
||||
seen[$t]=1
|
||||
tag_args+=(-t "${image}:${t}")
|
||||
fi
|
||||
}
|
||||
add_tag "latest"
|
||||
add_tag "$ts"
|
||||
while IFS= read -r raw; do
|
||||
line="${raw%%#*}"
|
||||
line="$(printf '%s' "$line" | awk '{$1=$1;print}')"
|
||||
[ -z "$line" ] && continue
|
||||
add_tag "$line"
|
||||
done <<EXTRA_TAGS_EOF
|
||||
${{ inputs.extra-tags }}
|
||||
EXTRA_TAGS_EOF
|
||||
|
||||
# Build-args: one --build-arg per non-empty / non-comment line.
|
||||
ba_args=()
|
||||
while IFS= read -r raw; do
|
||||
line="${raw%%#*}"
|
||||
line="$(printf '%s' "$line" | awk '{$1=$1;print}')"
|
||||
[ -z "$line" ] && continue
|
||||
case "$line" in
|
||||
*=*) ba_args+=(--build-arg "$line") ;;
|
||||
*) echo "::error::invalid build-arg (missing '='): $raw"; exit 1 ;;
|
||||
esac
|
||||
done <<BUILD_ARGS_EOF
|
||||
${{ inputs.build-args }}
|
||||
BUILD_ARGS_EOF
|
||||
|
||||
# Stash the tag list for the push step (separate SSH call,
|
||||
# so no shared shell variables). Same workdir → same host.
|
||||
: > .charlie-tags
|
||||
for t in "${!seen[@]}"; do
|
||||
printf '%s\n' "${image}:${t}" >> .charlie-tags
|
||||
done
|
||||
|
||||
# --pull keeps the base image fresh on long-lived hosts.
|
||||
# No buildx / multi-arch by default — every host in the
|
||||
# fleet is linux/amd64. Add a `platforms` input + buildx
|
||||
# branch here if/when that changes.
|
||||
docker build \
|
||||
--pull \
|
||||
-f "${context%/}/${dockerfile}" \
|
||||
"${tag_args[@]}" \
|
||||
"${ba_args[@]}" \
|
||||
"${context}"
|
||||
|
||||
- name: Push image
|
||||
id: push
|
||||
if: ${{ inputs.push }}
|
||||
shell: bash
|
||||
env:
|
||||
IMAGE: ${{ steps.resolve.outputs.image }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag_args=$(cat /tmp/charlie-build-tag-args)
|
||||
eval "set -- $tag_args"
|
||||
# `docker push -t` doesn't exist; the tag list was passed to
|
||||
# `docker build -t` already, so push each resolved tag. Walk
|
||||
# the same list by stripping the leading `-t` flags.
|
||||
tags=()
|
||||
while [ "$#" -gt 0 ]; do
|
||||
if [ "$1" = "-t" ]; then
|
||||
tags+=("$2")
|
||||
shift 2
|
||||
else
|
||||
shift
|
||||
fi
|
||||
done
|
||||
for ref in "${tags[@]}"; do
|
||||
echo "::notice::pushing ${ref}"
|
||||
docker push "${ref}"
|
||||
done
|
||||
# Capture the digest of the timestamp tag — stable, immutable
|
||||
# reference suitable for pinning in compose files.
|
||||
ts_ref="${IMAGE}:${{ steps.resolve.outputs.timestamp-tag }}"
|
||||
digest="$(docker inspect --format '{{index .RepoDigests 0}}' "${ts_ref}" 2>/dev/null | awk -F'@' '{print $2}')"
|
||||
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::pushed digest ${digest}"
|
||||
uses: https://${{ github.repository_owner }}:${{ secrets.CALLER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/actions/charlie-ssh-target@main
|
||||
with:
|
||||
workdir: ${{ steps.resolve.outputs.workdir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
while IFS= read -r ref; do
|
||||
[ -z "$ref" ] && continue
|
||||
echo "::notice::pushing ${ref}"
|
||||
docker push "${ref}"
|
||||
done < .charlie-tags
|
||||
|
||||
- name: Logout + cleanup
|
||||
- name: Cleanup build host
|
||||
if: always()
|
||||
shell: bash
|
||||
env:
|
||||
IMAGE: ${{ steps.resolve.outputs.image }}
|
||||
TS_TAG: ${{ steps.resolve.outputs.timestamp-tag }}
|
||||
run: |
|
||||
set -u
|
||||
docker logout gitea.alexandru.macocian.me >/dev/null 2>&1 || true
|
||||
# Best-effort local image GC: drop the just-built tags so the
|
||||
# runner host doesn't accumulate them forever. The registry has
|
||||
# the canonical copies. `latest` is kept because other builds /
|
||||
# caches on the host may legitimately reference it.
|
||||
docker rmi -f "${IMAGE}:${TS_TAG}" >/dev/null 2>&1 || true
|
||||
rm -f /tmp/charlie-build-tag-args /tmp/charlie-build-ba-args
|
||||
rm -rf src
|
||||
uses: https://${{ github.repository_owner }}:${{ secrets.CALLER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/actions/charlie-ssh-target@main
|
||||
with:
|
||||
run: |
|
||||
set -u
|
||||
workdir="${{ steps.resolve.outputs.workdir }}"
|
||||
image="${{ steps.resolve.outputs.image }}"
|
||||
ts="${{ steps.resolve.outputs.timestamp-tag }}"
|
||||
# Best-effort local image GC: drop the timestamp-tagged
|
||||
# image so the build host doesn't accumulate one per build.
|
||||
# The registry has the canonical copy. `latest` is kept
|
||||
# because other builds / runtime containers on the host may
|
||||
# legitimately reference it.
|
||||
docker rmi -f "${image}:${ts}" >/dev/null 2>&1 || true
|
||||
# Drop registry credentials. logout is namespaced per
|
||||
# registry, so it doesn't affect other registries the host
|
||||
# may be logged into.
|
||||
docker logout gitea.alexandru.macocian.me >/dev/null 2>&1 || true
|
||||
# Wipe the scratch tree (sources + .charlie-tags). The
|
||||
# one-shot `http.extraHeader` PAT was never persisted to
|
||||
# .git/config, so there's no credential to scrub here.
|
||||
rm -rf "$workdir"
|
||||
|
||||
- name: Close SSH session
|
||||
if: always()
|
||||
uses: https://${{ github.repository_owner }}:${{ secrets.CALLER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/actions/charlie-ssh-close@main
|
||||
|
||||
+69
-49
@@ -200,14 +200,20 @@ callers pass via `secrets: inherit`).
|
||||
|
||||
## Inputs reference — `build-image.yml`
|
||||
|
||||
Source repos under `amacocian/*` (newsletter2atom, redlib, caddy, gitea-runner,
|
||||
cert-watch, gssh, invidious, monica, directoryadmin, …) use this workflow to
|
||||
build and push their container image to the Gitea registry. Deployment repos
|
||||
(`Charlie/<stack>`) then pin the timestamp tag in their compose file and
|
||||
trigger `deploy-stack.yml` to roll out.
|
||||
Source repos use this workflow to build and push their container image
|
||||
to the Gitea registry. Deployment repos (`Charlie/<stack>`) then pin
|
||||
the timestamp tag in their compose file and trigger `deploy-stack.yml`
|
||||
to roll out.
|
||||
|
||||
Same SSH-into-host architecture as `deploy-stack.yml`: the runner is
|
||||
stateless; `docker build` / `docker push` execute on the build host's
|
||||
daemon as `sys-gitea-runner` over a short-lived step-ca-issued cert.
|
||||
The runner container deliberately does not have a docker socket bind
|
||||
mount.
|
||||
|
||||
| Input | Required | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `host` | yes | — | Build host the runner SSHes into. Must have docker + the standard `charlie-deploy` setup. Any deploy-target host qualifies. |
|
||||
| `image` | no | `gitea.alexandru.macocian.me/<github.repository>` (lowercased) | Fully-qualified image name without the tag. Override only if the image should not live at its repo's natural path. |
|
||||
| `context` | no | `.` | Docker build context, relative to the caller repo root. |
|
||||
| `dockerfile` | no | `Dockerfile` | Dockerfile path, relative to `context`. |
|
||||
@@ -215,6 +221,7 @@ trigger `deploy-stack.yml` to roll out.
|
||||
| `extra-tags` | no | `""` | Additional tags (one per line) pushed alongside `latest` + the timestamp. Just the tag, not the full `image:tag` form. |
|
||||
| `push` | no | `true` | If `false`, build only — useful for PR validation. |
|
||||
| `ref` | no | The branch the workflow ran on | Git ref to check out and build. |
|
||||
| `registry-user` | no | `${{ github.repository_owner }}` | Username for `docker login`. Must match the user that owns `CALLER_REGISTRY_PAT` and have `write:package` on the target image namespace. |
|
||||
|
||||
Every successful build pushes **two** tags:
|
||||
|
||||
@@ -227,16 +234,15 @@ Outputs (consumable by downstream jobs in the caller workflow):
|
||||
|
||||
| Output | Example | Use |
|
||||
|---|---|---|
|
||||
| `image` | `gitea.alexandru.macocian.me/amacocian/newsletter2atom` | The image ref without the tag. |
|
||||
| `timestamp-tag` | `2026-05-22T095126Z` | The tag the caller can write into the consumer stack's compose file (e.g. via a follow-up job that opens a PR against `Charlie/<stack>`). |
|
||||
| `digest` | `sha256:abc…` | The immutable content digest of the timestamp tag, in case you want to pin by digest instead of tag. |
|
||||
| `image` | `gitea.alexandru.macocian.me/<owner>/<repo>` | The image ref without the tag. |
|
||||
| `timestamp-tag` | `2026-05-22T130927Z` | The tag the caller can write into the consumer stack's compose file (e.g. via a follow-up job that opens a PR against `Charlie/<stack>`). |
|
||||
|
||||
Requires two `sys-gitea-runner` PATs — see [Secret placement](#secret-placement-image-source-repos) below.
|
||||
Requires two caller-owned PATs — see [Secret placement](#secret-placement-image-source-repos) below.
|
||||
|
||||
### Caller — minimal
|
||||
|
||||
```yaml
|
||||
# amacocian/newsletter2atom/.gitea/workflows/build.yaml
|
||||
# <owner>/<repo>/.gitea/workflows/build.yaml
|
||||
name: Build image
|
||||
on:
|
||||
push:
|
||||
@@ -245,7 +251,9 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: https://sys-gitea-runner:${{ secrets.RUNNER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/workflows/build-image.yml@main
|
||||
uses: https://${{ github.repository_owner }}:${{ secrets.CALLER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/workflows/build-image.yml@main
|
||||
with:
|
||||
host: melina
|
||||
secrets: inherit
|
||||
```
|
||||
|
||||
@@ -254,8 +262,9 @@ jobs:
|
||||
```yaml
|
||||
jobs:
|
||||
build:
|
||||
uses: https://sys-gitea-runner:${{ secrets.RUNNER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/workflows/build-image.yml@main
|
||||
uses: https://${{ github.repository_owner }}:${{ secrets.CALLER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/workflows/build-image.yml@main
|
||||
with:
|
||||
host: melina
|
||||
dockerfile: docker/Dockerfile
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
@@ -265,60 +274,71 @@ jobs:
|
||||
secrets: inherit
|
||||
```
|
||||
|
||||
### Runner-host prerequisites
|
||||
### Build-host prerequisites
|
||||
|
||||
Building runs **on the runner**, not on a remote host. The runner container
|
||||
therefore needs:
|
||||
The host named in `host:` runs the actual `docker build` / `docker push`.
|
||||
Requirements are a strict subset of the regular deploy-target prerequisites
|
||||
([Host prerequisites](#host-prerequisites)):
|
||||
|
||||
| Requirement | Why |
|
||||
|---|---|
|
||||
| `docker` binary inside the container (already shipped by `amacocian/gitea-runner`) | `docker build` / `docker push`. |
|
||||
| The host's `/var/run/docker.sock` bind-mounted into the runner | Build uses the host's docker daemon; no docker-in-docker. |
|
||||
| `sys-gitea-runner` PAT has `write:package` on the target image namespace | `docker login` + push to `gitea.alexandru.macocian.me`. |
|
||||
| `sys-gitea-runner` resolvable via LDAP + member of `charlie-deploy` | SSH session + docker socket access. |
|
||||
| sshd trusts the step-ca SSH user CA | Runner-issued user certs are accepted. |
|
||||
| `/var/run/docker.sock` is `root:charlie-deploy` | `docker build` / `push` work without sudo. |
|
||||
| `git` available on the host | Cloning the caller source into the scratch workdir. |
|
||||
| Outbound HTTPS to `gitea.alexandru.macocian.me` | `docker push` + `git clone`. |
|
||||
|
||||
Builds accumulate base-image layers in the runner host's docker storage.
|
||||
No SOPS / age key / `/mnt/nas/stacks` is touched by a build — those are
|
||||
deploy concerns only.
|
||||
|
||||
Builds accumulate base-image layers in the build host's docker storage.
|
||||
The workflow drops the timestamp-tagged image after pushing, but base
|
||||
layers stay cached (intentionally — speeds up subsequent builds). Run
|
||||
`docker system prune` on the runner host periodically.
|
||||
`docker system prune` on the build host periodically.
|
||||
|
||||
### Secret placement (image-source repos)
|
||||
|
||||
`build-image.yml` runs from image-source repos that live **outside** the
|
||||
`Charlie` org — they sit under the personal namespace that owns the
|
||||
images. Gitea Actions secrets are scoped by where the workflow executes,
|
||||
not by who the PAT authenticates as, so the `Charlie`-org-level
|
||||
`RUNNER_REPO_PAT` is **not** visible to those callers. Both secrets must
|
||||
be set on the namespace hosting the source repos.
|
||||
Unlike `deploy-stack.yml` (which relies on the `sys-gitea-runner` system
|
||||
account for everything), `build-image.yml` uses **caller-owned** PATs.
|
||||
Each user/org that wants to build images provides their own credentials
|
||||
— least privilege, no central PAT to share across namespaces, and the
|
||||
package-write PAT only ever has access to that user's own namespace.
|
||||
|
||||
Set once, at the **user-level** Settings → Actions → Secrets on the
|
||||
`sys-gitea-runner` is still the SSH identity into the build host (via
|
||||
step-ca cert) — that's unchanged and unrelated to these secrets.
|
||||
|
||||
Set both at the **user-level** Settings → Actions → Secrets on the
|
||||
account that owns the source repos. All repos under that personal
|
||||
namespace then inherit both via `secrets: inherit`:
|
||||
|
||||
| Secret | Gitea PAT scope | Used for |
|
||||
|---|---|---|
|
||||
| `RUNNER_REPO_PAT` | `read:repository` (All) | Resolving the absolute-URL `uses:` reference to this workflow + cloning the caller source inside the job. Same value as the `Charlie`-org secret of the same name (duplicate it — no cross-namespace inheritance). |
|
||||
| `RUNNER_REGISTRY_PAT` | `write:package` (All) | `docker login` + push to the Gitea container registry. |
|
||||
| Secret | Gitea PAT scope | Owner | Used for |
|
||||
|---|---|---|---|
|
||||
| `CALLER_REPO_PAT` | `read:repository` (All) | The calling user/org | Resolving the absolute-URL `uses:` reference to this workflow + its sibling composite actions in `Charlie/project-charlie`, and cloning the caller source on the build host. |
|
||||
| `CALLER_REGISTRY_PAT` | `write:package` (All, or scoped to the target namespace) | The calling user/org | `docker login` + push to the Gitea container registry. |
|
||||
|
||||
Both PATs belong to the `sys-gitea-runner` system account. Keeping them
|
||||
as two distinct tokens means the package-write PAT is never used over
|
||||
HTTPS git (and the repo-read PAT can never be used to push images);
|
||||
revoking one doesn't take the other down.
|
||||
Two distinct tokens so the package-write PAT is never used over HTTPS
|
||||
git (and the repo-read PAT can never be used to push images); revoking
|
||||
one doesn't take the other down.
|
||||
|
||||
#### Recipe — provisioning the registry PAT (one-time)
|
||||
#### Recipe — onboarding a new image-building namespace (one-time)
|
||||
|
||||
1. Log in to Gitea as `sys-gitea-runner`. Settings → Applications →
|
||||
"Generate New Token".
|
||||
2. Name: `gitea-runner-package-rw`. Repository and Organization Access:
|
||||
All. Permissions: `write:package`. Generate.
|
||||
3. Copy the token value (shown once).
|
||||
4. Log in to Gitea as the user that owns the image-source repos.
|
||||
Settings → Actions → Secrets → "Add Secret". Name:
|
||||
`RUNNER_REGISTRY_PAT`, value: the token. Save.
|
||||
5. Also add `RUNNER_REPO_PAT` here, value copied from the equivalent
|
||||
`Charlie`-org secret (or from the existing `gitea-runner-repo-ro`
|
||||
token if you can re-export it; otherwise mint a fresh `read:repository`
|
||||
PAT and store the new value in both places).
|
||||
6. Retire any prior `gitea-runner-package-ro` PAT — it's superseded.
|
||||
1. **Grant the calling user read access to `Charlie/project-charlie`.**
|
||||
Either add them as a Read collaborator on the repo (Charlie/project-charlie
|
||||
→ Settings → Collaborators) or as a Read member of the `Charlie` org.
|
||||
Required so their PAT can resolve the `uses:` URL.
|
||||
2. Log in to Gitea as the calling user. Settings → Applications →
|
||||
"Generate New Token" twice:
|
||||
- `build-repo-ro`: `read:repository`, Repository and Organization
|
||||
Access: All (or at minimum: `Charlie/project-charlie` + the user's
|
||||
own source repos).
|
||||
- `build-package-rw`: `write:package`, Repository and Organization
|
||||
Access: All (or scoped to the namespace receiving the images).
|
||||
3. Still logged in as the calling user → Settings → Actions → Secrets →
|
||||
"Add Secret":
|
||||
- `CALLER_REPO_PAT` = the `build-repo-ro` value.
|
||||
- `CALLER_REGISTRY_PAT` = the `build-package-rw` value.
|
||||
4. Drop the [Caller — minimal](#caller--minimal) snippet into each
|
||||
source repo at `.gitea/workflows/build.yaml`. Push.
|
||||
|
||||
## Inputs reference — `charlie-ssh-open`
|
||||
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ If both runners are down, neither cross-deploy job has anywhere to land. Use the
|
||||
| Identity | Where used | Purpose |
|
||||
|---|---|---|
|
||||
| `sys-gitea-runner` (LDAP) | `~sys-gitea-runner/.git-credentials` (host); SSH user via cert (target host); `RUNNER_REPO_PAT` org secret | THE deployment identity. Member of `charlie-deploy`. Read-only on `Charlie` org. |
|
||||
| `sys-gitea-runner` (Gitea user) | `RUNNER_REPO_PAT` (`read:repository`) + `RUNNER_REGISTRY_PAT` (`write:package`), set per Gitea namespace | Image builds run from non-`Charlie` namespaces (the personal account that owns the source repos); both PATs are set there so [`build-image.yml`](../.gitea/workflows/build-image.yml) can fetch + clone + push. See [automation.md → Secret placement](automation.md#secret-placement-image-source-repos). |
|
||||
| Image-source caller (Gitea user) | `CALLER_REPO_PAT` (`read:repository`) + `CALLER_REGISTRY_PAT` (`write:package`), set per Gitea namespace | Image builds run from non-`Charlie` namespaces under that namespace's own identity; both PATs are set there so [`build-image.yml`](../.gitea/workflows/build-image.yml) can fetch + clone + push without loaning out `sys-gitea-runner` credentials. The caller user must be a Read collaborator on `Charlie/project-charlie`. See [automation.md → Secret placement](automation.md#secret-placement-image-source-repos). |
|
||||
| `sys-host-<host>` (LDAP) | `/root/.git-credentials` + `/root/.docker/config.json` (host) | Root-level operations on each host: `sudo git ...`, `sudo docker pull`. Read-only on `Charlie` org. |
|
||||
|
||||
The act_runner registration credential is its **own** identity in Gitea. It can pick up jobs and report results; it cannot read other repos. When it needs to clone for `act`'s `uses:` resolution, it uses the `RUNNER_REPO_PAT` materialised by the entrypoint shim.
|
||||
|
||||
Reference in New Issue
Block a user