name: Build container image on: pull_request: branches: - main schedule: - cron: "0 1 * * SUN" push: branches: - main paths-ignore: - "**/README.md" workflow_dispatch: env: IMAGE_DESC: "OCI image for Homebrew" IMAGE_KEYWORDS: "homebrew" IMAGE_LOGO_URL: "https://avatars.githubusercontent.com/u/136393846?s=200&v=4" IMAGE_NAME: "${{ github.event.repository.name }}" IMAGE_REGISTRY: "ghcr.io/${{ github.repository_owner }}" DEFAULT_TAG: "latest" jobs: build_push: name: Build and push image permissions: contents: read packages: write id-token: write timeout-minutes: 60 strategy: fail-fast: true matrix: platform: ["amd64", "arm64"] runs-on: ${{ matrix.platform == 'amd64' && 'ubuntu-24.04' || 'ubuntu-24.04-arm' }} steps: - name: Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Build image run: podman build -t "${IMAGE_NAME}:${DEFAULT_TAG}" -f ./Containerfile . - name: Login to GitHub Container Registry if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} LOGIN_USER: ${{ github.actor }} run: | echo "${GITHUB_TOKEN}" | podman login -u "${LOGIN_USER}" --password-stdin "ghcr.io" echo "${GITHUB_TOKEN}" | docker login -u "${LOGIN_USER}" --password-stdin "ghcr.io" - name: Push to GHCR if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) id: push env: IMAGE_REGISTRY: ${{ env.IMAGE_REGISTRY }} IMAGE_NAME: ${{ env.IMAGE_NAME }} IMAGE_DIGEST: ${{ steps.load.outputs.digest }} PLATFORM: ${{ matrix.platform }} MAX_RETRIES: 3 run: | set -x for i in $(seq "${MAX_RETRIES}"); do podman push --digestfile=/tmp/digestfile "localhost/${IMAGE_NAME}:${DEFAULT_TAG}" "${IMAGE_REGISTRY}/${IMAGE_NAME}:${DEFAULT_TAG}-${PLATFORM}" && break || sleep $((5 * i)); done echo "remote_image_digest=$(< /tmp/digestfile)" | tee "${GITHUB_OUTPUT}" - name: Install Cosign uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) - name: Sign Image if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) env: REMOTE_IMAGE_DIGEST: ${{ steps.push.outputs.remote_image_digest }} COSIGN_EXPERIMENTAL: false COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }} run: cosign sign -y --new-bundle-format=false --use-signing-config=false --key env://COSIGN_PRIVATE_KEY "${IMAGE_REGISTRY}/${IMAGE_NAME}@${REMOTE_IMAGE_DIGEST}" - name: Create Job Outputs if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) env: IMAGE_NAME: ${{ env.IMAGE_NAME }} PLATFORM: ${{ matrix.platform }} DIGEST: ${{ steps.push.outputs.remote_image_digest }} run: | set -x mkdir -p /tmp/outputs/digests echo "${DIGEST}" | tee "/tmp/outputs/digests/${IMAGE_NAME}-${PLATFORM}.txt" - name: Upload Output Artifacts if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 with: name: ${{ env.IMAGE_NAME }}-${{ matrix.platform }} retention-days: 1 if-no-files-found: error path: | /tmp/outputs/digests/*.txt manifest: name: Create Manifest runs-on: ubuntu-latest if: always() needs: - build_push container: image: docker.io/library/alpine:latest options: --privileged --security-opt seccomp=unconfined permissions: contents: read packages: write id-token: write steps: - name: Install dependencies run: apk add bash coreutils curl docker findutils fuse-overlayfs git grep jq podman sed # Necessary until bootc supports Cosign v3 - name: Install old cosign env: GITHUB_TOKEN: ${{ github.token }} run: | # Thank you Bri! :3 @b- curl --retry 3 -H "Authorization: Bearer ${GITHUB_TOKEN}" -fsSLo "/usr/bin/cosign" "https://github.com/sigstore/cosign/releases/download/v2.6.1/cosign-linux-amd64" chmod +x /usr/bin/cosign - name: Exit on failure shell: bash env: JOBS: ${{ toJson(needs) }} run: | for i in $(jq -r 'to_entries[] | .value.result' <<< "${JOBS}"); do if [ "$i" != "success" ] && [ "$i" != "skipped" ]; then echo "Status check not okay!" exit 1 fi done # This generates 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 - name: Get current date id: date run: echo "date=$(date -u +%Y\-%m\-%d\T%H\:%M\:%S\Z)" | tee "${GITHUB_OUTPUT}" - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Read Brew Version id: brew_version run: echo "version=$(cat BREW_VERSION)" >> $GITHUB_OUTPUT - name: Image Metadata uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5 id: metadata with: tags: | type=raw,value=${{ env.DEFAULT_TAG }} type=raw,value=${{ env.DEFAULT_TAG }}.{{date 'YYYYMMDD'}} type=raw,value={{date 'YYYYMMDD'}} type=raw,value=${{ steps.brew_version.outputs.version }} type=ref,event=pr type=sha,enable=${{ github.event_name == 'pull_request' }} labels: | io.artifacthub.package.deprecated=false io.artifacthub.package.keywords=${{ env.IMAGE_KEYWORDS }} io.artifacthub.package.license=Apache-2.0 io.artifacthub.package.logo-url=${{ env.IMAGE_LOGO_URL }} io.artifacthub.package.prerelease=false io.artifacthub.package.readme-url=https://raw.githubusercontent.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/${{ github.sha }}/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 }}/${{ 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'}} - name: Fetch Build Outputs if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 with: pattern: ${{ env.IMAGE_NAME }}-* merge-multiple: true path: /tmp/artifacts - name: Load Outputs if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) shell: bash id: load-outputs run: | set -x OUTCSV="$(mktemp)" for digest_file in /tmp/artifacts/*.txt; do # Extract the platform from the file name PLATFORM="$(basename "${digest_file}" ".txt" | sed 's/.*-//g')" IMAGE_NAME="$(basename "${digest_file}" ".txt" | grep -Po ".*-" | sed 's/-$//')" DIGEST="$(< "${digest_file}")" echo "${PLATFORM},${IMAGE_NAME},${DIGEST}" | tee -a "${OUTCSV}" done echo "OUTPUT_CSV=$(base32 -w 0 < "${OUTCSV}")" | tee -a "${GITHUB_OUTPUT}" - name: Login to GitHub Container Registry if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) env: REGISTRY: ghcr.io run: | echo "${{ secrets.GITHUB_TOKEN }}" | podman login -u "${{ github.actor }}" --password-stdin "${REGISTRY}" echo "${{ secrets.GITHUB_TOKEN }}" | docker login -u "${{ github.actor }}" --password-stdin "${REGISTRY}" - name: Publish manifest with unified architecture and sign shell: bash if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) env: COSIGN_EXPERIMENTAL: false COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }} LABELS: ${{ steps.metadata.outputs.labels }} OUTPUT_CSV: ${{ steps.load-outputs.outputs.OUTPUT_CSV }} TAGS: ${{ steps.metadata.outputs.tags }} run: | set -x OUTPUT_CSV_FILE="$(mktemp)" echo "${OUTPUT_CSV}" | base32 -d | tee "${OUTPUT_CSV_FILE}" # This is insanely dumb, please PR if you have a better solution ~ @tulilirockz :3c IMAGES=$(cat "${OUTPUT_CSV_FILE}" | while IFS=$'\n' read -r ENTRY ; do echo "${ENTRY}" | cut -d, -f2 ; done | sort | uniq | tr "\n" " " | sed 's/.$//') for TARGET_IMAGE in ${IMAGES} ; do TARGET_MANIFEST="${IMAGE_REGISTRY}/${TARGET_IMAGE}" podman manifest create "${TARGET_MANIFEST}" PLATFORMS=$(grep -F -e "${IMAGE}" "${OUTPUT_CSV_FILE}" | while IFS=$'\n' read -r ENTRY ; do echo "${ENTRY}" | cut -d, -f1 ; done | sort | uniq | tr "\n" " " | sed 's/.$//') for TARGET_PLATFORM in ${PLATFORMS} ; do TARGET_DIGEST="$(grep -F -e ",${TARGET_IMAGE}," "${OUTPUT_CSV_FILE}" | grep -F -e "${TARGET_PLATFORM}" | cut -d, -f3 | tr -d "\n")" podman manifest add "${TARGET_MANIFEST}" "${TARGET_MANIFEST}@${TARGET_DIGEST}" --arch "${TARGET_PLATFORM}" done while IFS= read -r LABEL; do podman manifest annotate --index --annotation "${LABEL}" "${TARGET_MANIFEST}" done <<< "${LABELS}" while IFS= read -r TAG; do podman manifest push --all=false --digestfile=/tmp/digestfile "${TARGET_MANIFEST}" "${TARGET_MANIFEST}:${TAG}" done <<< "${TAGS}" cosign sign -y --new-bundle-format=false --use-signing-config=false --key env://COSIGN_PRIVATE_KEY "${TARGET_MANIFEST}@$(< /tmp/digestfile)" done