Skip to content

feat(e9): publish signed hub OCI image - #170

Merged
gnanirahulnutakki merged 1 commit into
devfrom
gnanirahulnutakki/feat/e9-hub-oci-publication
Jul 15, 2026
Merged

feat(e9): publish signed hub OCI image#170
gnanirahulnutakki merged 1 commit into
devfrom
gnanirahulnutakki/feat/e9-hub-oci-publication

Conversation

@gnanirahulnutakki

@gnanirahulnutakki gnanirahulnutakki commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

  • publish a release-tagged GHCR hub manifest for linux/amd64 and linux/arm64
  • sign its manifest digest, attach GitHub provenance and SPDX SBOM attestations, and publish digest evidence
  • prove the archive-to-OCI layout locally without a registry push; document digest-only Helm consumption

Validation

  • make ci
  • make release-check
  • make e2e-oci
  • make e2e-kind KIND=/Volumes/EXTENDED/MacData/tools/bin/kind
  • make e2e-isolation
  • GHCR missing-manifest guard check

Review

  • CodeRabbit final uncommitted review: 0 findings
  • Dependabot / code scanning / secret scanning: 0 / 0 / 0

Closes #169

Summary by CodeRabbit

  • New Features

    • Release builds now publish immutable, signed multi-platform hub container images.
    • Hub images include verified provenance and SPDX SBOM attestations.
    • Hub image artifacts and verification metadata are attached to releases.
  • Documentation

    • Added guidance for consuming and verifying hub images using immutable digest references.
    • Clarified release, deployment, and air-gapped verification requirements.
  • Bug Fixes

    • Added safeguards preventing overwriting existing hub image tags.
    • Added release checks validating required AMD64 and ARM64 image contents.

GSTACK-Checkpoint: 2026-07-14/e9-hub-oci-publication#1
Signed-off-by: Gnani Rahul <gnani.nutakki@gmail.com>
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The release workflow now publishes and signs an immutable multi-platform hub image, attaches provenance and SBOM attestations, verifies release archives locally, and uploads image evidence. Documentation and policy tests enforce digest-only consumption and fail-closed release behavior.

Changes

Hub OCI publication

Layer / File(s) Summary
Publish and attest hub image
.github/workflows/release.yml
The release job builds and pushes amd64/arm64 images, rejects existing tags, signs the manifest digest, publishes provenance and SPDX attestations, and uploads related release assets.
Verify reproducible multi-platform image
hack/verify-release-hub-image.sh, Makefile
The release check builds an OCI layout from both release archives and verifies the exact Linux platform set.
Enforce release policy
tests/scripts/release_hub_image_policy_test.sh, tools/internal/releasepack/policy_test.go
Policy checks validate immutable publishing, permissions, action pinning, signing, attestations, overwrite protection, and digest verification guidance.
Document digest-only consumption
README.md, docs/RELEASE.md, sessions/...
Documentation describes release-bound hub images, digest-based verification, attestations, fail-closed Helm inputs, and release process checkpoints.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReleaseTag
  participant ReleaseWorkflow
  participant GHCR
  participant Cosign
  participant GitHubAttestations
  ReleaseTag->>ReleaseWorkflow: Start signed release
  ReleaseWorkflow->>GHCR: Push amd64 and arm64 hub image
  ReleaseWorkflow->>Cosign: Sign and verify manifest digest
  ReleaseWorkflow->>GitHubAttestations: Attach provenance and SPDX SBOM
  ReleaseWorkflow->>ReleaseTag: Upload digest and verification artifacts
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: publishing a signed hub OCI image.
Linked Issues check ✅ Passed The changes implement the requested signed multi-arch hub image, digest-only docs, fail-closed checks, and verification coverage.
Out of Scope Changes check ✅ Passed I don't see unrelated feature work; the added workflow, docs, tests, and verification script all support the image publication scope.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gnanirahulnutakki/feat/e9-hub-oci-publication

Comment @coderabbitai help to get the list of available commands.

@gnanirahulnutakki

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gnanirahulnutakki
gnanirahulnutakki merged commit 6fe30e0 into dev Jul 15, 2026
6 of 7 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/scripts/release_hub_image_policy_test.sh (1)

12-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate helper: assert_text_contains is identical to assert_contains.

Both functions have the exact same body. Consolidate into a single helper to avoid confusion for future maintainers.

♻️ Proposed consolidation
-assert_text_contains() {
-  local text="$1"
-  local needle="$2"
-  local description="$3"
-
-  if [[ "$text" != *"$needle"* ]]; then
-    printf '[release-hub-image] FAIL: %s\n' "$description" >&2
-    exit 1
-  fi
-  printf '[release-hub-image] PASS: %s\n' "$description"
-}

