Merge pull request #271 from frostyard/auto-update-packages #639
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 Image Variants | |
| on: | |
| repository_dispatch: | |
| types: | |
| - build | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| permissions: {} | |
| env: | |
| REPO_NAME: snosi | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref || github.run_id }} | |
| cancel-in-progress: ${{ github.event_name == 'push' }} | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| attestations: write | |
| strategy: | |
| # Don't let one variant's failure (e.g. disk exhaustion on a "loaded" | |
| # image) cancel the other in-flight builds. Each variant is independent. | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - profile: cayo | |
| description: "Cayo Linux Server Image" | |
| - profile: cayoloaded | |
| description: "Cayo Loaded Linux Server Image" | |
| - profile: snow | |
| description: "Snow Linux OS Image" | |
| - profile: snowloaded | |
| description: "Snow Loaded Linux OS Image" | |
| - profile: snowfield | |
| description: "Snowfield Linux OS Image" | |
| - profile: snowfieldloaded | |
| description: "Snow Field Loaded Linux OS Image" | |
| steps: | |
| - name: Show disk space (before) | |
| run: sudo df -h | |
| - name: Free Disk Space (Ubuntu) | |
| uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 | |
| with: | |
| # We don't use the runner's preinstalled language toolchains; reclaim | |
| # the tool cache (~8-12GB) on / for extra headroom on large builds. | |
| tool-cache: true | |
| - name: Mount BTRFS for podman storage | |
| uses: ublue-os/container-storage-action@main | |
| with: | |
| target-dir: /var/lib/containers | |
| - name: Redirect temp to /mnt | |
| # / (/dev/root) has ~45G free after cleanup; /mnt has ~70G free. mkosi's | |
| # build workspace and podman/buildah tar staging default to TMPDIR | |
| # (/var/tmp on /), which overflows / on the large "loaded" variants and | |
| # crashes the runner with "No space left on device". Point TMPDIR at the | |
| # roomy /mnt volume for all subsequent steps. | |
| run: | | |
| sudo mkdir -p /mnt/tmp | |
| sudo chmod 1777 /mnt/tmp | |
| echo "TMPDIR=/mnt/tmp" >> "$GITHUB_ENV" | |
| - name: Show disk space (after) | |
| run: sudo df -h | |
| - name: Checkout repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| persist-credentials: false | |
| - name: Check mkosi package duplicates | |
| run: ./check-duplicate-packages.sh | |
| - name: Generate build date | |
| id: date | |
| run: echo "date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT | |
| - name: Generate version tag | |
| id: version | |
| run: echo "tag=$(date +%Y%m%d%H%M%S)" >> $GITHUB_OUTPUT | |
| - name: setup-mkosi | |
| uses: systemd/mkosi@3c3a08fb07d27fbe473625aa0725655cfb2c68bf | |
| - name: Build Image | |
| run: | | |
| sudo TMPDIR="$TMPDIR" mkosi --profile ${{ matrix.profile }} \ | |
| --image-version "${{ steps.version.outputs.tag }}" \ | |
| build | |
| - name: Package image | |
| env: | |
| IMAGE: ghcr.io/${{ github.repository_owner }}/${{ matrix.profile }} | |
| run: | | |
| sudo TMPDIR="$TMPDIR" ./shared/outformat/image/buildah-package.sh \ | |
| output/${{ matrix.profile }} \ | |
| "$IMAGE:${{ steps.version.outputs.tag }}" \ | |
| "org.opencontainers.image.title=${{ matrix.profile }}" \ | |
| "org.opencontainers.image.description=${{ matrix.description }}" \ | |
| "org.opencontainers.image.version=${{ steps.version.outputs.tag }}" \ | |
| "org.opencontainers.image.created=${{ steps.date.outputs.date }}" \ | |
| "org.opencontainers.image.source=https://github.com/${{ github.repository_owner }}/${{ env.REPO_NAME }}/blob/${{ github.sha }}/mkosi.conf" \ | |
| "org.opencontainers.image.url=https://github.com/${{ github.repository_owner }}/${{ env.REPO_NAME }}/tree/${{ github.sha }}" \ | |
| "org.opencontainers.image.documentation=https://raw.githubusercontent.com/${{ github.repository_owner }}/${{ env.REPO_NAME }}/${{ github.sha }}/README.md" | |
| - name: Chunk image | |
| if: github.event_name != 'pull_request' | |
| env: | |
| IMAGE: ghcr.io/${{ github.repository_owner }}/${{ matrix.profile }} | |
| MAX_LAYERS: 128 | |
| run: | | |
| sudo TMPDIR="$TMPDIR" ./shared/outformat/image/chunkah-package.sh \ | |
| "$IMAGE:${{ steps.version.outputs.tag }}" \ | |
| $(date -d '${{ github.event.head_commit.timestamp }}' +%s) | |
| - name: Smoke test - verify SUID bit on sudo | |
| env: | |
| IMAGE: ghcr.io/${{ github.repository_owner }}/${{ matrix.profile }} | |
| run: | | |
| mode=$(sudo podman run --rm "$IMAGE:${{ steps.version.outputs.tag }}" stat -c '%a' /usr/bin/sudo) | |
| echo "sudo permissions: $mode" | |
| if [[ "$mode" != "4755" ]]; then | |
| echo "::error::SUID bit not set on /usr/bin/sudo (expected 4755, got $mode)" | |
| exit 1 | |
| fi | |
| - name: Setup Syft | |
| id: setup-syft | |
| if: github.event_name != 'pull_request' | |
| uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 | |
| with: | |
| syft-version: v1.39.0 | |
| - name: Generate SBOM | |
| if: github.event_name != 'pull_request' | |
| id: generate-sbom | |
| env: | |
| PROFILE: ${{ matrix.profile }} | |
| SYFT_CMD: ${{ steps.setup-syft.outputs.cmd }} | |
| run: | | |
| SBOM="$(mktemp -d)/sbom.json" | |
| export SYFT_PARALLELISM=$(($(nproc)*2)) | |
| sudo TMPDIR="$TMPDIR" "${SYFT_CMD}" --source-name "${PROFILE}" "output/${PROFILE}" -o "syft-json=${SBOM}" | |
| du -sh "${SBOM}" | |
| echo "SBOM=${SBOM}" >> "$GITHUB_OUTPUT" | |
| - name: Push image | |
| if: github.event_name != 'pull_request' | |
| id: push | |
| env: | |
| IMAGE: ghcr.io/${{ github.repository_owner }}/${{ matrix.profile }} | |
| run: | | |
| sudo TMPDIR="$TMPDIR" buildah push \ | |
| --compression-format=zstd:chunked \ | |
| --digestfile=/tmp/image-digest \ | |
| --creds="${{ github.actor }}:${{ secrets.GHCR_PAT }}" \ | |
| "$IMAGE:${{ steps.version.outputs.tag }}" \ | |
| "docker://$IMAGE:${{ steps.version.outputs.tag }}" | |
| DIGEST=$(sudo cat /tmp/image-digest) | |
| if [[ ! "$DIGEST" =~ ^sha256: ]]; then | |
| echo "::error::Failed to extract valid digest: $DIGEST" | |
| exit 1 | |
| fi | |
| echo "digest=$DIGEST" >> $GITHUB_OUTPUT | |
| sudo buildah tag \ | |
| "$IMAGE:${{ steps.version.outputs.tag }}" \ | |
| "$IMAGE:latest" | |
| sudo TMPDIR="$TMPDIR" buildah push \ | |
| --compression-format=zstd:chunked \ | |
| --creds="${{ github.actor }}:${{ secrets.GHCR_PAT }}" \ | |
| "$IMAGE:latest" \ | |
| "docker://$IMAGE:latest" | |
| - name: Log in to ghcr.io | |
| if: github.event_name != 'pull_request' | |
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GHCR_PAT }} | |
| - name: Install ORAS | |
| if: github.event_name != 'pull_request' | |
| uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3 # v2 | |
| - name: Login to GHCR with ORAS | |
| if: github.event_name != 'pull_request' | |
| env: | |
| GHCR_PAT: ${{ secrets.GHCR_PAT }} | |
| run: | | |
| echo "${GHCR_PAT}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin | |
| - name: Install Cosign | |
| if: github.event_name != 'pull_request' | |
| uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 | |
| with: | |
| cosign-release: "v2.6.1" | |
| - name: Upload SBOM | |
| if: github.event_name != 'pull_request' | |
| id: upload-sbom | |
| env: | |
| IMAGE: ghcr.io/${{ github.repository_owner }}/${{ matrix.profile }} | |
| DIGEST: ${{ steps.push.outputs.digest }} | |
| SBOM: ${{ steps.generate-sbom.outputs.SBOM }} | |
| run: | | |
| cd "$(dirname "${SBOM}")" | |
| oras attach \ | |
| --artifact-type application/vnd.syft+json \ | |
| --annotation "filename=$(basename "${SBOM}")" \ | |
| "${IMAGE}@${DIGEST}" \ | |
| "$(basename "${SBOM}")" | |
| sbom_digest=$(oras discover --format json "${IMAGE}@${DIGEST}" | jq -r '[.referrers[] | select(.artifactType == "application/vnd.syft+json")] | last | .digest') | |
| if [[ -z "${sbom_digest}" || "${sbom_digest}" == "null" ]]; then | |
| echo "::error::Failed to discover SBOM artifact digest" | |
| exit 1 | |
| fi | |
| echo "sbom_digest=${sbom_digest}" >> "$GITHUB_OUTPUT" | |
| - name: Sign SBOM | |
| if: github.event_name != 'pull_request' | |
| env: | |
| COSIGN_EXPERIMENTAL: "false" | |
| COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }} | |
| IMAGE: ghcr.io/${{ github.repository_owner }}/${{ matrix.profile }} | |
| SBOM_DIGEST: ${{ steps.upload-sbom.outputs.sbom_digest }} | |
| run: | | |
| cosign sign --key env://COSIGN_PRIVATE_KEY --yes \ | |
| "${IMAGE}@${SBOM_DIGEST}" | |
| - name: Sign image | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| cosign login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GHCR_PAT }}" | |
| cosign sign --key env://COSIGN_PRIVATE_KEY --yes \ | |
| "$IMAGE@${{ steps.push.outputs.digest }}" | |
| env: | |
| IMAGE: ghcr.io/${{ github.repository_owner }}/${{ matrix.profile }} | |
| COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }} | |
| - name: Attest build provenance | |
| if: github.event_name != 'pull_request' | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | |
| with: | |
| subject-name: ghcr.io/${{ github.repository_owner }}/${{ matrix.profile }} | |
| subject-digest: ${{ steps.push.outputs.digest }} | |
| push-to-registry: true | |
| - name: Move manifests | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| sudo ./manifestmv.sh | |
| - name: Upload manifests to R2 | |
| if: github.event_name != 'pull_request' | |
| uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c | |
| with: | |
| r2-account-id: ${{ secrets.R2_ACCOUNT_ID }} | |
| r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }} | |
| r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }} | |
| r2-bucket: frostyardrepo | |
| source-dir: output/manifests | |
| destination-dir: manifests/ | |
| - name: cleanup build output | |
| run: | | |
| sudo -E mkosi clean | |
| release: | |
| runs-on: ubuntu-latest | |
| needs: build | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.build.result == 'success' | |
| continue-on-error: true | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Install ORAS | |
| uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3 # v2 | |
| - name: Login to GHCR with ORAS | |
| env: | |
| GHCR_PAT: ${{ secrets.GHCR_PAT }} | |
| run: echo "${GHCR_PAT}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin | |
| - name: Resolve previous and current snowloaded tags | |
| id: versions | |
| env: | |
| IMAGE: ghcr.io/${{ github.repository_owner }}/snowloaded | |
| run: | | |
| set -euo pipefail | |
| tags=$(oras repo tags "$IMAGE" | grep -E '^[0-9]{14}$' | sort -r || true) | |
| if [[ -z "$tags" ]]; then | |
| echo "::warning::No timestamped tags found on $IMAGE; skipping release" | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| current=$(echo "$tags" | head -n1) | |
| previous=$(echo "$tags" | sed -n '2p') | |
| if [[ -z "$previous" ]]; then | |
| echo "::warning::Only one timestamped tag on $IMAGE; nothing to diff against; skipping release" | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "previous=$previous" >> "$GITHUB_OUTPUT" | |
| echo "current=$current" >> "$GITHUB_OUTPUT" | |
| - name: Generate changelog | |
| id: changelog | |
| if: steps.versions.outputs.skip != 'true' | |
| uses: frostyard/changelog-generator@main | |
| with: | |
| image: ghcr.io/${{ github.repository_owner }}/snowloaded | |
| previous-tag: ${{ steps.versions.outputs.previous }} | |
| current-tag: ${{ steps.versions.outputs.current }} | |
| output-file: changelog.md | |
| - name: Compute release tag and title | |
| id: release-meta | |
| if: steps.versions.outputs.skip != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| CURRENT: ${{ steps.versions.outputs.current }} | |
| run: | | |
| set -euo pipefail | |
| date_part="${CURRENT:0:4}-${CURRENT:4:2}-${CURRENT:6:2}" | |
| hh="${CURRENT:8:2}"; mm="${CURRENT:10:2}"; ss="${CURRENT:12:2}" | |
| # Daily counter: find highest existing N in releases matching "<date>.N" | |
| existing=$(gh release list --repo "$GITHUB_REPOSITORY" --limit 200 \ | |
| --json tagName --jq \ | |
| "[.[] | .tagName | select(startswith(\"${date_part}.\")) \ | |
| | sub(\"^${date_part}\\\\.\"; \"\") | select(test(\"^[0-9]+$\")) | tonumber] | max // 0") | |
| next=$(( existing + 1 )) | |
| tag="${date_part}.${next}" | |
| title="Build ${date_part} ${hh}:${mm}:${ss} UTC" | |
| echo "tag=$tag" >> "$GITHUB_OUTPUT" | |
| echo "title=$title" >> "$GITHUB_OUTPUT" | |
| - name: Create GitHub Release | |
| if: steps.versions.outputs.skip != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ steps.release-meta.outputs.tag }} | |
| TITLE: ${{ steps.release-meta.outputs.title }} | |
| run: | | |
| gh release create "$TAG" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --target "$GITHUB_SHA" \ | |
| --title "$TITLE" \ | |
| --notes-file changelog.md |