Merge pull request 'Build-scan-promote pipeline: explicit trivy-scan, podman-promote, deploy-prod actions' (#2) from build-scan-promote into main
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
commit
01bc638759
5 files changed
+271
-75
No files matched your search
@@ -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-<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
|
||||
|
||||
### 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/<action-name>@<ref>
|
||||
- uses: https://gitea.noctrl.eu/public/actions/<action-name>@<ref>
|
||||
```
|
||||
|
||||
**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
|
||||
@@ -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.
|
||||
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
|
||||
|
||||
@@ -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