test: comprehensive unit + e2e coverage across SDK (stacked on #132) #49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [ main ] | |
| # Run on every PR regardless of base branch so stacked PRs get CI too. | |
| pull_request: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| 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: [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@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 |