From 2dd7e33aadf3095c74ea804d365b27a859b07744 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 13 Jun 2026 11:56:37 +0200 Subject: [PATCH] feat(sdd): add Gitea issue templates and SDD CI gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds .gitea/ISSUE_TEMPLATE/{feature,bug}.md (the feature template mirrors the EARS feature-spec) and .gitea/workflows/sdd-gate.yml — spec-lint, contract validation (Spectral), traceability check (all non-blocking during adoption), and a constitution-impact PR comment. Co-Authored-By: Claude Opus 4.8 --- .gitea/ISSUE_TEMPLATE/bug.md | 40 ++++++ .gitea/ISSUE_TEMPLATE/feature.md | 82 ++++++++++++ .gitea/workflows/sdd-gate.yml | 206 +++++++++++++++++++++++++++++++ 3 files changed, 328 insertions(+) create mode 100644 .gitea/ISSUE_TEMPLATE/bug.md create mode 100644 .gitea/ISSUE_TEMPLATE/feature.md create mode 100644 .gitea/workflows/sdd-gate.yml diff --git a/.gitea/ISSUE_TEMPLATE/bug.md b/.gitea/ISSUE_TEMPLATE/bug.md new file mode 100644 index 00000000..2805094d --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,40 @@ +--- +name: "Bug" +about: "Something is broken. Describe user-facing impact, not the technical cause." +title: " when " +labels: + - bug +assignees: [] +--- + + + +## What happens + + + +## Expected + + + +## Steps to reproduce + +1. +2. +3. + +## Originating requirement (if known) + + + +## Environment + + + +## Notes + + diff --git a/.gitea/ISSUE_TEMPLATE/feature.md b/.gitea/ISSUE_TEMPLATE/feature.md new file mode 100644 index 00000000..50cd8b5e --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,82 @@ +--- +name: "Feature (SDD spec)" +about: "Spec-driven feature request. Fill in EARS requirements before implementation starts." +title: "As a I want so " +labels: + - spec-required + - needs-review +assignees: [] +--- + + + +## Context & Why + + + +## User Journey + + + +## Requirements + + + +- **REQ-001** (Ubiquitous) — The `` shall ``. +- **REQ-002** (Event-driven) — When ``, the `` shall ``. +- **REQ-003** (State-driven) — While ``, the `` shall ``. +- **REQ-004** (Optional-feature) — Where ``, the `` shall ``. +- **REQ-005** (Unwanted-behavior) — If ``, then the `` shall ``. + +## Acceptance Criteria + + + +- **REQ-001** — . +- **REQ-002** — . + +## Out of Scope + +- + +## API / Contract Stub + +/api-contract.yaml. Name new paths/methods/status codes and the @RequirePermission on each mutating endpoint.> + +## Data Model Changes + + (verify on disk) + rollback note. "none" if not applicable.> + +## Security Considerations + + + +## Open Questions + + + +- [ ] — owner: + +## Traceability + +| REQ-ID | Task ID(s) | Test ID(s) | Status | +|---|---|---|---| +| REQ-001 | | | Planned | + + + +## Persona Review Results + +| Persona | Status | Key Findings | Resolved | +|---|---|---|---| +| Requirements Engineer | PENDING | | | +| Developer | PENDING | | | +| Security | PENDING | | | +| DevOps | PENDING | | | +| UI/UX | PENDING | | | +| Architect | PENDING | | | diff --git a/.gitea/workflows/sdd-gate.yml b/.gitea/workflows/sdd-gate.yml new file mode 100644 index 00000000..9b0483cc --- /dev/null +++ b/.gitea/workflows/sdd-gate.yml @@ -0,0 +1,206 @@ +name: SDD Gate + +# Spec-Driven Development quality gate. Runs on PRs and validates the SDD artifacts that +# the PR touches. The first three jobs are NON-BLOCKING for now (continue-on-error) so the +# team can adopt the workflow without CI immediately failing. +# +# TODO: flip spec-lint, contract-validate, and traceability-check to BLOCKING +# (remove `continue-on-error: true`) once SDD adoption has settled — target: after the +# first 5 real features have shipped through the .specify/ workflow. Tracked in ADR-041. + +on: + pull_request: + +jobs: + # ─── Spec structure lint ────────────────────────────────────────────────────── + # Every modified .specify/features/*/spec.md must contain the required SDD sections + # and at least one REQ-NNN requirement. Pure grep — no external tooling. + spec-lint: + name: Spec Lint + runs-on: ubuntu-latest + continue-on-error: true # TODO: remove to make blocking (see header) + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Lint changed spec.md files + shell: bash + run: | + set -uo pipefail + base="origin/${{ github.event.pull_request.base.ref }}" + git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.ref }}" || true + + required=( + "## Context & Why" + "## Requirements" + "## Acceptance Criteria" + "## Out of Scope" + "## Security Considerations" + "## Traceability" + ) + + # Self-test: a fixture with all sections + a REQ id must pass the section check. + fixture="$(mktemp)" + { for s in "${required[@]}"; do echo "$s"; done; echo "- **REQ-001** (Ubiquitous) — The x shall y."; } > "$fixture" + for s in "${required[@]}"; do + grep -qF "$s" "$fixture" || { echo "FAIL: spec-lint self-test missing '$s'"; exit 1; } + done + + changed="$(git diff --name-only "$base"...HEAD -- '.specify/features/*/spec.md' || true)" + if [ -z "$changed" ]; then + echo "No spec.md files changed — nothing to lint." + exit 0 + fi + + rc=0 + for f in $changed; do + [ -f "$f" ] || continue # deleted file + echo "── linting $f" + for s in "${required[@]}"; do + if ! grep -qF "$s" "$f"; then + echo "::error file=$f::missing required section '$s'" + rc=1 + fi + done + if ! grep -qE 'REQ-[0-9]{3}' "$f"; then + echo "::error file=$f::no REQ-NNN requirement found" + rc=1 + fi + done + exit $rc + + # ─── Contract validation ────────────────────────────────────────────────────── + # Validate any modified api-contract.yaml with Spectral (OpenAPI 3.1). REST stack — + # no GraphQL. Skips cleanly when no contract changed. + contract-validate: + name: Contract Validate + runs-on: ubuntu-latest + continue-on-error: true # TODO: remove to make blocking (see header) + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: '24' + + - name: Lint changed OpenAPI contracts + shell: bash + run: | + set -uo pipefail + base="origin/${{ github.event.pull_request.base.ref }}" + git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.ref }}" || true + changed="$(git diff --name-only "$base"...HEAD -- '.specify/features/*/api-contract.yaml' || true)" + if [ -z "$changed" ]; then + echo "No api-contract.yaml changed — nothing to validate." + exit 0 + fi + rc=0 + for f in $changed; do + [ -f "$f" ] || continue + echo "── spectral lint $f" + npx --yes @stoplight/spectral-cli@6 lint "$f" || rc=1 + done + exit $rc + + # ─── Traceability check ─────────────────────────────────────────────────────── + # Warn (non-blocking) when a changed spec.md references a REQ-NNN that is absent from + # .specify/rtm.md. Keeps the matrix honest without hard-failing during adoption. + traceability-check: + name: Traceability Check + runs-on: ubuntu-latest + continue-on-error: true # TODO: remove to make blocking (see header) + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cross-check spec REQ-IDs against rtm.md + shell: bash + run: | + set -uo pipefail + base="origin/${{ github.event.pull_request.base.ref }}" + git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.ref }}" || true + changed="$(git diff --name-only "$base"...HEAD -- '.specify/features/*/spec.md' || true)" + [ -z "$changed" ] && { echo "No spec.md changed."; exit 0; } + test -f .specify/rtm.md || { echo "::warning::.specify/rtm.md missing"; exit 0; } + missing=0 + for f in $changed; do + [ -f "$f" ] || continue + for req in $(grep -oE 'REQ-[0-9]{3}' "$f" | sort -u); do + if ! grep -qF "$req" .specify/rtm.md; then + echo "::warning file=$f::$req is not present in .specify/rtm.md" + missing=$((missing+1)) + fi + done + done + echo "$missing REQ-ID(s) missing from rtm.md (warning only)." + + # ─── Constitution change impact ─────────────────────────────────────────────── + # When .specify/constitution.md is modified, list every file that references it (and so + # may need a Sync Impact update) and post it as a PR comment. Best-effort: if no token is + # available the list is only echoed to the log. This job is informational, never blocking. + constitution-diff: + name: Constitution Impact + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: List files referencing the constitution + id: impact + shell: bash + run: | + set -uo pipefail + base="origin/${{ github.event.pull_request.base.ref }}" + git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.ref }}" || true + if ! git diff --name-only "$base"...HEAD -- '.specify/constitution.md' | grep -q .; then + echo "constitution.md not modified — skipping." + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "Files referencing constitution.md (review for Sync Impact):" + # rg-free: grep recursively, excluding the file itself and VCS/build dirs. + grep -rIl --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=target \ + -e 'constitution.md' -e 'constitution §' . \ + | grep -v '^\./.specify/constitution.md$' | sort > /tmp/refs.txt || true + cat /tmp/refs.txt + { + echo "body<> "$GITHUB_OUTPUT" + + - name: Post PR comment (best-effort) + if: steps.impact.outputs.changed == 'true' + shell: bash + env: + # Gitea Actions exposes the repo token as GITHUB_TOKEN; the API is Gitea-compatible. + TOKEN: ${{ secrets.GITHUB_TOKEN }} + SERVER: ${{ github.server_url }} + REPO: ${{ github.repository }} + PR: ${{ github.event.pull_request.number }} + BODY: ${{ steps.impact.outputs.body }} + run: | + set -uo pipefail + if [ -z "${TOKEN:-}" ]; then + echo "No token available — printing impact list to log only:" + echo "$BODY" + exit 0 + fi + payload="$(jq -n --arg b "$BODY" '{body:$b}')" + curl -sS -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments" \ + -d "$payload" >/dev/null \ + && echo "Posted Sync Impact comment to PR #${PR}." \ + || { echo "Comment POST failed (non-fatal); impact list:"; echo "$BODY"; }