Files
wehub-resource-sync 7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

335 lines
16 KiB
YAML

name: Publish Docker image
# SECURITY: Only invoked as a reusable workflow (`workflow_call`) from
# release.yml after the release security gate AND the `release` environment
# approval pass. We intentionally do NOT support workflow_dispatch — that
# would bypass gates. We also intentionally do NOT support
# repository_dispatch any more: with the atomicity refactor, this workflow
# runs as a job inside release.yml's run so its result is visible to
# downstream jobs (create-release, cleanup-on-rejection) — a property that
# repository_dispatch fanout broke.
#
# This workflow is a thin RETAG step in the build-once-promote pipeline.
# The actual build happens in prerelease-docker.yml; the multi-arch manifest
# is signed and attested there. Here we only:
# - Verify the source manifest digest matches what prerelease produced
# - Retag the prerelease manifest to release tags (:1.6.9, :1.6, :latest)
# - Verify the digest is preserved (defends against imagetools re-encoding)
# - Re-run Trivy against the digest (catches CVE-database updates between
# prerelease build and release promote)
# - Verify cosign signature transitivity from the original digest
# - Clean up the prerelease tags
#
# To re-publish, trigger a new release through release.yml.
on:
workflow_call:
inputs:
tag:
description: "Release tag, e.g. 'v1.6.9' (with leading 'v')"
type: string
required: true
source_tag:
description: "Prerelease manifest tag to retag, e.g. 'prerelease-v1.6.9-abc1234'"
type: string
required: true
expected_digest:
description: "sha256:... digest of the prerelease manifest, captured by prerelease-docker.yml. Used to verify retag preserves the digest end-to-end."
type: string
required: true
secrets:
DOCKER_USERNAME:
required: true
description: "Docker Hub username (env-scoped to `release`)"
DOCKER_PASSWORD:
required: true
description: "Docker Hub PAT with Read+Write+Delete scopes (env-scoped to `release`)"
permissions: {} # Minimal top-level for OSSF Scorecard Token-Permissions
# NOTE: no workflow-level `concurrency:` block. As a reusable workflow
# called from release.yml, this workflow runs as part of the caller's run,
# and release.yml's caller-level concurrency (keyed on github.workflow +
# github.ref) already serialises release runs for the same tag. Adding a
# callee-level block would be a no-op for the documented threat model
# (two simultaneous releases for the same ref) and would not be reachable
# anyway because there is no longer any standalone invocation path.
jobs:
promote:
name: Retag prerelease manifest as release
runs-on: ubuntu-latest
environment: release
permissions:
# cosign verify is read-only against public Rekor/Fulcio — does not
# mint a GitHub OIDC token, so id-token: write is not required here.
# Signing (which does need id-token: write) happens in prerelease-docker.yml.
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Check out the repo at the triggering commit
# Pin the checkout to the EXACT commit that the prerelease was
# built/scanned from, so .trivyignore (and any other repo-state-
# dependent file the promote step reads) matches that commit.
# We use github.sha, NOT inputs.tag — the v* git tag is created
# by create-release LATER in this run (after publish-docker
# completes), so it does not exist yet when this checkout runs
# on a push-to-main trigger. github.sha is the triggering commit
# for every event type (push to main, tag push, workflow_dispatch)
# and is the same SHA the build/prerelease-docker jobs used.
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.sha }}
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 to match the version that signed the artifact
# in prerelease-docker.yml. Mismatched versions across sign/verify
# work today but new-bundle-format (cosign v3 default) would only
# produce/consume on v3.
cosign-release: 'v2.6.3'
- name: Determine release tags
id: version
env:
DISPATCH_TAG: ${{ inputs.tag }}
SOURCE_TAG: ${{ inputs.source_tag }}
EXPECTED_DIGEST: ${{ inputs.expected_digest }}
run: |
set -euo pipefail
if [[ -z "$DISPATCH_TAG" || -z "$SOURCE_TAG" || -z "$EXPECTED_DIGEST" ]]; then
echo "::error::Missing required workflow_call input. Got tag='${DISPATCH_TAG}' source_tag='${SOURCE_TAG}' expected_digest='${EXPECTED_DIGEST}'"
exit 1
fi
if [[ "$EXPECTED_DIGEST" != sha256:* ]]; then
echo "::error::expected_digest must be of the form sha256:... — got '${EXPECTED_DIGEST}'"
exit 1
fi
VERSION="${DISPATCH_TAG#v}"
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
# Group writes into one redirect (shellcheck SC2129).
{
echo "tag=${DISPATCH_TAG}"
echo "version=${VERSION}"
echo "major_minor=${MAJOR_MINOR}"
echo "source_tag=${SOURCE_TAG}"
echo "expected_digest=${EXPECTED_DIGEST}"
} >> "$GITHUB_OUTPUT"
- name: Verify source digest matches expected
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
SOURCE_TAG: ${{ steps.version.outputs.source_tag }}
EXPECTED_DIGEST: ${{ steps.version.outputs.expected_digest }}
run: |
set -euo pipefail
# Defends against the prerelease tag being swapped between
# prerelease-docker's signing and this promote step.
SOURCE="${DOCKER_USERNAME}/local-deep-research:${SOURCE_TAG}"
ACTUAL=$(docker buildx imagetools inspect "$SOURCE" --format '{{json .Manifest.Digest}}' | tr -d '"')
if [[ "$ACTUAL" != "$EXPECTED_DIGEST" ]]; then
echo "::error::Source digest mismatch — possible tag tampering between prerelease and promote"
echo " expected: $EXPECTED_DIGEST"
echo " actual: $ACTUAL"
exit 1
fi
echo "Source digest verified: $ACTUAL"
- name: Promote (retag) prerelease manifest to release tags
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
SOURCE_TAG: ${{ steps.version.outputs.source_tag }}
VERSION: ${{ steps.version.outputs.version }}
MAJOR_MINOR: ${{ steps.version.outputs.major_minor }}
run: |
set -euo pipefail
SOURCE="${DOCKER_USERNAME}/local-deep-research:${SOURCE_TAG}"
# Single imagetools create with multiple -t — registry-side
# metadata-only operation, takes seconds, preserves digest.
docker buildx imagetools create \
-t "${DOCKER_USERNAME}/local-deep-research:${VERSION}" \
-t "${DOCKER_USERNAME}/local-deep-research:${MAJOR_MINOR}" \
-t "${DOCKER_USERNAME}/local-deep-research:latest" \
"$SOURCE"
echo "Promoted ${SOURCE} to :${VERSION}, :${MAJOR_MINOR}, :latest"
- name: Verify promoted tags share the source digest
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
VERSION: ${{ steps.version.outputs.version }}
MAJOR_MINOR: ${{ steps.version.outputs.major_minor }}
EXPECTED_DIGEST: ${{ steps.version.outputs.expected_digest }}
run: |
set -euo pipefail
# Defends against `imagetools create` re-encoding the manifest.
# If digests diverge, signatures and attestations (keyed by the
# original digest) won't be discoverable from the new tags.
for TAG in "${VERSION}" "${MAJOR_MINOR}" "latest"; do
REF="${DOCKER_USERNAME}/local-deep-research:${TAG}"
ACTUAL=$(docker buildx imagetools inspect "$REF" --format '{{json .Manifest.Digest}}' | tr -d '"')
if [[ "$ACTUAL" != "$EXPECTED_DIGEST" ]]; then
echo "::error::Digest mismatch on ${TAG} — imagetools create may have re-encoded the manifest"
echo " expected: $EXPECTED_DIGEST"
echo " actual: $ACTUAL"
exit 1
fi
echo "${TAG} -> ${ACTUAL} ✓"
done
# Catches CVE-database updates that landed between the prerelease
# build and this promote step. Use the SHA-pinned action wrapper
# (same pin as prerelease-docker.yml's security-scan) with an
# explicit binary version pin — the prior `apt-get install -y trivy`
# approach was unpinned and exposed the release path to the Trivy
# apt-repo supply chain. The pinned action downloads the v0.69.2
# binary from GitHub releases by exact tag, which is the same
# binary the prerelease scan validated, keeping the two scans
# consistent.
#
# Unlike prerelease-docker.yml's security-scan (which scans a
# locally-loaded image, no registry pull), this step scans by
# registry digest — Trivy must pull the manifest + layers from
# Docker Hub. TRIVY_USERNAME/TRIVY_PASSWORD is the action's
# documented auth path; the `docker/login-action` above also
# writes ~/.docker/config.json which Trivy reads as a fallback,
# but the explicit env vars are more reliable (Trivy has
# documented docker.io credential-helper quirks — aquasecurity/
# trivy#432, aquasecurity/trivy#8385) and the image we scan IS on
# Docker Hub so this is the path most likely to keep working.
- name: Re-scan release digest with Trivy
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
env:
TRIVY_USERNAME: ${{ secrets.DOCKER_USERNAME }}
TRIVY_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
with:
image-ref: ${{ secrets.DOCKER_USERNAME }}/local-deep-research@${{ steps.version.outputs.expected_digest }}
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
trivyignores: '.trivyignore'
exit-code: '1'
version: 'v0.69.2'
- name: Verify cosign signature on promoted digest
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
EXPECTED_DIGEST: ${{ steps.version.outputs.expected_digest }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
# The cert was issued to the prerelease-docker.yml workflow when
# signing happened there, so the identity regex must match that
# workflow's path. Fulcio's SAN is built from job_workflow_ref,
# which for reusable workflows is the CALLEE.
#
# Verify by IMMUTABLE digest (not the :VERSION tag) so this step
# is invariant under any retag race between the verify-promoted-
# tags step above and this one. Trivy's re-scan above also uses
# @${EXPECTED_DIGEST}; keeping cosign on the same reference is
# consistent and avoids a tag-resolution TOCTOU window.
IMAGE_REF="${DOCKER_USERNAME}/local-deep-research@${EXPECTED_DIGEST}"
echo "Verifying signature for: $IMAGE_REF"
cosign verify \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--certificate-identity-regexp "^https://github.com/${REPO}/\.github/workflows/prerelease-docker\.yml@refs/(heads|tags)/" \
--certificate-github-workflow-repository "${REPO}" \
"$IMAGE_REF"
echo "Signature transitivity verified ✓"
- name: Clean up prerelease tags
continue-on-error: true
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
RELEASE_VERSION: ${{ steps.version.outputs.version }}
run: |
set +e # Best-effort: never fail the workflow
# Scope deletion to prereleases of THIS version only, so concurrent
# prereleases for other versions (and any unrelated prerelease-* tags)
# are left untouched.
PREFIX="prerelease-v${RELEASE_VERSION}-"
echo "Cleaning up ${PREFIX}* tags from Docker Hub..."
# Authenticate with Docker Hub API (password-based JWT)
TOKEN=$(curl -s -X POST "https://hub.docker.com/v2/users/login/" \
-H "Content-Type: application/json" \
-d "{\"username\": \"${DOCKER_USERNAME}\", \"password\": \"${DOCKER_PASSWORD}\"}" | jq -r '.token')
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
echo "WARNING: Failed to authenticate with Docker Hub API. Skipping cleanup."
exit 0
fi
REPO="${DOCKER_USERNAME}/local-deep-research"
PAGE=1
DELETED=0
while true; do
RESPONSE=$(curl -s -H "Authorization: JWT ${TOKEN}" \
"https://hub.docker.com/v2/repositories/${REPO}/tags/?page=${PAGE}&page_size=100")
RESULTS=$(echo "$RESPONSE" | jq -r '.results[]?.name // empty')
if [ -z "$RESULTS" ]; then
break
fi
while IFS= read -r tag; do
if [[ "$tag" == "${PREFIX}"* ]]; then
echo "Deleting tag: ${tag}"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: JWT ${TOKEN}" \
"https://hub.docker.com/v2/repositories/${REPO}/tags/${tag}/")
if [ "$HTTP_CODE" -eq 204 ] || [ "$HTTP_CODE" -eq 200 ]; then
echo " Deleted: ${tag}"
DELETED=$((DELETED + 1))
else
echo " WARNING: Failed to delete ${tag} (HTTP ${HTTP_CODE})"
fi
fi
done <<< "$RESULTS"
# Check if there are more pages
NEXT=$(echo "$RESPONSE" | jq -r '.next // empty')
if [ -z "$NEXT" ]; then
break
fi
PAGE=$((PAGE + 1))
done
echo "Prerelease tag cleanup complete. Deleted ${DELETED} tag(s) matching ${PREFIX}*."
- name: Summary
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
VERSION: ${{ steps.version.outputs.version }}
MAJOR_MINOR: ${{ steps.version.outputs.major_minor }}
EXPECTED_DIGEST: ${{ steps.version.outputs.expected_digest }}
run: |
{
echo "## Docker Release Promoted"
echo ""
echo "**Digest:** \`${EXPECTED_DIGEST}\`"
echo ""
echo "**Tags:** \`${VERSION}\`, \`${MAJOR_MINOR}\`, \`latest\` — all share the same digest as the prerelease manifest, so cosign signatures, SBOM, and SLSA provenance from the prerelease step are transitively valid."
echo ""
echo '```'
echo "docker pull ${DOCKER_USERNAME}/local-deep-research:${VERSION}"
echo "docker pull ${DOCKER_USERNAME}/local-deep-research:latest"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"