|
| 1 | +name: 🐳 Deploy base images |
| 2 | + |
| 3 | +# Publishes the deploy base images (see base-images/README.md) to Docker Hub. |
| 4 | +# Tags are mutable and rebuilt in place; the CLI pins digests, so consumers |
| 5 | +# only move when a release bumps its pins. |
| 6 | + |
| 7 | +on: |
| 8 | + workflow_dispatch: |
| 9 | + inputs: |
| 10 | + debian_snapshot: |
| 11 | + description: "Debian snapshot timestamp (YYYYMMDDTHHMMSSZ). Defaults to yesterday 00:00 UTC." |
| 12 | + required: false |
| 13 | + type: string |
| 14 | + push: |
| 15 | + branches: [main] |
| 16 | + paths: |
| 17 | + - "base-images/**" |
| 18 | + - ".github/workflows/base-images.yml" |
| 19 | + pull_request: |
| 20 | + paths: |
| 21 | + - "base-images/**" |
| 22 | + - ".github/workflows/base-images.yml" |
| 23 | + |
| 24 | +concurrency: |
| 25 | + group: base-images-${{ github.ref }} |
| 26 | + cancel-in-progress: false |
| 27 | + |
| 28 | +permissions: {} |
| 29 | + |
| 30 | +jobs: |
| 31 | + setup: |
| 32 | + runs-on: ubuntu-latest |
| 33 | + timeout-minutes: 10 |
| 34 | + permissions: |
| 35 | + contents: read |
| 36 | + outputs: |
| 37 | + images: ${{ steps.config.outputs.images }} |
| 38 | + packages: ${{ steps.config.outputs.packages }} |
| 39 | + build_packages: ${{ steps.config.outputs.build_packages }} |
| 40 | + suite: ${{ steps.config.outputs.suite }} |
| 41 | + snapshot: ${{ steps.config.outputs.snapshot }} |
| 42 | + source_date_epoch: ${{ steps.config.outputs.source_date_epoch }} |
| 43 | + push: ${{ steps.config.outputs.push }} |
| 44 | + steps: |
| 45 | + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 |
| 46 | + with: |
| 47 | + persist-credentials: false |
| 48 | + |
| 49 | + - name: Read image matrix and resolve snapshot |
| 50 | + id: config |
| 51 | + env: |
| 52 | + SNAPSHOT_INPUT: ${{ inputs.debian_snapshot }} |
| 53 | + EVENT_NAME: ${{ github.event_name }} |
| 54 | + REF: ${{ github.ref }} |
| 55 | + run: | |
| 56 | + PACKAGES="$(jq -er '.packages' base-images/images.json)" |
| 57 | + BUILD_PACKAGES="$(jq -er '.buildPackages' base-images/images.json)" |
| 58 | + SUITE="$(jq -er '.suite' base-images/images.json)" |
| 59 | +
|
| 60 | + # Values land in build args and shell lines; keep them boring. |
| 61 | + # NUL-delimited whole-record match so multi-line values can't sneak through |
| 62 | + printf '%s\0' "$PACKAGES" | grep -zqxE '[a-z0-9][a-z0-9 .+:=~-]*' || { echo "invalid packages value"; exit 1; } |
| 63 | + printf '%s\0' "$BUILD_PACKAGES" | grep -zqxE '[a-z0-9][a-z0-9 .+:=~-]*' || { echo "invalid buildPackages value"; exit 1; } |
| 64 | + printf '%s\0' "$SUITE" | grep -zqxE '[a-z]+' || { echo "invalid suite value"; exit 1; } |
| 65 | + jq -e '.images | length > 0 and all((.repo | test("^[a-z0-9-]+$")) and (.tag | test("^[a-z0-9.-]+$")) and (.base | test("^[a-zA-Z0-9./:@-]+$")))' base-images/images.json > /dev/null \ |
| 66 | + || { echo "invalid images entries"; exit 1; } |
| 67 | +
|
| 68 | + SNAPSHOT="$SNAPSHOT_INPUT" |
| 69 | + if [ -z "$SNAPSHOT" ]; then |
| 70 | + SNAPSHOT="$(date -u -d yesterday +%Y%m%dT000000Z)" |
| 71 | + fi |
| 72 | + printf '%s\0' "$SNAPSHOT" | grep -zqxE '[0-9]{8}T[0-9]{6}Z' || { echo "invalid debian_snapshot: $SNAPSHOT"; exit 1; } |
| 73 | +
|
| 74 | + # Snapshot-derived timestamps: reproducible, with a real created date |
| 75 | + EPOCH="$(date -u -d "${SNAPSHOT:0:4}-${SNAPSHOT:4:2}-${SNAPSHOT:6:2} ${SNAPSHOT:9:2}:${SNAPSHOT:11:2}:${SNAPSHOT:13:2}Z" +%s)" |
| 76 | + # Future snapshots resolve to "latest" and break mtime normalization |
| 77 | + [ "$EPOCH" -le "$(date -u +%s)" ] || { echo "debian_snapshot is in the future: $SNAPSHOT"; exit 1; } |
| 78 | +
|
| 79 | + # Pull requests and branch dispatches build without pushing |
| 80 | + if [ "$EVENT_NAME" = "pull_request" ] || [ "$REF" != "refs/heads/main" ]; then |
| 81 | + PUSH=false |
| 82 | + else |
| 83 | + PUSH=true |
| 84 | + fi |
| 85 | +
|
| 86 | + { |
| 87 | + echo "images=$(jq -c '.images' base-images/images.json)" |
| 88 | + echo "packages=$PACKAGES" |
| 89 | + echo "build_packages=$BUILD_PACKAGES" |
| 90 | + echo "suite=$SUITE" |
| 91 | + echo "snapshot=$SNAPSHOT" |
| 92 | + echo "source_date_epoch=$EPOCH" |
| 93 | + echo "push=$PUSH" |
| 94 | + } >> "$GITHUB_OUTPUT" |
| 95 | +
|
| 96 | + publish: |
| 97 | + needs: setup |
| 98 | + runs-on: ubuntu-latest |
| 99 | + timeout-minutes: 60 |
| 100 | + permissions: |
| 101 | + contents: read |
| 102 | + id-token: write |
| 103 | + attestations: write |
| 104 | + strategy: |
| 105 | + fail-fast: false |
| 106 | + matrix: |
| 107 | + image: ${{ fromJSON(needs.setup.outputs.images) }} |
| 108 | + env: |
| 109 | + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} |
| 110 | + DOCKER_BUILD_SUMMARY: "false" |
| 111 | + DOCKER_BUILD_RECORD_UPLOAD: "false" |
| 112 | + steps: |
| 113 | + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 |
| 114 | + with: |
| 115 | + persist-credentials: false |
| 116 | + |
| 117 | + # Before any pull so rate limits are authenticated; fork PRs skip (no secrets) |
| 118 | + - name: 🐳 Login to Docker Hub |
| 119 | + if: env.DOCKERHUB_USERNAME != '' |
| 120 | + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 |
| 121 | + with: |
| 122 | + username: ${{ secrets.DOCKERHUB_USERNAME }} |
| 123 | + password: ${{ secrets.DOCKERHUB_TOKEN }} |
| 124 | + |
| 125 | + - name: 🐳 Set up QEMU |
| 126 | + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 |
| 127 | + with: |
| 128 | + image: docker.io/tonistiigi/binfmt:latest@sha256:400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0 |
| 129 | + |
| 130 | + - name: 🐳 Set up Docker Buildx |
| 131 | + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 |
| 132 | + |
| 133 | + # Build both targets before pushing either so the tag pair can't skew |
| 134 | + - name: 🐳 Build both targets (no push) |
| 135 | + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 |
| 136 | + with: |
| 137 | + context: base-images |
| 138 | + file: base-images/Dockerfile |
| 139 | + target: build |
| 140 | + platforms: linux/amd64,linux/arm64 |
| 141 | + provenance: false |
| 142 | + outputs: type=image,push=false,rewrite-timestamp=true |
| 143 | + tags: triggerdotdev/${{ matrix.image.repo }}:${{ matrix.image.tag }}-build |
| 144 | + build-args: | |
| 145 | + BASE_IMAGE=${{ matrix.image.base }} |
| 146 | + DEBIAN_SNAPSHOT=${{ needs.setup.outputs.snapshot }} |
| 147 | + DEBIAN_SUITE=${{ needs.setup.outputs.suite }} |
| 148 | + PACKAGES=${{ needs.setup.outputs.packages }} |
| 149 | + BUILD_PACKAGES=${{ needs.setup.outputs.build_packages }} |
| 150 | + SOURCE_DATE_EPOCH=${{ needs.setup.outputs.source_date_epoch }} |
| 151 | + labels: | |
| 152 | + org.opencontainers.image.source=https://github.com/${{ github.repository }} |
| 153 | + org.opencontainers.image.revision=${{ github.sha }} |
| 154 | + dev.trigger.debian-snapshot=${{ needs.setup.outputs.snapshot }} |
| 155 | +
|
| 156 | + - name: 🐳 Push runtime image |
| 157 | + id: build_runtime |
| 158 | + if: needs.setup.outputs.push == 'true' |
| 159 | + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 |
| 160 | + with: |
| 161 | + context: base-images |
| 162 | + file: base-images/Dockerfile |
| 163 | + target: runtime |
| 164 | + platforms: linux/amd64,linux/arm64 |
| 165 | + provenance: false |
| 166 | + outputs: type=image,push=true,rewrite-timestamp=true |
| 167 | + tags: triggerdotdev/${{ matrix.image.repo }}:${{ matrix.image.tag }} |
| 168 | + build-args: | |
| 169 | + BASE_IMAGE=${{ matrix.image.base }} |
| 170 | + DEBIAN_SNAPSHOT=${{ needs.setup.outputs.snapshot }} |
| 171 | + DEBIAN_SUITE=${{ needs.setup.outputs.suite }} |
| 172 | + PACKAGES=${{ needs.setup.outputs.packages }} |
| 173 | + SOURCE_DATE_EPOCH=${{ needs.setup.outputs.source_date_epoch }} |
| 174 | + labels: | |
| 175 | + org.opencontainers.image.source=https://github.com/${{ github.repository }} |
| 176 | + org.opencontainers.image.revision=${{ github.sha }} |
| 177 | + dev.trigger.debian-snapshot=${{ needs.setup.outputs.snapshot }} |
| 178 | +
|
| 179 | + - name: 🐳 Push build-variant image |
| 180 | + id: build_toolchain |
| 181 | + if: needs.setup.outputs.push == 'true' |
| 182 | + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 |
| 183 | + with: |
| 184 | + context: base-images |
| 185 | + file: base-images/Dockerfile |
| 186 | + target: build |
| 187 | + platforms: linux/amd64,linux/arm64 |
| 188 | + provenance: false |
| 189 | + outputs: type=image,push=true,rewrite-timestamp=true |
| 190 | + tags: triggerdotdev/${{ matrix.image.repo }}:${{ matrix.image.tag }}-build |
| 191 | + build-args: | |
| 192 | + BASE_IMAGE=${{ matrix.image.base }} |
| 193 | + DEBIAN_SNAPSHOT=${{ needs.setup.outputs.snapshot }} |
| 194 | + DEBIAN_SUITE=${{ needs.setup.outputs.suite }} |
| 195 | + PACKAGES=${{ needs.setup.outputs.packages }} |
| 196 | + BUILD_PACKAGES=${{ needs.setup.outputs.build_packages }} |
| 197 | + SOURCE_DATE_EPOCH=${{ needs.setup.outputs.source_date_epoch }} |
| 198 | + labels: | |
| 199 | + org.opencontainers.image.source=https://github.com/${{ github.repository }} |
| 200 | + org.opencontainers.image.revision=${{ github.sha }} |
| 201 | + dev.trigger.debian-snapshot=${{ needs.setup.outputs.snapshot }} |
| 202 | +
|
| 203 | + # An auto-created private repo would publish green while customer pulls fail |
| 204 | + - name: 🔎 Verify anonymous pullability |
| 205 | + if: needs.setup.outputs.push == 'true' |
| 206 | + env: |
| 207 | + IMAGE_REPO: ${{ matrix.image.repo }} |
| 208 | + RUNTIME_DIGEST: ${{ steps.build_runtime.outputs.digest }} |
| 209 | + BUILD_DIGEST: ${{ steps.build_toolchain.outputs.digest }} |
| 210 | + run: | |
| 211 | + for digest in "$RUNTIME_DIGEST" "$BUILD_DIGEST"; do |
| 212 | + TOKEN="$(curl -fsS --connect-timeout 10 --max-time 60 "https://auth.docker.io/token?service=registry.docker.io&scope=repository:triggerdotdev/$IMAGE_REPO:pull" | jq -r .token)" |
| 213 | + curl -fsS --connect-timeout 10 --max-time 60 -o /dev/null -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json" "https://registry-1.docker.io/v2/triggerdotdev/$IMAGE_REPO/manifests/$digest" || { echo "triggerdotdev/$IMAGE_REPO@$digest is not anonymously pullable; is the repo private?"; exit 1; } |
| 214 | + done |
| 215 | +
|
| 216 | + # Builds are reproducible, so re-running a red publish re-pushes the |
| 217 | + # same digests and re-attests them |
| 218 | + - name: 🔏 Attest runtime image provenance |
| 219 | + if: needs.setup.outputs.push == 'true' |
| 220 | + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 |
| 221 | + with: |
| 222 | + subject-name: index.docker.io/triggerdotdev/${{ matrix.image.repo }} |
| 223 | + subject-digest: ${{ steps.build_runtime.outputs.digest }} |
| 224 | + push-to-registry: false |
| 225 | + |
| 226 | + - name: 🔏 Attest build-variant image provenance |
| 227 | + if: needs.setup.outputs.push == 'true' |
| 228 | + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 |
| 229 | + with: |
| 230 | + subject-name: index.docker.io/triggerdotdev/${{ matrix.image.repo }} |
| 231 | + subject-digest: ${{ steps.build_toolchain.outputs.digest }} |
| 232 | + push-to-registry: false |
| 233 | + |
| 234 | + - name: 📋 Record digests |
| 235 | + if: needs.setup.outputs.push == 'true' |
| 236 | + env: |
| 237 | + IMAGE_REPO: ${{ matrix.image.repo }} |
| 238 | + IMAGE_TAG: ${{ matrix.image.tag }} |
| 239 | + RUNTIME_DIGEST: ${{ steps.build_runtime.outputs.digest }} |
| 240 | + BUILD_DIGEST: ${{ steps.build_toolchain.outputs.digest }} |
| 241 | + SNAPSHOT: ${{ needs.setup.outputs.snapshot }} |
| 242 | + run: | |
| 243 | + { |
| 244 | + echo "### triggerdotdev/$IMAGE_REPO:$IMAGE_TAG" |
| 245 | + echo '```' |
| 246 | + echo "runtime: $RUNTIME_DIGEST" |
| 247 | + echo "build: $BUILD_DIGEST" |
| 248 | + echo "debian snapshot: $SNAPSHOT" |
| 249 | + echo '```' |
| 250 | + } >> "$GITHUB_STEP_SUMMARY" |
| 251 | +
|
| 252 | + results: |
| 253 | + needs: [publish] |
| 254 | + if: always() |
| 255 | + runs-on: ubuntu-latest |
| 256 | + timeout-minutes: 5 |
| 257 | + permissions: {} |
| 258 | + steps: |
| 259 | + - name: Fail if any image build failed |
| 260 | + env: |
| 261 | + RESULT: ${{ needs.publish.result }} |
| 262 | + run: | |
| 263 | + [ "$RESULT" = "success" ] || { echo "one or more image builds failed: $RESULT"; exit 1; } |
0 commit comments