diff --git a/.github/workflows/build-mitmproxy-image.yml b/.github/workflows/build-mitmproxy-image.yml index 841f9f7f..a4885455 100644 --- a/.github/workflows/build-mitmproxy-image.yml +++ b/.github/workflows/build-mitmproxy-image.yml @@ -5,13 +5,15 @@ on: branches: [master] paths: - 'images/mitmproxy/**' + - 'images/mitmproxy.env' - '.github/workflows/build-mitmproxy-image.yml' pull_request: paths: - 'images/mitmproxy/**' + - 'images/mitmproxy.env' - '.github/workflows/build-mitmproxy-image.yml' schedule: - - cron: '0 6 * * 1' # Weekly Monday 6am — rebuild against latest mitmproxy + - cron: '0 6 * * 1' # Weekly Monday 6am — build-only canary against upstream mitmproxy:latest workflow_dispatch: jobs: @@ -35,16 +37,39 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Load pinned mitmproxy version + # Single source of truth — also consumed by the CLI (SCT_MITMPROXY_VERSION + # in cli/lib/constants.bash) and the contract test that checks they match. + if: github.event_name != 'schedule' + run: grep -m1 '^MITMPROXY_VERSION=' images/mitmproxy.env >> "$GITHUB_ENV" + + - name: Use latest mitmproxy for the scheduled canary build + # The weekly canary builds against upstream mitmproxy:latest to surface + # breaking upstream releases early (a red run is the alarm). It is + # build-only — nothing is pushed (see `push:` below) — so an + # unreviewed upstream image can never reach any published tag. + if: github.event_name == 'schedule' + run: echo "MITMPROXY_VERSION=latest" >> "$GITHUB_ENV" + - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/virtuslab/sandcat-mitmproxy-op + # Tag policy: every published image contains the PINNED mitmproxy + # (12.2.3 from images/mitmproxy.env). `latest` simply means the + # latest master build of OUR image — so projects that reference + # ghcr `latest` (including those generated before pinning) get the + # pinned base too. The tag is published from master + # pushes and master dispatches only, so a dispatch from a feature + # branch can never overwrite it (it gets branch + sha tags). + # Scheduled canary builds are never pushed at all. tags: | - type=ref,event=branch - type=ref,event=pr - type=sha - type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=latest,enable=${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master') }} + type=raw,value=${{ env.MITMPROXY_VERSION }},enable=${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master') }} + type=sha,enable=${{ github.event_name != 'schedule' && github.event_name != 'pull_request' }} + type=ref,event=branch,enable=${{ github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/master' }} + type=ref,event=pr,enable=${{ github.event_name == 'pull_request' }} - name: Build and push Docker image uses: docker/build-push-action@v5 @@ -52,9 +77,11 @@ jobs: context: images/mitmproxy file: images/mitmproxy/Dockerfile platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} + push: ${{ github.event_name != 'pull_request' && github.event_name != 'schedule' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - # Skip cache on scheduled builds to pick up upstream mitmproxy:latest changes + build-args: | + MITMPROXY_VERSION=${{ env.MITMPROXY_VERSION }} + # Skip cache on scheduled canary builds to pick up upstream mitmproxy:latest changes cache-from: ${{ github.event_name != 'schedule' && 'type=gha' || '' }} cache-to: type=gha,mode=max diff --git a/.github/workflows/build-mitmproxy-pass-image.yml b/.github/workflows/build-mitmproxy-pass-image.yml index 3a6b1ba5..82a896d2 100644 --- a/.github/workflows/build-mitmproxy-pass-image.yml +++ b/.github/workflows/build-mitmproxy-pass-image.yml @@ -5,13 +5,15 @@ on: branches: [master] paths: - 'images/mitmproxy-pass/**' + - 'images/mitmproxy.env' - '.github/workflows/build-mitmproxy-pass-image.yml' pull_request: paths: - 'images/mitmproxy-pass/**' + - 'images/mitmproxy.env' - '.github/workflows/build-mitmproxy-pass-image.yml' schedule: - - cron: '0 6 * * 1' # Weekly Monday 6am — rebuild against latest mitmproxy + - cron: '0 6 * * 1' # Weekly Monday 6am — build-only canary against upstream mitmproxy:latest workflow_dispatch: jobs: @@ -40,16 +42,39 @@ jobs: # test. Strip comments/blank lines before exporting to $GITHUB_ENV. run: grep -E '^[A-Za-z_][A-Za-z0-9_]*=' images/mitmproxy-pass/pass-cli.env >> "$GITHUB_ENV" + - name: Load pinned mitmproxy version + # Single source of truth — also consumed by the CLI (SCT_MITMPROXY_VERSION + # in cli/lib/constants.bash) and the contract test that checks they match. + if: github.event_name != 'schedule' + run: grep -m1 '^MITMPROXY_VERSION=' images/mitmproxy.env >> "$GITHUB_ENV" + + - name: Use latest mitmproxy for the scheduled canary build + # The weekly canary builds against upstream mitmproxy:latest to surface + # breaking upstream releases early (a red run is the alarm). It is + # build-only — nothing is pushed (see `push:` below) — so an + # unreviewed upstream image can never reach any published tag. + if: github.event_name == 'schedule' + run: echo "MITMPROXY_VERSION=latest" >> "$GITHUB_ENV" + - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/virtuslab/sandcat-mitmproxy-pass + # Tag policy: every published image contains the PINNED mitmproxy + # (12.2.3 from images/mitmproxy.env). `latest` simply means the + # latest master build of OUR image — so projects that reference + # ghcr `latest` (including those generated before pinning) get the + # pinned base too. The tag is published from master + # pushes and master dispatches only, so a dispatch from a feature + # branch can never overwrite it (it gets branch + sha tags). + # Scheduled canary builds are never pushed at all. tags: | - type=ref,event=branch - type=ref,event=pr - type=sha - type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=latest,enable=${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master') }} + type=raw,value=${{ env.MITMPROXY_VERSION }},enable=${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master') }} + type=sha,enable=${{ github.event_name != 'schedule' && github.event_name != 'pull_request' }} + type=ref,event=branch,enable=${{ github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/master' }} + type=ref,event=pr,enable=${{ github.event_name == 'pull_request' }} - name: Build and push Docker image uses: docker/build-push-action@v5 @@ -57,13 +82,14 @@ jobs: context: images/mitmproxy-pass file: images/mitmproxy-pass/Dockerfile platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} + push: ${{ github.event_name != 'pull_request' && github.event_name != 'schedule' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | + MITMPROXY_VERSION=${{ env.MITMPROXY_VERSION }} PASS_CLI_VERSION=${{ env.PASS_CLI_VERSION }} PASS_CLI_SHA256_X86_64=${{ env.PASS_CLI_SHA256_X86_64 }} PASS_CLI_SHA256_AARCH64=${{ env.PASS_CLI_SHA256_AARCH64 }} - # Skip cache on scheduled builds to pick up upstream mitmproxy:latest changes + # Skip cache on scheduled canary builds to pick up upstream mitmproxy:latest changes cache-from: ${{ github.event_name != 'schedule' && 'type=gha' || '' }} cache-to: type=gha,mode=max diff --git a/.github/workflows/cli-tests.yml b/.github/workflows/cli-tests.yml index 7326b664..dd37aca9 100644 --- a/.github/workflows/cli-tests.yml +++ b/.github/workflows/cli-tests.yml @@ -5,10 +5,12 @@ on: branches: [master] paths: - 'cli/**' + - 'images/mitmproxy.env' - '.github/workflows/cli-tests.yml' pull_request: paths: - 'cli/**' + - 'images/mitmproxy.env' - '.github/workflows/cli-tests.yml' workflow_dispatch: # Allow manual trigger diff --git a/README.md b/README.md index 847266b8..6b8d4109 100644 --- a/README.md +++ b/README.md @@ -460,7 +460,7 @@ into the container's copy of `hooks.json`. ```bash # Install rtk locally (needed once on the host) -brew install rtk # or: curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh +brew install rtk # or: curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/b34be37caf3796b69a50952a28e60e32b5daad43/install.sh | RTK_VERSION=v0.45.0 sh # Register the cursor hook in your host ~/.cursor/hooks.json rtk init -g --hook-only --auto-patch --agent cursor @@ -1606,7 +1606,7 @@ enough for most tools — but some runtimes bring their own CA handling: If your organization runs internal HTTPS services (e.g. an on-prem Nexus, GitLab, Artifactory) signed by an internal CA or with a self-signed certificate, sandcat's mitmproxy will fail to validate those upstreams by -default — the `mitmproxy/mitmproxy:latest` image ships a stock Debian +default — the `mitmproxy/mitmproxy` image ships a stock Debian public-CA bundle and does not know about your internal CA. Add the CA(s) to `upstream_ca_bundles` in diff --git a/cli/README.md b/cli/README.md index 1489f95d..398671f8 100644 --- a/cli/README.md +++ b/cli/README.md @@ -94,10 +94,24 @@ docker build \ PAT detection relies on the wording of `pass-cli info` output. Because the binary is pinned by version **and** sha256, that output cannot change without a deliberate bump. A contract test (`TestPassCliPatContract`) locks the detection regex against golden samples tagged with `PASS_CLI_VERSION`. When you bump `pass-cli.env`, you must also re-capture those samples — see [`cli/test/mitmproxy/fixtures/pass-cli/README.md`](test/mitmproxy/fixtures/pass-cli/README.md) — or CI will fail. -Note: Cursor agent support uses placeholder-based API key substitution and -Sandcat-managed CLI settings (`cursor.cli` in settings — permissions, model, -network flags). Put the API key in `secrets.CURSOR_API_KEY`, not in -`cursor.cli`. See the main README Cursor section for details. +##### Bumping the pinned mitmproxy version + +The mitmproxy container image version is pinned in two places to ensure consistency and enable reproducible builds: + +- **`cli/lib/constants.bash`** — `SCT_MITMPROXY_VERSION` (used by the CLI) +- **`images/mitmproxy.env`** — `MITMPROXY_VERSION` (used by Docker build) + +To bump the pinned mitmproxy version: + +1. Edit both files above to the same new version (e.g., `13.0.0`) +2. Push the changes to a branch +3. The contract test `mitmproxy_version.bats` will verify the two values match; CI will fail if they diverge +4. Merge to master — the image build workflows are triggered by changes to `images/mitmproxy.env` and publish the new versioned `ghcr.io/virtuslab/sandcat-mitmproxy-op` and `ghcr.io/virtuslab/sandcat-mitmproxy-pass` tags, moving `latest` to the same build (every published image contains the pinned mitmproxy; `latest` just means the newest master build) +5. The weekly cron job is a build-only canary against upstream `mitmproxy:latest` — a red run signals a breaking upstream release; nothing is ever published from it + +Generated projects reference the pinned version from the CLI-side constant, so projects created with `sandcat init` always use the stable versioned image. + +**Merge-day race:** after merging a bump, wait for both `build-mitmproxy-image.yml` and `build-mitmproxy-pass-image.yml` to finish publishing the new versioned ghcr tags before creating or upgrading a secret-provider (1password/protonpass) project against that version. In the window between merge and publish, `docker compose up` fails loudly with `manifest unknown`; it's safe to retry once the workflows complete. Provider `none` is unaffected since it resolves to the public Docker Hub `mitmproxy/mitmproxy` tag, which already exists. #### `sandcat init devcontainer` @@ -114,6 +128,11 @@ Options: - `--secret-provider` - `none`, `1password`, or `protonpass` (optional; default `none`) - `--1password` - Deprecated alias for `--secret-provider 1password` +Note: Cursor agent support uses placeholder-based API key substitution and +Sandcat-managed CLI settings (`cursor.cli` in settings — permissions, model, +network flags). Put the API key in `secrets.CURSOR_API_KEY`, not in +`cursor.cli`. See the main README Cursor section for details. + #### `sandcat init settings` Creates a network settings file for the proxy. diff --git a/cli/lib/composefile.bash b/cli/lib/composefile.bash index 74283f6d..c510ea6f 100644 --- a/cli/lib/composefile.bash +++ b/cli/lib/composefile.bash @@ -109,14 +109,14 @@ apply_secret_provider() { return 0 ;; 1password) - yq -i ' - .services.mitmproxy.image = "ghcr.io/virtuslab/sandcat-mitmproxy-op:latest" | + mitm_ver="$SCT_MITMPROXY_VERSION" yq -i ' + .services.mitmproxy.image = "ghcr.io/virtuslab/sandcat-mitmproxy-op:" + env(mitm_ver) | .services.mitmproxy.environment = ["OP_SERVICE_ACCOUNT_TOKEN"] ' "$compose_file" ;; protonpass) - yq -i ' - .services.mitmproxy.image = "ghcr.io/virtuslab/sandcat-mitmproxy-pass:latest" | + mitm_ver="$SCT_MITMPROXY_VERSION" yq -i ' + .services.mitmproxy.image = "ghcr.io/virtuslab/sandcat-mitmproxy-pass:" + env(mitm_ver) | .services.mitmproxy.environment = ["PROTON_PASS_PERSONAL_ACCESS_TOKEN"] ' "$compose_file" ;; diff --git a/cli/lib/constants.bash b/cli/lib/constants.bash index 4d47ca8d..731c3f9f 100644 --- a/cli/lib/constants.bash +++ b/cli/lib/constants.bash @@ -6,3 +6,8 @@ sct_home() { echo "$HOME/.config/sandcat"; } export SCT_PROJECT_DIR='.sandcat' + +# Pinned mitmproxy image version used by CLI-generated compose files. +# Keep in sync with the build-side counterpart in images/mitmproxy.env — +# a contract test asserts the two stay equal. +export SCT_MITMPROXY_VERSION="12.2.3" diff --git a/cli/lib/devcontainer.bash b/cli/lib/devcontainer.bash index 5145d965..90b873d9 100644 --- a/cli/lib/devcontainer.bash +++ b/cli/lib/devcontainer.bash @@ -1,5 +1,7 @@ #!/usr/bin/env bash +# shellcheck source=constants.bash +source "$SCT_LIBDIR/constants.bash" # shellcheck source=stacks.bash source "$SCT_LIBDIR/stacks.bash" # shellcheck source=agents.bash @@ -288,5 +290,6 @@ customize_agent_templates() { "$devcontainer_dir/sandcat/compose-proxy.yml" \ "__AGENT_MITM_ADDON__" "$mitm_addon_file" \ "__MITM_HTTP2__" "$mitm_http2" \ - "__AGENT_MITM_STREAMING_FLAGS__" "$mitm_streaming_flags" + "__AGENT_MITM_STREAMING_FLAGS__" "$mitm_streaming_flags" \ + "__MITMPROXY_VERSION__" "$SCT_MITMPROXY_VERSION" } diff --git a/cli/lib/rtk.bash b/cli/lib/rtk.bash index cea90361..56918dc9 100644 --- a/cli/lib/rtk.bash +++ b/cli/lib/rtk.bash @@ -16,6 +16,13 @@ sct_rtk_enabled() { # binary serves every supported agent; per-agent hook wiring happens # at container start via sct_rtk_user_init_block. # +# The install script URL is pinned to a commit SHA to freeze the fetched +# shell code and close the arbitrary-code-via-master vector. The binary +# itself is pinned separately via RTK_VERSION, which install.sh verifies +# with a SHA-256 check against the release's checksums.txt before +# installing — so both the script and the binary it fetches are pinned. +# When bumping rtk, update the commit SHA and RTK_VERSION together. +# # Emits an empty output when the feature is disabled so the caller can # unconditionally append it to Dockerfile fragments. sct_rtk_docker_install_block() { @@ -26,7 +33,8 @@ sct_rtk_docker_install_block() { # agent-home volume can't mask the binary on upgrade. Disable at init # time with `sandcat init --features no-rtk` or `SANDCAT_RTK=false`. USER root -RUN curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | RTK_INSTALL_DIR=/usr/local/bin sh +RUN curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/b34be37caf3796b69a50952a28e60e32b5daad43/install.sh | \ + RTK_INSTALL_DIR=/usr/local/bin RTK_VERSION=v0.45.0 sh USER vscode EOF } diff --git a/cli/templates/devcontainer/sandcat/compose-proxy.yml b/cli/templates/devcontainer/sandcat/compose-proxy.yml index 7d3be2c4..775bfcf6 100644 --- a/cli/templates/devcontainer/sandcat/compose-proxy.yml +++ b/cli/templates/devcontainer/sandcat/compose-proxy.yml @@ -51,7 +51,7 @@ services: start_period: 120s mitmproxy: - image: mitmproxy/mitmproxy:latest + image: mitmproxy/mitmproxy:__MITMPROXY_VERSION__ # Delete the stale dns.conf sentinel before mitmweb starts (see healthcheck # below). The mitmproxy-config volume persists across restarts, so a dns.conf # left by a previous run would satisfy the healthcheck immediately — letting diff --git a/cli/test/compat/mitmproxy_version.bats b/cli/test/compat/mitmproxy_version.bats new file mode 100644 index 00000000..e50799f6 --- /dev/null +++ b/cli/test/compat/mitmproxy_version.bats @@ -0,0 +1,46 @@ +#!/usr/bin/env bats +# Contract: cli/lib/constants.bash's SCT_MITMPROXY_VERSION (used by the CLI to +# render pinned image refs) must stay equal to images/mitmproxy.env's +# MITMPROXY_VERSION (used by the build workflows to publish those refs). +# Also guards both Dockerfiles against a stray re-hardcode of the base image +# tag once it's parameterized. + +setup() { + load test_helper + # shellcheck source=../../lib/constants.bash + source "$SCT_LIBDIR/constants.bash" + + REPO_ROOT="$SCT_ROOT/.." + ENV_FILE="$REPO_ROOT/images/mitmproxy.env" +} + +@test "images/mitmproxy.env exists" { + [ -f "$ENV_FILE" ] +} + +@test "SCT_MITMPROXY_VERSION matches MITMPROXY_VERSION in images/mitmproxy.env" { + local env_version + env_version="$(grep -m1 '^MITMPROXY_VERSION=' "$ENV_FILE" | cut -d= -f2-)" + + assert_equal "$SCT_MITMPROXY_VERSION" "$env_version" +} + +@test "images/mitmproxy/Dockerfile takes MITMPROXY_VERSION as a build arg with no default" { + run grep -xF 'ARG MITMPROXY_VERSION' "$REPO_ROOT/images/mitmproxy/Dockerfile" + assert_success +} + +@test "images/mitmproxy/Dockerfile FROM is parameterized on MITMPROXY_VERSION" { + run grep -xF 'FROM mitmproxy/mitmproxy:${MITMPROXY_VERSION}' "$REPO_ROOT/images/mitmproxy/Dockerfile" + assert_success +} + +@test "images/mitmproxy-pass/Dockerfile takes MITMPROXY_VERSION as a build arg with no default" { + run grep -xF 'ARG MITMPROXY_VERSION' "$REPO_ROOT/images/mitmproxy-pass/Dockerfile" + assert_success +} + +@test "images/mitmproxy-pass/Dockerfile FROM is parameterized on MITMPROXY_VERSION" { + run grep -xF 'FROM mitmproxy/mitmproxy:${MITMPROXY_VERSION}' "$REPO_ROOT/images/mitmproxy-pass/Dockerfile" + assert_success +} diff --git a/cli/test/composefile/composefile.bats b/cli/test/composefile/composefile.bats index e1ac4e59..a34dacf6 100644 --- a/cli/test/composefile/composefile.bats +++ b/cli/test/composefile/composefile.bats @@ -531,7 +531,7 @@ YAML apply_secret_provider "$proxy_compose" "1password" - yq -e '.services.mitmproxy.image == "ghcr.io/virtuslab/sandcat-mitmproxy-op:latest"' "$proxy_compose" + yq -e '.services.mitmproxy.image == "ghcr.io/virtuslab/sandcat-mitmproxy-op:'"$SCT_MITMPROXY_VERSION"'"' "$proxy_compose" yq -e '.services.mitmproxy.environment[] | select(. == "OP_SERVICE_ACCOUNT_TOKEN")' "$proxy_compose" } @@ -545,7 +545,7 @@ YAML apply_secret_provider "$proxy_compose" "protonpass" - yq -e '.services.mitmproxy.image == "ghcr.io/virtuslab/sandcat-mitmproxy-pass:latest"' "$proxy_compose" + yq -e '.services.mitmproxy.image == "ghcr.io/virtuslab/sandcat-mitmproxy-pass:'"$SCT_MITMPROXY_VERSION"'"' "$proxy_compose" yq -e '.services.mitmproxy.environment[] | select(. == "PROTON_PASS_PERSONAL_ACCESS_TOKEN")' "$proxy_compose" } diff --git a/cli/test/init/extensions.bats b/cli/test/init/extensions.bats index 25e2e9fe..014c3a2e 100644 --- a/cli/test/init/extensions.bats +++ b/cli/test/init/extensions.bats @@ -144,6 +144,24 @@ teardown() { assert_failure } +@test "customize_agent_templates pins mitmproxy image version" { + { + echo 'include: []' + echo 'services: {agent: {environment: []}}' + } > "$BATS_TEST_TMPDIR/compose-all.yml" + echo "__AGENT_DOCKER_INSTALL__" > "$BATS_TEST_TMPDIR/Dockerfile.app" + echo "__AGENT_USER_INIT__" > "$BATS_TEST_TMPDIR/sandcat/scripts/app-user-init.sh" + + customize_agent_templates "$BATS_TEST_TMPDIR" "claude" + + run grep "mitmproxy/mitmproxy:$SCT_MITMPROXY_VERSION" "$BATS_TEST_TMPDIR/sandcat/compose-proxy.yml" + assert_success + + # Placeholder must be fully resolved. + run grep '__MITMPROXY_VERSION__' "$BATS_TEST_TMPDIR/sandcat/compose-proxy.yml" + assert_failure +} + @test "customize_agent_templates adds cursor bootstrap settings" { { echo 'include: []' diff --git a/cli/test/init/regression.bats b/cli/test/init/regression.bats index adea6532..f57f0168 100644 --- a/cli/test/init/regression.bats +++ b/cli/test/init/regression.bats @@ -22,7 +22,7 @@ teardown() { assert_proxy_service() { local compose_file=$1 - yq -e '.services.mitmproxy.image == "mitmproxy/mitmproxy:latest"' "$compose_file" + yq -e '.services.mitmproxy.image == "mitmproxy/mitmproxy:'"$SCT_MITMPROXY_VERSION"'"' "$compose_file" # FIXME vscode startup fails with capabilities dropped # yq -e '.services.mitmproxy.cap_drop[] | select(. == "ALL")' "$compose_file" diff --git a/cli/test/rtk/rtk.bats b/cli/test/rtk/rtk.bats index e61c6423..e9e6e50e 100644 --- a/cli/test/rtk/rtk.bats +++ b/cli/test/rtk/rtk.bats @@ -41,6 +41,13 @@ setup() { assert_output --partial "RTK_INSTALL_DIR=/usr/local/bin" assert_output --partial "USER root" assert_output --partial "USER vscode" + # Verify the URL is pinned to a 40-character hex commit SHA (not master branch) + # Uses grep to check for the SHA pattern: rtk-ai/rtk/[40-hex-chars]/install.sh + grep -qE "rtk-ai/rtk/[0-9a-f]{40}/install.sh" <<<"$output" + # Guard against dropping the binary pin: RTK_VERSION must be set so + # install.sh fetches (and checksum-verifies) a pinned release, not + # whatever is currently latest. + assert_output --partial "RTK_VERSION=v" } @test "sct_rtk_docker_install_block emits nothing when disabled" { diff --git a/images/mitmproxy-pass/Dockerfile b/images/mitmproxy-pass/Dockerfile index 4cbee306..efcd65a1 100644 --- a/images/mitmproxy-pass/Dockerfile +++ b/images/mitmproxy-pass/Dockerfile @@ -13,9 +13,20 @@ # pass-cli pat access grant --pat-name "sandcat" --vault-name "MyVault" --role viewer # # Published as ghcr.io/virtuslab/sandcat-mitmproxy-pass. -# Rebuilt weekly to track mitmproxy:latest. +# +# Dual-channel tag policy: versioned tags (e.g. 12.2.3) are built from master +# pushes against the pin in images/mitmproxy.env; they're never overwritten +# by the weekly cron and are treated as immutable by convention (digest +# pinning is a possible future hardening). The `latest` tag is rebuilt +# weekly from upstream mitmproxy:latest as a best-effort tracking channel — +# it is NOT what pinned installs resolve to. +# +# MITMPROXY_VERSION has no default on purpose: the pin lives in exactly one +# place (images/mitmproxy.env) and MUST be supplied as a build arg (the build +# workflow sources that file; see images/mitmproxy.env for all consumers). +ARG MITMPROXY_VERSION -FROM mitmproxy/mitmproxy:latest +FROM mitmproxy/mitmproxy:${MITMPROXY_VERSION} # Version + per-arch checksums are the single source of truth in # images/mitmproxy-pass/pass-cli.env and MUST be supplied as build args diff --git a/images/mitmproxy.env b/images/mitmproxy.env new file mode 100644 index 00000000..70c275c4 --- /dev/null +++ b/images/mitmproxy.env @@ -0,0 +1,31 @@ +# Single source of truth for the pinned mitmproxy base image version. +# +# Placement note: this file lives at the images/ level (not inside a single +# image directory like pass-cli.env) because BOTH image builds consume it — +# images/mitmproxy/ and images/mitmproxy-pass/ each build FROM the mitmproxy +# base pinned here. Per-image config stays next to its Dockerfile; config +# shared across images lives in their common parent. +# +# Consumed by: +# - .github/workflows/build-mitmproxy-image.yml (loaded, then passed +# in as --build-arg for non-scheduled builds) +# - .github/workflows/build-mitmproxy-pass-image.yml (same) +# - images/mitmproxy/Dockerfile (ARG MITMPROXY_VERSION) +# - images/mitmproxy-pass/Dockerfile (ARG MITMPROXY_VERSION) +# - cli/test/compat/mitmproxy_version.bats (contract test) +# +# Must stay equal to SCT_MITMPROXY_VERSION in cli/lib/constants.bash — the +# CLI-side counterpart used to render pinned image refs into generated +# compose files. The contract test above asserts the two match. +# +# Weekly scheduled builds bypass this pin on purpose: they build +# MITMPROXY_VERSION=latest to track upstream mitmproxy:latest as a +# tracking channel, publishing only the `latest` tag. Master pushes build +# this pinned version and publish it as a versioned tag that is never +# overwritten by the weekly cron and treated as immutable by convention +# (digest pinning is a possible future hardening). +# +# Format note: simple KEY=value lines only (no quotes, no spaces around `=`) +# so this file is consumable by `source`, GitHub Actions $GITHUB_ENV, and the +# minimal parser in the contract test alike. +MITMPROXY_VERSION=12.2.3 diff --git a/images/mitmproxy/Dockerfile b/images/mitmproxy/Dockerfile index ae041151..0b842b4c 100644 --- a/images/mitmproxy/Dockerfile +++ b/images/mitmproxy/Dockerfile @@ -6,9 +6,20 @@ # `op read`, authenticated with OP_SERVICE_ACCOUNT_TOKEN. # # Published as ghcr.io/virtuslab/sandcat-mitmproxy-op. -# Rebuilt weekly to track mitmproxy:latest. +# +# Dual-channel tag policy: versioned tags (e.g. 12.2.3) are built from master +# pushes against the pin in images/mitmproxy.env; they're never overwritten +# by the weekly cron and are treated as immutable by convention (digest +# pinning is a possible future hardening). The `latest` tag is rebuilt +# weekly from upstream mitmproxy:latest as a best-effort tracking channel — +# it is NOT what pinned installs resolve to. +# +# MITMPROXY_VERSION has no default on purpose: the pin lives in exactly one +# place (images/mitmproxy.env) and MUST be supplied as a build arg (the build +# workflow sources that file; see images/mitmproxy.env for all consumers). +ARG MITMPROXY_VERSION -FROM mitmproxy/mitmproxy:latest +FROM mitmproxy/mitmproxy:${MITMPROXY_VERSION} ARG OP_CLI_VERSION=2.30.3