From f438bc37aec713703b171897993d62e1c7a2496d Mon Sep 17 00:00:00 2001 From: Zeev Manilovich Date: Sun, 28 Jun 2026 17:51:41 +0300 Subject: [PATCH 1/4] ci: pin actions to SHA and run full test suite on PRs Pin actions/checkout (v7.0.0) and actions/setup-node (v6.4.0) to full commit SHAs in both workflows, set persist-credentials: false on all checkouts, and bump the CI node matrix from 18/20 to 20/22 (18 is EOL). Run the full suite on PRs/pushes, not only on release. Same-repo events provision a throwaway Permit env via PROJECT_API_KEY, run a dockerized PDP (now with -e PDP_API_KEY/PERMIT_API_KEY and a /healthy readiness wait), execute test:ci:full, and delete the env on always(). Fork and secret-less runs fall back to the no-backend test:ci:unit suite. Add the two supporting scripts and quote $GITHUB_ENV in the publish workflow. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yaml | 172 ++++++++++++++++++++---- .github/workflows/node_sdk_publish.yaml | 9 +- package.json | 2 + 3 files changed, 154 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 46a6ce7..59d516c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,35 +6,155 @@ on: pull_request: branches: [ main ] -env: - PDP_API_KEY: test +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read jobs: - test-and-lint: + lint-and-build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 22 + cache: 'yarn' + - name: Install dependencies + run: yarn install --frozen-lockfile + - name: Run linting + run: yarn lint + - name: Build project + run: yarn build + + test: + needs: lint-and-build runs-on: ubuntu-latest - strategy: + fail-fast: false matrix: - node-version: [18, 20] - + node-version: [20, 22] + env: + # Backend suite runs only for same-repo events AND when the provisioning + # secret is available. Fork PRs and secret-less runs (e.g. Dependabot) + # fall back to the no-backend suite. + RUN_BACKEND: ${{ (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && secrets.PROJECT_API_KEY != '' }} steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: 'yarn' - - - name: Install dependencies - run: yarn install --frozen-lockfile - - - name: Run linting - run: yarn lint - - - name: Build project - run: yarn build - - - name: Run all tests - run: yarn test \ No newline at end of file + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: ${{ matrix.node-version }} + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + # Fail fast if a same-repo run lacks the backend secret, so the full + # suite can't be silently downgraded to unit-only and still report green. + # Forks (different head repo) intentionally skip this and fall back below. + - name: Require backend secret on same-repo runs + if: ${{ (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && env.RUN_BACKEND != 'true' }} + run: | + echo "::error::PROJECT_API_KEY is not set for a same-repo run; integration/e2e would be silently skipped. Failing instead of reporting a misleading green." >&2 + exit 1 + + # ---------- Backend path (same-repo only) ---------- + - name: Provision temp Permit env + id: provision + if: env.RUN_BACKEND == 'true' + env: + PROJECT_API_KEY: ${{ secrets.PROJECT_API_KEY }} + PROJECT_ID: 7f55831d77c642739bc17733ab0af138 + ENV_KEY: node-sdk-ci-${{ github.run_id }}-${{ github.run_attempt }}-n${{ matrix.node-version }} + run: | + response=$(curl -sS -X POST \ + "https://api.permit.io/v2/projects/${PROJECT_ID}/envs" \ + -H "Authorization: Bearer ${PROJECT_API_KEY}" \ + -H 'Content-Type: application/json' \ + -d "{\"key\":\"${ENV_KEY}\",\"name\":\"${ENV_KEY}\"}") + env_id=$(printf '%s' "$response" | jq -r '.id // empty') + if [ -z "$env_id" ]; then + echo "Failed to create env (key=${ENV_KEY})." >&2 + exit 1 + fi + echo "env_id=${env_id}" >> "$GITHUB_OUTPUT" + + - name: Fetch env API key + id: fetch_key + if: env.RUN_BACKEND == 'true' + env: + PROJECT_API_KEY: ${{ secrets.PROJECT_API_KEY }} + PROJECT_ID: 7f55831d77c642739bc17733ab0af138 + ENV_ID: ${{ steps.provision.outputs.env_id }} + run: | + response=$(curl -sS -X GET \ + "https://api.permit.io/v2/api-key/${PROJECT_ID}/${ENV_ID}" \ + -H "Authorization: Bearer ${PROJECT_API_KEY}") + env_api_key=$(printf '%s' "$response" | jq -r '.secret // empty') + if [ -z "$env_api_key" ]; then + echo "Failed to fetch env API key." >&2 + exit 1 + fi + # Mask BEFORE writing to the env file so it is redacted everywhere. + echo "::add-mask::${env_api_key}" + echo "ENV_API_KEY=${env_api_key}" >> "$GITHUB_ENV" + + - name: Start local PDP + if: env.RUN_BACKEND == 'true' + env: + ENV_API_KEY: ${{ env.ENV_API_KEY }} + run: | + docker run -d --name pdp -p 7766:7000 \ + -e PDP_API_KEY="$ENV_API_KEY" \ + -e PERMIT_API_KEY="$ENV_API_KEY" \ + permitio/pdp-v2:latest + + - name: Wait for PDP to be ready + if: env.RUN_BACKEND == 'true' + run: | + for i in $(seq 1 60); do + if curl -sf http://localhost:7766/healthy >/dev/null; then + echo "PDP ready after ${i} attempt(s)" + exit 0 + fi + sleep 2 + done + echo "PDP did not become healthy in time; dumping logs:" >&2 + docker logs pdp || true + exit 1 + + - name: Run full test suite (backend) + if: env.RUN_BACKEND == 'true' + env: + PDP_API_KEY: ${{ env.ENV_API_KEY }} + PERMIT_API_KEY: ${{ env.ENV_API_KEY }} + API_TIER: prod + run: yarn test:ci:full + + # ---------- No-backend path (forks / secret-less) ---------- + - name: Run no-backend test suite + if: env.RUN_BACKEND != 'true' + run: yarn test:ci:unit + + # ---------- Cleanup (always, even on failure/cancel) ---------- + - name: Delete temp Permit env + if: ${{ always() && steps.provision.outputs.env_id != '' }} + env: + PROJECT_API_KEY: ${{ secrets.PROJECT_API_KEY }} + PROJECT_ID: 7f55831d77c642739bc17733ab0af138 + ENV_ID: ${{ steps.provision.outputs.env_id }} + run: | + curl -sS -X DELETE \ + "https://api.permit.io/v2/projects/${PROJECT_ID}/envs/${ENV_ID}" \ + -H "Authorization: Bearer ${PROJECT_API_KEY}" || true diff --git a/.github/workflows/node_sdk_publish.yaml b/.github/workflows/node_sdk_publish.yaml index ab6b1a2..919b040 100644 --- a/.github/workflows/node_sdk_publish.yaml +++ b/.github/workflows/node_sdk_publish.yaml @@ -15,11 +15,14 @@ jobs: contents: read id-token: write # Required for npm Trusted Publishing (OIDC) steps: + # Safe: this workflow makes only local git commits (docs/version bump); nothing is pushed. - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '20' registry-url: 'https://registry.npmjs.org' @@ -51,7 +54,7 @@ jobs: }') # Extract the new env id - echo "ENV_ID=$(echo "$response" | jq -r '.id')" >> $GITHUB_ENV + echo "ENV_ID=$(echo "$response" | jq -r '.id')" >> "$GITHUB_ENV" echo "New env ID: $ENV_ID" diff --git a/package.json b/package.json index 7106bc1..18c0741 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "test:e2e:rbac": "run-s build && ava --verbose build/tests/e2e/rbac.e2e.spec.js", "test:e2e:rebac": "run-s build && ava --verbose build/tests/e2e/rebac.e2e.spec.js", "test:e2e:local_facts": "run-s build && ava --verbose build/tests/e2e/local_facts.e2e.spec.js", + "test:ci:unit": "run-s test:unit test:module-imports", + "test:ci:full": "run-s test test:e2e:rbac test:e2e:rebac test:e2e:local_facts", "check-cli": "run-s test diff-integration-tests check-integration-tests", "check-integration-tests": "run-s check-integration-test:*", "diff-integration-tests": "mkdir -p diff && rm -rf diff/test && cp -r test diff/test && rm -rf diff/test/test-*/.git && cd diff && git init --quiet && git add -A && git commit --quiet --no-verify --allow-empty -m 'WIP' && echo '\\n\\nCommitted most recent integration test output in the \"diff\" directory. Review the changes with \"cd diff && git diff HEAD\" or your preferred git diff viewer.'", From 5e29bcce5156194ff5e9de23e527e1e6f8513790 Mon Sep 17 00:00:00 2001 From: Zeev Manilovich Date: Sun, 28 Jun 2026 23:40:52 +0300 Subject: [PATCH 2/4] ci: run unit/integration/module-imports on PR, defer e2e to next PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The e2e suites are AVA with fixed sleep(10s) waits and fail fast on the first error. Against a freshly started PDP they hit a momentary ECONNREFUSED window right after the write burst (OPA reload), which kills the whole run even though the env, key, and policy sync are all healthy (/healthy passes). Scope the PR backend run to the suite that reliably passes — unit + integration + module-imports (what `yarn test` runs, the same set the publish workflow runs). The event-based, error-tolerant e2e lands in the stacked test-migration PR, which re-includes e2e in CI. Also add a PDP diagnostics step (docker logs + container state + /healthy) on backend-run failure so PDP connection errors, which surface with no HTTP response, are debuggable. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yaml | 22 ++++++++++++++++++++++ package.json | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 59d516c..86ff838 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -142,6 +142,28 @@ jobs: API_TIER: prod run: yarn test:ci:full + # Dump PDP diagnostics whenever the backend path fails. The SDK reports + # PDP connection errors with no HTTP response, so the PDP side is otherwise + # invisible; this captures container state and logs to tell a transient + # restart/readiness blip apart from a crashed/exited container. + - name: Dump PDP diagnostics on failure + if: ${{ failure() && env.RUN_BACKEND == 'true' }} + run: | + echo "::group::docker ps -a" + docker ps -a --filter name=pdp || true + echo "::endgroup::" + echo "::group::PDP container state" + docker inspect -f \ + 'status={{.State.Status}} restartCount={{.RestartCount}} exitCode={{.State.ExitCode}} oomKilled={{.State.OOMKilled}} startedAt={{.State.StartedAt}} finishedAt={{.State.FinishedAt}}' \ + pdp || true + echo "::endgroup::" + echo "::group::docker logs pdp" + docker logs pdp || true + echo "::endgroup::" + echo "::group::curl -sv http://localhost:7766/healthy" + curl -sv http://localhost:7766/healthy || true + echo "::endgroup::" + # ---------- No-backend path (forks / secret-less) ---------- - name: Run no-backend test suite if: env.RUN_BACKEND != 'true' diff --git a/package.json b/package.json index 18c0741..63ea4a2 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "test:e2e:rebac": "run-s build && ava --verbose build/tests/e2e/rebac.e2e.spec.js", "test:e2e:local_facts": "run-s build && ava --verbose build/tests/e2e/local_facts.e2e.spec.js", "test:ci:unit": "run-s test:unit test:module-imports", - "test:ci:full": "run-s test test:e2e:rbac test:e2e:rebac test:e2e:local_facts", + "test:ci:full": "run-s test", "check-cli": "run-s test diff-integration-tests check-integration-tests", "check-integration-tests": "run-s check-integration-test:*", "diff-integration-tests": "mkdir -p diff && rm -rf diff/test && cp -r test diff/test && rm -rf diff/test/test-*/.git && cd diff && git init --quiet && git add -A && git commit --quiet --no-verify --allow-empty -m 'WIP' && echo '\\n\\nCommitted most recent integration test output in the \"diff\" directory. Review the changes with \"cd diff && git diff HEAD\" or your preferred git diff viewer.'", From f5dffbf9e271128a18dd220930d61de81ec9b5ff Mon Sep 17 00:00:00 2001 From: Zeev Manilovich Date: Mon, 29 Jun 2026 03:46:33 +0300 Subject: [PATCH 3/4] ci: run on all pull requests, not only those targeting main Stacked PRs target feature branches, so a pull_request filter of branches:[main] meant they never ran CI. Drop the base-branch filter so every PR is tested regardless of base. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 86ff838..5d9b677 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,8 +3,8 @@ name: CI on: push: branches: [ main ] + # Run on every PR regardless of base branch so stacked PRs get CI too. pull_request: - branches: [ main ] concurrency: group: ${{ github.workflow }}-${{ github.ref }} From fc2529baa7ba9a14fa06321e957da8f3975e4f20 Mon Sep 17 00:00:00 2001 From: Zeev Manilovich Date: Mon, 29 Jun 2026 03:55:26 +0300 Subject: [PATCH 4/4] ci: pin PDP connection to IPv4 (127.0.0.1) in the backend test run Node resolves `localhost` to ::1 (IPv6) first, but the GitHub runner's Docker IPv6 port publish refuses connections, so e2e permit.check() calls hit ECONNREFUSED even though the PDP is healthy on IPv4 (curl /healthy returns 200). Set PDP_URL to http://127.0.0.1:7766 so the SDK uses the working IPv4 path, and pin the readiness probe to 127.0.0.1 too. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5d9b677..1481851 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -124,7 +124,7 @@ jobs: if: env.RUN_BACKEND == 'true' run: | for i in $(seq 1 60); do - if curl -sf http://localhost:7766/healthy >/dev/null; then + if curl -sf http://127.0.0.1:7766/healthy >/dev/null; then echo "PDP ready after ${i} attempt(s)" exit 0 fi @@ -140,6 +140,10 @@ jobs: PDP_API_KEY: ${{ env.ENV_API_KEY }} PERMIT_API_KEY: ${{ env.ENV_API_KEY }} API_TIER: prod + # Force IPv4: Node resolves `localhost` to ::1 first, but the runner's + # Docker IPv6 publish refuses connections, so PDP checks would hit + # ECONNREFUSED. 127.0.0.1 pins the working IPv4 path. + PDP_URL: http://127.0.0.1:7766 run: yarn test:ci:full # Dump PDP diagnostics whenever the backend path fails. The SDK reports