diff --git a/.claude/commands/address-pr-feedback.md b/.claude/commands/address-pr-feedback.md new file mode 100644 index 0000000..cbe8ed6 --- /dev/null +++ b/.claude/commands/address-pr-feedback.md @@ -0,0 +1,116 @@ +# Address PR Feedback + +Fetch and address all review feedback on the current PR. + +## Instructions + +### 1. Identify the PR + +```bash +gh pr view --json number,url,state --jq '{number, url, state}' +``` + +If no PR exists for the current branch, abort with a message. + +### 2. Fetch review comments + +```bash +# Get all review comments (inline) +gh api "repos/{owner}/{repo}/pulls/{pr}/comments" --paginate \ + --jq '.[] | select(.in_reply_to_id == null) | {id, author: .user.login, path, line, body: (.body[:300])}' + +# Get IDs already replied to +gh api "repos/{owner}/{repo}/pulls/{pr}/comments" --paginate \ + --jq '[.[] | select(.in_reply_to_id != null) | .in_reply_to_id] | unique' + +# Get general PR comments +gh pr view --json comments --jq '.comments[] | {author: .author.login, bodyPreview: (.body[:300])}' + +# Get reviews +gh api "repos/{owner}/{repo}/pulls/{pr}/reviews" --paginate \ + --jq '.[] | select(.body | length > 0) | {id, author: .user.login, state, body: (.body[:500])}' +``` + +Cross-reference to find **unreplied** comments. + +### 3. Identify actionable feedback + +For each comment, determine: +1. **Valid concern** — fix it +2. **False positive** — reply explaining why +3. **Stale** — code was already changed/removed since the comment was posted +4. **Ambiguous** — ask the user which direction to take + +### 4. Present decisions for approval + +**STOP and present a table** before making any changes: + +| # | Source | File | Line | Comment Summary | Decision | Rationale | +|---|--------|------|------|-----------------|----------|-----------| +| 1 | reviewer | `path/file.rego` | 42 | Brief summary | Fix / Dismiss / Stale | Why | + +**Wait for the user to approve** before proceeding. + +### 5. Address each item + +For valid concerns: +1. Read the file and understand the context +2. Apply the fix +3. Reply using the correct channel by source type: + ```bash + # Inline review comment + gh api "repos/{owner}/{repo}/pulls/{pr}/comments/{comment_id}/replies" \ + -X POST -f body="Fixed — " + ``` + ```bash + # General PR comment (issue comment on PR) + gh pr comment {pr} --body "Addressed: " + ``` + ```bash + # Review-body level feedback + gh pr review {pr} --comment --body "Addressed review feedback: " + ``` + +For false positives: +1. Reply using the matching channel (inline, general, or review): + ```bash + # Inline review comment + gh api "repos/{owner}/{repo}/pulls/{pr}/comments/{comment_id}/replies" \ + -X POST -f body="" + ``` + ```bash + # General PR comment + gh pr comment {pr} --body "" + ``` + ```bash + # Review-body level feedback + gh pr review {pr} --comment --body "" + ``` + +### 6. Run tests + +After all fixes are applied, run OPA tests for affected environments: + +```bash +opa test dev/ -v # if dev/ was changed +opa test stage/ -v # if stage/ was changed +opa test prod/ -v # if prod/ was changed +``` + +### 7. Commit and push + +If any code changes were made: + +```bash +git add -A +git commit -m "fix: address PR review feedback" +git push +``` + +### 8. Report summary + +- **Fixed**: List of issues fixed +- **Dismissed**: False positives with reasoning +- **Stale**: Comments on already-changed code +- **Needs user input**: Ambiguous items requiring user decision +- **Tests**: Pass/fail status diff --git a/.claude/commands/commit.md b/.claude/commands/commit.md new file mode 100644 index 0000000..9717ca6 --- /dev/null +++ b/.claude/commands/commit.md @@ -0,0 +1,41 @@ +# Commit Changes + +Stage and commit the current changes with a well-crafted message. + +## Instructions + +When activated, commit the current working tree changes: + +1. **Sync with remote**: + - Run `git fetch origin main` to get latest upstream + - Run `git log HEAD..origin/main --oneline` to check if main has moved ahead + - If it has, warn the user but don't rebase automatically + +2. **Ensure we're not on main**: + - Run `git branch --show-current` + - If on `main`, create a new feature branch: + - Look at the staged/unstaged changes to infer a branch name + - Run `git checkout -b feat/` + - Inform the user of the new branch name + +3. **Review changes**: + - Run `git diff --stat` and `git diff --staged --stat` to see what's changed + - If nothing is staged, run `git add -A` to stage everything + - Run `git diff --staged --stat` to confirm what will be committed + +4. **Generate commit message**: + - Use conventional commit format: `type: short description` + - Types: `feat`, `fix`, `refactor`, `chore`, `docs`, `test` + - Scope is optional but useful for env-specific changes: `feat(dev): ...`, `fix(prod): ...` + - If the change is substantial, add a body paragraph separated by a blank line + - Body should explain **what** changed and **why**, not how (the diff shows how) + - Keep the subject line under 72 characters + +5. **Commit**: + ```bash + git commit -m "" + ``` + +6. **Report** the commit hash and summary to the user + +If the user provides arguments (e.g., `/commit "fix(prod): tighten bot threshold"`), use that as the commit message instead of generating one. diff --git a/.claude/commands/handoff.md b/.claude/commands/handoff.md new file mode 100644 index 0000000..2f5d17f --- /dev/null +++ b/.claude/commands/handoff.md @@ -0,0 +1,91 @@ +--- +name: handoff +description: Structured handoff between agents +--- + +# Session Handoff + +Generate a structured handoff for the next session. Output must be self-contained — the next agent should be able to resume without reading this session's history. + +## Instructions + +### 1. Gather current state + +Run these in parallel: + +```bash +git branch --show-current +git log --oneline -5 +git status --short +gh pr view --json number,url,state,title 2>/dev/null || echo "No PR for this branch" +``` + +### 2. Produce the handoff document + +Output the following sections in order: + +--- + +## Handoff — {feature or change description} + +**Date:** {today} +**Branch:** `{current branch}` +**PR:** {PR URL and state, or "none"} + +--- + +### Completed this session + +Table format. Be specific — what was done, where, and what artifact proves it. + +| Item | Status | Artifact | +|------|--------|----------| +| ... | Done | PR #NNN / commit SHA | + +--- + +### Current state + +- What branch is active and whether it's clean or has uncommitted work +- Open PRs and their review status +- Which environments have been tested + +--- + +### Next session: what to do + +Be concrete. Name the files, the policies, the commands. The next agent should be able to start without asking clarifying questions. + +**Immediate (< 30 min):** +- [ ] {specific action} + +**Main work:** +- Files: {list the key files to read first} +- Environments affected: {dev, stage, prod} +- Test command: {e.g., `opa test dev/ -v`} + +--- + +### Decisions made this session + +Any non-obvious choices that the next agent needs to know about. Keep it brief — one bullet per decision, include the rationale. + +- {decision}: {why} + +--- + +### Open questions / blockers + +What is unresolved that may block next session. If none, omit this section. + +--- + +### Key files for next session + +List the files the next agent should read first, in priority order. Use relative paths. + +## Notes + +- Do NOT summarize the whole session history — focus on what the next session needs +- If the branch has uncommitted changes, say so explicitly with what they are +- If tests were left failing, say so and why — do not hide failures diff --git a/.claude/commands/pr.md b/.claude/commands/pr.md new file mode 100644 index 0000000..42c1947 --- /dev/null +++ b/.claude/commands/pr.md @@ -0,0 +1,38 @@ +# Create Pull Request + +Create a pull request for the current branch. + +## Instructions + +When activated, create a pull request for the current branch: + +1. **Verify branch state**: + - Run `git branch --show-current` to get the current branch name + - Ensure we're not on `main` (abort if so) + - Run `git log main..HEAD --oneline` to see commits to include + +2. **Push the branch** (if not already pushed): + - Run `git push -u origin ` + +3. **Determine which environments are affected**: + - Run `git diff main..HEAD --stat` to see changed files + - Identify which env folders (`dev/`, `stage/`, `prod/`) are touched + - This informs the PR title and description + +4. **Generate PR title and body**: + - Title: Conventional commit format (e.g., `feat(dev): add loyalty tier rate limiting`) + - Use env scope when changes target specific environments: `(dev)`, `(stage)`, `(prod)`, or omit for cross-env changes + - Body should include: + - **Summary**: Brief description of what this PR does + - **Environments affected**: Which env folders have changes + - **Changes**: Bullet list of key policy changes + - **Testing**: OPA test commands to verify (e.g., `opa test dev/ -v`) + +5. **Create the PR**: + ```bash + gh pr create --title "" --body "<body>" --base main --assignee @me + ``` + +6. **Report the PR URL** to the user + +If the user provides arguments (e.g., `/pr "Custom title"`), use that as the PR title instead of generating one. diff --git a/.claude/commands/review.md b/.claude/commands/review.md new file mode 100644 index 0000000..c24d62e --- /dev/null +++ b/.claude/commands/review.md @@ -0,0 +1,70 @@ +# Code Review Branch Commits + +Review all commits on the current branch since diverging from main. + +## Prerequisites + +**IMPORTANT**: Before starting the review, check if this is a fresh context/session: +- If there is prior conversation history in this session (e.g., you helped write the code being reviewed), STOP immediately +- Inform the user: "Code reviews should be done in a fresh context to avoid bias. Please start a new Claude Code session and run /review there." +- A reviewer should not be the same "person" who wrote the code + +## Instructions + +When activated, perform a thorough review of OPA/Rego policy changes: + +1. **Gather changes**: + - Run `git log main..HEAD --oneline` to see all commits on this branch + - Run `git diff main..HEAD` to see all changes + - For each file changed, read enough context to understand the changes + +2. **Run OPA tests**: + - Determine which environments were modified from the diff + - Run `opa test <env>/ -v` for each affected environment + - All tests must pass before proceeding + +3. **Review the changes** across these dimensions: + + **Policy correctness**: + - Are allow/deny rules logically correct? + - Are there unintended overlaps or gaps in rules? + - Could any rule combination produce unexpected results? + - Are default deny semantics preserved? + + **Environment consistency**: + - If a policy was changed in one env, should it also change in others? + - Are intentional env differences documented (see README table)? + - Do data.json files match the expected schema for their env? + + **Test coverage**: + - Do new/changed rules have corresponding test cases? + - Are edge cases covered (empty input, missing fields, boundary values)? + - Are both allow and deny paths tested? + + **Security**: + - Are authorization boundaries correct (role escalation, cross-tenant access)? + - Are there overly permissive rules? + - Is input validation sufficient? + + **Code quality**: + - Are package names and rule names consistent with conventions? + - Is there duplicated logic that should be in `shared/`? + - Are helper rules used appropriately? + +4. **Present the review** with severity-rated findings: + + | # | Severity | File | Finding | Recommendation | + |---|----------|------|---------|----------------| + | 1 | 🔴 Critical | path/to/file.rego | ... | ... | + | 2 | 🟡 Warning | path/to/file.rego | ... | ... | + | 3 | 🔵 Suggestion | path/to/file.rego | ... | ... | + +## Follow-up + +After presenting the review, present a **fix plan table** for the user to approve before making any changes: + +| # | File | Issue | Proposed Action | +|---|------|-------|-----------------| +| 1 | path/to/file.rego | Brief description | Fix / Skip / Ask | + +**Wait for the user to approve the plan** before applying fixes. Run `opa test` for affected environments after all fixes are applied. diff --git a/.github/actions/rego-lint/action.yml b/.github/actions/rego-lint/action.yml new file mode 100644 index 0000000..020e18c --- /dev/null +++ b/.github/actions/rego-lint/action.yml @@ -0,0 +1,40 @@ +name: Rego Lint (Regal) +description: Lint changed Rego files using Regal + +inputs: + regal-version: + description: Regal version to install + required: false + default: v0.39.0 + +runs: + using: composite + steps: + - uses: open-policy-agent/setup-regal@v1 + with: + version: ${{ inputs.regal-version }} + + - name: Get changed Rego files + id: changed + shell: bash + run: | + if [[ "${{ github.event_name }}" == "pull_request" && -n "${{ github.base_ref }}" ]]; then + FILES=$(git diff --name-only --diff-filter=ACMR "origin/${{ github.base_ref }}...HEAD" -- '*.rego') + elif [[ -n "${{ github.event.before }}" && "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]]; then + FILES=$(git diff --name-only --diff-filter=ACMR "${{ github.event.before }}...HEAD" -- '*.rego') + else + # Fallback for manual/initial runs — lint all Rego files + FILES=$(git ls-files '*.rego') + fi + if [ -z "$FILES" ]; then + echo "has_files=false" >> "$GITHUB_OUTPUT" + else + DIRS=$(printf '%s\n' "$FILES" | while IFS= read -r f; do dirname "$f"; done | sort -u | tr '\n' ' ') + echo "has_files=true" >> "$GITHUB_OUTPUT" + echo "dirs=$DIRS" >> "$GITHUB_OUTPUT" + fi + + - name: Lint changed Rego files + if: steps.changed.outputs.has_files == 'true' + shell: bash + run: regal lint "${{ steps.changed.outputs.dirs }}" diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000..c5d45bd --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,43 @@ +# Deploys dev policies to EnforceAuth when dev/ changes on main. +# Entity ID configured in EA_ENTITY_ID secret (dev environment). +name: "Deploy: dev" + +on: + workflow_dispatch: + push: + branches: [main] + paths: + - "dev/**" + +permissions: + contents: read + +jobs: + lint: + name: Lint dev policies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: ./.github/actions/rego-lint + + deploy: + name: Deploy via EnforceAuth + needs: lint + runs-on: ubuntu-latest + environment: dev + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Deploy Policy Bundle + uses: EnforceAuth/deploy-action@v1 + with: + entity-id: ${{ secrets.EA_ENTITY_ID }} + api-url: ${{ vars.EA_API_URL }} + environment: dev + wait-for-completion: true + timeout-minutes: 10 diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 0000000..ba28ca9 --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,86 @@ +# Deploys prod policies to EnforceAuth when prod/ changes on main. +# Entity ID configured in EA_ENTITY_ID secret (production environment). +# +# Safety controls: +# - Dry-run validation before deploy +# - GitHub environment protection rules (configure required reviewers +# on the "production" environment in repo settings) +# - Concurrency lock prevents parallel production deploys +name: "Deploy: prod" + +on: + workflow_dispatch: + push: + branches: [main] + paths: + - "prod/**" + +concurrency: + group: deploy-production + cancel-in-progress: false + +permissions: + contents: read + +jobs: + lint: + name: Lint prod policies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + - uses: ./.github/actions/rego-lint + + dry-run: + name: Validate (Dry Run) + needs: lint + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Dry-run deployment + id: dry-run + uses: EnforceAuth/deploy-action@v1 + with: + entity-id: ${{ secrets.EA_ENTITY_ID }} + api-url: ${{ vars.EA_API_URL }} + environment: production + dry-run: true + + - name: Print dry-run summary + if: always() + run: | + echo "### Dry-Run Summary" >> "$GITHUB_STEP_SUMMARY" + if [ "${{ steps.dry-run.outcome }}" = "success" ]; then + echo "- **Status:** ${{ steps.dry-run.outputs.status }}" >> "$GITHUB_STEP_SUMMARY" + echo "- **Bundle Version:** ${{ steps.dry-run.outputs.bundle-version }}" >> "$GITHUB_STEP_SUMMARY" + else + echo "- **Status:** unknown (step failed)" >> "$GITHUB_STEP_SUMMARY" + echo "- **Bundle Version:** N/A" >> "$GITHUB_STEP_SUMMARY" + fi + + deploy: + name: Deploy via EnforceAuth + needs: dry-run + if: github.ref == 'refs/heads/main' && needs.dry-run.result == 'success' + runs-on: ubuntu-latest + environment: production + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Deploy Policy Bundle + uses: EnforceAuth/deploy-action@v1 + with: + entity-id: ${{ secrets.EA_ENTITY_ID }} + api-url: ${{ vars.EA_API_URL }} + environment: production + wait-for-completion: true + timeout-minutes: 10 diff --git a/.github/workflows/deploy-stage.yml b/.github/workflows/deploy-stage.yml new file mode 100644 index 0000000..2db17b7 --- /dev/null +++ b/.github/workflows/deploy-stage.yml @@ -0,0 +1,43 @@ +# Deploys stage policies to EnforceAuth when stage/ changes on main. +# Entity ID configured in EA_ENTITY_ID secret (stage environment). +name: "Deploy: stage" + +on: + workflow_dispatch: + push: + branches: [main] + paths: + - "stage/**" + +permissions: + contents: read + +jobs: + lint: + name: Lint stage policies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: ./.github/actions/rego-lint + + deploy: + name: Deploy via EnforceAuth + needs: lint + runs-on: ubuntu-latest + environment: stage + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Deploy Policy Bundle + uses: EnforceAuth/deploy-action@v1 + with: + entity-id: ${{ secrets.EA_ENTITY_ID }} + api-url: ${{ vars.EA_API_URL }} + environment: stage + wait-for-completion: true + timeout-minutes: 10 diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000..dc61a01 --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,17 @@ +# On pull requests, lint changed Rego files. + +name: PR Check + +on: + pull_request: + branches: [main] + +jobs: + lint: + name: Rego Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: ./.github/actions/rego-lint