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 }}