diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51201fb..cb446dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,31 +1,59 @@ -name: CI Pipeline for ghostcript-printer-app +name: CI on: push: - branches: - - main - pull_request: - branches: - - main + branches: [main] + pull_request: {} workflow_dispatch: +concurrency: + group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" + cancel-in-progress: true + + +permissions: {} jobs: - build-rock: - runs-on: ubuntu-22.04 + build-fsdk: + name: FSDK (${{ matrix.arch }}) + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + runner: ubuntu-24.04 + - arch: aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + timeout-minutes: 360 + permissions: + contents: read steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Pack with Rockcraft - uses: canonical/craft-actions/rockcraft-pack@main - id: rockcraft + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: taiki-e/install-action@d438492cf8a250514fa2d34b30bc3c0dc37c65ff # v2 + with: + tool: just + - name: Install host dependencies + run: sudo apt-get update && sudo apt-get install --yes fuse3 podman + - name: Fetch sources with retries + run: just fetch + - name: Build and verify FSDK appliance + run: just verify build-snap: runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout repository - uses: actions/checkout@v4 - - - name: Build Snap Package - uses: snapcore/action-build@v1 - id: snapcraft \ No newline at end of file + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Build Snap package + id: snapcraft + continue-on-error: true + uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1 + - name: Retry Snap build after transient failure + if: steps.snapcraft.outcome == 'failure' + uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1 diff --git a/.github/workflows/registry-actions.yml b/.github/workflows/registry-actions.yml index 8639d35..c513464 100644 --- a/.github/workflows/registry-actions.yml +++ b/.github/workflows/registry-actions.yml @@ -1,109 +1,279 @@ -name: Pack and Publish OCI Image to Docker Registry and GitHub Packages +name: Publish immutable OCI release on: push: - branches: - - main - workflow_dispatch: - inputs: - workflow_choice: - description: "Choose Release Channel" - required: true - default: "edge" - type: choice - options: - - edge - - stable - - both - workflow_run: - workflows: ["Push new tag update to stable branch"] - types: - - completed + tags: + - "v*" -jobs: - build-rock: - runs-on: ubuntu-22.04 - steps: - - name: Checkout repository - uses: actions/checkout@v4 +concurrency: + group: oci-release-${{ github.ref }} + cancel-in-progress: false - - name: Pack with Rockcraft - uses: canonical/craft-actions/rockcraft-pack@main - id: rockcraft +permissions: {} - - name: Upload Rock Artifact - uses: actions/upload-artifact@v4 - with: - name: cups-rock - path: ${{ steps.rockcraft.outputs.rock }} +env: + IMAGE: ghcr.io/${{ github.repository_owner }}/ghostscript-printer-app - publish-rock: - needs: build-rock - if: github.ref_name == 'main' - runs-on: ubuntu-latest +jobs: + metadata: + runs-on: ubuntu-24.04 + permissions: + contents: read + outputs: + version: ${{ steps.release.outputs.version }} + revision: ${{ steps.release.outputs.revision }} + created: ${{ steps.release.outputs.created }} + fsdk_version: ${{ steps.release.outputs.fsdk_version }} + fsdk_ref: ${{ steps.release.outputs.fsdk_ref }} steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Download Rock Artifact - uses: actions/download-artifact@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - name: cups-rock - - - name: Install Dependencies + persist-credentials: false + - name: Validate release tag and metadata + id: release + shell: bash run: | - sudo snap install rockcraft --classic - sudo snap install docker - sudo snap install yq + set -euo pipefail + version="$(< VERSION)" + if [[ "$GITHUB_REF_NAME" != "v${version}" ]]; then + printf 'Tag %s does not match application version v%s\n' "$GITHUB_REF_NAME" "$version" >&2 + exit 1 + fi + ref_line="$(sed -n 's/^[[:space:]]*ref: freedesktop-sdk-\(.*\)-0-g\([0-9a-f]\{40\}\)$/\1 \2/p' elements/freedesktop-sdk.bst)" + read -r fsdk_version fsdk_ref <<< "$ref_line" + test -n "$fsdk_version" + test -n "$fsdk_ref" + { + echo "version=$version" + echo "revision=$(git rev-parse HEAD)" + echo "created=$(git show -s --format=%cI HEAD)" + echo "fsdk_version=$fsdk_version" + echo "fsdk_ref=$fsdk_ref" + } >> "$GITHUB_OUTPUT" - - name: Ensure Docker Daemon is Running + build: + needs: metadata + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + platform: amd64 + runner: ubuntu-24.04 + - arch: aarch64 + platform: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + timeout-minutes: 360 + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: taiki-e/install-action@d438492cf8a250514fa2d34b30bc3c0dc37c65ff # v2 + with: + tool: just + - name: Install host dependencies + run: sudo apt-get update && sudo apt-get install --yes fuse3 podman skopeo + - name: Fetch sources with retries + run: just fetch + - name: Build and verify FSDK appliance + run: just verify + - name: Add release config labels + env: + VERSION: ${{ needs.metadata.outputs.version }} + REVISION: ${{ needs.metadata.outputs.revision }} + CREATED: ${{ needs.metadata.outputs.created }} + FSDK_VERSION: ${{ needs.metadata.outputs.fsdk_version }} + FSDK_REF: ${{ needs.metadata.outputs.fsdk_ref }} run: | - sudo systemctl start docker - sudo systemctl enable docker - sudo systemctl is-active --quiet docker || sudo systemctl start docker - - # - name: Log in to Docker Hub - # uses: docker/login-action@v3.2.0 - # with: - # username: ${{ secrets.DOCKER_USERNAME }} - # password: ${{ secrets.DOCKER_PASSWORD }} + set -euo pipefail + source_image="ghcr.io/projectbluefin/ghostscript-printer-app:build" + container="$(podman create "$source_image")" + trap 'podman rm "$container" >/dev/null 2>&1 || true' EXIT + podman commit --format oci \ + --change "LABEL org.opencontainers.image.version=$VERSION" \ + --change "LABEL org.opencontainers.image.revision=$REVISION" \ + --change "LABEL org.opencontainers.image.created=$CREATED" \ + --change "LABEL org.opencontainers.image.source=https://github.com/$GITHUB_REPOSITORY" \ + --change "LABEL io.projectbluefin.fsdk.version=$FSDK_VERSION" \ + --change "LABEL io.projectbluefin.fsdk.ref=$FSDK_REF" \ + "$container" "$source_image" + - name: Log in and publish architecture image + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.metadata.outputs.version }} + run: | + set -euo pipefail + owner="${GITHUB_REPOSITORY_OWNER,,}" + image="ghcr.io/${owner}/ghostscript-printer-app" + target="${image}:${VERSION}-${{ matrix.arch }}" + echo "$GH_TOKEN" | podman login ghcr.io --username "$GITHUB_ACTOR" --password-stdin + if probe="$(skopeo inspect --creds "$GITHUB_ACTOR:$GH_TOKEN" "docker://${target}" 2>&1)"; then + printf 'Refusing to overwrite immutable architecture tag %s\n' "$target" >&2 + exit 1 + elif [[ "${probe,,}" != *"manifest unknown"* && "${probe,,}" != *"name unknown"* ]]; then + printf 'Could not prove architecture tag %s is absent: %s\n' "$target" "$probe" >&2 + exit 1 + fi + podman push "ghcr.io/projectbluefin/ghostscript-printer-app:build" "docker://${target}" - - name: Log in to GitHub Packages - uses: docker/login-action@v3.2.0 + publish: + needs: [metadata, build] + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + id-token: write + attestations: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: taiki-e/install-action@d438492cf8a250514fa2d34b30bc3c0dc37c65ff # v2 + with: + tool: just + - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and Push Docker Image (Edge & Latest Channel) - if: github.event.inputs.workflow_choice == 'edge' || github.event.inputs.workflow_choice == 'both' || github.event_name == 'push' || github.event_name == 'workflow_run' + username: ${{ github.actor }} + password: ${{ github.token }} + - name: Install registry tools + run: sudo apt-get update && sudo apt-get install --yes fuse3 jq podman skopeo + - name: Install Cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + - name: Install ORAS + uses: oras-project/setup-oras@1d808f7d7f6995cc68b7bf507bfe5c5446e1dc9d # v2.0.1 + - name: Publish annotated multi-architecture index + id: publish env: - ORG: ${{ github.repository_owner }} + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.metadata.outputs.version }} + REVISION: ${{ needs.metadata.outputs.revision }} + CREATED: ${{ needs.metadata.outputs.created }} + FSDK_VERSION: ${{ needs.metadata.outputs.fsdk_version }} + FSDK_REF: ${{ needs.metadata.outputs.fsdk_ref }} run: | - IMAGE="$(yq '.name' rockcraft.yaml)" - VERSION="$(yq '.version' rockcraft.yaml)" - ROCK="$(find . -maxdepth 1 -type f -name '*.rock' -print -quit)" - test -n "$ROCK" - ORG_NAME="${ORG,,}" - sudo rockcraft.skopeo --insecure-policy copy oci-archive:"${ROCK}" docker-daemon:"${ORG_NAME}/${IMAGE}:${VERSION}-edge" - # Push to GitHub Packages - GITHUB_IMAGE="ghcr.io/${ORG_NAME}/${IMAGE}" - docker tag "${ORG_NAME}/${IMAGE}:${VERSION}-edge" "${GITHUB_IMAGE}:${VERSION}-edge" - docker push "${GITHUB_IMAGE}:${VERSION}-edge" - docker tag "${GITHUB_IMAGE}:${VERSION}-edge" "${GITHUB_IMAGE}:latest" - docker push "${GITHUB_IMAGE}:latest" - - - name: Build and Push Docker Image (Stable Channel) - if: github.event.inputs.workflow_choice == 'stable' || github.event.inputs.workflow_choice == 'both' + set -euo pipefail + owner="${GITHUB_REPOSITORY_OWNER,,}" + image="ghcr.io/${owner}/ghostscript-printer-app" + target="${image}:${VERSION}" + authfile="$HOME/.docker/config.json" + if probe="$(skopeo inspect --authfile "$authfile" "docker://${target}" 2>&1)"; then + printf 'Refusing to overwrite immutable release tag %s\n' "$target" >&2 + exit 1 + elif [[ "${probe,,}" != *"manifest unknown"* && "${probe,,}" != *"name unknown"* ]]; then + printf 'Could not prove release tag %s is absent: %s\n' "$target" "$probe" >&2 + exit 1 + fi + for arch in amd64 arm64; do + source="${image}:${VERSION}-$([[ "$arch" == amd64 ]] && echo x86_64 || echo aarch64)" + actual="$(skopeo inspect --authfile "$authfile" "docker://${source}" | jq -r .Architecture)" + [[ "$actual" == "$arch" ]] + config="$(skopeo inspect --config --authfile "$authfile" "docker://${source}")" + jq -e \ + --arg arch "$arch" \ + --arg version "$VERSION" \ + --arg revision "$REVISION" \ + --arg created "$CREATED" \ + --arg fsdk_version "$FSDK_VERSION" \ + --arg fsdk_ref "$FSDK_REF" \ + --arg source_url "https://github.com/${GITHUB_REPOSITORY}" \ + '.architecture == $arch and + .config.User == "65532:65532" and + .config.Entrypoint == ["/usr/bin/catatonit", "--", "/usr/bin/bash", "/usr/libexec/ghostscript-printer-app/container-entrypoint"] and + .config.Labels["org.opencontainers.image.title"] == "ghostscript-printer-app" and + .config.Labels["org.opencontainers.image.source"] == $source_url and + .config.Labels["org.opencontainers.image.licenses"] == "Apache-2.0" and + .config.Labels["org.opencontainers.image.version"] == $version and + .config.Labels["org.opencontainers.image.revision"] == $revision and + .config.Labels["org.opencontainers.image.created"] == $created and + .config.Labels["io.projectbluefin.fsdk.version"] == $fsdk_version and + .config.Labels["io.projectbluefin.fsdk.ref"] == $fsdk_ref' <<< "$config" + done + docker buildx imagetools create \ + --tag "$target" \ + --annotation "index:org.opencontainers.image.title=ghostscript-printer-app" \ + --annotation "index:org.opencontainers.image.description=Ghostscript Printer Application built from freedesktop-sdk" \ + --annotation "index:org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}" \ + --annotation "index:org.opencontainers.image.licenses=Apache-2.0" \ + --annotation "index:org.opencontainers.image.version=$VERSION" \ + --annotation "index:org.opencontainers.image.revision=$REVISION" \ + --annotation "index:org.opencontainers.image.created=$CREATED" \ + --annotation "index:io.projectbluefin.fsdk.version=$FSDK_VERSION" \ + --annotation "index:io.projectbluefin.fsdk.ref=$FSDK_REF" \ + "${image}:${VERSION}-x86_64" \ + "${image}:${VERSION}-aarch64" + digest="$(skopeo inspect --authfile "$authfile" "docker://${target}" | jq -r .Digest)" + [[ "$digest" == sha256:* ]] + echo "image=$image" >> "$GITHUB_OUTPUT" + echo "digest=$digest" >> "$GITHUB_OUTPUT" + - name: Generate BuildStream-native SPDX SBOM + run: just sbom + - name: Sign image and attach signed SBOM + id: evidence + env: + IMAGE: ${{ steps.publish.outputs.image }} + DIGEST: ${{ steps.publish.outputs.digest }} + run: | + set -euo pipefail + cosign sign --yes "${IMAGE}@${DIGEST}" + sbom_digest="$(oras attach --format json --no-tty \ + --artifact-type application/vnd.spdx+json \ + --annotation org.opencontainers.image.title=ghostscript-printer-app-sbom \ + "${IMAGE}@${DIGEST}" \ + ghostscript-printer-app.spdx.json:application/spdx+json | jq -r .digest)" + [[ "$sbom_digest" == sha256:* ]] + cosign sign --yes "${IMAGE}@${sbom_digest}" + echo "sbom-digest=$sbom_digest" >> "$GITHUB_OUTPUT" + - name: Generate GitHub provenance attestation + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-name: ${{ steps.publish.outputs.image }} + subject-digest: ${{ steps.publish.outputs.digest }} + push-to-registry: true + - name: Verify published release evidence env: - ORG: ${{ github.repository_owner }} + GH_TOKEN: ${{ github.token }} + IMAGE: ${{ steps.publish.outputs.image }} + DIGEST: ${{ steps.publish.outputs.digest }} + SBOM_DIGEST: ${{ steps.evidence.outputs.sbom-digest }} + VERSION: ${{ needs.metadata.outputs.version }} + REVISION: ${{ needs.metadata.outputs.revision }} + CREATED: ${{ needs.metadata.outputs.created }} + FSDK_VERSION: ${{ needs.metadata.outputs.fsdk_version }} + FSDK_REF: ${{ needs.metadata.outputs.fsdk_ref }} run: | - IMAGE="$(yq '.name' rockcraft.yaml)" - VERSION="$(yq '.version' rockcraft.yaml)" - ROCK="$(find . -maxdepth 1 -type f -name '*.rock' -print -quit)" - test -n "$ROCK" - ORG_NAME="${ORG,,}" - sudo rockcraft.skopeo --insecure-policy copy oci-archive:"${ROCK}" docker-daemon:"${ORG_NAME}/${IMAGE}:${VERSION}-stable" - GITHUB_IMAGE="ghcr.io/${ORG_NAME}/${IMAGE}" - docker tag "${ORG_NAME}/${IMAGE}:${VERSION}-stable" "${GITHUB_IMAGE}:${VERSION}-stable" - docker push "${GITHUB_IMAGE}:${VERSION}-stable" + set -euo pipefail + identity="https://github.com/${GITHUB_REPOSITORY}/.github/workflows/registry-actions.yml@refs/tags/v${VERSION}" + cosign verify \ + --certificate-identity "$identity" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + "${IMAGE}@${DIGEST}" + cosign verify \ + --certificate-identity "$identity" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + "${IMAGE}@${SBOM_DIGEST}" + gh attestation verify "oci://${IMAGE}@${DIGEST}" --repo "$GITHUB_REPOSITORY" + oras discover --format json "${IMAGE}@${DIGEST}" | + jq -e --arg digest "$SBOM_DIGEST" '.manifests[] | select(.digest == $digest and .artifactType == "application/vnd.spdx+json")' + raw="$(skopeo inspect --raw "docker://${IMAGE}@${DIGEST}")" + [[ "$(jq -r '[.manifests[].platform.architecture] | sort | join(" ")' <<< "$raw")" == "amd64 arm64" ]] + jq -e \ + --arg version "$VERSION" \ + --arg revision "$REVISION" \ + --arg created "$CREATED" \ + --arg fsdk_version "$FSDK_VERSION" \ + --arg fsdk_ref "$FSDK_REF" \ + --arg source_url "https://github.com/${GITHUB_REPOSITORY}" \ + '.annotations["org.opencontainers.image.title"] == "ghostscript-printer-app" and + .annotations["org.opencontainers.image.source"] == $source_url and + .annotations["org.opencontainers.image.version"] == $version and + .annotations["org.opencontainers.image.revision"] == $revision and + .annotations["org.opencontainers.image.created"] == $created and + .annotations["org.opencontainers.image.licenses"] == "Apache-2.0" and + .annotations["io.projectbluefin.fsdk.version"] == $fsdk_version and + .annotations["io.projectbluefin.fsdk.ref"] == $fsdk_ref' <<< "$raw" diff --git a/Justfile b/Justfile index 2d55782..6788662 100644 --- a/Justfile +++ b/Justfile @@ -1,6 +1,5 @@ # BuildStream runs in the pinned freedesktop-sdk builder image. bst2_image := env("BST2_IMAGE", "registry.gitlab.com/freedesktop-sdk/infrastructure/freedesktop-sdk-docker-images/bst2:64eb0b4930d57a92710822898fb73af6cc1ae35d") -sudo_cmd := if `podman info >/dev/null 2>&1 && echo 1 || echo 0` == "1" { "" } else { "sudo" } image_ref := "ghcr.io/projectbluefin/ghostscript-printer-app:build" default: @@ -33,7 +32,7 @@ bst *ARGS: EOF RE_FLAG=(--config /src/.bst-re.conf) fi - {{ sudo_cmd }} podman run --rm \ + podman run --rm \ --privileged \ --device /dev/fuse \ --network=host \ @@ -46,6 +45,19 @@ bst *ARGS: validate: just bst show --deps all oci/ghostscript-printer-app.bst +fetch: + #!/usr/bin/env bash + set -euo pipefail + for attempt in 1 2 3; do + if just bst source fetch --deps all oci/ghostscript-printer-app.bst; then + exit 0 + fi + echo "source fetch failed (attempt ${attempt}/3)" >&2 + if [[ "$attempt" -lt 3 ]]; then sleep 15; fi + done + exit 1 + + build: #!/usr/bin/env bash set -euo pipefail @@ -57,9 +69,9 @@ export: set -euo pipefail rm -rf .build-out just bst artifact checkout oci/ghostscript-printer-app.bst --directory /src/.build-out - IMAGE_ID=$({{ sudo_cmd }} podman pull -q oci:.build-out) + IMAGE_ID=$(podman pull -q oci:.build-out) rm -rf .build-out - {{ sudo_cmd }} podman tag "$IMAGE_ID" "{{ image_ref }}" + podman tag "$IMAGE_ID" "{{ image_ref }}" verify-core: tests/core-appliance.sh @@ -88,3 +100,43 @@ verify: just verify-packaged-drivers just verify-stateful-drivers tests/appliance-parity.sh + + +sbom: + #!/usr/bin/env bash + set -euo pipefail + mkdir -p "${HOME}/.cache/buildstream" "${HOME}/.cache/pip" + git_sha="$(git rev-parse HEAD 2>/dev/null || echo unknown)" + podman run --rm \ + --privileged \ + --device /dev/fuse \ + --network=host \ + -v "{{ justfile_directory() }}:/src:rw" \ + -v "${HOME}/.cache/buildstream:/root/.cache/buildstream:rw" \ + -v "${HOME}/.cache/pip:/root/.cache/pip:rw" \ + -w /src \ + -e GIT_SHA="$git_sha" \ + "{{ bst2_image }}" \ + bash -c ' + installed=0 + for attempt in 1 2 3; do + if pip install --quiet \ + git+https://gitlab.com/BuildStream/buildstream-sbom.git@0706fec3bedf6f73bd9d2fed32c2aed585feef8d; then + installed=1 + break + fi + echo "buildstream-sbom install failed (attempt ${attempt}/3)" >&2 + if [[ "$attempt" -lt 3 ]]; then sleep 5; fi + done + if [[ "$installed" != 1 ]]; then + echo "buildstream-sbom installation failed after 3 attempts" >&2 + exit 1 + fi + buildstream-sbom oci/ghostscript-printer-app.bst \ + --spdx-name ghostscript-printer-app \ + --spdx-namespace "https://github.com/projectbluefin/ghostscript-printer-app/sbom/${GIT_SHA}" \ + --spdx-creator "Tool: buildstream-sbom" \ + --spdx-creator "Organization: projectbluefin" \ + --deps all \ + --output /src/ghostscript-printer-app.spdx.json + ' \ No newline at end of file diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..ee351fe --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +10.07.1-1 diff --git a/docs/skills/ci-tooling.md b/docs/skills/ci-tooling.md new file mode 100644 index 0000000..1f3849e --- /dev/null +++ b/docs/skills/ci-tooling.md @@ -0,0 +1,67 @@ +--- +name: ghostscript-printer-app-ci-tooling +description: Use when changing pull-request validation, FSDK source updates, OCI release publication, signatures, SBOMs, or provenance for ghostscript-printer-app. +metadata: + context7-sources: + - /websites/github_en_actions + - /apache/buildstream +--- + +# CI tooling + +## When to Use + +- Changing `.github/workflows/ci.yml`, `registry-actions.yml`, or `update-fsdk-sources.yml`. +- Changing `VERSION`, FSDK release metadata, GHCR tags, SBOM attachment, signing, or provenance. + +## When NOT to Use + +- Local BuildStream element work that does not change CI or release behavior. +- Snap packaging internals outside workflow triggers and permissions. + +## Core Process + +1. Keep pull-request CI credential-free: `contents: read`, native amd64/arm64 runners, and `just verify`. +2. Treat `VERSION` as the application release source. A release tag must equal `v$(cat VERSION)` before any write-capable job starts. +3. Grant `packages: write`, `id-token: write`, and `attestations: write` only to tag-release jobs that need them. +4. Refuse an existing immutable tag. Proceed only when the authenticated registry response explicitly reports a missing manifest or repository; network and authentication failures are fatal. +5. Add version, revision, creation time, license, source URL, FSDK version, and FSDK ref to every architecture image config and to the multi-architecture index. +6. Generate one BuildStream-native SPDX JSON document for the complete dependency graph, attach it to the index, keyless-sign the index and SBOM artifact, publish GitHub provenance with `actions/attest`, then verify all three forms of evidence. +7. Run dependency tracking and full verification with only `contents: read`. Mint the short-lived Mergeraptor installation token afterward, expose it only to the proposal step, and push one atomic update branch without auto-merge. GitHub App-authored pushes trigger pull-request CI; do not add a redundant dispatch. +8. Keep the Snap update/build lanes independent from FSDK OCI publication. +9. Give every external BuildStream source a project alias. Prefer an authoritative, checksummed release archive over a personal Git mirror when upstream Git is unreliable. +10. Give pull-request CI a PR-scoped concurrency group with `cancel-in-progress: true`; stacked force-pushes must not leave duplicate multi-hour architecture jobs consuming the runner pool. + +## Common Rationalizations + +| Rationalization | Reality | +| --- | --- | +| “A failed registry lookup means the tag is absent.” | Authentication and network failures also return nonzero; accept only explicit manifest/name-not-found responses. | +| “The index inherits child labels.” | GHCR renders index metadata; copy required OCI labels into index annotations explicitly. | +| “One host can emulate both architectures.” | Native runners expose architecture-specific source and runtime failures that emulation can hide. | +| “Signing the image covers the SBOM.” | The SBOM is a separate OCI referrer and must be signed and verified separately. | +| “A personal mirror is reachable, so it is a safe fallback.” | Reachability is not provenance. Use an authoritative archive with a verified digest or an organization-controlled mirror. | + +## Red Flags + +- Registry login, package write permission, or OIDC access in a pull-request job. +- A publish trigger other than a matching `v` tag. +- `latest`, `edge`, or `stable` in the OCI release workflow. +- Unpinned third-party actions. +- Source tracking after a write-capable token has been minted. +- Index creation without checking both native architecture manifests and their config labels. +- An SBOM generated for only the runner's architecture. +- An unaliased external source URL or a source pinned only to a personal fork. +- Pull-request CI without cancellation of superseded runs. + +## Verification + +- [ ] `actionlint .github/workflows/*.yml` succeeds. +- [ ] `just verify` succeeds locally. +- [ ] Pull-request CI completes on native amd64 and arm64 runners without registry credentials. +- [ ] BuildStream resolves and fetches every repository-owned source without `[unaliased-url]` warnings. +- [ ] Pushing a replacement commit cancels the superseded run for the same pull request. +- [ ] A mismatched tag fails in the metadata job before any write-capable job. +- [ ] The published index contains exactly amd64 and arm64 and has the required annotations. +- [ ] `cosign verify` succeeds for the index and SBOM artifact. +- [ ] `gh attestation verify oci://@ --repo ` succeeds. diff --git a/docs/superpowers/plans/2026-09-16-fsdk-secure-releases.md b/docs/superpowers/plans/2026-09-16-fsdk-secure-releases.md new file mode 100644 index 0000000..e6873d0 --- /dev/null +++ b/docs/superpowers/plans/2026-09-16-fsdk-secure-releases.md @@ -0,0 +1,40 @@ +# FSDK Secure Multi-Architecture Releases Implementation Plan + +**Goal:** Build and verify native amd64/arm64 FSDK appliances on pull requests, and publish an immutable, attestable multi-architecture GHCR release only when a Git tag exactly matches the packaged application version. + +**Architecture:** Make the application version a repository-level source consumed by both the application build and OCI metadata. Keep pull-request CI read-only and run the complete `just verify` gate on native GitHub-hosted x86_64 and arm64 runners. A separate tag-only release workflow builds and verifies both architectures, adds release-specific config labels, pushes immutable architecture manifests, assembles one versioned OCI index with matching annotations, attaches a BuildStream-native SPDX document for the complete dependency graph, keyless-signs the index and SBOM artifact, creates GitHub provenance, and verifies every published object. No mutable channel tag is created. + +## Task 1: Establish canonical release metadata + +- [x] Add one `VERSION` file containing the Ghostscript-derived application version. +- [x] Make the application build and OCI version label read `VERSION` rather than duplicating literals. +- [x] Add the exact FSDK commit label and retain the FSDK release label. +- [x] Extend the appliance parity gate to compare the binary version, image label, `VERSION`, FSDK junction ref, and FSDK labels. + +## Task 2: Validate pull requests on both architectures + +- [x] Preserve the independent Snap build job. +- [x] Replace the Rockcraft CI job with a native `x86_64`/`aarch64` matrix on GitHub-hosted runners. +- [x] Install only the host tools needed to run BuildStream and Podman, then run `just verify`. +- [x] Give pull-request jobs read-only repository permission and no package, attestation, or identity-token permission. + +## Task 3: Publish immutable multi-architecture releases + +- [x] Replace the mutable Rock publication workflow with a `v*` tag-only workflow. +- [x] Reject tags other than `v$(cat VERSION)` before any login or write-capable job runs. +- [x] Build and verify natively on amd64 and arm64, then add source revision, creation time, exact FSDK version, and exact FSDK ref to each image config. +- [x] Push immutable architecture manifests and create exactly one `${VERSION}` multi-architecture index with matching OCI annotations. +- [x] Never publish `latest`, `edge`, or `stable` aliases. + +## Task 4: Attach and verify supply-chain evidence + +- [x] Generate a BuildStream-native SPDX JSON SBOM for the complete dependency graph and attach it to the index as an OCI referrer artifact. +- [x] Keyless-sign the image index and SBOM artifact with GitHub OIDC. +- [x] Publish GitHub build provenance for the index digest. +- [x] Verify the index platforms, OCI annotations, keyless signature, SBOM referrer/signature, and GitHub attestation after publication. + +## Task 5: Verify and publish + +- [x] Run `just verify`, workflow lint, shell syntax checks, version-contract checks, and `git diff --check`. +- [x] Review the full diff from `feat/fsdk-appliance-parity` and resolve all blocking findings. +- [x] Resolve issue 08, commit and push `feat/fsdk-secure-releases`, and open a stacked PR based on `feat/fsdk-appliance-parity`. diff --git a/elements/oci/ghostscript-printer-app.bst b/elements/oci/ghostscript-printer-app.bst index aa660d5..9d99222 100644 --- a/elements/oci/ghostscript-printer-app.bst +++ b/elements/oci/ghostscript-printer-app.bst @@ -5,6 +5,9 @@ build-depends: - freedesktop-sdk.bst:bootstrap/coreutils.bst - freedesktop-sdk.bst:components/oci-builder.bst - freedesktop-sdk.bst:components/sed.bst + - filename: printer-app/version.bst + config: + location: /metadata - filename: printer-app/core-runtime.bst config: location: /layer @@ -12,6 +15,7 @@ build-depends: config: commands: - | + version="$(cat /metadata/VERSION)" install -d /layer/etc printf '%s\n' \ 'root:x:0:0:root:/root:/usr/bin/bash' \ @@ -55,6 +59,7 @@ config: 'org.opencontainers.image.description': 'Ghostscript Printer Application built from freedesktop-sdk' 'org.opencontainers.image.source': 'https://github.com/projectbluefin/ghostscript-printer-app' 'org.opencontainers.image.licenses': 'Apache-2.0' - 'org.opencontainers.image.version': '10.07.1-1' + 'org.opencontainers.image.version': '${version}' 'io.projectbluefin.fsdk.version': '26.08rc.1' + 'io.projectbluefin.fsdk.ref': 'e076d4978ee6945763486f6ebd755d189460e4e7' EOF diff --git a/elements/printer-app/application.bst b/elements/printer-app/application.bst index e4096e3..583dfb9 100644 --- a/elements/printer-app/application.bst +++ b/elements/printer-app/application.bst @@ -12,6 +12,8 @@ sources: path: ghostscript-printer-app.service - kind: local path: testpage.ps + - kind: local + path: VERSION build-depends: - freedesktop-sdk.bst:public-stacks/buildsystem-autotools.bst @@ -23,6 +25,6 @@ depends: config: build-commands: - make clean - - make -j2 VERSION=10.07.1-1 LDFLAGS="$LDFLAGS -ljpeg" + - make -j2 VERSION="$(cat VERSION)" LDFLAGS="$LDFLAGS -ljpeg" install-commands: - - make DESTDIR="%{install-root}" VERSION=10.07.1-1 LDFLAGS="$LDFLAGS -ljpeg" unitdir= install + - make DESTDIR="%{install-root}" VERSION="$(cat VERSION)" LDFLAGS="$LDFLAGS -ljpeg" unitdir= install diff --git a/elements/printer-app/jbigkit.bst b/elements/printer-app/jbigkit.bst index c2af54a..250ff75 100644 --- a/elements/printer-app/jbigkit.bst +++ b/elements/printer-app/jbigkit.bst @@ -2,10 +2,9 @@ kind: manual description: Build the JBIG and JBIG85 shared libraries required by legacy drivers. sources: - - kind: git_repo - url: https://www.cl.cam.ac.uk/~mgk25/git/jbigkit - track: v2.1 - ref: v2.1-0-g60bd8bd6579c12e5e3a24782bd49af2046a9738a + - kind: tar + url: debian:pool/main/j/jbigkit/jbigkit_2.1.orig.tar.gz + ref: de7106b6bfaf495d6865c7dd7ac6ca1381bd12e0d81405ea81e7f2167263d932 build-depends: - freedesktop-sdk.bst:public-stacks/buildsystem-make.bst diff --git a/elements/printer-app/perl-clone.bst b/elements/printer-app/perl-clone.bst index cce4677..c34afb5 100644 --- a/elements/printer-app/perl-clone.bst +++ b/elements/printer-app/perl-clone.bst @@ -9,6 +9,7 @@ depends: sources: - kind: cpan + index: "cpan:" name: Clone suffix: authors/id/A/AT/ATOOMIC/Clone-0.50.tar.gz sha256sum: f9732a4a857974db30905233589113003301b585b0cecda29a21cfba5bb014f9 diff --git a/elements/printer-app/perl-xml-libxml.bst b/elements/printer-app/perl-xml-libxml.bst index a4c8ab9..da5bb9c 100644 --- a/elements/printer-app/perl-xml-libxml.bst +++ b/elements/printer-app/perl-xml-libxml.bst @@ -13,6 +13,7 @@ depends: sources: - kind: cpan + index: "cpan:" name: XML::LibXML suffix: authors/id/T/TO/TODDR/XML-LibXML-2.0213.tar.gz sha256sum: 2af21c5d61ac34ea26a5fabf15ba5a5841e648f7189db3e33b6f28b5489802ab diff --git a/elements/printer-app/perl-xml-namespace-support.bst b/elements/printer-app/perl-xml-namespace-support.bst index 2258d28..d0c67b3 100644 --- a/elements/printer-app/perl-xml-namespace-support.bst +++ b/elements/printer-app/perl-xml-namespace-support.bst @@ -9,6 +9,7 @@ depends: sources: - kind: cpan + index: "cpan:" name: XML::NamespaceSupport suffix: authors/id/P/PE/PERIGRIN/XML-NamespaceSupport-1.12.tar.gz sha256sum: 47e995859f8dd0413aa3f22d350c4a62da652e854267aa0586ae544ae2bae5ef diff --git a/elements/printer-app/perl-xml-sax-base.bst b/elements/printer-app/perl-xml-sax-base.bst index 283edaf..47b57c0 100644 --- a/elements/printer-app/perl-xml-sax-base.bst +++ b/elements/printer-app/perl-xml-sax-base.bst @@ -9,6 +9,7 @@ depends: sources: - kind: cpan + index: "cpan:" name: XML::SAX::Base suffix: authors/id/G/GR/GRANTM/XML-SAX-Base-1.09.tar.gz sha256sum: 66cb355ba4ef47c10ca738bd35999723644386ac853abbeb5132841f5e8a2ad0 diff --git a/elements/printer-app/perl-xml-sax.bst b/elements/printer-app/perl-xml-sax.bst index fea36ad..911a907 100644 --- a/elements/printer-app/perl-xml-sax.bst +++ b/elements/printer-app/perl-xml-sax.bst @@ -11,6 +11,7 @@ depends: sources: - kind: cpan + index: "cpan:" name: XML::SAX suffix: authors/id/G/GR/GRANTM/XML-SAX-1.02.tar.gz sha256sum: 4506c387043aa6a77b455f00f57409f3720aa7e553495ab2535263b4ed1ea12a diff --git a/elements/printer-app/version.bst b/elements/printer-app/version.bst new file mode 100644 index 0000000..1f363c1 --- /dev/null +++ b/elements/printer-app/version.bst @@ -0,0 +1,6 @@ +kind: import +description: Stage the canonical application version. + +sources: + - kind: local + path: VERSION diff --git a/include/aliases.yml b/include/aliases.yml index 8bb9ced..4af67e8 100644 --- a/include/aliases.yml +++ b/include/aliases.yml @@ -3,3 +3,5 @@ aliases: gitlab: https://gitlab.com/ pypi: https://files.pythonhosted.org/packages/ salsa: https://salsa.debian.org/ + debian: https://deb.debian.org/debian/ + cpan: https://cpan.metacpan.org/ diff --git a/tests/appliance-parity.sh b/tests/appliance-parity.sh index 5384a61..de81d80 100755 --- a/tests/appliance-parity.sh +++ b/tests/appliance-parity.sh @@ -27,17 +27,17 @@ print(" ".join(match.group(1).replace(",", " ").split())) PY )" -fsdk_version="$(python3 - <<'PY' +read -r fsdk_version fsdk_ref < <(python3 - <<'PY' import pathlib import re junction = pathlib.Path("elements/freedesktop-sdk.bst").read_text() -match = re.search(r"ref: freedesktop-sdk-(.+?)-0-g[0-9a-f]{40}$", junction, re.MULTILINE) +match = re.search(r"ref: freedesktop-sdk-(.+?)-0-g([0-9a-f]{40})$", junction, re.MULTILINE) if match is None: raise SystemExit("FAIL: pinned freedesktop-sdk release is missing") -print(match.group(1)) +print(*match.groups()) PY -)" +) size_bytes="$(podman image inspect "$image" --format '{{.Size}}')" if ((size_bytes > size_limit_bytes)); then @@ -58,8 +58,10 @@ test "$(podman image inspect "$image" --format '{{index .Config.Labels "org.open test "$(podman image inspect "$image" --format '{{index .Config.Labels "org.opencontainers.image.source"}}')" = https://github.com/projectbluefin/ghostscript-printer-app test "$(podman image inspect "$image" --format '{{index .Config.Labels "org.opencontainers.image.licenses"}}')" = Apache-2.0 application_version="$(podman run --rm --entrypoint /usr/bin/ghostscript-printer-app "$image" --version)" +test "$application_version" = "$(< VERSION)" test "$(podman image inspect "$image" --format '{{index .Config.Labels "org.opencontainers.image.version"}}')" = "$application_version" test "$(podman image inspect "$image" --format '{{index .Config.Labels "io.projectbluefin.fsdk.version"}}')" = "$fsdk_version" +test "$(podman image inspect "$image" --format '{{index .Config.Labels "io.projectbluefin.fsdk.ref"}}')" = "$fsdk_ref" podman run --rm --user 0:0 --entrypoint /usr/bin/bash \ -e ADVERTISED_GHOSTSCRIPT_DRIVERS="$advertised_ghostscript_drivers" \