🏡 index : quine-global-labs/mirror-ublue-os-packages.git

name: Build ublue-builder
on:
  workflow_dispatch: # allow manually triggering builds
  pull_request:
    branches:
      - main
    paths:
      - 'ublue-builder/*'
      - '.github/workflows/builder.yml'
  push:
    branches:
      - main
    paths:
      - 'ublue-builder/*'
  schedule:
    - cron: '0 0 * * TUE'

env:
  IMAGE_NAME: ublue-builder
  IMAGE_DESC: "Builder image for Universal Blue package building"
  IMAGE_REGISTRY: "ghcr.io/${{ github.repository_owner }}"
  DEFAULT_TAG: "latest"
  PLATFORMS: "amd64,arm64"

jobs:
  generate_matrix:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - name: Set matrix
        id: set-matrix
        run: |
          # turn the comma separated string into a list
          platforms=()
          IFS=',' read -r -a platforms <<< "${{ env.PLATFORMS }}"

          MATRIX="{\"include\":[]}"
          for platform in "${platforms[@]}"; do
            MATRIX=$(echo $MATRIX | jq ".include += [{\"platform\": \"$platform\"}]")
          done
          echo "matrix=$(echo $MATRIX | jq -c '.')" >> $GITHUB_OUTPUT

  build_push:
    name: Build and push image
    runs-on: ${{ matrix.platform == 'amd64' && 'ubuntu-26.04' || 'ubuntu-26.04-arm' }}
    timeout-minutes: 30
    needs: generate_matrix
    permissions:
      contents: read
      packages: write
      id-token: write
    strategy:
      fail-fast: false
      matrix: ${{fromJson(needs.generate_matrix.outputs.matrix)}}
    steps:
      - name: Checkout
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

      - name: Show container tooling versions
        run: |
          podman --version
          buildah --version
          podman info

      - name: Build Image
        id: build_image
        uses: redhat-actions/buildah-build@719e3c40d8af9790c23eca13f7daa339f2867034 # v3
        with:
          containerfiles: |
            ./${{ env.IMAGE_NAME }}/Containerfile
          context: ${{ env.IMAGE_NAME }}
          image: ${{ env.IMAGE_NAME }}
          tags: ${{ env.DEFAULT_TAG }}
          labels: ${{ steps.meta.outputs.labels }}
          oci: true

      - name: Login to GHCR
        if: github.event_name != 'pull_request'
        run: |
          echo ${{ secrets.GITHUB_TOKEN }} | podman login -u ${{ github.actor }} --password-stdin ghcr.io
          echo ${{ secrets.GITHUB_TOKEN }} | docker login -u ${{ github.actor }} --password-stdin ghcr.io

      - name: Push to GHCR
        if: github.event_name != 'pull_request'
        id: push
        env:
          IMAGE_REGISTRY: ${{ env.IMAGE_REGISTRY }}
          IMAGE_NAME: ${{ env.IMAGE_NAME }}
          IMAGE_DIGEST: ${{ steps.build_image.outputs.digest }}
          PLATFORM: ${{ matrix.platform }}
        run: |
          set -x
          podman tag localhost/${IMAGE_NAME}:${DEFAULT_TAG} ${{ env.IMAGE_REGISTRY }}/${IMAGE_NAME}:${DEFAULT_TAG}
          podman tag ${{ env.IMAGE_REGISTRY }}/${IMAGE_NAME}:${DEFAULT_TAG} $IMAGE_REGISTRY/$IMAGE_NAME:$DEFAULT_TAG-$PLATFORM
          for i in {1..3}; do
            podman push --digestfile=/tmp/digestfile $IMAGE_REGISTRY/$IMAGE_NAME:$DEFAULT_TAG-$PLATFORM && break || sleep $((5 * i));
          done
          REMOTE_IMAGE_DIGEST=$(cat /tmp/digestfile)
          echo "remote_image_digest=$REMOTE_IMAGE_DIGEST" >> $GITHUB_OUTPUT
          cat /tmp/digestfile

      - name: Install Cosign
        uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
        if: github.event_name != 'pull_request'

      - name: Sign Image
        if: github.event_name != 'pull_request'
        run: |
          IMAGE_FULL="${{ env.IMAGE_REGISTRY }}/${IMAGE_NAME}"
          cosign sign -y --key env://COSIGN_PRIVATE_KEY ${IMAGE_FULL}@${{ steps.push.outputs.remote_image_digest }}
        env:
          TAGS: ${{ steps.push.outputs.digest }}
          COSIGN_EXPERIMENTAL: false
          COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }}

      - name: Create Job Outputs
        if: github.event_name != 'pull_request'
        env:
          IMAGE_NAME: ${{ env.IMAGE_NAME }}
          PLATFORM: ${{ matrix.platform }}
          DIGEST: ${{ steps.push.outputs.remote_image_digest }}
        run: |
          mkdir -p /tmp/outputs/digests
          echo "${DIGEST}" > /tmp/outputs/digests/${IMAGE_NAME}-${PLATFORM}.txt

      - name: Upload Output Artifacts
        if: github.event_name != 'pull_request'
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
        with:
          name: ${{ env.IMAGE_NAME }}-${{ matrix.platform }}
          retention-days: 1
          if-no-files-found: error
          path: |
            /tmp/outputs/digests/*.txt

  manifest:
    name: Create shared manifest
    runs-on: ubuntu-latest
    if: always()
    needs:
      - generate_matrix
      - build_push
    container:
      image:  cgr.dev/chainguard/wolfi-base:latest
      options: --privileged --security-opt seccomp=unconfined
    permissions:
      contents: read
      packages: write
      id-token: write
    outputs:
      image: ${{ steps.push_manifest.outputs.IMAGE }}
      digest: ${{ steps.push_manifest.outputs.DIGEST }}
    steps:
      - name: Install dependencies
        run: |
          apk add \
            jq \
            git \
            podman \
            uutils \
            bash \
            conmon \
            crun \
            netavark \
            libstdc++ \
            fuse-overlayfs
          ln -sf /bin/bash /bin/sh
          mkdir -p /etc/containers
          echo '{"default":[{"type":"insecureAcceptAnything"}]}' | jq . > /etc/containers/policy.json

      - name: Exit on failure
        env:
          JOBS: ${{ toJson(needs) }}
        run: |
          echo "Job status:"
          echo $JOBS | jq -r 'to_entries[] | " - \(.key): \(.value.result)"'

          for i in $(echo $JOBS | jq -r 'to_entries[] | .value.result'); do
            if [ "$i" != "success" ] && [ "$i" != "skipped" ]; then
              echo ""
              echo "Status check not okay!"
              exit 1
            fi
          done

      - name: Get current date
        id: date
        run: |
          # Should generate a timestamp like what is defined on the ArtifactHub documentation
          # E.G: 2022-02-08T15:38:15Z'
          # https://artifacthub.io/docs/topics/repositories/container-images/
          # https://linux.die.net/man/1/date
          echo "date=$(date -u +%Y\-%m\-%d\T%H\:%M\:%S\Z)" >> $GITHUB_OUTPUT

      - name: Image Metadata
        uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
        id: metadata
        with:
          tags: |
            type=raw,value=latest
            type=ref,event=pr
          labels: |
            io.artifacthub.package.readme-url=https://raw.githubusercontent.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/refs/heads/main/README.md
            org.opencontainers.image.created=${{ steps.date.outputs.date }}
            org.opencontainers.image.description=${{ env.IMAGE_DESC }}
            org.opencontainers.image.documentation=https://raw.githubusercontent.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/refs/heads/main/README.md
            org.opencontainers.image.source=https://github.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/blob/main/Containerfile
            org.opencontainers.image.title=${{ env.IMAGE_NAME }}
            org.opencontainers.image.url=https://github.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}
            org.opencontainers.image.vendor=${{ github.repository_owner }}
            org.opencontainers.image.version=${{ env.DEFAULT_TAG }}
            io.artifacthub.package.deprecated=false
            io.artifacthub.package.keywords=ublue,universal-blue,copr,fedora
            io.artifacthub.package.license=Apache-2.0
            io.artifacthub.package.logo-url=https://avatars.githubusercontent.com/u/120078124?s=200&v=4
            io.artifacthub.package.maintainers=[{\"name\":\"tulilirockz\",\"email\":\"tulilirockz@outlook.com\"}]

      - name: Fetch Build Outputs
        if: github.event_name != 'pull_request'
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
        with:
          pattern: ${{ env.IMAGE_NAME }}-*
          merge-multiple: true
          path: /tmp/artifacts

      - name: Load Outputs
        if: github.event_name != 'pull_request'
        id: load-outputs
        run: |
          set -x
          DIGESTS_JSON=$(jq -n '{}')
          for digest_file in /tmp/artifacts/*.txt; do
            # Extract the platform from the file name
            PLATFORM=$(basename $digest_file | rev | cut -d'-' -f1 | rev | cut -d'.' -f1)
            DIGEST=$(cat $digest_file)
            # Add the platform and digest to the JSON object
            DIGESTS_JSON=$(echo "$DIGESTS_JSON" | jq --arg key "$PLATFORM" --arg value "$DIGEST" '. + {($key): $value}')
          done
          echo "DIGESTS_JSON=$(echo $DIGESTS_JSON | jq -c '.')" >> $GITHUB_OUTPUT

      - name: Create Manifest
        if: github.event_name != 'pull_request'
        id: create-manifest
        run: |
          podman manifest create ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}
          echo "MANIFEST=${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_OUTPUT

      - name: Populate Manifest
        if: github.event_name != 'pull_request'
        env:
          MANIFEST: ${{ steps.create-manifest.outputs.MANIFEST }}
          DIGESTS_JSON: ${{ steps.load-outputs.outputs.DIGESTS_JSON }}
          LABELS: ${{ steps.metadata.outputs.labels }}
        run: |
          set -x
          DIGESTS=$(echo "$DIGESTS_JSON" | jq -c '.')
          # turn the comma separated string into a list
          platforms=()
          IFS=',' read -r -a platforms <<< "${{ env.PLATFORMS }}"

          for platform in ${platforms[@]}; do
            digest=$(echo $DIGESTS | jq -r ".$platform")
            echo "Adding ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}@$digest for $platform"
            podman manifest add $MANIFEST ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}@$digest --arch $platform
          done

          # Apply the labels to the manifest (separated by newlines)
          while IFS= read -r label; do
            echo "Applying label $label to manifest"
            podman manifest annotate --index --annotation "$label" $MANIFEST
          done <<< "$LABELS"

      - name: Login to GHCR
        if: github.event_name != 'pull_request'
        run: echo ${{ secrets.GITHUB_TOKEN }} | podman login -u ${{ github.actor }} --password-stdin ghcr.io

      - name: Push Manifest
        if: github.event_name != 'pull_request'
        id: push_manifest
        env:
          MANIFEST: ${{ steps.create-manifest.outputs.MANIFEST }}
          TAGS: ${{ steps.metadata.outputs.tags }}
          IMAGE_REGISTRY: ${{ env.IMAGE_REGISTRY }}
          IMAGE_NAME: ${{ env.IMAGE_NAME }}
        run: |
          set -x
          while IFS= read -r tag; do
            podman manifest push --all=false --digestfile=/tmp/digestfile $MANIFEST $IMAGE_REGISTRY/$IMAGE_NAME:$tag
          done <<< "$TAGS"

          DIGEST=$(cat /tmp/digestfile)
          echo "DIGEST=$DIGEST" >> $GITHUB_OUTPUT
          echo "IMAGE=$IMAGE_REGISTRY/$IMAGE_NAME" >> $GITHUB_OUTPUT

  # Cosign throws errors when ran inside the Fedora container for one reason or another
  # so we move this to another step in order to run on Ubuntu
  sign:
    needs: manifest
    if: github.event_name != 'pull_request'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write
    steps:
      - name: Login to GHCR
        run: |
          echo ${{ secrets.GITHUB_TOKEN }} | docker login -u ${{ github.actor }} --password-stdin ghcr.io
          echo ${{ secrets.GITHUB_TOKEN }} | podman login -u ${{ github.actor }} --password-stdin ghcr.io

      - name: Install Cosign
        uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1

      - name: Sign Manifest
        run: |
          cosign sign -y --key env://COSIGN_PRIVATE_KEY $IMAGE@$DIGEST
        env:
          DIGEST: ${{ needs.manifest.outputs.digest }}
          IMAGE: ${{ needs.manifest.outputs.image }}
          COSIGN_EXPERIMENTAL: false
          COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }}