Build container image #158
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| name: Build container image | |
| on: | |
| pull_request: | |
| branches: | |
| - main | |
| paths-ignore: | |
| - 'README.md' | |
| - 'docs/**' | |
| schedule: | |
| - cron: '00 05 * * 0' # 5:00am UTC Sunday; about 1:00am Eastern during daylight time | |
| push: | |
| branches: | |
| - main | |
| paths-ignore: | |
| - '**/README.md' | |
| - 'docs/**' | |
| workflow_dispatch: | |
| env: | |
| IMAGE_NAME: "${{ github.event.repository.name }}" # output image name, usually same as repo name | |
| IMAGE_REGISTRY: "ghcr.io/${{ github.repository_owner }}" # do not edit | |
| DEFAULT_TAG: "latest" | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref || github.run_id }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| build_push: | |
| name: Build and push image | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 90 | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Prepare environment | |
| run: | | |
| # Lowercase the image uri | |
| echo "IMAGE_REGISTRY=${IMAGE_REGISTRY,,}" >> ${GITHUB_ENV} | |
| echo "IMAGE_NAME=${IMAGE_NAME,,}" >> ${GITHUB_ENV} | |
| # These stage versions are pinned by https://github.com/renovatebot/renovate | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| # This is optional, but if you see that your builds are way too big for the runners, you can enable this by uncommenting the following lines: | |
| - name: Maximize build space | |
| uses: ublue-os/remove-unwanted-software@695eb75bc387dbcd9685a8e72d23439d8686cba6 | |
| # Ubuntu 24.04's Podman does not preserve Chunkah's layer annotations | |
| # when pushing. Use the same newer toolchain as Aurora until the hosted | |
| # runner includes a sufficiently new Podman release. The step skips | |
| # itself once the runner's podman major version is >= 5, so it retires | |
| # automatically when GitHub updates the hosted image. | |
| - name: Update Podman | |
| run: | | |
| set -eux | |
| need_major=5 | |
| have_major=$(podman --version | grep -oE '[0-9]+' | head -n 1) | |
| if [ "${have_major}" -ge "${need_major}" ]; then | |
| echo "Runner podman is already >= ${need_major}; skipping cross-release install" | |
| exit 0 | |
| fi | |
| echo "deb http://azure.archive.ubuntu.com/ubuntu resolute universe main" | sudo tee /etc/apt/sources.list.d/resolute.list | |
| sudo apt-get update | |
| sudo apt-get install -y --allow-downgrades \ | |
| crun/resolute \ | |
| buildah/resolute \ | |
| podman/resolute \ | |
| skopeo/resolute | |
| podman --version | |
| # GitHub-hosted runners keep most of their free space on /mnt (~66G), | |
| # while / has far less headroom. Building the image and then re-layering | |
| # it with Chunkah keeps two copies of the root filesystem in podman's | |
| # storage at once, which can exhaust /. Relocate podman's rootless | |
| # storage onto /mnt so the rechunk has room. The image-copy temp dir is | |
| # deliberately left on / (freed by relocating storage) so the two large | |
| # consumers land on separate disks instead of competing for one. | |
| - name: Move container storage to the large runner disk | |
| run: | | |
| set -eux | |
| sudo mkdir -p /mnt/containers | |
| sudo chown "$(id -u):$(id -g)" /mnt/containers | |
| mkdir -p "${HOME}/.local/share" | |
| rm -rf "${HOME}/.local/share/containers" | |
| ln -s /mnt/containers "${HOME}/.local/share/containers" | |
| df -h / /mnt | |
| - name: Get current date | |
| id: date | |
| run: | | |
| echo "date=$(date -u +%Y\-%m\-%d\T%H\:%M\:%S\Z)" >> $GITHUB_OUTPUT | |
| - name: Image Metadata | |
| uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 | |
| id: metadata | |
| with: | |
| # This generates all the tags for your image, you can add custom tags here too! | |
| # Default tags are "$DEFAULT_TAG" and "$DEFAULT_TAG.$date". | |
| tags: | | |
| type=raw,value=${{ env.DEFAULT_TAG }} | |
| type=raw,value=${{ env.DEFAULT_TAG }}.{{date 'YYYYMMDD'}} | |
| type=raw,value={{date 'YYYYMMDD'}} | |
| type=sha,enable=${{ github.event_name == 'pull_request' }} | |
| type=ref,event=pr | |
| labels: | | |
| org.opencontainers.image.created=${{ steps.date.outputs.date }} | |
| org.opencontainers.image.description=Aurora DX image with ZFS support | |
| org.opencontainers.image.documentation=https://raw.githubusercontent.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/${{ github.sha }}/README.md | |
| org.opencontainers.image.source=https://github.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/blob/${{ github.sha }}/Containerfile | |
| org.opencontainers.image.title=${{ env.IMAGE_NAME }} | |
| org.opencontainers.image.url=https://github.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/tree/${{ github.sha }} | |
| org.opencontainers.image.vendor=${{ github.repository_owner }} | |
| org.opencontainers.image.version=${{ env.DEFAULT_TAG }}.{{date 'YYYYMMDD'}} | |
| containers.bootc=1 | |
| sep-tags: " " | |
| sep-annotations: " " | |
| - name: Build Image | |
| id: build_image | |
| uses: redhat-actions/buildah-build@7a95fa7ee0f02d552a32753e7414641a04307056 # v2 | |
| with: | |
| containerfiles: | | |
| ./Containerfile | |
| # Postfix image name with -custom to make it a little more descriptive | |
| # Syntax: https://docs.github.com/en/actions/learn-github-actions/expressions#format | |
| image: ${{ env.IMAGE_NAME }} | |
| tags: ${{ steps.metadata.outputs.tags }} | |
| labels: ${{ steps.metadata.outputs.labels }} | |
| oci: false | |
| # Post-process the completed image so the ZFS and replacement-kernel | |
| # content is chunked along with the inherited Aurora filesystem. | |
| # | |
| # Chunkah's output used to be piped straight into `podman load`, which | |
| # forced the source image and the re-layered copy to coexist in one | |
| # container storage (~2x the unpacked image) and exhausted the runner | |
| # disk intermittently. Instead, buffer the compressed archive to a file | |
| # on / , delete the source image to free its layers, and only then load | |
| # the archive with its unpack temp dir pointed at /mnt. Neither disk | |
| # ever has to hold two unpacked copies of the image at once. | |
| - name: Rechunk Image with Chunkah | |
| env: | |
| CHUNKAH_IMAGE: quay.io/coreos/chunkah:v0.6.0 | |
| run: | | |
| set -euo pipefail | |
| source_image="localhost/${IMAGE_NAME}:${DEFAULT_TAG}" | |
| chunked_image="localhost/${IMAGE_NAME}:chunkah" | |
| archive=/tmp/chunkah-oci.tar | |
| export CHUNKAH_CONFIG_STR | |
| CHUNKAH_CONFIG_STR="$(podman inspect "${source_image}")" | |
| echo "==> Disk usage before rechunk" && df -h / /mnt | |
| podman run --rm \ | |
| --mount=type=image,src="${source_image}",target=/chunkah \ | |
| -e CHUNKAH_CONFIG_STR \ | |
| "${CHUNKAH_IMAGE}" build \ | |
| --verbose \ | |
| --compressed \ | |
| --max-layers 128 \ | |
| --prune /sysroot/ \ | |
| --label ostree.commit- \ | |
| --label ostree.final-diffid- \ | |
| --tag "${chunked_image}" > "${archive}" | |
| # The archive is self-contained, so nothing left in container | |
| # storage is needed anymore: not the source image, not the Aurora | |
| # base or akmods images (tagged, so a plain prune would keep | |
| # them), not the Chunkah image. Remove them all so the load starts | |
| # against an empty store. | |
| podman image prune -af | |
| echo "==> Disk usage after pruning container storage" && df -h / /mnt | |
| # podman unpacks oci archives into TMPDIR before applying them; | |
| # keep that on /mnt next to container storage instead of /var/tmp. | |
| # /mnt is root-owned on the hosted runners, hence sudo + chown. | |
| sudo mkdir -p /mnt/tmp | |
| sudo chown "$(id -u):$(id -g)" /mnt/tmp | |
| TMPDIR=/mnt/tmp podman load -i "${archive}" | |
| rm -f "${archive}" | |
| echo "==> Disk usage after load" && df -h / /mnt | |
| for tag in ${{ steps.metadata.outputs.tags }}; do | |
| podman tag "${chunked_image}" "${IMAGE_NAME}:${tag}" | |
| done | |
| # These `if` statements are so that pull requests for your custom images do not make it publish any packages under your name without you knowing | |
| # They also check if the runner is on the default branch so that things like the merge queue (if you enable it), are going to work | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 | |
| if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Push To GHCR | |
| uses: redhat-actions/push-to-registry@5ed88d269cf581ea9ef6dd6806d01562096bee9c # v2 | |
| if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) | |
| id: push | |
| env: | |
| REGISTRY_USER: ${{ github.actor }} | |
| REGISTRY_PASSWORD: ${{ github.token }} | |
| with: | |
| registry: ${{ env.IMAGE_REGISTRY }} | |
| image: ${{ env.IMAGE_NAME }} | |
| # Push exactly one tag. The remaining tags are copied from the | |
| # pushed digest in the next step so every tag is byte-identical. | |
| tags: ${{ env.DEFAULT_TAG }} | |
| username: ${{ env.REGISTRY_USER }} | |
| password: ${{ env.REGISTRY_PASSWORD }} | |
| # podman can emit different manifest bytes for sequential pushes of | |
| # the same local image: the first push compresses and uploads, later | |
| # pushes reuse cached blob info, and the resulting manifests can carry | |
| # different digests. That split what `latest` and the date tags pointed | |
| # at and left `latest` outside the signed digest. Copy the single | |
| # pushed manifest to the remaining tags server-side instead; | |
| # --preserve-digests makes skopeo fail rather than rewrite the | |
| # manifest, so every tag is identical by construction. | |
| - name: Propagate tags from the pushed digest | |
| if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) | |
| run: | | |
| set -euo pipefail | |
| digest="${{ steps.push.outputs.digest }}" | |
| if [ -z "${digest}" ]; then | |
| echo "push step did not expose a digest" >&2 | |
| exit 1 | |
| fi | |
| for tag in ${{ steps.metadata.outputs.tags }}; do | |
| if [ "${tag}" = "${DEFAULT_TAG}" ]; then | |
| continue | |
| fi | |
| skopeo copy --preserve-digests \ | |
| "docker://${IMAGE_REGISTRY}/${IMAGE_NAME}@${digest}" \ | |
| "docker://${IMAGE_REGISTRY}/${IMAGE_NAME}:${tag}" | |
| done | |
| # All tags in this job point at one manifest pushed from one local | |
| # image. Enforce that as an invariant instead of trusting it: if any | |
| # published tag resolves to a different digest than the one about to | |
| # be signed, fail before signing rather than publish a half-signed | |
| # tag set. | |
| - name: Verify pushed tags share one digest | |
| if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) | |
| run: | | |
| set -euo pipefail | |
| expected="${{ steps.push.outputs.digest }}" | |
| if [ -z "${expected}" ]; then | |
| echo "push step did not expose a digest" >&2 | |
| exit 1 | |
| fi | |
| for tag in ${{ steps.metadata.outputs.tags }}; do | |
| actual=$(skopeo inspect --format '{{.Digest}}' "docker://${IMAGE_REGISTRY}/${IMAGE_NAME}:${tag}") | |
| if [ "${actual}" != "${expected}" ]; then | |
| echo "Tag ${tag} resolves to ${actual}, expected ${expected}" >&2 | |
| exit 1 | |
| fi | |
| echo "Tag ${tag} -> ${actual} OK" | |
| done | |
| # This section is optional and only needs to be enabled if you plan on distributing | |
| # your project for others to consume. You will need to create a public and private key | |
| # using Cosign and save the private key as a repository secret in Github for this workflow | |
| # to consume. For more details, review the image signing section of the README. | |
| - name: Install Cosign | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) | |
| with: | |
| cosign-release: 'v2.6.1' | |
| - name: Sign container image | |
| if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) | |
| run: | | |
| cosign sign -y --key env://COSIGN_PRIVATE_KEY \ | |
| "${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.push.outputs.digest }}" | |
| env: | |
| COSIGN_EXPERIMENTAL: false | |
| COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }} |