Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ jobs:
run: pip install -r requirements.txt coverage
- name: Install Playwright
run: playwright install --with-deps chromium
- name: Collect static files
run: python manage.py collectstatic --noinput --verbosity 0
- name: Run E2E tests
run: |
export SECRET_KEY=$(python -c 'import secrets; print(secrets.token_hex(50))')
Expand Down
124 changes: 124 additions & 0 deletions .github/workflows/opencode.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: opencode

on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]

jobs:
opencode:
if: |
github.event.comment.user.type != 'Bot' &&
(contains(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, '/opencode'))
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
id-token: write
actions: write
contents: write
steps:
- name: job-start
run: echo "job-start:$(date -u +%Y-%m-%dT%H:%M:%SZ) sha:${{ github.sha }}"

- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 1
persist-credentials: false

- name: Configure git identity
run: |
git config --global user.name "opencode-agent[bot]"
git config --global user.email "opencode-agent[bot]@users.noreply.github.com"

# Install the same commit-msg hook humans get via scripts/commit-msg.sh
# (see .pre-commit-config.yaml), so opencode's own `git commit` gets
# rejected with the actual regex+reason inline and can retry, instead
# of only finding out after pushing. Only the commit-msg hook type --
# not the full pre-commit stage, which runs unrelated file-content
# checks that could block a commit for issues in files opencode
# didn't touch.
- name: Install commit-msg hook
run: |
pip install --quiet pre-commit
pre-commit install --hook-type commit-msg

- name: Run OpenCode
uses: anomalyco/opencode/github@31406ccc51b4bd2a4e1e086b2bcaa5f7f804f26d # v1.18.18
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
with:
# Model routing by comment tag: default is the free Zen model;
# add [security] or [feature] anywhere in the comment to route to
# NVIDIA NIM's nemotron-3-ultra (1M context) instead, e.g.
# "/oc [security] fix this CVE". build.nvidia.com labels this
# endpoint "Free Endpoint" in its own product metadata — do not
# trust models.dev's cost field for NVIDIA NIM models without
# cross-checking build.nvidia.com directly, it has been wrong here.
# The bracket tag is required for THIS routing expression to match
# ("/oc-security ..." does not contain "[security]" so it silently
# falls through to the default model below) — confirmed live on a
# sibling repo (fawkes, issue #1587): "/oc-security ..." does still
# trigger the action, just without escalating the model.
#
# Model string is doubled ("nvidia/nvidia/...") deliberately, not a
# typo: NVIDIA-published models on NVIDIA's own NIM catalog carry
# "nvidia/" as part of the model's own org/name identifier (an
# org/model convention, same as e.g. "microsoft/phi-4-..." models
# hosted on the same catalog) -- confirmed against models.dev's raw
# JSON directly (data['nvidia']['models'] keys). opencode splits on
# the FIRST "/" only for provider vs. model, so referencing this
# specific model needs provider "nvidia" + catalog key
# "nvidia/nemotron-3-ultra-550b-a55b" == "nvidia/nvidia/...".
model: ${{ (contains(github.event.comment.body, '[security]') || contains(github.event.comment.body, '[feature]')) && 'nvidia/nvidia/nemotron-3-ultra-550b-a55b' || 'opencode/deepseek-v4-flash-free' }}

# opencode does not reliably follow Conventional Commits format even
# with documentation in place (confirmed on a sibling repo, fawkes,
# PRs #1619 and #1625). Documentation alone isn't reliable enough;
# enforce mechanically as a backstop in case the commit-msg hook above
# doesn't catch it (e.g. opencode retries and still gets it wrong).
- name: Normalize commit message if non-compliant
if: always()
run: |
set -euo pipefail
if ! git rev-parse --verify HEAD >/dev/null 2>&1; then
echo "No commits to check."
exit 0
fi
if [ "$(git rev-parse HEAD)" = "${{ github.sha }}" ]; then
echo "No new commits; nothing to normalize."
exit 0
fi
subject="$(git log -1 --format=%s)"
if echo "$subject" | grep -qE '^(feat|fix|docs|style|refactor|test|chore|ci|perf|build|revert)(\(.+\))?: .{1,72}$'; then
echo "Commit message already compliant: $subject"
exit 0
fi
echo "Non-compliant commit subject, rewriting: $subject"
desc="$subject"
if [ "${#desc}" -gt 69 ]; then
desc="${desc:0:69}..."
fi
body="$(git log -1 --format=%b)"
{
echo "fix: ${desc}"
echo
echo "Original subject: ${subject}"
if [ -n "$body" ]; then
echo
echo "$body"
fi
} > /tmp/normalized_commit_msg.txt
git commit --amend -F /tmp/normalized_commit_msg.txt
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
git push --force-with-lease
env:
GH_TOKEN: ${{ github.token }}

