diff --git a/.dockerignore b/.dockerignore index 9722a63..eeea248 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,13 +10,15 @@ 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 !README.md +!LICENSE.md # Python build artifacts __pycache__/ diff --git a/.github/workflows/_docker-pipeline.yml b/.github/workflows/_docker-pipeline.yml index 91d8367..654dc1b 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,25 +36,40 @@ 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: + 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_name: 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. + 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 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" + 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 @@ -60,15 +78,20 @@ on: jobs: pipeline: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runs_on }} timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + 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 @@ -87,31 +110,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: @@ -123,10 +125,10 @@ 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 }} - 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. @@ -144,16 +146,16 @@ 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: | 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 +166,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 }} + VCS_REF=${{ steps.source.outputs.revision }} 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.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 # 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..d5a8034 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -1,11 +1,18 @@ name: publish-docker -# Builds, tests, and publishes the socket-basics image to GHCR and Docker Hub. +# 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 → create-release +# Flow: +# resolve-version +# → 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: # 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: @@ -19,7 +26,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. @@ -38,56 +45,234 @@ jobs: runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.clean }} + ref: ${{ steps.version.outputs.ref }} 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: 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. 1.1.3 or v2.0.0) + 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.0 + RAW="$REF_NAME" # e.g. v2.0.3 + fi + 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 - CLEAN="${CLEAN#v}" # strip leading v if present → 2.0.0 or 1.1.3 + + 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/$REF_TAG" >> "$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 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 (socket-basics) + name: publish (${{ matrix.image }}, ${{ matrix.arch }}) needs: resolve-version permissions: contents: read packages: write # push images to GHCR + strategy: + fail-fast: false + matrix: + include: + - image: socket-basics + dockerfile: Dockerfile + check_set: main + arch: amd64 + runs_on: ubuntu-latest + - 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 }} + # 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 - tag_push: ${{ github.ref_type == 'tag' }} version: ${{ needs.resolve-version.outputs.version }} + ref: ${{ needs.resolve-version.outputs.ref }} 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 (${{ matrix.variant }}) + needs: [resolve-version, build-test-push] + permissions: + contents: read + packages: write + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # 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.variant }}__* + 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. + # 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}} + # 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 }} + 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.variant }}, 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 + 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 "::error::no tags resolved for $image" + exit 1 + fi + sources=() + for digest in "${digests[@]}"; 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 + 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}${TAG_SUFFIX}" + 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 # 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..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' @@ -29,12 +33,41 @@ 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.image }}, ${{ matrix.arch }}) + strategy: + fail-fast: false + matrix: + include: + - image: socket-basics + dockerfile: Dockerfile + check_set: main + arch: amd64 + runs_on: ubuntu-latest + - 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/CHANGELOG.md b/CHANGELOG.md index ddc0828..a6b84ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,17 +8,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] -## [2.1.0] - 2026-06-02 +## [2.1.0] - 2026-07-21 ### Added - Diff-only scan scoping now applies to SAST/OpenGrep via `changed_files` and `scan_files`. - Added GitHub Action inputs for `changed_files` and `scan_files`. +- Publish multi-arch Docker images for `linux/amd64` and `linux/arm64`. +- 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 workspace scan. - Updated parameter docs to reflect SAST/OpenGrep diff-only scoping. +- Normalize manual Docker release tag inputs before checkout. ## [2.0.3] - 2026-04-24 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 new file mode 100644 index 0000000..dfe9ea9 --- /dev/null +++ b/Dockerfile.heavy @@ -0,0 +1,69 @@ +# 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.githubusercontent.com/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.md uv.lock /socket-basics/ +RUN cp /socket-basics/LICENSE.md /socket-basics/LICENSE + +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/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/smoke-test-docker.sh b/scripts/smoke-test-docker.sh index 2962951..14c2243 100644 --- a/scripts/smoke-test-docker.sh +++ b/scripts/smoke-test-docker.sh @@ -8,6 +8,8 @@ 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" +DOCKERFILE_SET=false BUILD_PROGRESS="${SMOKE_TEST_BUILD_PROGRESS:-}" MAIN_TOOLS=( @@ -23,6 +25,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 +40,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 +60,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"; DOCKERFILE_SET=true; shift 2 + ;; --build-progress) [[ $# -lt 2 ]] && { echo "Error: --build-progress requires a value"; exit 1; } BUILD_PROGRESS="$2"; shift 2 @@ -58,10 +73,14 @@ 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 [[ "$CHECK_SET" == "heavy" && "$DOCKERFILE_SET" == "false" ]]; then + DOCKERFILE="Dockerfile.heavy" +fi + if [[ -z "$BUILD_PROGRESS" ]]; then if [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then BUILD_PROGRESS="plain" @@ -133,6 +152,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 +162,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 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)