From c335f6e017f1e912b9ec631f01214316a2ddfb1a Mon Sep 17 00:00:00 2001 From: Kaushik Date: Thu, 20 Aug 2026 11:05:33 +1000 Subject: [PATCH 1/3] Add CI workflow with lint, tests, build and semgrep --- .github/workflows/ci.yml | 83 ++++++++++++++++++++++++++++++++++++++++ .gitignore | 1 + 2 files changed, 84 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..492e12e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,83 @@ +# CI - the gate everything else keys off. No Claude, no tokens. +# Keep the workflow name "CI" and the job ids "ci" and "semgrep": branch +# protection requires both checks, and the review workflow triggers on +# workflow_run of the workflow named "CI". +# +# Lint / Test / Build below are this repo's real commands, verified locally +# 2026-08-20 before this file was written: +# Lint = frontend eslint -> 14 errors (pre-existing, see PR) +# Test = backend unittest, 23 tests -> passes (1 skipped: the live Gemini +# call, gated behind NUTRI_LIVE, which needs a paid key) +# Build = frontend vite build -> passes +# The project block in CLAUDE.md still says "no test suite yet" and gives +# py_compile as the backend check. That is stale - backend/tests/ exists and +# runs - so the real suite is wired here instead. +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + # --- Python (backend/) ------------------------------------------------ + - uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + - name: Install backend + run: | + uv venv .venv --python 3.11 + uv pip install -r backend/requirements.txt + + # --- Node (frontend/) ------------------------------------------------- + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: frontend/package-lock.json + - name: Install frontend + working-directory: frontend + run: npm ci + + # --- The three commands ---------------------------------------------- + # `if: always()` on Test and Build is deliberate. Lint is red today (14 + # pre-existing errors), and without it the job would stop at Lint and + # tell us nothing about whether Test and Build pass in CI. All three run, + # the job still fails, and one PR shows the complete picture instead of + # three red cycles. Remove it once main is green if you prefer fail-fast. + - name: Lint + working-directory: frontend + run: npm run lint + + - name: Test + if: always() + run: .venv/bin/python -m unittest discover -s backend/tests -t . -v + + - name: Build + if: always() + working-directory: frontend + run: npm run build + + semgrep: + runs-on: ubuntu-latest + container: + image: semgrep/semgrep + steps: + - uses: actions/checkout@v6 + # --config auto picks the registry rulesets for the languages present. + # --error fails the job on findings. This repo has never been scanned, so + # the first run is a discovery run: expect findings, triage before + # requiring this check in branch protection. + - run: semgrep scan --config auto --error diff --git a/.gitignore b/.gitignore index 95b8fac..da81477 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ !/SECURITY.md !/backend/ !/frontend/ +!/.github/ # 3) Inside the allowed dirs, still keep secrets, deps and build output out. # (.env.example is intentionally NOT matched by **/.env, so it stays tracked.) From 27b8f92e635275318a7076e350885b195ef30b0a Mon Sep 17 00:00:00 2001 From: Kaushik Date: Thu, 20 Aug 2026 11:21:28 +1000 Subject: [PATCH 2/3] Pin action SHAs and semgrep rulesets, add the comment-only responder --- .github/workflows/ci.yml | 28 ++++++++++------ .github/workflows/claude.yml | 62 ++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 492e12e..93a970d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,10 +30,10 @@ jobs: ci: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 # --- Python (backend/) ------------------------------------------------ - - uses: astral-sh/setup-uv@v6 + - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 with: enable-cache: true - name: Install backend @@ -42,7 +42,7 @@ jobs: uv pip install -r backend/requirements.txt # --- Node (frontend/) ------------------------------------------------- - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 cache: npm @@ -75,9 +75,19 @@ jobs: container: image: semgrep/semgrep steps: - - uses: actions/checkout@v6 - # --config auto picks the registry rulesets for the languages present. - # --error fails the job on findings. This repo has never been scanned, so - # the first run is a discovery run: expect findings, triage before - # requiring this check in branch protection. - - run: semgrep scan --config auto --error + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + # Rulesets are PINNED, not `--config auto`. `auto` resolves the registry + # at run time, so the rule set drifts underneath you: the same commit + # scanned twice can give different results, and a green main can go red + # overnight with no code change. That is disqualifying for a check that + # branch protection requires - the gate must mean the same thing tomorrow + # as it does today. Proven here on 2026-08-20: a local semgrep 1.173.0 + # run and this container's `auto` run returned different findings on the + # identical tree. + # Adding a ruleset is a deliberate edit to this line, and the PR that + # does it shows exactly what new class of finding was turned on. + - run: > + semgrep scan --error + --config p/python + --config p/javascript + --config p/secrets diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..5269c53 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,62 @@ +# Responder - answers @claude mentions on issues and PR comments. +# Comment-only: it reads the repo and the thread, answers and proposes patches +# IN COMMENTS. It never edits files, never runs git, never pushes, so Claude +# never appears as a contributor. Auth is CLAUDE_CODE_OAUTH_TOKEN from +# `claude setup-token` (Pro profile), set by the owner via the secret-slot +# skill; never an API key. +# +# FORK PRs: this is a public repo, and a pull_request from a fork gets a +# read-only GITHUB_TOKEN and NO repository secrets. CLAUDE_CODE_OAUTH_TOKEN is +# therefore absent there and the responder cannot run - an @claude mention from +# an outside contributor does nothing. That is the accepted trade. The "fix" +# would be pull_request_target, which runs WITH secrets against untrusted fork +# code; that is a credential-theft primitive, not a feature, and is refused. +# CI and semgrep still run on fork PRs - they need no secrets. +# +# Action refs are pinned to 40-char commit SHAs, not tags: a tag can be +# silently repointed by its owner (the trivy-action and kics-github-action +# compromises did exactly that). semgrep enforces this in the CI workflow. +name: Claude + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + pull_request_review: + types: [submitted] + issues: + types: [opened] + +jobs: + respond: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read # read only: the allowlist below forbids every write path anyway + pull-requests: write # post and update comments + issues: write + id-token: write # required by the action + actions: read # lets it read CI results when asked "why is CI red" + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + fetch-depth: 1 + + - uses: anthropics/claude-code-action@5ee796a55f92566ecd7e39d70dd613abcbea0d7c # v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # Pinned: cheap model, three turns, comment-only. + # The action's tag mode allows file edits and git add/commit/push by + # default; the deny list below switches all of that off. Deny rules + # beat allow rules in Claude Code, so this holds even if the action + # adds tools later. + claude_args: | + --model claude-sonnet-5 + --max-turns 3 + --disallowedTools "Edit,MultiEdit,Write,NotebookEdit,Bash,mcp__github_file_ops__commit_files,mcp__github_file_ops__delete_files" + --append-system-prompt "You answer in comments only. Never create, edit, commit or push files. When a code change is needed, propose it as a fenced diff in your comment and say the owner applies it. Keep answers short and plain." From 060d6a952f539f26c98790c1c3e45c12172251e2 Mon Sep 17 00:00:00 2001 From: Kaushik Date: Thu, 20 Aug 2026 12:22:30 +1000 Subject: [PATCH 3/3] Drop the responder workflow; CodeRabbit covers PR review --- .github/workflows/claude.yml | 62 ------------------------------------ 1 file changed, 62 deletions(-) delete mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index 5269c53..0000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,62 +0,0 @@ -# Responder - answers @claude mentions on issues and PR comments. -# Comment-only: it reads the repo and the thread, answers and proposes patches -# IN COMMENTS. It never edits files, never runs git, never pushes, so Claude -# never appears as a contributor. Auth is CLAUDE_CODE_OAUTH_TOKEN from -# `claude setup-token` (Pro profile), set by the owner via the secret-slot -# skill; never an API key. -# -# FORK PRs: this is a public repo, and a pull_request from a fork gets a -# read-only GITHUB_TOKEN and NO repository secrets. CLAUDE_CODE_OAUTH_TOKEN is -# therefore absent there and the responder cannot run - an @claude mention from -# an outside contributor does nothing. That is the accepted trade. The "fix" -# would be pull_request_target, which runs WITH secrets against untrusted fork -# code; that is a credential-theft primitive, not a feature, and is refused. -# CI and semgrep still run on fork PRs - they need no secrets. -# -# Action refs are pinned to 40-char commit SHAs, not tags: a tag can be -# silently repointed by its owner (the trivy-action and kics-github-action -# compromises did exactly that). semgrep enforces this in the CI workflow. -name: Claude - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - pull_request_review: - types: [submitted] - issues: - types: [opened] - -jobs: - respond: - if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - runs-on: ubuntu-latest - permissions: - contents: read # read only: the allowlist below forbids every write path anyway - pull-requests: write # post and update comments - issues: write - id-token: write # required by the action - actions: read # lets it read CI results when asked "why is CI red" - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - with: - fetch-depth: 1 - - - uses: anthropics/claude-code-action@5ee796a55f92566ecd7e39d70dd613abcbea0d7c # v1 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - # Pinned: cheap model, three turns, comment-only. - # The action's tag mode allows file edits and git add/commit/push by - # default; the deny list below switches all of that off. Deny rules - # beat allow rules in Claude Code, so this holds even if the action - # adds tools later. - claude_args: | - --model claude-sonnet-5 - --max-turns 3 - --disallowedTools "Edit,MultiEdit,Write,NotebookEdit,Bash,mcp__github_file_ops__commit_files,mcp__github_file_ops__delete_files" - --append-system-prompt "You answer in comments only. Never create, edit, commit or push files. When a code change is needed, propose it as a fenced diff in your comment and say the owner applies it. Keep answers short and plain."