- name: job-finish
if: always()
run: echo "job-finish:$(date -u +%Y-%m-%dT%H:%M:%SZ)"
48 changes: 48 additions & 0 deletions .opencode/skills/commit-message-format/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
name: commit-message-format
description: Use before running `git commit` on this repo — the subject line format is enforced by CI and a bad one blocks the whole PR from merging.
---

# Commit Message Format

This repo's `ci-commit-lint.yml` check runs this exact regex against every
commit in a PR (not just the PR title, not just the latest commit — every
one):

```
^(feat|fix|docs|style|refactor|test|chore|ci|perf|build|revert)(\(.+\))?: .{1,72}$
```

One commit that doesn't match fails the whole PR's Commit Lint check, even
if every other commit and the actual code change is fine.

## Before running `git commit`

1. Pick a `type`: `feat fix docs style refactor test chore ci perf build revert`.
2. Optionally add `(scope)` — a short parenthesized area, e.g. `(security)`, `(ci)`.
3. Write the description in **1–72 characters**. Count it — don't guess. A
precise-but-long subject ("fix(ci): correct the model provider id, wire
up multiple new provider keys, and add debugging skills") still fails.
Move detail into the commit body below the subject line instead.

## Quick self-check

```bash
python3 -c "
import re, sys
regex = re.compile(r'^(feat|fix|docs|style|refactor|test|chore|ci|perf|build|revert)(\(.+\))?: .{1,72}\$')
subject = sys.argv[1]
print(len(subject), bool(regex.match(subject)))
" "type(scope): your subject here"
```

If it prints `False`, shorten the description (or fix the type) before
committing — don't push and let CI catch it after the fact.

## If a commit was already pushed with a bad subject

The message must be reworded and the branch force-pushed — there's no way to
fix a commit's message without rewriting its hash. Reuse the exact tree
(`git commit-tree <original-tree> -p <parent> -F <new-message-file>`) so the
code diff is provably unchanged, verify the new subject against the regex
above, then force-push with `--force-with-lease`.
37 changes: 37 additions & 0 deletions .opencode/skills/root-cause-debugging/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
name: root-cause-debugging
description: Use before proposing any fix for a failing test, CI job, security scan finding, or bug report. Front-load root-cause investigation over guessing.
---

# Root-Cause Debugging

**No fix without root-cause investigation first.** A fix that only makes the
symptom disappear (widening a try/except, bumping a timeout, pinning around
an error instead of understanding it) is not a fix — it is deferred rework,
and on this repo it gets caught in review.

## Before touching code

1. Read the full error/log output, not just the last line — stack traces and
CI job logs usually name the exact failing line and cause.
2. Reproduce it. If you can't reproduce it, gather more evidence before
guessing (add logging, run the failing step in isolation) — don't ship a
fix for a failure you can't trigger.
3. Check what changed: `git log`/`git blame`/`git diff` on the affected file
and its recent commits.
4. For multi-component failures (CI → build → deploy, API → service → DB),
trace which layer actually breaks before fixing any of them — add a log
line at each boundary if needed.
5. Grep every caller of the function/config you're about to touch. The
root-cause fix is usually the smaller diff: one guard in the shared
function beats patching every call site that happens to hit it.

## Fixing

- State the root cause in one sentence before writing the fix.
- Make the smallest change that addresses that cause — no bundled
refactors, no "while I'm here" cleanup.
- If a fix doesn't resolve the issue, don't stack a second fix on top —
re-open the investigation. Three failed fixes in a row on the same issue
means the architecture is wrong, not that you need a fourth attempt —
stop and say so in the PR/issue instead of continuing to guess.
48 changes: 48 additions & 0 deletions .opencode/skills/security-fix-guardrails/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
name: security-fix-guardrails
description: Use before implementing any issue labeled type-security or comp-security, or any fix touching auth, RBAC, secrets, IAM, security groups, network policy, or crypto.
---

# Security Fix Guardrails

Most `type-security` issues in this repo are mechanical and safe to fix
directly: add a missing `securityContext` block, bump a dependency to a
patched version, add encryption/`readOnlyRootFilesystem` to a Terraform or
K8s resource. Fix those the same way as any other issue — root cause, small
diff, verify.

A smaller set need a human in the loop before or instead of an auto-fix.
Route to that path whenever the issue or the fix touches:

- Authentication, authorization, or RBAC logic
- Secrets, credentials, tokens, or any `ConfigMap`/`Secret` handling
- IAM policies, security groups, network ingress/egress rules
- Cryptographic code (hashing, signing, encryption implementations)
- Anything already flagged in the issue as "inspect before fixing" or similar

For these: still do the investigation and propose the fix, but say so
explicitly in the PR description or comment — call out that it's a
security-sensitive change needing manual review rather than opening it as if
it were routine. Never merge a security-sensitive change yourself even if
`/oc` has write access to open PRs.

## Repo-specific rules that already cover this

- `AGENTS.md` §9: any `infra/` change requires a second human reviewer —
applies to every Terraform security fix (SG rules, IAM, encryption), not
just infra changes in general.
- `AGENTS.md` §12: "Auth, RBAC, secrets, or any infra-touching change" is
security-sensitive by default.
- Never widen a `try`/`except` or add a broad exception handler to make a
security scanner finding disappear — that hides the finding, it doesn't
fix it. If a scanner flag looks like a false positive, say so and explain
why instead of suppressing it.

## Verifying a security fix specifically

Beyond the normal test/lint run (see `verify-before-done`):
- For a CVE/dependency bump: confirm the new version range actually excludes
the vulnerable version (check the advisory, don't just bump and assume).
- For a K8s/Terraform hardening change: run `helm template`/`terraform plan`
and confirm the rendered output actually has the intended field — a
misplaced YAML key silently no-ops the fix.
48 changes: 48 additions & 0 deletions .opencode/skills/tdd-workflow/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
name: tdd-workflow
description: Use before implementing any bug fix, new function, or config change — write and run a failing test first, confirm it fails for the right reason, then implement to pass.
---

# Test-Driven: Red, Then Green

**Never write implementation before a test that proves it's needed.** This
applies to bug fixes, new functions, and infra/config changes alike — not
just application code.

## The loop

1. **Write the test first.** For a bug: reproduce it. For new behavior: assert
the contract you're about to build. For infra/config: the check that
proves the config has the intended effect (see commands below).
2. **Run it. Confirm it fails — and read *why*.** A test that fails for the
wrong reason (typo, missing import, wrong file path) proves nothing. Fix
the test setup until the failure is the one you actually expect, before
touching implementation.
3. **Write the minimum code to pass.** No extra scope, no "while I'm here"
changes — see the surgical-changes rule in `AGENTS.md`.
4. **Run it again. Confirm green.** Then run the broader suite for that area,
not just the one test — see `verify-before-done`.

Never write the test after the code to match what the code already does —
that only proves the code does what it does, not that it's correct.

## Commands by area (this repo)

| Area | Command |
|---|---|
| Python | `pytest <path>` (or `pytest --cov=. --cov-report=term-missing` for coverage) |
| Shell scripts | `make test-bats` (single file: `bats tests/<file>.bats`) |
| Terraform modules | `make terraform-test` (unit-level, no resources deployed) |
| BDD / acceptance | `make test-bdd COMPONENT=<name>` |
| Everything | `make test-unit` then `make test-all` before opening a PR |

For a Kubernetes/Helm config change with no test framework, the equivalent
red/green check is `helm template` / `kubeconform -strict` before vs. after —
render it first, confirm the field you're adding is absent (red), add it,
confirm it's present in the rendered output (green).

## When there's no existing test framework for the file

Still do it: a small standalone script (`scripts/`) or a one-off assertion
block is enough — see `AGENTS.md` §7's per-language required checks for what
already exists to build on before inventing a new pattern.
34 changes: 34 additions & 0 deletions .opencode/skills/verify-before-done/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
name: verify-before-done
description: Use before claiming a fix works, tests pass, or a PR is ready — before committing, pushing, or replying "fixed" on an issue/PR comment.
---

# Verify Before Claiming Done

**Evidence before claims.** Don't tell a human (or write in a commit/PR/issue
comment) that something is fixed, passing, or ready unless you just ran the
command that proves it, in this session, and read its output.

## Gate before any completion claim

1. Identify the exact command that proves the claim (test suite, linter,
`terraform validate`, `helm lint`, the specific failing CI job re-run).
2. Run it fresh — not "ran it earlier," not "should still pass."
3. Read the full output and exit code, not just the last line.
4. Only then state the claim, citing what you ran and its result.

| Claim | Needs | Not enough |
|---|---|---|
| Tests pass | Fresh test run, 0 failures | "Should pass now" |
| Bug fixed | Test for the original symptom, now green | Code changed, assumed fixed |
| Lint/build clean | Fresh linter/build output, exit 0 | Linter passed (≠ compiler/build) |
| PR ready | Checklist against the issue's acceptance criteria | Tests passing alone |

## On this repo specifically

- `make lint` output goes in the PR body per `AGENTS.md` §8 — run it, don't
paraphrase it.
- Never delete or weaken a test to make CI green; if a test seems wrong, say
so and ask, don't route around it.
- If you can't run something in this environment (e.g. no live cluster), say
exactly that instead of asserting success.
Loading
Loading