Then replace all assert_text_contains call sites with assert_contains.

Also applies to: 51-61

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/scripts/release_hub_image_policy_test.sh` around lines 12 - 22, Remove
the duplicate assert_text_contains helper and retain assert_contains as the
single containment assertion helper. Replace every assert_text_contains call
site with assert_contains, preserving the existing arguments and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release.yml:
- Around line 174-181: Update the “Generate hub image SPDX SBOM” workflow step
to resolve and scan the hub image separately for linux/amd64 and linux/arm64
rather than scanning the multi-architecture index via HUB_DIGEST. Produce
distinct platform-specific SPDX files and ensure the corresponding
attestation/publishing steps reference each per-platform SBOM, following the
existing per-arch release pattern.

In `@docs/RELEASE.md`:
- Around line 34-72: Update the “Verify a hub OCI image” workflow to download
the release’s sith_${version}_hub.image.sigstore.json bundle alongside the
digest file, then run cosign verify-blob against the digest file using that
bundle and the exact tag workflow identity before assigning or consuming $image.
Preserve the existing digest-format validation and subsequent image signature,
provenance, and SBOM checks.

In `@hack/verify-release-hub-image.sh`:
- Around line 16-17: Split the readonly declarations for REPOSITORY_ROOT and
DIST_DIRECTORY from their command substitutions: assign each resolved directory
path first, then apply readonly in separate statements. Preserve the existing
path calculations while ensuring a failing cd, especially for the "$2"
distribution directory, propagates under set -e instead of being masked.

---

Nitpick comments:
In `@tests/scripts/release_hub_image_policy_test.sh`:
- Around line 12-22: Remove the duplicate assert_text_contains helper and retain
assert_contains as the single containment assertion helper. Replace every
assert_text_contains call site with assert_contains, preserving the existing
arguments and behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 964bc8ff-e2ca-4b89-aefb-aa6ab14df715

📥 Commits

Reviewing files that changed from the base of the PR and between 7f9302d and 9ff3758.

📒 Files selected for processing (8)
  • .github/workflows/release.yml
  • Makefile
  • README.md
  • docs/RELEASE.md
  • hack/verify-release-hub-image.sh
  • sessions/2026-07-14-e9-hub-oci-publication.md
  • tests/scripts/release_hub_image_policy_test.sh
  • tools/internal/releasepack/policy_test.go

Comment on lines +174 to +181
- name: Generate hub image SPDX SBOM
env:
HUB_DIGEST: ${{ steps.hub_image.outputs.digest }}
run: |
set -euo pipefail
test -n "$HUB_DIGEST"
syft "${HUB_IMAGE}@${HUB_DIGEST}" \
--output "spdx-json=dist/sith_${VERSION}_hub.spdx.json"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant workflow section with line numbers.
sed -n '150,240p' .github/workflows/release.yml

echo
echo "---- syft usage in repo ----"
rg -n "syft .*--platform|syft " .github/workflows . || true

Repository: ArdurAI/sith

Length of output: 4225


🌐 Web query:

Syft default behavior when analyzing a multi-arch image index with no --platform flag documented default linux/amd64

💡 Result:

When analyzing a multi-architecture container image index without an explicitly provided --platform flag, Syft defaults to analyzing the linux/amd64 manifest [1]. While Syft provides a --platform flag to allow users to specify a target architecture (e.g., linux/arm64) [2][3], in the absence of this configuration, the tool automatically targets the linux/amd64 architecture for unspecific image references [1]. Historically, this behavior has been a point of discussion in the community regarding how Syft handles multi-platform images and OCI layouts [4][5][6], but the documented default remains linux/amd64 for multi-arch image indexes [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Gather syft platform/docs references from public docs if available via common README snippets in repo references.
rg -n "platform" .github/workflows/release.yml README.md docs . 2>/dev/null | head -n 200 || true

Repository: ArdurAI/sith

Length of output: 38044


🌐 Web query:

Syft analyze multi-platform image index without --platform which manifest is selected default linux/amd64 or current platform

💡 Result:

When analyzing a multi-platform container image index without specifying the --platform flag, Syft does not strictly default to linux/amd64. Instead, Syft uses the architecture of the host machine (the environment where the tool is being executed) as the default platform [1][2]. While some older documentation or specific contexts may have referenced linux/amd64, updates to Syft and its underlying library, stereoscope, have aligned the default behavior to match the host architecture [1][2]. If your host machine is running on an arm64 architecture, Syft will attempt to pull the linux/arm64 manifest by default [1]. Key points regarding this behavior: - Host-based selection: Syft detects the host platform and uses that for manifest selection unless explicitly overridden by the --platform flag [1][2]. - Explicit override: To ensure a specific platform is analyzed, you must use the --platform flag (e.g., --platform linux/amd64), which forces Syft to target that specific manifest regardless of the host environment [3][4]. - Limitations: Syft currently analyzes only the single manifest resolved by the platform selection (either the default host-based one or the user-specified one); it does not automatically aggregate dependencies from all manifests within a multi-platform index into a single SBOM [5][6].

Citations:


🌐 Web query:

anchore syft platform flag image index default platform documentation

💡 Result:

When scanning container images with Anchore Syft, the --platform flag is used to specify the operating system and architecture of the image to be analyzed [1][2]. Default Behavior: When scanning multi-platform images or image references that do not specify a platform, Syft defaults to targeting the linux/amd64 manifest [1]. Using the --platform Flag: The --platform flag allows you to override this default and target a specific architecture or OS/architecture combination [1][3]. The flag accepts values in formats similar to those used by Docker, such as: - Architecture only: --platform arm64 - OS/Architecture combination: --platform linux/arm64 - Architecture variants: --platform linux/arm/v8 Configuration: You can also set the platform via the configuration file or an environment variable (SYFT_PLATFORM) [2][4]. Important Limitations: Syft currently analyzes a single specific platform's manifest at a time when scanning an image index (or multi-arch manifest list) [5][6]. It is unable to automatically combine multiple architectures into a single unified SBOM in one command [5]. If you need an SBOM that covers all architectures in a multi-platform image, you must perform separate scans for each desired platform and manage the resulting SBOMs accordingly [7].

Citations:


Generate separate SBOMs for each hub platform

syft "${HUB_IMAGE}@${HUB_DIGEST}" resolves and scans only one platform from the multi-arch hub index, but the attestation is published against the index digest itself. That makes the SBOM appear to cover both linux/amd64 and linux/arm64 when it only describes a single manifest. Generate and attest one SBOM per platform instead, matching the existing per-arch release pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 174 - 181, Update the “Generate
hub image SPDX SBOM” workflow step to resolve and scan the hub image separately
for linux/amd64 and linux/arm64 rather than scanning the multi-architecture
index via HUB_DIGEST. Produce distinct platform-specific SPDX files and ensure
the corresponding attestation/publishing steps reference each per-platform SBOM,
following the existing per-arch release pattern.

Comment thread docs/RELEASE.md
Comment on lines +34 to +72
## Verify a hub OCI image

Hub images are published only by a signed Sith release tag and must be consumed by immutable
manifest digest. A release includes a signed `sith_<version>_hub.image` file whose only line is the
digest address; do not substitute the convenient version tag or add `latest` to a Helm value.

```bash
tag=vX.Y.Z # use a tag released after hub-image publication is enabled
version=${tag#v}
gh release download "$tag" --repo ArdurAI/sith \
--pattern "sith_${version}_hub.image" \
--dir "sith-$version"
image=$(cat "sith-$version/sith_${version}_hub.image")
case "$image" in ghcr.io/ardurai/sith-hub@sha256:*) ;; *) exit 1 ;; esac
```

Verify the keyless image signature against the exact tag workflow identity, then verify GitHub
provenance and the SPDX SBOM attestation. These commands require registry access; the later air-gap
workflow consumes mirrored, pre-verified material rather than weakening this verification boundary.

```bash
identity="https://github.com/ArdurAI/sith/.github/workflows/release.yml@refs/tags/${tag}"
cosign verify \
--certificate-identity "$identity" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$image"
gh attestation verify "oci://$image" \
--repo ArdurAI/sith \
--signer-workflow ArdurAI/sith/.github/workflows/release.yml
gh attestation verify "oci://$image" \
--repo ArdurAI/sith \
--signer-workflow ArdurAI/sith/.github/workflows/release.yml \
--predicate-type https://spdx.dev/Document/v2.3
```

The existing Helm chart remains fail-closed: it accepts this digest and names of pre-materialized
runtime and migration Secrets only. It neither creates secret data nor supplies a KMS provider,
database, ingress, or mutable image reference.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Verify the signed digest evidence before consuming it.

The workflow emits sith_<version>_hub.image.sigstore.json, but this guide downloads only sith_<version>_hub.image and never runs cosign verify-blob on it. Download the bundle and verify it against the exact release-tag workflow identity before passing $image to Helm; otherwise the documented “signed digest evidence” check is skipped.

Suggested verification
 gh release download "$tag" --repo ArdurAI/sith \
   --pattern "sith_${version}_hub.image" \
+  --pattern "sith_${version}_hub.image.sigstore.json" \
   --dir "sith-$version"
 image=$(cat "sith-$version/sith_${version}_hub.image")
+cosign verify-blob \
+  --bundle "sith-$version/sith_${version}_hub.image.sigstore.json" \
+  --certificate-identity "https://github.com/ArdurAI/sith/.github/workflows/release.yml@refs/tags/${tag}" \
+  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
+  "sith-$version/sith_${version}_hub.image"
 case "$image" in ghcr.io/ardurai/sith-hub@sha256:*) ;; *) exit 1 ;; esac
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Verify a hub OCI image
Hub images are published only by a signed Sith release tag and must be consumed by immutable
manifest digest. A release includes a signed `sith_<version>_hub.image` file whose only line is the
digest address; do not substitute the convenient version tag or add `latest` to a Helm value.
```bash
tag=vX.Y.Z # use a tag released after hub-image publication is enabled
version=${tag#v}
gh release download "$tag" --repo ArdurAI/sith \
--pattern "sith_${version}_hub.image" \
--dir "sith-$version"
image=$(cat "sith-$version/sith_${version}_hub.image")
case "$image" in ghcr.io/ardurai/sith-hub@sha256:*) ;; *) exit 1 ;; esac
```
Verify the keyless image signature against the exact tag workflow identity, then verify GitHub
provenance and the SPDX SBOM attestation. These commands require registry access; the later air-gap
workflow consumes mirrored, pre-verified material rather than weakening this verification boundary.
```bash
identity="https://github.com/ArdurAI/sith/.github/workflows/release.yml@refs/tags/${tag}"
cosign verify \
--certificate-identity "$identity" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$image"
gh attestation verify "oci://$image" \
--repo ArdurAI/sith \
--signer-workflow ArdurAI/sith/.github/workflows/release.yml
gh attestation verify "oci://$image" \
--repo ArdurAI/sith \
--signer-workflow ArdurAI/sith/.github/workflows/release.yml \
--predicate-type https://spdx.dev/Document/v2.3
```
The existing Helm chart remains fail-closed: it accepts this digest and names of pre-materialized
runtime and migration Secrets only. It neither creates secret data nor supplies a KMS provider,
database, ingress, or mutable image reference.
## Verify a hub OCI image
Hub images are published only by a signed Sith release tag and must be consumed by immutable
manifest digest. A release includes a signed `sith_<version>_hub.image` file whose only line is the
digest address; do not substitute the convenient version tag or add `latest` to a Helm value.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/RELEASE.md` around lines 34 - 72, Update the “Verify a hub OCI image”
workflow to download the release’s sith_${version}_hub.image.sigstore.json
bundle alongside the digest file, then run cosign verify-blob against the digest
file using that bundle and the exact tag workflow identity before assigning or
consuming $image. Preserve the existing digest-format validation and subsequent
image signature, provenance, and SBOM checks.

