diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..3e0179b --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,163 @@ +name: docker-build + +# Reusable BuildKit image build whose layer cache outlives the runner. +# +# Disposable runners rebuild every layer from scratch unless the cache lives +# somewhere durable. This workflow puts it in a registry -- +# `{image}/buildcache` by default, which on ghcr.io is currently free storage +# -- so hosted and self-hosted runners share one cache across runs and +# repositories. `cache: gha` selects the GitHub Actions cache backend instead +# (needs Docker Engine >= 28 / Buildx >= 0.21, both satisfied by +# ubuntu-latest and the nddev images); `cache: none` disables caching. +# +# Runner contract: any Linux runner with a Docker daemon -- ubuntu-latest or +# a docker-capable nddev class (nddev-linux-integration and friends). Pushing +# to ghcr.io authenticates with the ambient workflow token, which needs +# `packages: write` in the caller when push or a ghcr cache ref is used. + +on: + workflow_call: + inputs: + runner: + description: 'Linux runner label with a Docker daemon.' + type: string + default: 'ubuntu-latest' + image: + description: 'Image repository without a tag, e.g. ghcr.io/acme/app.' + type: string + required: true + tags: + description: 'Comma-separated tags; empty tags the image with the commit SHA.' + type: string + default: '' + context: + description: 'Build context path inside the repository.' + type: string + default: '.' + dockerfile: + description: 'Dockerfile path relative to the repository root.' + type: string + default: 'Dockerfile' + platforms: + description: 'Target platforms.' + type: string + default: 'linux/amd64' + push: + description: 'Push the built image and tags to the registry.' + type: boolean + default: false + cache: + description: 'Layer cache backend: registry, gha, or none.' + type: string + default: 'registry' + cache_ref: + description: 'Registry cache ref; empty derives {image}/buildcache.' + type: string + default: '' + build_args: + description: 'Newline-separated KEY=value build arguments.' + type: string + default: '' + timeout_minutes: + type: number + default: 30 + outputs: + digest: + description: 'Content digest of the built image.' + value: ${{ jobs.build.outputs.digest }} + +permissions: {} + +jobs: + build: + runs-on: ${{ inputs.runner }} + timeout-minutes: ${{ inputs.timeout_minutes }} + permissions: + # Checkout of the caller repository only; no writes back to it. + contents: read + # ghcr.io push and the registry layer cache authenticate with the + # ambient token; both write to the caller's own package namespace. + packages: write + outputs: + digest: ${{ steps.build.outputs.digest }} + steps: + - name: Validate immutable inputs + env: + IMAGE: ${{ inputs.image }} + TAGS: ${{ inputs.tags }} + CONTEXT: ${{ inputs.context }} + DOCKERFILE: ${{ inputs.dockerfile }} + CACHE: ${{ inputs.cache }} + CACHE_REF: ${{ inputs.cache_ref }} + run: | + set -euo pipefail + [[ "$IMAGE" =~ ^[a-z0-9][a-z0-9._/-]*$ ]] + [[ -z "$TAGS" || "$TAGS" =~ ^[A-Za-z0-9._,-]+$ ]] + for value in "$CONTEXT" "$DOCKERFILE"; do + [[ "$value" =~ ^[A-Za-z0-9._/-]+$ && "$value" != /* && "$value" != *..* ]] + done + [[ "$CACHE" == registry || "$CACHE" == gha || "$CACHE" == none ]] + [[ -z "$CACHE_REF" || "$CACHE_REF" =~ ^[a-z0-9][a-z0-9._/-]*$ ]] + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Resolve tags and cache references + id: refs + env: + IMAGE: ${{ inputs.image }} + TAGS: ${{ inputs.tags }} + CACHE: ${{ inputs.cache }} + CACHE_REF: ${{ inputs.cache_ref }} + SHA: ${{ github.sha }} + run: | + set -euo pipefail + if [ -n "$TAGS" ]; then + full="" + IFS=',' read -ra parts <<< "$TAGS" + for tag in "${parts[@]}"; do full="${full:+$full,}$IMAGE:$tag"; done + else + full="$IMAGE:$SHA" + fi + printf 'tags=%s\n' "$full" >> "$GITHUB_OUTPUT" + case "$CACHE" in + registry) + ref="${CACHE_REF:-$IMAGE/buildcache}" + printf 'cache_from=type=registry,ref=%s\n' "$ref" >> "$GITHUB_OUTPUT" + printf 'cache_to=type=registry,ref=%s,mode=max,image-manifest=true,oci-mediatypes=true\n' "$ref" >> "$GITHUB_OUTPUT" + ;; + gha) + printf 'cache_from=type=gha\n' >> "$GITHUB_OUTPUT" + printf 'cache_to=type=gha,mode=max\n' >> "$GITHUB_OUTPUT" + ;; + none) + printf 'cache_from=\n' >> "$GITHUB_OUTPUT" + printf 'cache_to=\n' >> "$GITHUB_OUTPUT" + ;; + esac + + - name: Log in to ghcr.io + if: ${{ inputs.push || (inputs.cache == 'registry' && startsWith(inputs.image, 'ghcr.io/')) }} + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Set up BuildKit + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + + - name: Build and push + id: build + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: ${{ inputs.context }} + file: ${{ inputs.dockerfile }} + platforms: ${{ inputs.platforms }} + push: ${{ inputs.push }} + tags: ${{ steps.refs.outputs.tags }} + cache-from: ${{ steps.refs.outputs.cache_from }} + cache-to: ${{ steps.refs.outputs.cache_to }} + build-args: ${{ inputs.build_args }} diff --git a/catalog/cache-contract.yml b/catalog/cache-contract.yml index d96d7c4..8f8a8d9 100644 --- a/catalog/cache-contract.yml +++ b/catalog/cache-contract.yml @@ -103,6 +103,14 @@ producers: upstream_default: null default_caches: true note: Explicit cache; the key is written by the calling workflow. + - action: docker/build-push-action + control: cache-to + upstream_default: null + default_caches: false + note: >- + Caches only when docker-build.yml passes cache-from/cache-to, and the + workflow computes them from its `cache` input: a registry ref + ({image}/buildcache by default) or type=gha. No input means no cache. - action: astral-sh/setup-uv control: enable-cache upstream_default: auto diff --git a/catalog/capabilities.yml b/catalog/capabilities.yml index cc503b9..e26520f 100644 --- a/catalog/capabilities.yml +++ b/catalog/capabilities.yml @@ -1172,6 +1172,32 @@ capabilities: - "https://github.com/aquasecurity/trivy" - "https://github.com/aquasecurity/trivy-action" + - id: docker-build-registry-cache + name: Docker build with a registry layer cache + cluster: actions-core + status: ga + public_oss: free + private_free: free + private_paid: available + workflow: .github/workflows/docker-build.yml + # build-push-action shells out to the Docker daemon through BuildKit. + runtime_requirements: ["container-runtime"] + example: examples/infra/docker-build.yml + required_permissions: + - "contents: read" + - "packages: write" + required_settings: + - "ghcr.io push and registry cache authenticate with the ambient workflow token" + risks: + - "Workflow is present on disk and validated by generated workflow inventory" + - "Registry cache refs accumulate untagged manifests; clean them with a retention policy" + - "type=gha cache needs Docker Engine >= 28 and Buildx >= 0.21 on the runner" + deprecations: null + last_verified: "2026-09-01" + sources: + - "https://github.com/docker/build-push-action" + - "https://docs.docker.com/build/cache/backends/registry/" + - id: terraform-ci name: Terraform CI cluster: actions-core diff --git a/catalog/tools.yml b/catalog/tools.yml index 4c55fba..de06954 100644 --- a/catalog/tools.yml +++ b/catalog/tools.yml @@ -181,13 +181,14 @@ tools: - .github/workflows/coverage-gate.yml - .github/workflows/cpp-ci.yml - .github/workflows/cross-platform-smoke.yml - - .github/workflows/dependabot-catalog-convergence.yml - .github/workflows/dart-flutter-ci.yml + - .github/workflows/dependabot-catalog-convergence.yml + - .github/workflows/docker-build.yml - .github/workflows/docs-ci.yml - .github/workflows/docs-quality.yml - - .github/workflows/gds-anchor-contract.yml - .github/workflows/dotnet-ci.yml - .github/workflows/fuzzing.yml + - .github/workflows/gds-anchor-contract.yml - .github/workflows/go-ci.yml - .github/workflows/grype-scan.yml - .github/workflows/hadolint-ci.yml @@ -197,16 +198,16 @@ tools: - .github/workflows/maintenance.yml - .github/workflows/monorepo-changed-paths.yml - .github/workflows/mutation-testing.yml + - .github/workflows/nddev-security-bundle.yml - .github/workflows/node-ci.yml - .github/workflows/osv-scan.yml - .github/workflows/pr-hygiene.yml - - .github/workflows/nddev-security-bundle.yml - .github/workflows/private-security-bundle-free.yml - .github/workflows/private-static.yml - .github/workflows/public-codeql.yml - .github/workflows/public-dependency-review.yml - - .github/workflows/public-scorecard-json.yml - .github/workflows/public-scorecard-analysis.yml + - .github/workflows/public-scorecard-json.yml - .github/workflows/public-scorecard.yml - .github/workflows/python-ci.yml - .github/workflows/qt-ci.yml @@ -214,9 +215,9 @@ tools: - .github/workflows/release-supply-chain-free.yml - .github/workflows/release-supply-chain.yml - .github/workflows/release.yml - - .github/workflows/runtime-fixtures.yml - .github/workflows/runtime-fixtures-event-write.yml - .github/workflows/runtime-fixtures-languages.yml + - .github/workflows/runtime-fixtures.yml - .github/workflows/rust-ci.yml - .github/workflows/rust-supply-chain.yml - .github/workflows/secret-scan.yml @@ -618,6 +619,36 @@ tools: - .github/workflows/container-ci.yml last_verified: "2026-07-12" + - id: docker-login-action + name: docker/login-action + homepage: "https://github.com/docker/login-action" + kind: action + current_version: "v4.6.0" + pin: "docker/login-action@dbcb813823bdd20940b903addbd779551569679f" + used_by: + - .github/workflows/docker-build.yml + last_verified: "2026-09-01" + + - id: docker-setup-buildx-action + name: docker/setup-buildx-action + homepage: "https://github.com/docker/setup-buildx-action" + kind: action + current_version: "v4.3.0" + pin: "docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e" + used_by: + - .github/workflows/docker-build.yml + last_verified: "2026-09-01" + + - id: docker-build-push-action + name: docker/build-push-action + homepage: "https://github.com/docker/build-push-action" + kind: action + current_version: "v7.3.0" + pin: "docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a" + used_by: + - .github/workflows/docker-build.yml + last_verified: "2026-09-01" + # Registered late: these three workflows (rust-supply-chain, clusterfuzzlite) # shipped without catalog entries because the pin validators check pin format, # never catalog membership. scripts/check_tool_registry.py now derives the diff --git a/catalog/workflow-routing.yml b/catalog/workflow-routing.yml index 0aa414a..92c9568 100644 --- a/catalog/workflow-routing.yml +++ b/catalog/workflow-routing.yml @@ -31,6 +31,7 @@ groups: runtime_requirements: [container-runtime] workflows: - .github/workflows/container-ci.yml + - .github/workflows/docker-build.yml - .github/workflows/secret-scan.yml - id: linux-shell supported_os: [linux] diff --git a/docs/generated/capability-matrix.md b/docs/generated/capability-matrix.md index 00cd380..6be6aea 100644 --- a/docs/generated/capability-matrix.md +++ b/docs/generated/capability-matrix.md @@ -8,6 +8,7 @@ | C/C++ CI (`cpp-ci`) | actions-core | ga | free | conditional | available | `.github/workflows/cpp-ci.yml` | `examples/languages/cpp.yml` | | Cross-platform smoke test (`cross-platform-smoke`) | actions-core | ga | free | free | available | `.github/workflows/cross-platform-smoke.yml` | `examples/infra/cross-platform.yml` | | Dart/Flutter CI (`dart-flutter-ci`) | actions-core | ga | free | conditional | available | `.github/workflows/dart-flutter-ci.yml` | `examples/languages/dart-flutter.yml` | +| Docker build with a registry layer cache (`docker-build-registry-cache`) | actions-core | ga | free | free | available | `.github/workflows/docker-build.yml` | `examples/infra/docker-build.yml` | | Docs CI (`docs-ci`) | actions-core | ga | free | conditional | available | `.github/workflows/docs-ci.yml` | `examples/infra/docs.yml` | | Docs quality (links, spelling, markdown) (`docs-quality`) | actions-core | ga | free | free | available | `.github/workflows/docs-quality.yml` | `examples/quality/docs-quality.yml` | | .NET CI (`dotnet-ci`) | actions-core | ga | free | conditional | available | `.github/workflows/dotnet-ci.yml` | `examples/languages/dotnet.yml` | @@ -83,4 +84,4 @@ | SLSA build provenance (`slsa-build-provenance`) | supply-chain | ga | free | paid | conditional | `.github/workflows/release-supply-chain.yml` | `-` | --- -Source data verified through: 2026-08-24 +Source data verified through: 2026-09-01 diff --git a/docs/generated/profile-matrix.md b/docs/generated/profile-matrix.md index a1e9ed1..9cc737c 100644 --- a/docs/generated/profile-matrix.md +++ b/docs/generated/profile-matrix.md @@ -10,10 +10,10 @@ catalogs, so prose cannot drift away from it. | Profile | Visibility | Base plan | C/S/Q | Compute billing | Licence billing | CodeQL | Runner | Provenance | Enforcement | Fixed cost | Programme | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `public-free-standalone` | public | free, pro, team, enterprise-cloud | `000` | public-standard-unmetered | no-paid-addons | default | github-hosted-standard | attestations | active | free | 65 run / 7 conditional / 6 unavailable | -| `private-free-max` | private | free, pro, team | `000` | private-self-hosted | no-paid-addons | none | self-hosted-ephemeral | checksums | active | free | 30 run / 29 conditional / 19 unavailable | -| `public-enterprise-max` | public | enterprise-cloud | `111` | public-standard-unmetered | selected-addons | default | github-hosted-standard | attestations | active | free | 66 run / 7 conditional / 5 unavailable | -| `enterprise-full-private-fixed80` | private, internal | enterprise-cloud | `111` | private-self-hosted | fixed-license-envelope | default | self-hosted-ephemeral | attestations | active | $80/month | 38 run / 35 conditional / 5 unavailable | +| `public-free-standalone` | public | free, pro, team, enterprise-cloud | `000` | public-standard-unmetered | no-paid-addons | default | github-hosted-standard | attestations | active | free | 66 run / 7 conditional / 6 unavailable | +| `private-free-max` | private | free, pro, team | `000` | private-self-hosted | no-paid-addons | none | self-hosted-ephemeral | checksums | active | free | 31 run / 29 conditional / 19 unavailable | +| `public-enterprise-max` | public | enterprise-cloud | `111` | public-standard-unmetered | selected-addons | default | github-hosted-standard | attestations | active | free | 67 run / 7 conditional / 5 unavailable | +| `enterprise-full-private-fixed80` | private, internal | enterprise-cloud | `111` | private-self-hosted | fixed-license-envelope | default | self-hosted-ephemeral | attestations | active | $80/month | 39 run / 35 conditional / 5 unavailable | ## Entitlement combinations diff --git a/docs/generated/workflow-inventory.md b/docs/generated/workflow-inventory.md index f914010..2e08721 100644 --- a/docs/generated/workflow-inventory.md +++ b/docs/generated/workflow-inventory.md @@ -16,6 +16,7 @@ | `.github/workflows/dart-flutter-ci.yml` | `dart-flutter-ci` | ga | | `.github/workflows/dependabot-catalog-convergence.yml` | internal | internal | | `.github/workflows/dependency-review.yml` | internal | internal | +| `.github/workflows/docker-build.yml` | `docker-build-registry-cache` | ga | | `.github/workflows/docs-ci.yml` | `docs-ci` | ga | | `.github/workflows/docs-quality.yml` | `docs-quality` | ga | | `.github/workflows/dotnet-ci.yml` | `dotnet-ci` | ga | @@ -67,4 +68,4 @@ | `.github/workflows/zizmor-sarif.yml` | `zizmor` | ga | --- -Source data verified through: 2026-08-24 +Source data verified through: 2026-09-01 diff --git a/docs/generated/workflow-routing.md b/docs/generated/workflow-routing.md index ebe956b..db93562 100644 --- a/docs/generated/workflow-routing.md +++ b/docs/generated/workflow-routing.md @@ -15,6 +15,7 @@ an operating system this table does not list for it. | `.github/workflows/cpp-ci.yml` | `linux-shell` | linux | shell only | | `.github/workflows/cross-platform-smoke.yml` | `portable-shell` | linux, macos, windows | shell only | | `.github/workflows/dart-flutter-ci.yml` | `linux-shell` | linux | shell only | +| `.github/workflows/docker-build.yml` | `linux-container` | linux | container-runtime | | `.github/workflows/docs-ci.yml` | `linux-shell` | linux | shell only | | `.github/workflows/docs-quality.yml` | `linux-shell` | linux | shell only | | `.github/workflows/dotnet-ci.yml` | `portable-shell` | linux, macos, windows | shell only | diff --git a/examples/infra/docker-build.yml b/examples/infra/docker-build.yml new file mode 100644 index 0000000..d119880 --- /dev/null +++ b/examples/infra/docker-build.yml @@ -0,0 +1,24 @@ +# Build a container image with a layer cache that survives disposable +# runners. The registry cache defaults to {image}/buildcache -- on ghcr.io +# that storage is currently free -- so every run starts from the layers the +# last one built, on hosted and self-hosted runners alike. +name: docker-build + +on: + push: + branches: [main] + +permissions: {} + +jobs: + image: + permissions: + contents: read + packages: write + uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/docker-build.yml@ + with: + runner: ubuntu-latest + image: ghcr.io/acme/app + push: true + # cache: registry (default) shares {image}/buildcache across runs; + # switch to gha for the Actions cache backend, or none.