diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 46a6ce7..1481851 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,38 +3,184 @@ name: CI on: push: branches: [ main ] + # Run on every PR regardless of base branch so stacked PRs get CI too. 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://127.0.0.1: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 + # 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 + # 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' + 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..63ea4a2 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", "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.'",