Build-scan-promote pipeline: explicit trivy-scan, podman-promote, deploy-prod actions
- 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-<commit> 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5NjXQ3jmyVjimHQxnyKhg
This commit is contained in:
1 parent
2e0291a757
commit
0807dc6434
5 files changed
+271
-75
No files matched your search
@@ -2,54 +2,132 @@
|
|||||||
|
|
||||||
Reusable Gitea Actions composite workflows for noctrl infrastructure automation.
|
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-<commit>[-<arch>]`);
|
||||||
|
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-<commit> → 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
|
## Available Actions
|
||||||
|
|
||||||
### Podman Build And Publish
|
### Podman Build And Publish
|
||||||
|
|
||||||
Builds and optionally pushes OCI container images to `registry.noctrl.eu` using Podman with isolated storage context.
|
Builds and optionally pushes OCI container images to `registry.noctrl.eu` using
|
||||||
|
Podman with isolated storage context. Build and candidate-push are deliberately
|
||||||
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`).
|
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)
|
**Location:** [`./podman-build-publish`](./podman-build-publish)
|
||||||
|
|
||||||
**Use in workflows:**
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: https://gitea.noctrl.eu/noctrl/actions/podman-build-publish@v1
|
- uses: https://gitea.noctrl.eu/public/actions/podman-build-publish@v11
|
||||||
with:
|
with:
|
||||||
image-name: noctrl/myapp
|
image-name: noctrl/myapp
|
||||||
tags: |
|
tags: |
|
||||||
latest
|
sha-${{ github.sha }}-amd64
|
||||||
sha-${{ github.sha }}
|
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
|
### Podman Manifest Publish
|
||||||
|
|
||||||
Creates and pushes OCI multi-arch manifest tags to `registry.noctrl.eu` using
|
Creates and pushes OCI multi-arch manifest tags to `registry.noctrl.eu` —
|
||||||
Podman with isolated storage context.
|
used to assemble the per-arch candidates into the multi-arch candidate.
|
||||||
|
|
||||||
**Location:** [`./podman-manifest-publish`](./podman-manifest-publish)
|
**Location:** [`./podman-manifest-publish`](./podman-manifest-publish)
|
||||||
|
|
||||||
**Use in workflows:**
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: https://gitea.noctrl.eu/noctrl/actions/podman-manifest-publish@v1
|
- uses: https://gitea.noctrl.eu/public/actions/podman-manifest-publish@v11
|
||||||
with:
|
with:
|
||||||
image-name: noctrl/myapp
|
image-name: noctrl/myapp
|
||||||
manifest-tag: v1.2.3
|
manifest-tag: sha-${{ github.sha }}
|
||||||
source-tags: |
|
source-tags: |
|
||||||
v1.2.3-tmp-123-amd64
|
sha-${{ github.sha }}-amd64
|
||||||
v1.2.3-tmp-123-arm64
|
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
|
## Usage
|
||||||
|
|
||||||
Reference actions by absolute URL in your workflow:
|
Reference actions by absolute URL in your workflow:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: https://gitea.noctrl.eu/noctrl/actions/<action-name>@<ref>
|
- uses: https://gitea.noctrl.eu/public/actions/<action-name>@<ref>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Ref Options:**
|
**Ref Options:**
|
||||||
@@ -63,6 +141,7 @@ Actions reference caller repository secrets. Ensure your repository has required
|
|||||||
|
|
||||||
- **REGISTRY_USERNAME** — registry.noctrl.eu authentication username
|
- **REGISTRY_USERNAME** — registry.noctrl.eu authentication username
|
||||||
- **REGISTRY_PASSWORD** — registry.noctrl.eu authentication password
|
- **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**.
|
Set these in repository **Settings** → **Secrets**.
|
||||||
|
|
||||||
@@ -71,6 +150,5 @@ Set these in repository **Settings** → **Secrets**.
|
|||||||
When adding new actions:
|
When adding new actions:
|
||||||
1. Create action directory: `./action-name/`
|
1. Create action directory: `./action-name/`
|
||||||
2. Include `action.yaml` (composite action definition)
|
2. Include `action.yaml` (composite action definition)
|
||||||
3. Include `README.md` with inputs, outputs, and usage example
|
3. Tag stable versions: `git tag v1`, `git tag v2`, etc.
|
||||||
4. Tag stable versions: `git tag v1`, `git tag v2`, etc.
|
4. Update this root README
|
||||||
5. Update this root README
|
|
||||||
@@ -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
|
||||||
@@ -37,20 +37,6 @@ inputs:
|
|||||||
Set to "false" for build-only verification workflows.
|
Set to "false" for build-only verification workflows.
|
||||||
required: false
|
required: false
|
||||||
default: "true"
|
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:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
@@ -155,46 +141,6 @@ runs:
|
|||||||
build_cmd+=("${{ inputs.context }}")
|
build_cmd+=("${{ inputs.context }}")
|
||||||
"${build_cmd[@]}"
|
"${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' }}
|
- if: ${{ inputs.push != 'false' }}
|
||||||
id: push
|
id: push
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -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}"
|
||||||
@@ -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}"
|
||||||
Reference in new issue
Block a user