Refine public bean page annotations #18
Workflow file for this run
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: Release Container | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| release_tag: | |
| description: Existing release tag to build, for example v0.1.0 | |
| required: true | |
| type: string | |
| publish_release: | |
| description: Publish the GitHub Release after attaching generated assets | |
| required: true | |
| default: true | |
| type: boolean | |
| permissions: | |
| actions: read | |
| attestations: write | |
| contents: write | |
| id-token: write | |
| packages: write | |
| artifact-metadata: write | |
| env: | |
| REGISTRY: ghcr.io | |
| RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} | |
| jobs: | |
| publish: | |
| name: Build, sign, and publish GHCR image | |
| if: ${{ github.server_url == 'https://github.com' }} | |
| runs-on: ubuntu-latest | |
| env: | |
| CONTAINER_SPDX_SBOM: roastnode-container-${{ inputs.release_tag || github.ref_name }}.spdx.json | |
| CONTAINER_CDX_SBOM: roastnode-container-${{ inputs.release_tag || github.ref_name }}.cdx.json | |
| EXODOS_API_TOKEN: ${{ secrets.EXODOS_API_TOKEN }} | |
| EXODOS_API_URL: ${{ vars.EXODOS_API_URL }} | |
| EXODOS_INVENTORYROOT_SPDX_ID: ${{ vars.EXODOS_INVENTORYROOT_SPDX_ID }} | |
| EXODOS_INVENTORYROOT_CDX_ID: ${{ vars.EXODOS_INVENTORYROOT_CDX_ID }} | |
| IMAGE_DIGEST_FILE: roastnode-image-${{ inputs.release_tag || github.ref_name }}.txt | |
| PUBLISH_RELEASE: ${{ github.event_name == 'push' || inputs.publish_release }} | |
| steps: | |
| - name: Validate release tag | |
| run: | | |
| set -euo pipefail | |
| if ! echo "$RELEASE_TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$'; then | |
| echo "::error::Release tag must look like vX.Y.Z or vX.Y.Z-rc.1." | |
| exit 1 | |
| fi | |
| - name: Checkout release tag | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ env.RELEASE_TAG }} | |
| - name: Read GitHub release state | |
| id: release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if release_json="$(gh release view "$RELEASE_TAG" --json isDraft,isImmutable,isPrerelease 2>/dev/null)"; then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| echo "is_draft=$(printf '%s' "$release_json" | jq -r '.isDraft')" >> "$GITHUB_OUTPUT" | |
| echo "is_immutable=$(printf '%s' "$release_json" | jq -r '.isImmutable')" >> "$GITHUB_OUTPUT" | |
| echo "is_prerelease=$(printf '%s' "$release_json" | jq -r '.isPrerelease')" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| echo "is_draft=false" >> "$GITHUB_OUTPUT" | |
| echo "is_immutable=false" >> "$GITHUB_OUTPUT" | |
| if [[ "$RELEASE_TAG" == *-* ]]; then | |
| echo "is_prerelease=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "is_prerelease=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| fi | |
| - name: Refuse already immutable release | |
| if: ${{ steps.release.outputs.exists == 'true' && steps.release.outputs.is_draft != 'true' && steps.release.outputs.is_immutable == 'true' }} | |
| run: | | |
| echo "::error::Release ${RELEASE_TAG} is already immutable, so generated assets cannot be added or replaced. Create the release as a draft, run this workflow, then publish it after the assets are attached." | |
| exit 1 | |
| - name: Read source commit | |
| id: source | |
| run: | | |
| echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| echo "short_sha=$(git rev-parse --short=12 HEAD)" >> "$GITHUB_OUTPUT" | |
| - name: Prepare image reference | |
| id: image | |
| run: echo "ref=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v3 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract Docker metadata | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ${{ steps.image.outputs.ref }} | |
| tags: | | |
| type=raw,value=${{ env.RELEASE_TAG }} | |
| type=semver,pattern={{version}},value=${{ env.RELEASE_TAG }} | |
| type=semver,pattern={{major}}.{{minor}},value=${{ env.RELEASE_TAG }} | |
| type=raw,value=latest,enable=${{ !contains(env.RELEASE_TAG, '-') }} | |
| type=raw,value=sha-${{ steps.source.outputs.short_sha }} | |
| labels: | | |
| org.opencontainers.image.title=Roastnode | |
| org.opencontainers.image.description=Rails-first self-hostable coffee tracking app for private household workspaces | |
| org.opencontainers.image.licenses=AGPL-3.0-only | |
| - name: Build and push image | |
| id: build | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| annotations: ${{ steps.meta.outputs.annotations }} | |
| build-args: | | |
| ROASTNODE_VERSION=${{ env.RELEASE_TAG }} | |
| provenance: mode=max | |
| sbom: true | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@v4.1.0 | |
| - name: Sign image digest | |
| env: | |
| COSIGN_YES: "true" | |
| IMAGE_REF: ${{ steps.image.outputs.ref }} | |
| IMAGE_DIGEST: ${{ steps.build.outputs.digest }} | |
| run: cosign sign "${IMAGE_REF}@${IMAGE_DIGEST}" | |
| - name: Generate container SPDX SBOM Syft | |
| uses: anchore/sbom-action@v0 | |
| with: | |
| image: ${{ steps.image.outputs.ref }}@${{ steps.build.outputs.digest }} | |
| format: spdx-json | |
| output-file: ${{ env.CONTAINER_SPDX_SBOM }} | |
| artifact-name: ${{ env.CONTAINER_SPDX_SBOM }} | |
| upload-artifact: false | |
| - name: Generate container CDX SBOM Syft | |
| uses: anchore/sbom-action@v0 | |
| with: | |
| image: ${{ steps.image.outputs.ref }}@${{ steps.build.outputs.digest }} | |
| format: cyclonedx-json | |
| output-file: ${{ env.CONTAINER_CDX_SBOM }} | |
| artifact-name: ${{ env.CONTAINER_CDX_SBOM }} | |
| upload-artifact: false | |
| - name: Attest build provenance | |
| uses: actions/attest@v4 | |
| if: ${{ github.event.repository.visibility == 'public' }} | |
| with: | |
| subject-name: ${{ steps.image.outputs.ref }} | |
| subject-digest: ${{ steps.build.outputs.digest }} | |
| push-to-registry: true | |
| - name: Attest container SBOM | |
| uses: actions/attest@v4 | |
| if: ${{ github.event.repository.visibility == 'public' }} | |
| with: | |
| subject-name: ${{ steps.image.outputs.ref }} | |
| subject-digest: ${{ steps.build.outputs.digest }} | |
| sbom-path: ${{ env.CONTAINER_CDX_SBOM }} | |
| push-to-registry: true | |
| - name: Explain skipped attestations | |
| if: ${{ github.event.repository.visibility != 'public' }} | |
| run: echo "Skipping GitHub artifact attestations until the repository is public." | |
| - name: Write image digest asset | |
| env: | |
| IMAGE_REF: ${{ steps.image.outputs.ref }} | |
| IMAGE_DIGEST: ${{ steps.build.outputs.digest }} | |
| SOURCE_SHA: ${{ steps.source.outputs.sha }} | |
| TAGS: ${{ steps.meta.outputs.tags }} | |
| run: | | |
| set -euo pipefail | |
| { | |
| echo "image=${IMAGE_REF}@${IMAGE_DIGEST}" | |
| echo "digest=${IMAGE_DIGEST}" | |
| echo "release_tag=${RELEASE_TAG}" | |
| echo "source_sha=${SOURCE_SHA}" | |
| echo | |
| echo "tags:" | |
| printf '%s\n' "${TAGS}" | |
| } > "$IMAGE_DIGEST_FILE" | |
| - name: Add image digest to workflow summary | |
| env: | |
| IMAGE_REF: ${{ steps.image.outputs.ref }} | |
| IMAGE_DIGEST: ${{ steps.build.outputs.digest }} | |
| SOURCE_SHA: ${{ steps.source.outputs.sha }} | |
| run: | | |
| set -euo pipefail | |
| { | |
| echo "## Container image" | |
| echo | |
| echo "Digest-pinned image:" | |
| echo | |
| echo '```' | |
| echo "${IMAGE_REF}@${IMAGE_DIGEST}" | |
| echo '```' | |
| echo | |
| echo "Digest:" | |
| echo | |
| echo '```' | |
| echo "${IMAGE_DIGEST}" | |
| echo '```' | |
| echo | |
| echo "- Release tag: \`${RELEASE_TAG}\`" | |
| echo "- Source commit: \`${SOURCE_SHA}\`" | |
| echo "- Release asset: \`${IMAGE_DIGEST_FILE}\`" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Attach SBOM and digest to GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| assets=("$CONTAINER_SPDX_SBOM" "$CONTAINER_CDX_SBOM" "$IMAGE_DIGEST_FILE") | |
| if [[ "${{ steps.release.outputs.exists }}" == "true" ]]; then | |
| gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber | |
| else | |
| create_flags=(--verify-tag --title "$RELEASE_TAG" --generate-notes) | |
| if [[ "${{ steps.release.outputs.is_prerelease }}" == "true" ]]; then | |
| create_flags+=(--prerelease) | |
| fi | |
| if [[ "$PUBLISH_RELEASE" != "true" ]]; then | |
| create_flags+=(--draft) | |
| fi | |
| gh release create "$RELEASE_TAG" "${assets[@]}" "${create_flags[@]}" | |
| fi | |
| if [[ "${{ steps.release.outputs.exists }}" == "true" && "${{ steps.release.outputs.is_draft }}" == "true" && "$PUBLISH_RELEASE" == "true" ]]; then | |
| gh release edit "$RELEASE_TAG" --draft=false | |
| fi | |
| - name: Notify private deploy webhook | |
| env: | |
| GITEA_DEPLOY_WEBHOOK_URL: ${{ secrets.GITEA_DEPLOY_WEBHOOK_URL }} | |
| GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} | |
| GITEA_DEPLOY_REF: ${{ secrets.GITEA_DEPLOY_REF }} | |
| IMAGE_REF: ${{ steps.image.outputs.ref }} | |
| IMAGE_DIGEST: ${{ steps.build.outputs.digest }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "$GITEA_DEPLOY_WEBHOOK_URL" || -z "$GITEA_TOKEN" || -z "$GITEA_DEPLOY_REF" ]]; then | |
| echo "Skipping private deploy webhook because GITEA_DEPLOY_WEBHOOK_URL, GITEA_TOKEN, or GITEA_DEPLOY_REF is not configured." | |
| exit 0 | |
| fi | |
| image="${IMAGE_REF}:${RELEASE_TAG}@${IMAGE_DIGEST}" | |
| payload="$(jq -n \ | |
| --arg ref "$GITEA_DEPLOY_REF" \ | |
| --arg image "$image" \ | |
| '{ref: $ref, inputs: {roastnode_image: $image}}')" | |
| curl -fsS -X POST \ | |
| -H "Authorization: token ${GITEA_TOKEN}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$payload" \ | |
| "$GITEA_DEPLOY_WEBHOOK_URL" | |
| - name: Upload container CycloneDX SBOM to exodos.io | |
| if: ${{ env.EXODOS_API_TOKEN != '' && env.EXODOS_INVENTORYROOT_CDX_ID != '' }} | |
| env: | |
| SOURCE_SHA: ${{ steps.source.outputs.sha }} | |
| run: | | |
| set -euo pipefail | |
| api_url="${EXODOS_API_URL:-https://api.exodos.io}" | |
| curl -fsS -X POST "${api_url}/api/v1/boms/inventory/bom/upload" \ | |
| -H "X-API-Key: ${EXODOS_API_TOKEN}" \ | |
| -H "accept: application/json" \ | |
| -F "params={\"inventoryroot_id\":\"${EXODOS_INVENTORYROOT_CDX_ID}\",\"tags\":[\"roastnode\",\"container\",\"${RELEASE_TAG}\",\"sha-${SOURCE_SHA}\",{\"name\":\"latest-container\",\"unique\":true}]}" \ | |
| -F "file=@${CONTAINER_CDX_SBOM};type=application/json" | |
| - name: Explain skipped exodos.io upload | |
| if: ${{ env.EXODOS_API_TOKEN == '' || env.EXODOS_INVENTORYROOT_CDX_ID == '' }} | |
| run: echo "Skipping exodos.io upload because EXODOS_API_TOKEN or EXODOS_INVENTORYROOT_CDX_ID is not configured." | |
| - name: Upload container SPDX SBOM to exodos.io | |
| if: ${{ env.EXODOS_API_TOKEN != '' && env.EXODOS_INVENTORYROOT_SPDX_ID != '' }} | |
| env: | |
| SOURCE_SHA: ${{ steps.source.outputs.sha }} | |
| run: | | |
| set -euo pipefail | |
| api_url="${EXODOS_API_URL:-https://api.exodos.io}" | |
| curl -fsS -X POST "${api_url}/api/v1/boms/inventory/bom/upload" \ | |
| -H "X-API-Key: ${EXODOS_API_TOKEN}" \ | |
| -H "accept: application/json" \ | |
| -F "params={\"inventoryroot_id\":\"${EXODOS_INVENTORYROOT_SPDX_ID}\",\"tags\":[\"roastnode\",\"container\",\"${RELEASE_TAG}\",\"sha-${SOURCE_SHA}\",{\"name\":\"latest-container\",\"unique\":true}]}" \ | |
| -F "file=@${CONTAINER_SPDX_SBOM};type=application/json" | |
| - name: Explain skipped exodos.io upload | |
| if: ${{ env.EXODOS_API_TOKEN == '' || env.EXODOS_INVENTORYROOT_SPDX_ID == '' }} | |
| run: echo "Skipping exodos.io upload because EXODOS_API_TOKEN or EXODOS_INVENTORYROOT_SPDX_ID is not configured." |