From bc3b5d39c1051592c6f76d31605270bf698a2601 Mon Sep 17 00:00:00 2001 From: Isaac Thor Date: Sun, 8 Mar 2026 23:38:55 -0500 Subject: [PATCH] scripts: add GHCR integration runner and Docker E2E harness Made-with: Cursor --- .gitignore | 10 ++ docs/repro.md | 44 +++++-- scripts/docker-e2e.sh | 176 +++++++++++++++++++++++++++ scripts/login-and-run-integration.sh | 71 ++++++++--- scripts/purge-ghcr-creds.sh | 6 +- scripts/run-integration.sh | 33 ++++- 6 files changed, 306 insertions(+), 34 deletions(-) create mode 100755 scripts/docker-e2e.sh diff --git a/.gitignore b/.gitignore index 53e2697..dca2fda 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,17 @@ Thumbs.db *.pem *.key +# Fuzz corpus (generated by go test -fuzz) +**/testdata/fuzz/ + # Coverage coverage.out coverage.html coverage_*.out + +# Local evidence (no secrets; do not commit) +LIVE_E2E_EVIDENCE.md +DOCKER_E2E_REPORT.md + +# Local E2E helper (operator runs manually with GH_PAT) +dockercomms_e2e.sh diff --git a/docs/repro.md b/docs/repro.md index f29a292..e111dec 100644 --- a/docs/repro.md +++ b/docs/repro.md @@ -79,24 +79,44 @@ go test -tags=integration -run TestDockerHubTagListing -v ./test/integration/... # Expected: exit 0, tag count logged ``` -## GHCR Integration Test (Scripted) +## GHCR Integration Test (Scripted) — Fast path -For non-TTY environments (e.g. Cursor) or when stored creds are invalid: +1. Create a GitHub PAT with `read:packages` and `write:packages`. If org uses SSO, authorize the token. +2. Run (pick one path): -1. Create a GitHub PAT with `read:packages` and `write:packages`. -2. Save it (pick one): - - `printf '%s' 'ghp_...' > ~/.dockercomms_gh_pat && chmod 600 ~/.dockercomms_gh_pat` - - Or `export GH_PAT='ghp_...'` -3. Set env (or use defaults): - - `export DOCKERCOMMS_IT_GHCR_REPO=ghcr.io/OWNER/REPO` - - `export DOCKERCOMMS_IT_RECIPIENT=team-b` -4. Run: `./scripts/login-and-run-integration.sh` +**Path A — env vars:** + +```bash +export DOCKERCOMMS_IT_GHCR_REPO="ghcr.io/OWNER/REPO" +export DOCKERCOMMS_IT_RECIPIENT="team-b" +export GH_USER="codethor0" +export GH_PAT="ghp_..." + +# If login previously failed with "denied": +./scripts/purge-ghcr-creds.sh + +./scripts/login-and-run-integration.sh +``` + +**Path B — PAT file:** + +```bash +printf '%s' 'ghp_...' > ~/.dockercomms_gh_pat +chmod 600 ~/.dockercomms_gh_pat + +export DOCKERCOMMS_IT_GHCR_REPO="ghcr.io/OWNER/REPO" +export DOCKERCOMMS_IT_RECIPIENT="team-b" + +./scripts/login-and-run-integration.sh +``` Never paste PAT into issues or logs. -Preflight check (no login): `./scripts/login-and-run-integration.sh --check` +**Dry-run (no login):** `./scripts/login-and-run-integration.sh --check` + +**If login shows "denied":** `./scripts/purge-ghcr-creds.sh` then retry. -If login shows "denied", purge bad creds: `./scripts/purge-ghcr-creds.sh` +**If repo has no `:latest` tag:** Set `DOCKERCOMMS_IT_AUTH_TAG` to an existing tag (e.g. `v1.0.0`) so auth proof can verify; otherwise the script prints guidance and proceeds. ## Integration Tests (opt-in) diff --git a/scripts/docker-e2e.sh b/scripts/docker-e2e.sh new file mode 100755 index 0000000..09d2637 --- /dev/null +++ b/scripts/docker-e2e.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +# Docker-based E2E validation harness for dockercomms. +# Modes: gates | integration | cli | full +# Usage: ./scripts/docker-e2e.sh [gates|integration|cli|full] +# Integration/full require: GH_PAT (or ~/.dockercomms_gh_pat), DOCKERCOMMS_IT_GHCR_REPO, DOCKERCOMMS_IT_RECIPIENT +set -euo pipefail + +PROJECT="$(cd "$(dirname "$0")/.." && pwd)" +GO_VERSION="${GO_VERSION:-1.25}" +IMAGE="golang:${GO_VERSION}" +MODE="${1:-gates}" +PAT_FILE="${HOME}/.dockercomms_gh_pat" + +# Resolve GH_PAT: env or secure file (never print) +GH_PAT="${GH_PAT:-}" +if [[ -z "${GH_PAT}" ]] && [[ -f "${PAT_FILE}" ]] && [[ -r "${PAT_FILE}" ]]; then + GH_PAT=$(cat "${PAT_FILE}") +fi + +# Export for container (do not echo) +export DOCKERCOMMS_IT_GHCR_REPO="${DOCKERCOMMS_IT_GHCR_REPO:-}" +export DOCKERCOMMS_IT_RECIPIENT="${DOCKERCOMMS_IT_RECIPIENT:-}" +export DOCKERCOMMS_IT_DH_REPO="${DOCKERCOMMS_IT_DH_REPO:-}" +export DOCKERCOMMS_IT_LARGE_PAYLOAD="${DOCKERCOMMS_IT_LARGE_PAYLOAD:-}" +export DOCKERCOMMS_IT_AUTH_TAG="${DOCKERCOMMS_IT_AUTH_TAG:-}" +export DOCKERCOMMS_IT_SINCE="${DOCKERCOMMS_IT_SINCE:-}" +export GO_TEST_TIMEOUT="${GO_TEST_TIMEOUT:-240s}" + +host_login_ghcr() { + if [[ -z "${GH_PAT}" ]]; then + echo "[docker-e2e] GH_PAT not set; integration tests will fail auth" + return 1 + fi + GH_USER="${GH_USER:-codethor0}" + echo "[docker-e2e] Logging in to ghcr.io on host (credentials in ~/.docker for container)..." + printf '%s' "${GH_PAT}" | docker login ghcr.io -u "${GH_USER}" --password-stdin 2>/dev/null || { + echo "[docker-e2e] docker login ghcr.io failed" + return 1 + } +} + +docker_run() { + docker run --rm \ + -v "${PROJECT}:/workspace" \ + -w /workspace \ + -v "${HOME}/.docker:/root/.docker:ro" \ + -e DOCKERCOMMS_IT_GHCR_REPO \ + -e DOCKERCOMMS_IT_RECIPIENT \ + -e DOCKERCOMMS_IT_DH_REPO \ + -e DOCKERCOMMS_IT_LARGE_PAYLOAD \ + -e DOCKERCOMMS_IT_AUTH_TAG \ + -e DOCKERCOMMS_IT_SINCE \ + -e GO_TEST_TIMEOUT \ + "$IMAGE" \ + bash -c "$1" +} + +case "$MODE" in + gates) + echo "[docker-e2e] gates mode: build, test, race, lint, coverage-gate" + docker_run ' + set -e + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1 + make build + go test ./... + go test -race ./... + golangci-lint run ./... + make coverage-gate + ' + ;; + integration) + echo "[docker-e2e] integration mode: host login + go test -tags=integration" + : "${DOCKERCOMMS_IT_GHCR_REPO:=ghcr.io/codethor0/dockercomms}" + : "${DOCKERCOMMS_IT_RECIPIENT:=team-b}" + export DOCKERCOMMS_IT_GHCR_REPO DOCKERCOMMS_IT_RECIPIENT + if ! host_login_ghcr; then + echo "[docker-e2e] Skipping integration: set GH_PAT or ~/.dockercomms_gh_pat" + exit 1 + fi + docker_run ' + set -e + make build + if [[ -z "${DOCKERCOMMS_IT_GHCR_REPO:-}" ]] || [[ -z "${DOCKERCOMMS_IT_RECIPIENT:-}" ]]; then + echo "DOCKERCOMMS_IT_GHCR_REPO and DOCKERCOMMS_IT_RECIPIENT required" + exit 1 + fi + go test -tags=integration ./test/integration/... -run Test -count=1 -v -timeout "${GO_TEST_TIMEOUT:-240s}" + ' + ;; + cli) + echo "[docker-e2e] cli mode: send/recv round-trip, verify-failure no-materialize, resume" + : "${DOCKERCOMMS_IT_GHCR_REPO:=ghcr.io/codethor0/dockercomms}" + : "${DOCKERCOMMS_IT_RECIPIENT:=team-b}" + export DOCKERCOMMS_IT_GHCR_REPO DOCKERCOMMS_IT_RECIPIENT + if ! host_login_ghcr; then + echo "[docker-e2e] Skipping cli: set GH_PAT or ~/.dockercomms_gh_pat" + exit 1 + fi + docker_run ' + set -e + make build + E2E=/tmp/dockercomms-e2e + rm -rf "$E2E" && mkdir -p "$E2E" "$E2E/out" "$E2E/bad" + dd if=/dev/urandom of="$E2E/payload.bin" bs=1M count=4 2>/dev/null + echo "=== Payload SHA256 ===" + sha256sum "$E2E/payload.bin" + + echo "=== 7.1 Send (no sign) ===" + ./dockercomms send "$E2E/payload.bin" --repo "$DOCKERCOMMS_IT_GHCR_REPO" --recipient "$DOCKERCOMMS_IT_RECIPIENT" --sign=false --ttl-seconds 3600 + + echo "=== 7.2 Recv (Verify=false) ===" + rm -rf "$E2E/out" && mkdir -p "$E2E/out" + ./dockercomms recv --repo "$DOCKERCOMMS_IT_GHCR_REPO" --me "$DOCKERCOMMS_IT_RECIPIENT" --out "$E2E/out" --verify=false + echo "=== Output SHA256 ===" + sha256sum "$E2E/out/payload.bin" 2>/dev/null || true + cmp -s "$E2E/payload.bin" "$E2E/out/payload.bin" && echo "OK: payloads match" + + echo "=== 7.3 Verify-failure no-materialize ===" + rm -rf "$E2E/bad" && mkdir -p "$E2E/bad" + set +e + ./dockercomms recv --repo "$DOCKERCOMMS_IT_GHCR_REPO" --me "$DOCKERCOMMS_IT_RECIPIENT" --out "$E2E/bad" --verify=true --trusted-root /workspace/testdata/bad-trusted-root.json 2>/dev/null + REXIT=$? + set -e + echo "recv exit: $REXIT (expect 2)" + test ! -f "$E2E/bad/payload.bin" && echo "OK: no output on verify failure" + ls -la "$E2E/bad" 2>/dev/null || true + + echo "=== 7.4 Resume (send same payload twice) ===" + ./dockercomms send "$E2E/payload.bin" --repo "$DOCKERCOMMS_IT_GHCR_REPO" --recipient "$DOCKERCOMMS_IT_RECIPIENT" --sign=false --chunk-bytes 1048576 + ./dockercomms send "$E2E/payload.bin" --repo "$DOCKERCOMMS_IT_GHCR_REPO" --recipient "$DOCKERCOMMS_IT_RECIPIENT" --sign=false --chunk-bytes 1048576 + echo "OK: both sends completed (blobs reused via HEAD)" + ' + ;; + full) + echo "[docker-e2e] full mode: gates + integration + cli" + : "${DOCKERCOMMS_IT_GHCR_REPO:=ghcr.io/codethor0/dockercomms}" + : "${DOCKERCOMMS_IT_RECIPIENT:=team-b}" + export DOCKERCOMMS_IT_GHCR_REPO DOCKERCOMMS_IT_RECIPIENT + docker_run ' + set -e + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1 + make build + go test ./... + go test -race ./... + golangci-lint run ./... + make coverage-gate + ' + if host_login_ghcr; then + docker_run ' + set -e + go test -tags=integration ./test/integration/... -run Test -count=1 -v -timeout "${GO_TEST_TIMEOUT:-240s}" + ' + docker_run ' + set -e + make build + E2E=/tmp/dockercomms-e2e + rm -rf "$E2E" && mkdir -p "$E2E" "$E2E/out" "$E2E/bad" + dd if=/dev/urandom of="$E2E/payload.bin" bs=1M count=4 2>/dev/null + sha256sum "$E2E/payload.bin" + ./dockercomms send "$E2E/payload.bin" --repo "$DOCKERCOMMS_IT_GHCR_REPO" --recipient "$DOCKERCOMMS_IT_RECIPIENT" --sign=false --ttl-seconds 3600 + rm -rf "$E2E/out" && mkdir -p "$E2E/out" + ./dockercomms recv --repo "$DOCKERCOMMS_IT_GHCR_REPO" --me "$DOCKERCOMMS_IT_RECIPIENT" --out "$E2E/out" --verify=false + cmp -s "$E2E/payload.bin" "$E2E/out/payload.bin" && echo "OK: round-trip" + rm -rf "$E2E/bad" && mkdir -p "$E2E/bad" + ./dockercomms recv --repo "$DOCKERCOMMS_IT_GHCR_REPO" --me "$DOCKERCOMMS_IT_RECIPIENT" --out "$E2E/bad" --verify=true --trusted-root /workspace/testdata/bad-trusted-root.json 2>/dev/null || true + test ! -f "$E2E/bad/payload.bin" && echo "OK: no materialize on verify fail" + ' + else + echo "[docker-e2e] integration and cli skipped: set GH_PAT or ~/.dockercomms_gh_pat" + fi + ;; + *) + echo "Usage: $0 [gates|integration|full|cli]" + exit 1 + ;; +esac diff --git a/scripts/login-and-run-integration.sh b/scripts/login-and-run-integration.sh index bff6655..719c159 100755 --- a/scripts/login-and-run-integration.sh +++ b/scripts/login-and-run-integration.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash # GHCR login + integration tests. Non-interactive when GH_PAT provided. +# Exit 3 = auth failure (login or manifest inspect). # Usage: ./scripts/login-and-run-integration.sh [--help|--check] # Requires: export GH_PAT="ghp_..." OR ~/.dockercomms_gh_pat (chmod 600) set -euo pipefail -# Restrict new file perms; PAT file must already be chmod 600 umask 077 PROJECT="$(cd "$(dirname "$0")/.." && pwd)" @@ -26,6 +26,7 @@ Required env (or defaults used): DOCKERCOMMS_IT_RECIPIENT (default: team-b) Optional: GH_USER (default: codethor0) + DOCKERCOMMS_IT_AUTH_TAG (default: latest; use existing tag if :latest missing) EOF } @@ -40,11 +41,12 @@ preflight() { fi echo "[preflight] GHCR connectivity..." - if ! curl -sS -I --max-time 15 https://ghcr.io/v2/ >/dev/null 2>&1; then - echo " FAIL: Cannot reach ghcr.io (network/proxy/DNS?)" - err=1 + code=$(curl -sS -o /dev/null -w "%{http_code}" --max-time 10 https://ghcr.io/v2/ 2>/dev/null) || true + if [[ "$code" == "401" ]] || [[ "$code" == "405" ]]; then + echo " OK (got $code)" else - echo " OK" + echo " FAIL: Cannot reach ghcr.io (got ${code:-timeout})" + err=1 fi echo "[preflight] Env vars..." @@ -61,6 +63,24 @@ preflight() { fi } +auth_proof() { + local auth_err tag + : "${DOCKERCOMMS_IT_AUTH_TAG:=latest}" + tag="${DOCKERCOMMS_IT_AUTH_TAG}" + auth_err=$(DOCKER_CLIENT_TIMEOUT=20 DOCKER_HTTP_TIMEOUT=20 docker manifest inspect "${DOCKERCOMMS_IT_GHCR_REPO}:${tag}" 2>&1) || true + if echo "${auth_err}" | grep -qE "manifest unknown|not found|no such manifest"; then + echo "Login succeeded but ${DOCKERCOMMS_IT_GHCR_REPO}:${tag} not found." + echo " Set DOCKERCOMMS_IT_AUTH_TAG to an existing tag or proceed (tests will validate auth via registry operations)." + return 0 + fi + if echo "${auth_err}" | grep -qE "unauthorized|denied|authentication required|insufficient_scope|insufficient scope|invalid token|invalid username/password|access to the resource is denied"; then + echo "Auth to GHCR failed (docker manifest inspect). Exiting 3." + echo " PAT must have read:packages + write:packages." + echo " Re-run ./scripts/purge-ghcr-creds.sh if old creds interfere." + exit 3 + fi +} + main() { case "${1:-}" in --help|-h) usage; exit 0 ;; @@ -76,14 +96,16 @@ main() { GH_USER="${GH_USER:-codethor0}" GH_PAT="${GH_PAT:-}" if [[ -z "${GH_PAT}" ]] && [[ -f "${PAT_FILE}" ]]; then - GH_PAT=$(cat "${PAT_FILE}") + if [[ -r "${PAT_FILE}" ]]; then + GH_PAT=$(cat "${PAT_FILE}") + fi fi if [[ -z "${GH_PAT}" ]]; then if [[ ! -t 0 ]] || [[ ! -t 1 ]]; then echo "ERROR: Non-TTY and GH_PAT not set. Cannot prompt for credentials." echo " Set: export GH_PAT='ghp_...'" - echo " Or: echo 'ghp_...' > ~/.dockercomms_gh_pat && chmod 600 ~/.dockercomms_gh_pat" + echo " Or: printf '%s' 'ghp_...' > ~/.dockercomms_gh_pat && chmod 600 ~/.dockercomms_gh_pat" echo " Never paste PAT into issues or logs." exit 1 fi @@ -96,20 +118,39 @@ main() { preflight - echo "[1/4] Logging out of ghcr.io (ignore errors if not logged in)..." + echo "[1/5] Logging out of ghcr.io (ignore errors if not logged in)..." docker logout ghcr.io >/dev/null 2>&1 || true - echo "[2/4] Logging in to ghcr.io non-interactively as ${GH_USER}..." - printf '%s' "${GH_PAT}" | docker login ghcr.io -u "${GH_USER}" --password-stdin + echo "[2/5] Logging in to ghcr.io non-interactively as ${GH_USER}..." + if ! printf '%s' "${GH_PAT}" | DOCKER_CLIENT_TIMEOUT=20 DOCKER_HTTP_TIMEOUT=20 docker login ghcr.io -u "${GH_USER}" --password-stdin; then + echo "Auth to GHCR failed (docker login). Exiting 3." + echo " PAT must have read:packages + write:packages." + echo " Re-run ./scripts/purge-ghcr-creds.sh if old creds interfere." + exit 3 + fi - echo "[3/4] Verifying Docker can hit GHCR with auth..." - docker pull "ghcr.io/${GH_USER}/nonexistent-verify" 2>/dev/null || true - echo "OK (auth path exercised; nonexistent image pull failure is expected)." + echo "[3/5] Auth proof (must require auth, return quickly)..." + auth_proof - echo "[4/4] Running integration script..." + echo "[4/5] Running integration script..." cd "${PROJECT}" chmod +x "${SCRIPT}" - "${SCRIPT}" + "${SCRIPT}" || { + e=$? + if [[ $e -ne 0 ]]; then + echo "" + echo "Integration test failed. If you saw 'context deadline exceeded'," + echo "this is almost certainly missing/invalid GHCR auth; re-run this script" + echo "after verifying PAT and purge if needed." + fi + exit $e + } + + echo "[5/5] Paste-back template (no secrets):" + echo " Repo: $DOCKERCOMMS_IT_GHCR_REPO" + echo " Recipient: $DOCKERCOMMS_IT_RECIPIENT" + echo " Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo " Cosign: $(cosign version 2>/dev/null || echo 'NOT INSTALLED')" } main "$@" diff --git a/scripts/purge-ghcr-creds.sh b/scripts/purge-ghcr-creds.sh index c498408..7107b5a 100755 --- a/scripts/purge-ghcr-creds.sh +++ b/scripts/purge-ghcr-creds.sh @@ -32,6 +32,6 @@ echo " export GH_PAT='ghp_...'" echo " ./scripts/login-and-run-integration.sh" echo "" echo "If credential helper (credsStore) still returns old creds, manually:" -echo " 1. Open Docker Desktop -> Settings -> Docker Engine" -echo " 2. Or remove creds via: docker-credential-desktop erase ghcr.io" -echo " 3. Re-run: ./scripts/login-and-run-integration.sh" +echo " - Docker Desktop: docker-credential-desktop erase ghcr.io" +echo " - macOS Keychain: delete 'ghcr.io' entry in login keychain" +echo " - Then: ./scripts/login-and-run-integration.sh" diff --git a/scripts/run-integration.sh b/scripts/run-integration.sh index eb8267b..5c5d664 100755 --- a/scripts/run-integration.sh +++ b/scripts/run-integration.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash # Run integration tests against GHCR. Requires: docker login ghcr.io first. +# Exit 3 = auth failure (see login-and-run-integration.sh). # Usage: ./scripts/run-integration.sh [--check] # Optional env: DOCKERCOMMS_IT_DH_REPO, DOCKERCOMMS_IT_LARGE_PAYLOAD set -euo pipefail @@ -21,12 +22,25 @@ check_mode() { echo " DOCKERCOMMS_IT_LARGE_PAYLOAD=${DOCKERCOMMS_IT_LARGE_PAYLOAD}" fi echo "[check] Docker daemon..." - if docker info >/dev/null 2>&1; then - echo " OK" - else + if ! docker info >/dev/null 2>&1; then echo " FAIL: Start Docker Desktop" exit 1 fi + echo " OK" + echo "[check] GHCR connectivity..." + code=$(curl -sS -o /dev/null -w "%{http_code}" --max-time 10 https://ghcr.io/v2/ 2>/dev/null) || true + if [[ "$code" == "401" ]] || [[ "$code" == "405" ]]; then + echo " OK (got $code)" + else + echo " FAIL: Cannot reach ghcr.io (got ${code:-timeout})" + exit 1 + fi + echo "[check] Auth (best-effort)..." + if docker manifest inspect "${DOCKERCOMMS_IT_GHCR_REPO}:latest" >/dev/null 2>&1; then + echo " OK (auth verified)" + else + echo " auth not verified (expected if not logged in)" + fi echo "[check] Go test path..." if [[ -d "${PROJECT}/test/integration" ]]; then echo " OK" @@ -49,6 +63,9 @@ main() { echo "== Go ==" go version echo + echo "== Docker ==" + docker version --format '{{.Client.Version}}' 2>/dev/null || true + echo echo "== Docker daemon check ==" if ! docker info >/dev/null 2>&1; then echo "Docker daemon: NOT reachable" @@ -73,7 +90,15 @@ main() { echo echo "== Run integration tests ==" cd "${PROJECT}" - go test -tags=integration ./test/integration/... -run Test -count=1 -v -timeout "${GO_TEST_TIMEOUT}" + set +e + go test -tags=integration ./test/integration/... -run Test -count=1 -v -timeout "${GO_TEST_TIMEOUT}" 2>&1 | tee /tmp/dockercomms_it.out + r=${PIPESTATUS[0]} + set -e + if [[ $r -ne 0 ]] && grep -q "context deadline exceeded" /tmp/dockercomms_it.out 2>/dev/null; then + echo "" + echo "This is almost certainly missing/invalid GHCR auth; run ./scripts/login-and-run-integration.sh" + fi + exit $r } main "$@"