From 0807dc64347c01a6b5f5d17db52ed6da6b0000cc Mon Sep 17 00:00:00 2001 From: peet Date: Sat, 29 Aug 2026 13:40:46 +0200 Subject: [PATCH] Build-scan-promote pipeline: explicit trivy-scan, podman-promote, deploy-prod actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - podman-build-publish: reverted to pure build+push (v8 behavior) — the Trivy gate briefly embedded in v10 moves to an explicit action instead. - trivy-scan (new): scans a registry image ref, fails on fixable HIGH/CRITICAL findings; jobs pin it to amd64 runners. - podman-promote (new): server-side retag via skopeo copy --all — releases promote the scanned sha- candidate instead of rebuilding. - deploy-prod (new): shared deploy body for the *.podman repos (with the BatchMode/IdentitiesOnly ssh flags only two of the four copies had). - README: documents the standard build.yml/release.yml pipeline; refs standardized on public/actions. Plan: noctrl.podman tmp/WORKFLOW_REWRITE_PLAN.md. Release as v11. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P5NjXQ3jmyVjimHQxnyKhg --- README.md | 120 +++++++++++++++++++++++++------ deploy-prod/action.yaml | 66 +++++++++++++++++ podman-build-publish/action.yaml | 54 -------------- podman-promote/action.yaml | 52 ++++++++++++++ trivy-scan/action.yaml | 54 ++++++++++++++ 5 files changed, 271 insertions(+), 75 deletions(-) create mode 100644 deploy-prod/action.yaml create mode 100644 podman-promote/action.yaml create mode 100644 trivy-scan/action.yaml diff --git a/README.md b/README.md index 14b7423..7ff55d0 100644 --- a/README.md +++ b/README.md @@ -2,54 +2,132 @@ Reusable Gitea Actions composite workflows for noctrl infrastructure automation. +## The standard pipeline (since v11) + +Build once, scan explicitly, promote — the registry is the hand-off between +workflows. A push to the default branch builds each architecture exactly once +and pushes immutable, never-deployed candidate tags (`sha-[-]`); +an explicit Trivy job scans the candidate. A release rebuilds nothing: it +re-scans the candidate and retags it server-side. + +`build.yml` (push to default branch): + +``` +validate (pre-commit) → build-amd64 ─┐ + build-arm64 ─┤→ manifest (multi-arch only) → scan +``` + +`release.yml` (tag `v*`): + +``` +scan (candidate of the tagged commit) → promote (sha- → vX) +``` + +The scan in `release.yml` is the enforcing gate: a release tag can only come +into existence behind a scan that passed at promotion time. Candidates carrying +CVEs may exist in the registry — nothing consumes `sha-*` tags, so they are +inert. Scan and promote jobs always run on `linux,build,amd64` (never on the +slow arm runners; vulnerability results are package-based and arch-independent). + +Before v11, `podman-build-publish@v10` briefly embedded the Trivy gate itself; +v11 moved it into the explicit `trivy-scan` action. + ## Available Actions ### Podman Build And Publish -Builds and optionally pushes OCI container images to `registry.noctrl.eu` using Podman with isolated storage context. - -Since `v10`, the built image is scanned with Trivy before the push and the action fails on fixable HIGH/CRITICAL vulnerabilities (tune with `scan-severity`, opt out with `scan: "false"`). Requires the `trivy` binary on the runner (present in `noctrl/gitea-runner` since `v9`). +Builds and optionally pushes OCI container images to `registry.noctrl.eu` using +Podman with isolated storage context. Build and candidate-push are deliberately +one action: each invocation initializes its own podman storage, so a split +build/push pair would have to re-pull what was just built. **Location:** [`./podman-build-publish`](./podman-build-publish) -**Use in workflows:** ```yaml -- uses: https://gitea.noctrl.eu/noctrl/actions/podman-build-publish@v1 +- uses: https://gitea.noctrl.eu/public/actions/podman-build-publish@v11 with: image-name: noctrl/myapp tags: | - latest - sha-${{ github.sha }} + sha-${{ github.sha }}-amd64 + registry-username: ${{ secrets.REGISTRY_USERNAME }} + registry-password: ${{ secrets.REGISTRY_PASSWORD }} ``` -See [podman-build-publish README](./podman-build-publish/README.md) for full documentation. - ### Podman Manifest Publish -Creates and pushes OCI multi-arch manifest tags to `registry.noctrl.eu` using -Podman with isolated storage context. +Creates and pushes OCI multi-arch manifest tags to `registry.noctrl.eu` — +used to assemble the per-arch candidates into the multi-arch candidate. **Location:** [`./podman-manifest-publish`](./podman-manifest-publish) -**Use in workflows:** ```yaml -- uses: https://gitea.noctrl.eu/noctrl/actions/podman-manifest-publish@v1 +- uses: https://gitea.noctrl.eu/public/actions/podman-manifest-publish@v11 with: image-name: noctrl/myapp - manifest-tag: v1.2.3 + manifest-tag: sha-${{ github.sha }} source-tags: | - v1.2.3-tmp-123-amd64 - v1.2.3-tmp-123-arm64 + sha-${{ github.sha }}-amd64 + sha-${{ github.sha }}-arm64 ``` -See [podman-manifest-publish README](./podman-manifest-publish/README.md) for full documentation. +### Trivy Scan + +Scans an image ref in `registry.noctrl.eu` and fails on fixable HIGH/CRITICAL +vulnerabilities (severities configurable). Requires `trivy` on the runner +(`noctrl/gitea-runner` >= v9). Pin the job to `linux,build,amd64`. + +**Location:** [`./trivy-scan`](./trivy-scan) + +```yaml +- uses: https://gitea.noctrl.eu/public/actions/trivy-scan@v11 + with: + image-name: noctrl/myapp + tag: sha-${{ github.sha }} + registry-username: ${{ secrets.REGISTRY_USERNAME }} + registry-password: ${{ secrets.REGISTRY_PASSWORD }} +``` + +### Podman Promote + +Retags an existing image (or multi-arch manifest list) server-side via +`skopeo copy --all` — no rebuild, no transfer, fails if the source tag does not +exist. Requires `skopeo` on the runner (`noctrl/gitea-runner` >= v10). + +**Location:** [`./podman-promote`](./podman-promote) + +```yaml +- uses: https://gitea.noctrl.eu/public/actions/podman-promote@v11 + with: + image-name: noctrl/myapp + source-tag: sha-${{ github.sha }} + target-tag: ${{ github.ref_name }} + registry-username: ${{ secrets.REGISTRY_USERNAME }} + registry-password: ${{ secrets.REGISTRY_PASSWORD }} +``` + +### Deploy Prod + +Shared deploy body for the `*.podman` infrastructure repos: installs the SSH +deploy key, validates the mounted deploy-target env file, and pipes the deploy +runner's `deploy-prod.sh` to the production host over SSH. Runs on that host's +dedicated deploy runner (`gitea-runner-deploy` image). + +**Location:** [`./deploy-prod`](./deploy-prod) + +```yaml +- uses: https://gitea.noctrl.eu/public/actions/deploy-prod@v11 + with: + deploy-target-file: /run/noctrl/prod-deploy-target.env + ssh-key-b64: ${{ secrets.PROD_DEPLOY_SSH_KEY }} + target-sha: ${{ github.sha }} +``` ## Usage Reference actions by absolute URL in your workflow: ```yaml -- uses: https://gitea.noctrl.eu/noctrl/actions/@ +- uses: https://gitea.noctrl.eu/public/actions/@ ``` **Ref Options:** @@ -63,6 +141,7 @@ Actions reference caller repository secrets. Ensure your repository has required - **REGISTRY_USERNAME** — registry.noctrl.eu authentication username - **REGISTRY_PASSWORD** — registry.noctrl.eu authentication password +- **PROD_DEPLOY_SSH_KEY** — base64-encoded SSH deploy key (`*.podman` repos only) Set these in repository **Settings** → **Secrets**. @@ -71,6 +150,5 @@ Set these in repository **Settings** → **Secrets**. When adding new actions: 1. Create action directory: `./action-name/` 2. Include `action.yaml` (composite action definition) -3. Include `README.md` with inputs, outputs, and usage example -4. Tag stable versions: `git tag v1`, `git tag v2`, etc. -5. Update this root README +3. Tag stable versions: `git tag v1`, `git tag v2`, etc. +4. Update this root README diff --git a/deploy-prod/action.yaml b/deploy-prod/action.yaml new file mode 100644 index 0000000..ec27bfa --- /dev/null +++ b/deploy-prod/action.yaml @@ -0,0 +1,66 @@ +name: Deploy Prod +description: > + Deploy a commit of a *.podman infrastructure repo on its production host. + Runs on that host's dedicated deploy runner (gitea-runner-deploy image): + installs the deployment SSH key, reads the mounted deploy-target env file, + and pipes the runner image's deploy-prod.sh to the host over SSH. Host key + verification uses the known_hosts file mounted into the deploy runner. + +inputs: + deploy-target-file: + description: > + Path of the mounted deploy target env file, for example + /run/noctrl/prod-deploy-target.env. Must define REPO_DIR, PROD_HOST, + PROD_USER, PROD_PORT. + required: true + ssh-key-b64: + description: Base64-encoded private SSH deploy key (PROD_DEPLOY_SSH_KEY secret). + required: true + target-sha: + description: Commit SHA to deploy (normally github.sha). + required: true + +runs: + using: composite + steps: + - id: install-key + shell: bash + env: + SSH_KEY_B64: ${{ inputs.ssh-key-b64 }} + run: | + set -euo pipefail + : "${SSH_KEY_B64:?missing ssh-key-b64 input (PROD_DEPLOY_SSH_KEY secret)}" + install -d -m 0700 ~/.ssh + printf '%s' "${SSH_KEY_B64}" | base64 -d > ~/.ssh/id_ed25519 + chmod 0600 ~/.ssh/id_ed25519 + ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null + + - id: deploy + shell: bash + env: + TARGET_SHA: ${{ inputs.target-sha }} + DEPLOY_TARGET_FILE: ${{ inputs.deploy-target-file }} + run: | + set -euo pipefail + + if [[ ! -f "${DEPLOY_TARGET_FILE}" ]]; then + echo "missing mounted deploy target file: ${DEPLOY_TARGET_FILE}" >&2 + exit 1 + fi + + set -a + . "${DEPLOY_TARGET_FILE}" + set +a + + : "${REPO_DIR:?missing REPO_DIR in ${DEPLOY_TARGET_FILE}}" + : "${PROD_HOST:?missing PROD_HOST in ${DEPLOY_TARGET_FILE}}" + : "${PROD_USER:?missing PROD_USER in ${DEPLOY_TARGET_FILE}}" + : "${PROD_PORT:?missing PROD_PORT in ${DEPLOY_TARGET_FILE}}" + + ssh \ + -o BatchMode=yes \ + -o IdentitiesOnly=yes \ + -i ~/.ssh/id_ed25519 \ + -p "${PROD_PORT}" "${PROD_USER}@${PROD_HOST}" \ + "REPO_DIR='${REPO_DIR}' bash -s -- '${TARGET_SHA}'" \ + < /usr/local/bin/deploy-prod.sh diff --git a/podman-build-publish/action.yaml b/podman-build-publish/action.yaml index bd2063e..e06b4a8 100644 --- a/podman-build-publish/action.yaml +++ b/podman-build-publish/action.yaml @@ -37,20 +37,6 @@ inputs: Set to "false" for build-only verification workflows. required: false default: "true" - scan: - description: | - Whether to scan the built image with Trivy before pushing. The step - fails — blocking the push — when vulnerabilities at or above the - configured severities with an available fix are found (unfixed CVEs - are ignored: nothing actionable, and Debian/Alpine bases always carry - some). Requires the trivy binary on the runner. Set to "false" only - with a documented reason. - required: false - default: "true" - scan-severity: - description: Comma-separated Trivy severities that fail the scan. - required: false - default: HIGH,CRITICAL runs: using: composite @@ -155,46 +141,6 @@ runs: build_cmd+=("${{ inputs.context }}") "${build_cmd[@]}" - - if: ${{ inputs.scan != 'false' }} - id: scan - shell: bash - run: | - set -euo pipefail - - : "${PODMAN_STORAGE_DRIVER:=vfs}" - : "${PODMAN_STORAGE_OPT:=}" - - podman_args=( - --root "${RUNNER_TEMP}/podman-root" - --runroot "${RUNNER_TEMP}/podman-runroot" - --storage-driver "${PODMAN_STORAGE_DRIVER}" - ) - - if [[ -n "${PODMAN_STORAGE_OPT}" ]]; then - podman_args+=(--storage-opt "${PODMAN_STORAGE_OPT}") - fi - - image_base="registry.noctrl.eu/${{ inputs.image-name }}" - first_tag="$(printf '%s\n' "${{ inputs.tags }}" | tr ', ' '\n\n' | sed '/^$/d' | head -n1)" - - # Trivy cannot read podman's storage under a custom --root, so scan - # an exported archive instead. Must be docker-archive: trivy's - # --input does not understand oci-archive tarballs (verified). - archive="${RUNNER_TEMP}/trivy-scan-image.tar" - rm -f "${archive}" - podman "${podman_args[@]}" save --format docker-archive -o "${archive}" "${image_base}:${first_tag}" - - echo "Scanning ${image_base}:${first_tag} (fail on: ${{ inputs.scan-severity }}, fixed only)" - trivy image \ - --input "${archive}" \ - --scanners vuln \ - --severity "${{ inputs.scan-severity }}" \ - --ignore-unfixed \ - --exit-code 1 \ - --no-progress - - rm -f "${archive}" - - if: ${{ inputs.push != 'false' }} id: push shell: bash diff --git a/podman-promote/action.yaml b/podman-promote/action.yaml new file mode 100644 index 0000000..84ff526 --- /dev/null +++ b/podman-promote/action.yaml @@ -0,0 +1,52 @@ +name: Podman Promote +description: > + Promote an existing image in registry.noctrl.eu by retagging it server-side + (skopeo copy within the same registry — blobs are reused, no rebuild, no + transfer). Works for single-arch images and multi-arch manifest lists alike. + Fails if the source tag does not exist; it never falls back to building. + +inputs: + image-name: + description: Repository/image name path, for example noctrl/gitea-runner + required: true + source-tag: + description: Existing tag to promote, for example sha-abc123 + required: true + target-tag: + description: Tag to publish, for example v1.2.3 + required: true + registry-username: + description: Registry username for login. + required: true + registry-password: + description: Registry password for login. + required: true + +runs: + using: composite + steps: + - id: promote + shell: bash + run: | + set -euo pipefail + + export REGISTRY_AUTH_FILE="${RUNNER_TEMP}/promote-auth.json" + + if ! command -v skopeo >/dev/null 2>&1; then + echo "ERROR: skopeo not found on this runner (needs noctrl/gitea-runner >= v10)" >&2 + exit 1 + fi + + image_base="registry.noctrl.eu/${{ inputs.image-name }}" + source_ref="docker://${image_base}:${{ inputs.source-tag }}" + target_ref="docker://${image_base}:${{ inputs.target-tag }}" + + cleanup() { rm -f "${REGISTRY_AUTH_FILE}"; } + trap cleanup EXIT + + echo "${{ inputs.registry-password }}" | skopeo login registry.noctrl.eu \ + --username "${{ inputs.registry-username }}" --password-stdin + + echo "Promoting ${source_ref} -> ${target_ref}" + # --all: copy every manifest of a multi-arch list, or the single image. + skopeo copy --all "${source_ref}" "${target_ref}" diff --git a/trivy-scan/action.yaml b/trivy-scan/action.yaml new file mode 100644 index 0000000..99b60b1 --- /dev/null +++ b/trivy-scan/action.yaml @@ -0,0 +1,54 @@ +name: Trivy Scan +description: > + Scan an image in registry.noctrl.eu for vulnerabilities and fail on findings. + Pulls the image directly from the registry — no podman storage involved. + Pin the calling job to an amd64 runner (never arm); vulnerability results are + package-based and arch-independent, so scanning a multi-arch ref once + (trivy picks the host platform's variant) covers all variants. + +inputs: + image-name: + description: Repository/image name path, for example noctrl/gitea-runner + required: true + tag: + description: Tag to scan, for example sha-abc123 or sha-abc123-amd64 + required: true + severity: + description: Comma-separated Trivy severities that fail the scan. + required: false + default: HIGH,CRITICAL + registry-username: + description: Registry username for pulling the image. + required: true + registry-password: + description: Registry password for pulling the image. + required: true + +runs: + using: composite + steps: + - id: scan + shell: bash + env: + TRIVY_USERNAME: ${{ inputs.registry-username }} + TRIVY_PASSWORD: ${{ inputs.registry-password }} + run: | + set -euo pipefail + + if ! command -v trivy >/dev/null 2>&1; then + echo "ERROR: trivy not found on this runner (needs noctrl/gitea-runner >= v9)" >&2 + exit 1 + fi + + image_ref="registry.noctrl.eu/${{ inputs.image-name }}:${{ inputs.tag }}" + + # --ignore-unfixed: unfixed base-image CVEs are unactionable and would + # perma-block builds; the gate is on vulnerabilities with a fix. + echo "Scanning ${image_ref} (fail on: ${{ inputs.severity }}, fixed only)" + trivy image \ + --scanners vuln \ + --severity "${{ inputs.severity }}" \ + --ignore-unfixed \ + --exit-code 1 \ + --no-progress \ + "${image_ref}"