Skip to content

Refine public bean page annotations #18

Refine public bean page annotations

Refine public bean page annotations #18

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."