name: Build and Push Docker Image on: push: tags: ['v*'] workflow_dispatch: env: IMAGE_NAME: ghcr.io/${{ github.repository }} concurrency: group: docker-publish-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ${{ matrix.runner }} permissions: contents: read packages: write strategy: matrix: include: - platform: linux/amd64 runner: ubuntu-latest - platform: linux/arm64 runner: ubuntu-24.04-arm steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) id: meta uses: docker/metadata-action@v5 with: images: ${{ env.IMAGE_NAME }} tags: | type=raw,value=latest type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - name: Build and push by digest id: build uses: docker/build-push-action@v5 with: context: . file: ./Dockerfile target: runtime platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true cache-from: type=gha,scope=${{ matrix.platform }} cache-to: type=gha,mode=max,scope=${{ matrix.platform }} - name: Export digest run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@v4 with: name: digests-${{ matrix.runner }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 merge: needs: build runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Download digests uses: actions/download-artifact@v4 with: path: /tmp/digests pattern: digests-* merge-multiple: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) id: meta uses: docker/metadata-action@v5 with: images: ${{ env.IMAGE_NAME }} tags: | type=raw,value=latest type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - name: Create manifest list and push working-directory: /tmp/digests run: | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *) cleanup: name: Cleanup old runs and artifacts if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest needs: merge permissions: actions: write steps: - name: Cleanup old workflow runs and artifacts uses: actions/github-script@v7 with: script: | const keepCount = 5; const { owner, repo } = context.repo; core.info('Fetching workflow runs...'); const runs = await github.paginate( github.rest.actions.listWorkflowRunsForRepo, { owner, repo, per_page: 100 } ); const sortedRuns = runs.sort((a, b) => new Date(b.created_at) - new Date(a.created_at) ); const runsToDelete = sortedRuns.slice(keepCount); core.info(`Found ${runs.length} workflow runs, deleting ${runsToDelete.length} older runs...`); for (const run of runsToDelete) { try { await github.rest.actions.deleteWorkflowRun({ owner, repo, run_id: run.id }); } catch (error) { core.warning(`Failed to delete run ${run.id}: ${error.message}`); } } core.info('Fetching artifacts...'); const artifacts = await github.paginate( github.rest.actions.listArtifactsForRepo, { owner, repo, per_page: 100 } ); const sortedArtifacts = artifacts .filter(artifact => !artifact.expired) .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); const artifactsToDelete = sortedArtifacts.slice(keepCount); core.info(`Found ${artifacts.length} artifacts, deleting ${artifactsToDelete.length} older artifacts...`); for (const artifact of artifactsToDelete) { try { await github.rest.actions.deleteArtifact({ owner, repo, artifact_id: artifact.id }); } catch (error) { core.warning(`Failed to delete artifact ${artifact.id}: ${error.message}`); } } cleanup-ghcr: name: Cleanup old GHCR images if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest needs: merge permissions: packages: write steps: - name: Delete untagged and old images uses: actions/github-script@v7 with: github-token: ${{ secrets.RELEASES_TOKEN || secrets.GITHUB_TOKEN }} script: | const keepCount = 5; const { owner, repo } = context.repo; const packageName = repo.toLowerCase(); try { const ownerInfo = await github.rest.users.getByUsername({ username: owner }); const isOrg = ownerInfo.data.type === 'Organization'; const ownerArgs = isOrg ? { org: owner } : { username: owner }; const listVersions = isOrg ? github.rest.packages.getAllPackageVersionsForPackageOwnedByOrg : github.rest.packages.getAllPackageVersionsForPackageOwnedByUser; const deleteVersion = isOrg ? github.rest.packages.deletePackageVersionForOrg : github.rest.packages.deletePackageVersionForUser; const versions = await github.paginate( listVersions, { package_type: 'container', package_name: packageName, per_page: 100, state: 'active', ...ownerArgs, } ); const taggedVersions = versions.filter(v => v.metadata?.container?.tags?.length > 0); const untaggedVersions = versions.filter(v => !v.metadata?.container?.tags || v.metadata.container.tags.length === 0); const sortedTagged = taggedVersions.sort((a, b) => new Date(b.created_at) - new Date(a.created_at) ); const taggedToDelete = sortedTagged.slice(keepCount); const oldestKept = sortedTagged.length > 0 ? new Date(sortedTagged[Math.min(keepCount, sortedTagged.length) - 1].created_at) : new Date(); const untaggedToDelete = untaggedVersions.filter(v => new Date(v.created_at) < oldestKept ); for (const version of untaggedToDelete) { try { await deleteVersion({ package_type: 'container', package_name: packageName, package_version_id: version.id, ...ownerArgs, }); } catch (error) { core.warning(`Failed to delete untagged version ${version.id}: ${error.message}`); } } for (const version of taggedToDelete) { try { await deleteVersion({ package_type: 'container', package_name: packageName, package_version_id: version.id, ...ownerArgs, }); } catch (error) { core.warning(`Failed to delete tagged version ${version.id}: ${error.message}`); } } } catch (error) { if (error.status === 404) { core.info('No package found; skipping GHCR cleanup.'); } else { core.setFailed(`Error managing package versions: ${error.message}`); } }