Comment on lines +16 to +17
readonly REPOSITORY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
readonly DIST_DIRECTORY="$(cd "$2" && pwd -P)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Split readonly declaration from assignment (SC2155).

Under set -e, a failing cd "$2" here would be masked since the exit status of the compound readonly VAR=$(...) statement is that of readonly, not the subshell — leaving DIST_DIRECTORY empty instead of failing immediately with a clear diagnostic.

🔧 Suggested fix
-readonly REPOSITORY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
-readonly DIST_DIRECTORY="$(cd "$2" && pwd -P)"
+REPOSITORY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
+readonly REPOSITORY_ROOT
+DIST_DIRECTORY="$(cd "$2" && pwd -P)"
+readonly DIST_DIRECTORY
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
readonly REPOSITORY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
readonly DIST_DIRECTORY="$(cd "$2" && pwd -P)"
REPOSITORY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
readonly REPOSITORY_ROOT
DIST_DIRECTORY="$(cd "$2" && pwd -P)"
readonly DIST_DIRECTORY
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 16-16: Declare and assign separately to avoid masking return values.

(SC2155)


[warning] 17-17: Declare and assign separately to avoid masking return values.

(SC2155)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/verify-release-hub-image.sh` around lines 16 - 17, Split the readonly
declarations for REPOSITORY_ROOT and DIST_DIRECTORY from their command
substitutions: assign each resolved directory path first, then apply readonly in
separate statements. Preserve the existing path calculations while ensuring a
failing cd, especially for the "$2" distribution directory, propagates under set
-e instead of being masked.

Source: Linters/SAST tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant