7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
513 lines
22 KiB
YAML
513 lines
22 KiB
YAML
name: Prerelease Docker Image
|
|
|
|
# Build the canonical Docker image for a release. In the build-once-promote
|
|
# pipeline, this workflow IS the build — docker-publish.yml only retags the
|
|
# manifest produced here. Cosign signing, SBOM attestation, and SLSA
|
|
# provenance are attached here once, keyed by manifest digest, so they're
|
|
# discoverable from any tag (including the release tags later created by
|
|
# imagetools create).
|
|
#
|
|
# Triggered exclusively via workflow_call from release.yml (after security
|
|
# gates pass). No workflow_dispatch — security and gate semantics are
|
|
# enforced by the caller. The build runs automatically; the only human
|
|
# approval in the release flow is the `release` env on this workflow's
|
|
# jobs + publish-docker + trigger-pypi + create-release in release.yml
|
|
# (gates the actual publish, not the canonical build).
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
version:
|
|
description: "Bare semver, e.g. '1.6.9' (no leading 'v')"
|
|
type: string
|
|
required: true
|
|
short_sha:
|
|
description: "First 7 chars of commit SHA (used in the prerelease tag)"
|
|
type: string
|
|
required: true
|
|
secrets:
|
|
# Explicit secrets contract instead of `secrets: inherit` on the
|
|
# caller side — narrower blast radius if a future caller misuses
|
|
# this reusable workflow.
|
|
DOCKER_USERNAME:
|
|
required: true
|
|
description: "Docker Hub username for image push"
|
|
DOCKER_PASSWORD:
|
|
required: true
|
|
description: "Docker Hub PAT (Read+Write+Delete scopes)"
|
|
outputs:
|
|
manifest_digest:
|
|
description: "sha256:... digest of the multi-arch prerelease manifest. Used by docker-publish.yml to verify retag preserves the digest."
|
|
value: ${{ jobs.create-manifest.outputs.digest }}
|
|
|
|
permissions: {} # Minimal top-level for OSSF Scorecard Token-Permissions
|
|
|
|
# No approval gate at the build step — the build runs automatically once
|
|
# security gates and CI gates in release.yml pass. The only meaningful
|
|
# human decision in the release flow is "should this signed, attested,
|
|
# tested image become the official release?" — gated by the `release`
|
|
# environment on this workflow's jobs + `publish-docker` + `trigger-pypi`
|
|
# + `create-release` in release.yml. The maintainer can pull
|
|
# `:prerelease-v<ver>-<sha>` and smoke-test between build completion
|
|
# and approving the release env.
|
|
|
|
jobs:
|
|
build-amd64:
|
|
name: Build AMD64 Prerelease Image
|
|
runs-on: ubuntu-latest
|
|
environment: release
|
|
permissions:
|
|
contents: read
|
|
|
|
steps:
|
|
- name: Harden Runner
|
|
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- name: Check out the repo
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 0
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
|
|
- name: Log in to Docker Hub
|
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
with:
|
|
username: ${{ secrets.DOCKER_USERNAME }}
|
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
|
|
- name: Build and push AMD64 image
|
|
id: build
|
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
|
with:
|
|
context: .
|
|
platforms: linux/amd64
|
|
push: true
|
|
tags: ${{ secrets.DOCKER_USERNAME }}/local-deep-research:prerelease-v${{ inputs.version }}-${{ inputs.short_sha }}-amd64
|
|
cache-from: type=gha,scope=linux-amd64
|
|
cache-to: type=gha,mode=max,scope=linux-amd64
|
|
|
|
build-arm64:
|
|
name: Build ARM64 Prerelease Image
|
|
runs-on: ubuntu-24.04-arm
|
|
environment: release
|
|
permissions:
|
|
contents: read
|
|
|
|
steps:
|
|
- name: Harden Runner
|
|
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- name: Check out the repo
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 0
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
|
|
- name: Log in to Docker Hub
|
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
with:
|
|
username: ${{ secrets.DOCKER_USERNAME }}
|
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
|
|
- name: Build and push ARM64 image
|
|
id: build
|
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
|
with:
|
|
context: .
|
|
platforms: linux/arm64
|
|
push: true
|
|
tags: ${{ secrets.DOCKER_USERNAME }}/local-deep-research:prerelease-v${{ inputs.version }}-${{ inputs.short_sha }}-arm64
|
|
cache-from: type=gha,scope=linux-arm64
|
|
cache-to: type=gha,mode=max,scope=linux-arm64
|
|
|
|
security-scan:
|
|
name: Security Scan
|
|
runs-on: ubuntu-latest
|
|
environment: release
|
|
permissions:
|
|
contents: read
|
|
|
|
steps:
|
|
- name: Harden the runner (Audit all outbound calls)
|
|
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- name: Check out the repo
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 0
|
|
|
|
- name: Free disk space
|
|
run: |
|
|
sudo rm -rf /usr/share/dotnet || true
|
|
sudo rm -rf /usr/local/lib/android || true
|
|
sudo rm -rf /opt/ghc || true
|
|
sudo rm -rf /opt/hostedtoolcache/CodeQL || true
|
|
sudo docker image prune --all --force || true
|
|
df -h
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
|
|
- name: Build Docker image for security scan
|
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
|
with:
|
|
context: .
|
|
platforms: linux/amd64
|
|
push: false
|
|
load: true
|
|
tags: local-deep-research:security-scan
|
|
cache-from: type=gha,scope=linux-amd64
|
|
cache-to: type=gha,mode=max,scope=linux-amd64
|
|
|
|
# Generate Trivy SARIF for archival as a workflow artifact (all severities, never fails).
|
|
# Severity-gating happens in the next step.
|
|
- name: Generate Trivy SARIF report
|
|
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
|
with:
|
|
image-ref: local-deep-research:security-scan
|
|
format: 'sarif'
|
|
output: 'trivy-prerelease-scan.sarif'
|
|
ignore-unfixed: true
|
|
exit-code: '0'
|
|
version: 'v0.69.2'
|
|
|
|
# Separate scan that fails build only on fixable HIGH/CRITICAL vulnerabilities
|
|
- name: Check for fixable HIGH/CRITICAL vulnerabilities
|
|
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
|
with:
|
|
image-ref: local-deep-research:security-scan
|
|
severity: 'CRITICAL,HIGH'
|
|
ignore-unfixed: true
|
|
trivyignores: '.trivyignore'
|
|
exit-code: '1'
|
|
version: 'v0.69.2'
|
|
|
|
- name: Upload Trivy scan results
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
if: always()
|
|
with:
|
|
name: trivy-prerelease-scan
|
|
path: trivy-prerelease-scan.sarif
|
|
retention-days: 7
|
|
|
|
create-manifest:
|
|
name: Create Multi-Platform Prerelease Manifest
|
|
needs: [build-amd64, build-arm64, security-scan]
|
|
runs-on: ubuntu-latest
|
|
environment: release
|
|
permissions:
|
|
contents: read
|
|
id-token: write # Required for cosign keyless OIDC signing
|
|
# No `packages: write` — Docker Hub auth uses DOCKER_PASSWORD secret,
|
|
# not GITHUB_TOKEN. `packages: write` only matters for ghcr.io pushes.
|
|
outputs:
|
|
digest: ${{ steps.capture-digest.outputs.digest }}
|
|
|
|
steps:
|
|
- name: Harden Runner
|
|
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- name: Check out the repo
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 0
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
|
|
- name: Log in to Docker Hub
|
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
with:
|
|
username: ${{ secrets.DOCKER_USERNAME }}
|
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
|
|
- name: Install Cosign
|
|
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
|
with:
|
|
# Pin to cosign v2.x — see release.yml for rationale (v3 enables
|
|
# --new-bundle-format by default which changes the on-wire format
|
|
# and breaks downstream verifiers still on v2).
|
|
cosign-release: 'v2.6.3'
|
|
|
|
- name: Install Syft
|
|
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
|
|
|
- name: Create and push multi-platform manifest
|
|
env:
|
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
|
VERSION: ${{ inputs.version }}
|
|
SHORT_SHA: ${{ inputs.short_sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
TAG="prerelease-v${VERSION}-${SHORT_SHA}"
|
|
echo "Creating manifest for: ${DOCKER_USERNAME}/local-deep-research:${TAG}"
|
|
docker buildx imagetools create -t "${DOCKER_USERNAME}/local-deep-research:${TAG}" \
|
|
"${DOCKER_USERNAME}/local-deep-research:${TAG}-amd64" \
|
|
"${DOCKER_USERNAME}/local-deep-research:${TAG}-arm64"
|
|
echo "Manifest created successfully"
|
|
|
|
# Floating tag: re-point :prerelease at the manifest just created so
|
|
# testers can pin compose to `:prerelease` and pull the latest RC via
|
|
# `docker compose pull` without editing the tag each cycle. The
|
|
# versioned tag above remains for reproducibility (and is what
|
|
# docker-publish.yml retags by digest into :1.6.9 / :1.6 / :latest).
|
|
echo "Updating floating tag: ${DOCKER_USERNAME}/local-deep-research:prerelease"
|
|
docker buildx imagetools create -t "${DOCKER_USERNAME}/local-deep-research:prerelease" \
|
|
"${DOCKER_USERNAME}/local-deep-research:${TAG}"
|
|
echo "Floating :prerelease tag updated"
|
|
|
|
- name: Capture manifest digest
|
|
id: capture-digest
|
|
env:
|
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
|
VERSION: ${{ inputs.version }}
|
|
SHORT_SHA: ${{ inputs.short_sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
IMAGE_REF="${DOCKER_USERNAME}/local-deep-research:prerelease-v${VERSION}-${SHORT_SHA}"
|
|
# Same form as the existing docker-publish.yml inspector — avoids jq.
|
|
DIGEST=$(docker buildx imagetools inspect "$IMAGE_REF" --format '{{json .Manifest.Digest}}' | tr -d '"')
|
|
if [[ -z "$DIGEST" || "$DIGEST" != sha256:* ]]; then
|
|
echo "::error::Failed to capture manifest digest (got '${DIGEST}')"
|
|
exit 1
|
|
fi
|
|
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
|
|
echo "Manifest digest: ${DIGEST}"
|
|
|
|
- name: Sign manifest with Cosign
|
|
env:
|
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
|
DIGEST: ${{ steps.capture-digest.outputs.digest }}
|
|
run: |
|
|
set -euo pipefail
|
|
# Sign by digest — signature artifact lands at sha256-<digest>.sig
|
|
# in the same repo, discoverable from ANY tag pointing at the same
|
|
# digest (including release tags created later by docker-publish.yml's
|
|
# imagetools-create retag).
|
|
IMAGE_REF="${DOCKER_USERNAME}/local-deep-research@${DIGEST}"
|
|
echo "Signing image by digest: $IMAGE_REF"
|
|
cosign sign --yes "$IMAGE_REF"
|
|
# Brief sleep to allow registry to propagate signature
|
|
sleep 5
|
|
|
|
- name: Generate SLSA provenance attestation
|
|
env:
|
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
|
DIGEST: ${{ steps.capture-digest.outputs.digest }}
|
|
run: |
|
|
set -euo pipefail
|
|
IMAGE_REF="${DOCKER_USERNAME}/local-deep-research@${DIGEST}"
|
|
|
|
# entryPoint is the TOP-LEVEL caller (release.yml), not this
|
|
# reusable workflow. Per SLSA GHA buildtype v1 and the canonical
|
|
# slsa-github-generator, reusable workflows are explicitly NOT
|
|
# entryPoints. github.run_id / github.repository / github.sha all
|
|
# resolve to the caller's run context inside a reusable workflow.
|
|
# builder.id pins the workflow that actually defines the build
|
|
# steps — the trust root a verifier policy can pin against. We
|
|
# compose it from `github.repository` and a hardcoded path to
|
|
# THIS workflow file, with `github.ref` for the ref portion.
|
|
# Rationale: inside a workflow_call callee, the `github` context
|
|
# is scoped to the CALLER, so `github.workflow_ref` would point
|
|
# at release.yml (the wrong builder). The `job` context has no
|
|
# `workflow_ref` property either (only check_run_id, container,
|
|
# services, status — actionlint confirms). For a local-path
|
|
# reusable workflow (`uses: ./.github/workflows/...`), the
|
|
# callee's ref equals the caller's `github.ref`, so composing
|
|
# the path manually gives the correct
|
|
# `<owner>/<repo>/.github/workflows/prerelease-docker.yml@<ref>`
|
|
# format that matches the Fulcio cert SAN. Cosign and
|
|
# slsa-verifier both anchor on the cert anyway, so this fix is
|
|
# about correctness for raw-JSON policy engines / audit tools
|
|
# that read builder.id directly.
|
|
#
|
|
# completeness.* are FALSE because we don't capture invocation
|
|
# parameters or environment, and the build does network I/O for
|
|
# apt/pip/npm. Honest emptiness > false claims of completeness.
|
|
#
|
|
# buildInvocationId includes run_attempt so re-runs are
|
|
# distinguishable in audit logs.
|
|
#
|
|
# Uses auto-injected GITHUB_* shell env vars (GITHUB_REPOSITORY,
|
|
# GITHUB_REF, GITHUB_SHA, GITHUB_RUN_ID, GITHUB_RUN_ATTEMPT) instead
|
|
# of github-context template expansion to satisfy zizmor's
|
|
# template-injection check. The values are semantically identical
|
|
# inside a workflow_call callee (both scope to the caller's context),
|
|
# but shell-var expansion happens at runtime in a confined string
|
|
# context, eliminating any theoretical injection surface from a
|
|
# ref/branch name containing shell metacharacters.
|
|
cat > provenance.json <<EOF
|
|
{
|
|
"buildType": "https://github.com/${GITHUB_REPOSITORY}/docker-build@v1",
|
|
"builder": {
|
|
"id": "https://github.com/${GITHUB_REPOSITORY}/.github/workflows/prerelease-docker.yml@${GITHUB_REF}"
|
|
},
|
|
"invocation": {
|
|
"configSource": {
|
|
"uri": "https://github.com/${GITHUB_REPOSITORY}",
|
|
"digest": {
|
|
"sha1": "${GITHUB_SHA}"
|
|
},
|
|
"entryPoint": ".github/workflows/release.yml"
|
|
}
|
|
},
|
|
"metadata": {
|
|
"buildInvocationId": "${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}",
|
|
"buildStartedOn": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
"completeness": {
|
|
"parameters": false,
|
|
"environment": false,
|
|
"materials": false
|
|
},
|
|
"reproducible": false
|
|
},
|
|
"materials": [
|
|
{
|
|
"uri": "https://github.com/${GITHUB_REPOSITORY}",
|
|
"digest": {
|
|
"sha1": "${GITHUB_SHA}"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
EOF
|
|
|
|
# --replace prevents duplicate SLSA attestations on re-run. Cosign's
|
|
# Replace logic is keyed by predicate-type URI, so it leaves the
|
|
# SBOM SPDX attestation (different predicateType) untouched.
|
|
cosign attest --yes --replace --predicate provenance.json --type slsaprovenance "$IMAGE_REF"
|
|
|
|
- name: Verify image signature
|
|
env:
|
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
|
DIGEST: ${{ steps.capture-digest.outputs.digest }}
|
|
run: |
|
|
set -euo pipefail
|
|
IMAGE_REF="${DOCKER_USERNAME}/local-deep-research@${DIGEST}"
|
|
echo "Verifying signature for: $IMAGE_REF"
|
|
# Retry to handle registry propagation delay after signing
|
|
MAX_RETRIES=5
|
|
RETRY_DELAY=10
|
|
for i in $(seq 1 "$MAX_RETRIES"); do
|
|
echo "Verification attempt $i of $MAX_RETRIES..."
|
|
# Use GITHUB_REPOSITORY shell env var (auto-injected) instead of
|
|
# github.repository template expansion to avoid zizmor's
|
|
# template-injection finding.
|
|
if cosign verify \
|
|
--certificate-identity-regexp="https://github.com/${GITHUB_REPOSITORY}" \
|
|
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
|
|
"$IMAGE_REF"; then
|
|
echo "Signature verification successful!"
|
|
exit 0
|
|
fi
|
|
if [ "$i" -lt "$MAX_RETRIES" ]; then
|
|
echo "Verification failed, waiting ${RETRY_DELAY}s before retry..."
|
|
sleep "$RETRY_DELAY"
|
|
fi
|
|
done
|
|
echo "::error::Signature verification failed after $MAX_RETRIES attempts"
|
|
exit 1
|
|
|
|
- name: Generate per-platform SBOMs and attest each
|
|
env:
|
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
|
DIGEST: ${{ steps.capture-digest.outputs.digest }}
|
|
run: |
|
|
set -euo pipefail
|
|
REPO="${DOCKER_USERNAME}/local-deep-research"
|
|
MANIFEST_REF="${REPO}@${DIGEST}"
|
|
|
|
# Multi-arch SBOM correctness: syft against a manifest list digest
|
|
# only scans the host platform's layers (per anchore/syft#1708),
|
|
# which would lie to ARM64 consumers. We attest each per-arch
|
|
# digest with its OWN SBOM so end-user verification is honest.
|
|
# We deliberately do NOT also produce a "manifest-level SBOM" —
|
|
# that would be amd64-only (host arch) and re-introduce the lie
|
|
# for any arm64 consumer running the README verifier recipe.
|
|
# The README documents the per-arch verification flow instead.
|
|
MANIFEST_JSON=$(docker buildx imagetools inspect "${MANIFEST_REF}" --raw)
|
|
|
|
# Defense against future buildx output changes: assert at least
|
|
# one per-arch entry exists. Without this, an empty/malformed
|
|
# manifest list would silently produce zero SBOMs and pass CI green.
|
|
PER_ARCH_COUNT=$(echo "${MANIFEST_JSON}" \
|
|
| jq '[.manifests[] | select(.platform.architecture != "unknown")] | length')
|
|
if [[ "${PER_ARCH_COUNT}" -lt 1 ]]; then
|
|
echo "::error::No per-arch manifest entries found in ${MANIFEST_REF} — SBOM generation cannot proceed"
|
|
echo "Raw manifest: ${MANIFEST_JSON}"
|
|
exit 1
|
|
fi
|
|
echo "Found ${PER_ARCH_COUNT} per-arch manifest(s) to scan"
|
|
|
|
echo "$MANIFEST_JSON" \
|
|
| jq -r '.manifests[] | select(.platform.architecture != "unknown") | "\(.platform.os)/\(.platform.architecture)\t\(.digest)"' \
|
|
| while IFS=$'\t' read -r PLAT PER_ARCH_DIGEST; do
|
|
ARCH="${PLAT##*/}"
|
|
PER_ARCH_REF="${REPO}@${PER_ARCH_DIGEST}"
|
|
SBOM_FILE="sbom-${ARCH}.spdx.json"
|
|
echo "=== Scanning ${PLAT} (${PER_ARCH_DIGEST}) ==="
|
|
# --platform tells syft which arch to scan — matters when
|
|
# the host runner can't natively execute the image.
|
|
syft --platform "${PLAT}" "${PER_ARCH_REF}" -o spdx-json > "${SBOM_FILE}"
|
|
# --replace prevents accumulation when a re-run lands on
|
|
# the same digest (e.g. "Re-run failed jobs" after a flake).
|
|
# Per cosign source pkg/cosign/remote/remote.go, --replace
|
|
# is per-predicate-type, so it doesn't disturb the SLSA
|
|
# attestation already on the manifest list digest.
|
|
cosign attest --yes --replace \
|
|
--predicate "${SBOM_FILE}" --type spdxjson "${PER_ARCH_REF}"
|
|
done
|
|
|
|
- name: Upload SBOMs artifact
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: sbom
|
|
# Per-arch SBOMs only — `sbom-amd64.spdx.json`, `sbom-arm64.spdx.json`,
|
|
# one per platform in the manifest list. No manifest-level SBOM is
|
|
# produced (would be host-arch-only and misleading for non-amd64
|
|
# consumers).
|
|
path: sbom-*.spdx.json
|
|
retention-days: 90
|
|
|
|
- name: Summary
|
|
env:
|
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
|
VERSION: ${{ inputs.version }}
|
|
SHORT_SHA: ${{ inputs.short_sha }}
|
|
DIGEST: ${{ steps.capture-digest.outputs.digest }}
|
|
run: |
|
|
TAG="prerelease-v${VERSION}-${SHORT_SHA}"
|
|
{
|
|
echo "## Prerelease Docker Image"
|
|
echo ""
|
|
echo "**Versioned tag:** \`${TAG}\`"
|
|
echo "**Floating tag:** \`prerelease\` (now points at this build)"
|
|
echo ""
|
|
echo "**Digest:** \`${DIGEST}\`"
|
|
echo ""
|
|
echo '```'
|
|
echo "docker pull ${DOCKER_USERNAME}/local-deep-research:${TAG}"
|
|
echo "docker pull ${DOCKER_USERNAME}/local-deep-research:prerelease"
|
|
echo '```'
|
|
echo ""
|
|
echo "Signed and attested. After release approval, docker-publish.yml"
|
|
echo "will retag this exact digest as \`:${VERSION}\`, \`:major.minor\`,"
|
|
echo "and \`:latest\`."
|
|
} >> "$GITHUB_STEP_SUMMARY"
|