mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
ci(docker): mirror signed images to Docker Hub alongside GHCR (#1029)
* ci(docker): mirror signed images to Docker Hub alongside GHCR
docker.yml now publishes to docker.io/abhigyanpatwari/gitnexus{,-web} in
the same build step as the existing GHCR push, so both registries receive
the same digest, the same Cosign keyless signature, and the same SBOM /
build-provenance attestations. The Docker Hub login uses new repo secrets
DOCKERHUB_USERNAME / DOCKERHUB_TOKEN (scoped PAT, not account password).
Supply-chain guarantees carry over unchanged: the signing loop iterates
metadata-action's full tag set, so Docker Hub tags get signed at the
identical digest under the same docker.yml@refs/tags/v* identity. The
ClusterImagePolicy is extended with docker.io / index.docker.io / bare-
namespace globs so admission cannot be sidestepped by registry-prefix
choice. README and .env.example document both registries; RC section in
CONTRIBUTING.md notes the Docker Hub mirror tag.
Closes #1027
* ci(docker): publish to akonlabs Docker Hub namespace; add PR dry-run CI
- Hardcode `akonlabs` as the Docker Hub namespace in metadata-action and
both attestation subject-names (Docker Hub org differs from GitHub org
`abhigyanpatwari`, so `github.repository_owner` would produce the wrong ref)
- Update docs (.env.example, README, CONTRIBUTING) and the Kubernetes
ClusterImagePolicy globs to reference `akonlabs/gitnexus{,-web}`
- Add `pull_request` trigger so the image build runs as CI on every PR
(build only — no push, sign, or attestation)
- Add `workflow_dispatch` with `dry_run: boolean` (default true) for
manual build-only runs; all publish steps gated on
`github.event_name != 'pull_request' && !inputs.dry_run`
This commit is contained in:
parent
62023440bd
commit
ac9246cfd3
5 changed files with 99 additions and 24 deletions
|
|
@ -1,4 +1,8 @@
|
|||
# Images (signed Cosign keyless on every push from main / vX.Y.Z tags)
|
||||
# Images (signed Cosign keyless on every push from main / vX.Y.Z tags).
|
||||
# Available from both GHCR (default below) and Docker Hub — pick one:
|
||||
# GHCR: ghcr.io/abhigyanpatwari/gitnexus{,-web}:latest
|
||||
# Docker Hub: akonlabs/gitnexus{,-web}:latest
|
||||
# Both registries receive the same digest from a single signed build.
|
||||
SERVER_IMAGE=ghcr.io/abhigyanpatwari/gitnexus:latest
|
||||
WEB_IMAGE=ghcr.io/abhigyanpatwari/gitnexus-web:latest
|
||||
|
||||
|
|
|
|||
64
.github/workflows/docker.yml
vendored
64
.github/workflows/docker.yml
vendored
|
|
@ -4,9 +4,18 @@ on:
|
|||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
# No workflow_dispatch: publishing is exclusively tag-driven so that every
|
||||
# signed image corresponds 1:1 to a published `gitnexus@X.Y.Z` on npm. A
|
||||
# manual run from a branch ref would fail the version check below anyway.
|
||||
pull_request:
|
||||
# workflow_dispatch is allowed for dry-run testing only. Publishing is still
|
||||
# exclusively tag-driven so that every signed image corresponds 1:1 to a
|
||||
# published `gitnexus@X.Y.Z` on npm. dry_run:true (the default) skips all
|
||||
# push, sign, and attestation steps — the build runs but nothing is published.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Build only — skip push, signing, and attestations'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
|
|
@ -61,7 +70,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Validate tag input
|
||||
if: github.event_name == 'workflow_call'
|
||||
if: github.event_name == 'workflow_call' || github.event_name == 'push'
|
||||
shell: bash
|
||||
env:
|
||||
TAG_INPUT: ${{ inputs.tag }}
|
||||
|
|
@ -85,6 +94,7 @@ jobs:
|
|||
# `gitnexus@X.Y.Z` published to npm — no drift, no surprises.
|
||||
- name: Verify tag matches gitnexus/package.json version
|
||||
id: version
|
||||
if: github.event_name != 'workflow_dispatch' && github.event_name != 'pull_request'
|
||||
shell: bash
|
||||
env:
|
||||
# For workflow_call the tag comes from the caller input; for push events
|
||||
|
|
@ -119,12 +129,27 @@ jobs:
|
|||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
if: ${{ github.event_name != 'pull_request' && !inputs.dry_run }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Docker Hub is a mirror of GHCR: same tags, same digests, same Cosign
|
||||
# signatures. GHCR remains authoritative (it is the registry the
|
||||
# ClusterImagePolicy globs against by default), but Docker Hub is the
|
||||
# registry most users reach for first, so we publish there too.
|
||||
# Requires repo secrets DOCKERHUB_USERNAME and DOCKERHUB_TOKEN (a scoped
|
||||
# access token, NOT the account password) with write access to the
|
||||
# `akonlabs/gitnexus` and `akonlabs/gitnexus-web` repos.
|
||||
- name: Log in to Docker Hub
|
||||
if: ${{ github.event_name != 'pull_request' && !inputs.dry_run }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Computes image tags and labels from the verified semver tag:
|
||||
# v1.2.3 → :1.2.3, :1.2, :1, :latest (auto, only for non-prerelease)
|
||||
# v1.2.3-rc.1 → :1.2.3-rc.1 only (prereleases never become :latest)
|
||||
|
|
@ -142,7 +167,16 @@ jobs:
|
|||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/${{ matrix.image.slug }}
|
||||
# Dual-registry publish. metadata-action expands the same tag set
|
||||
# against every image ref listed here, and build-push-action pushes
|
||||
# one build to all of them, so the GHCR and Docker Hub images share
|
||||
# a digest and are byte-identical. The Docker Hub namespace
|
||||
# (`akonlabs`) is hardcoded because it differs from the GitHub org
|
||||
# (`abhigyanpatwari`) — `github.repository_owner` would produce the
|
||||
# wrong ref.
|
||||
images: |
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image.slug }}
|
||||
docker.io/akonlabs/${{ matrix.image.slug }}
|
||||
flavor: latest=auto
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
|
|
@ -157,7 +191,7 @@ jobs:
|
|||
context: .
|
||||
file: ${{ matrix.image.dockerfile }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
push: ${{ github.event_name != 'pull_request' && !inputs.dry_run }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=${{ matrix.image.slug }}
|
||||
|
|
@ -178,6 +212,7 @@ jobs:
|
|||
# Do NOT relax to `@.*` — that accepts signatures from any ref, including
|
||||
# unprotected branches and PRs, and defeats the supply-chain guarantee.
|
||||
- name: Sign image with Cosign (keyless)
|
||||
if: ${{ github.event_name != 'pull_request' && !inputs.dry_run }}
|
||||
env:
|
||||
# Cosign v2 (installed by sigstore/cosign-installer above) makes
|
||||
# keyless the default. COSIGN_EXPERIMENTAL is a v1-only opt-in flag
|
||||
|
|
@ -193,10 +228,23 @@ jobs:
|
|||
[[ -n "$tag" ]] && cosign sign --yes "${tag}@${DIGEST}"
|
||||
done <<< "$TAGS"
|
||||
|
||||
# Attach the SBOM produced by buildx as a verifiable attestation on the digest.
|
||||
- name: Generate build provenance attestation
|
||||
# Attach the SBOM produced by buildx as a verifiable attestation on the
|
||||
# digest. Attestations are pushed as OCI referrers to the registry named
|
||||
# in `subject-name`, so we call the action once per registry. The digest
|
||||
# is identical across registries (same build, same push), so consumers
|
||||
# pulling from either GHCR or Docker Hub see the same provenance.
|
||||
- name: Generate build provenance attestation (GHCR)
|
||||
if: ${{ github.event_name != 'pull_request' && !inputs.dry_run }}
|
||||
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
||||
with:
|
||||
subject-name: ghcr.io/${{ github.repository_owner }}/${{ matrix.image.slug }}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Generate build provenance attestation (Docker Hub)
|
||||
if: ${{ github.event_name != 'pull_request' && !inputs.dry_run }}
|
||||
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
||||
with:
|
||||
subject-name: docker.io/akonlabs/${{ matrix.image.slug }}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
|
|
|||
|
|
@ -129,9 +129,10 @@ Two publish workflows ship `gitnexus` to npm:
|
|||
registry. First rc for a given base is `rc.1`.
|
||||
- After the npm publish succeeds, the workflow calls `docker.yml` as a
|
||||
reusable workflow to build and push the corresponding RC Docker images
|
||||
(e.g. `ghcr.io/abhigyanpatwari/gitnexus:1.7.0-rc.1`). The images are
|
||||
signed with Cosign; the OIDC identity is `docker.yml@refs/heads/main`
|
||||
(the caller's ref — see README.md § Docker for the verify command).
|
||||
(e.g. `ghcr.io/abhigyanpatwari/gitnexus:1.7.0-rc.1`, mirrored to
|
||||
`docker.io/akonlabs/gitnexus:1.7.0-rc.1`). The images are signed
|
||||
with Cosign; the OIDC identity is `docker.yml@refs/heads/main` (the
|
||||
caller's ref — see README.md § Docker for the verify command).
|
||||
|
||||
Idempotency: the workflow pushes an `rc/<HEAD_SHA>` marker tag and a
|
||||
`v<RC>` release tag **atomically, before** calling `npm publish`. The guard
|
||||
|
|
|
|||
26
README.md
26
README.md
|
|
@ -338,12 +338,12 @@ npm run dev
|
|||
|
||||
## Docker
|
||||
|
||||
The official Docker setup ships **two signed images** orchestrated by `docker-compose.yaml`:
|
||||
The official Docker setup ships **two signed images** orchestrated by `docker-compose.yaml`. Each image is published to both **GitHub Container Registry** (GHCR) and **Docker Hub** — same build, same digest, same Cosign signature — so pick whichever registry you prefer:
|
||||
|
||||
| Image | Purpose |
|
||||
| -------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `ghcr.io/abhigyanpatwari/gitnexus:latest` | CLI / `gitnexus serve` backend (HTTP API on port `4747`, MCP, indexer) |
|
||||
| `ghcr.io/abhigyanpatwari/gitnexus-web:latest` | Static web UI (port `4173`) |
|
||||
| Purpose | GHCR (default in `docker-compose.yaml`) | Docker Hub mirror |
|
||||
| ---------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------- |
|
||||
| CLI / `gitnexus serve` backend (HTTP API on port `4747`, MCP, indexer) | `ghcr.io/abhigyanpatwari/gitnexus:latest` | `akonlabs/gitnexus:latest` |
|
||||
| Static web UI (port `4173`) | `ghcr.io/abhigyanpatwari/gitnexus-web:latest` | `akonlabs/gitnexus-web:latest` |
|
||||
|
||||
> **Heads-up — image rename.** Earlier releases published the web UI under
|
||||
> `ghcr.io/abhigyanpatwari/gitnexus`. Starting with the introduction of the
|
||||
|
|
@ -404,8 +404,11 @@ The Docker images are version-locked to the npm package:
|
|||
- Stable images are **only published from `vX.Y.Z` git tags** (via `docker.yml`
|
||||
triggered directly by the tag push), and the workflow refuses to build unless
|
||||
the tag exactly matches `gitnexus/package.json`'s version. So
|
||||
`ghcr.io/abhigyanpatwari/gitnexus:1.6.2` is byte-for-byte the same release
|
||||
as `npm install gitnexus@1.6.2` — no drift, no floating builds from `main`.
|
||||
`ghcr.io/abhigyanpatwari/gitnexus:1.6.2` (and its Docker Hub mirror
|
||||
`akonlabs/gitnexus:1.6.2`) is byte-for-byte the same release as
|
||||
`npm install gitnexus@1.6.2` — no drift, no floating builds from `main`.
|
||||
Both registries receive the same digest from a single build step, so you can
|
||||
pull from either and the signature verifies identically.
|
||||
- Release-candidate images (e.g. `:1.7.0-rc.1`) are published alongside each
|
||||
RC npm release. They are built by `release-candidate.yml` calling `docker.yml`
|
||||
as a reusable workflow after the RC tag is created and pushed.
|
||||
|
|
@ -426,11 +429,18 @@ sensitive environments:
|
|||
cosign verify ghcr.io/abhigyanpatwari/gitnexus:1.6.2 \
|
||||
--certificate-identity-regexp '^https://github\.com/abhigyanpatwari/GitNexus/\.github/workflows/docker\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$' \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com
|
||||
|
||||
# Same signature verifies the Docker Hub mirror (identical digest):
|
||||
cosign verify docker.io/akonlabs/gitnexus:1.6.2 \
|
||||
--certificate-identity-regexp '^https://github\.com/abhigyanpatwari/GitNexus/\.github/workflows/docker\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$' \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com
|
||||
```
|
||||
|
||||
The regex pins the certificate identity to this repo's `docker.yml` workflow
|
||||
**run from a `v*` tag** — rejecting unsigned images, images signed by other
|
||||
workflows, and images signed from unprotected refs.
|
||||
workflows, and images signed from unprotected refs. It is identical for both
|
||||
registries because both sets of tags were signed at the same digest in one
|
||||
workflow run.
|
||||
|
||||
**Release candidates** — signed from `refs/heads/main` (the caller's ref when
|
||||
`release-candidate.yml` invokes `docker.yml` as a reusable workflow):
|
||||
|
|
|
|||
|
|
@ -36,12 +36,24 @@ kind: ClusterImagePolicy
|
|||
metadata:
|
||||
name: gitnexus-signed-images
|
||||
spec:
|
||||
# Apply to both published GitNexus images on GHCR. Image references always
|
||||
# carry a tag or digest at admission time, so these two globs cover every
|
||||
# `gitnexus:<tag>`, `gitnexus@sha256:...`, `gitnexus-web:<tag>`, and
|
||||
# `gitnexus-web@sha256:...` reference.
|
||||
# Apply to both published GitNexus images on both registries. Image
|
||||
# references always carry a tag or digest at admission time, so these globs
|
||||
# cover every `gitnexus:<tag>`, `gitnexus@sha256:...`, `gitnexus-web:<tag>`,
|
||||
# and `gitnexus-web@sha256:...` reference on either GHCR or Docker Hub.
|
||||
# The Docker Hub images are byte-for-byte mirrors of the GHCR images (same
|
||||
# build, same digest, same Cosign signature), so the same keyless identity
|
||||
# authority verifies both.
|
||||
images:
|
||||
- glob: 'ghcr.io/abhigyanpatwari/gitnexus*'
|
||||
# Docker Hub references can appear in three forms at admission time
|
||||
# (`docker.io/...`, `index.docker.io/...`, and bare `akonlabs/...` with
|
||||
# the default registry implied). List all three so the policy cannot be
|
||||
# sidestepped by the choice of registry prefix. The Docker Hub namespace
|
||||
# is `akonlabs` rather than `abhigyanpatwari` because the Docker Hub org
|
||||
# differs from the GitHub org.
|
||||
- glob: 'docker.io/akonlabs/gitnexus*'
|
||||
- glob: 'index.docker.io/akonlabs/gitnexus*'
|
||||
- glob: 'akonlabs/gitnexus*'
|
||||
authorities:
|
||||
- name: gitnexus-cosign-keyless
|
||||
keyless:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue