feat(sdd): add Gitea issue templates and SDD CI gate
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 <noreply@anthropic.com>
This commit is contained in:
40
.gitea/ISSUE_TEMPLATE/bug.md
Normal file
40
.gitea/ISSUE_TEMPLATE/bug.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: "Bug"
|
||||
about: "Something is broken. Describe user-facing impact, not the technical cause."
|
||||
title: "<What breaks> when <trigger>"
|
||||
labels:
|
||||
- bug
|
||||
assignees: []
|
||||
---
|
||||
|
||||
<!--
|
||||
Title format (COLLABORATING.md): "<What breaks> when <trigger>", e.g.
|
||||
"Upload fails silently when file exceeds 50MB". Keep it focused — a bug is small and direct.
|
||||
A failing test is written first, then the fix (red/green TDD).
|
||||
-->
|
||||
|
||||
## What happens
|
||||
|
||||
<The observed broken behavior, from the user's perspective.>
|
||||
|
||||
## Expected
|
||||
|
||||
<What should happen instead.>
|
||||
|
||||
## Steps to reproduce
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Originating requirement (if known)
|
||||
|
||||
<REQ-NNN + feature this regresses, from .specify/rtm.md — e.g. "REQ-008 (profile-picture-upload)". Helps target the failing test. Write "unknown" if not traceable.>
|
||||
|
||||
## Environment
|
||||
|
||||
<Browser / role / data state / deploy (local vs prod) as relevant.>
|
||||
|
||||
## Notes
|
||||
|
||||
<Logs, GlitchTip link, screenshots. Redact PII.>
|
||||
82
.gitea/ISSUE_TEMPLATE/feature.md
Normal file
82
.gitea/ISSUE_TEMPLATE/feature.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
name: "Feature (SDD spec)"
|
||||
about: "Spec-driven feature request. Fill in EARS requirements before implementation starts."
|
||||
title: "As a <role> I want <capability> so <reason>"
|
||||
labels:
|
||||
- spec-required
|
||||
- needs-review
|
||||
assignees: []
|
||||
---
|
||||
|
||||
<!--
|
||||
This is a Spec-Driven feature spec. Mirror it to .specify/features/<name>/spec.md once the
|
||||
feature has a working name. Every requirement uses an EARS pattern + a REQ-NNN id.
|
||||
Reference: .specify/templates/feature-spec.md and the worked example .specify/features/_example/.
|
||||
Delete the placeholder hints as you fill each section.
|
||||
-->
|
||||
|
||||
## Context & Why
|
||||
|
||||
<Who needs this and why now (2–4 sentences). Link the constitution principle(s) this depends on: .specify/constitution.md>
|
||||
|
||||
## User Journey
|
||||
|
||||
<Plain-prose steps the user takes to get value, from the user's perspective. Anything not here is out of scope.>
|
||||
|
||||
## Requirements
|
||||
|
||||
<!-- One per line, each REQ-NNN + one EARS pattern. A mutating feature needs at least one Event-driven and one Unwanted-behavior requirement. -->
|
||||
|
||||
- **REQ-001** (Ubiquitous) — The `<component>` shall `<always-true behavior>`.
|
||||
- **REQ-002** (Event-driven) — When `<trigger>`, the `<component>` shall `<response>`.
|
||||
- **REQ-003** (State-driven) — While `<state>`, the `<component>` shall `<behavior>`.
|
||||
- **REQ-004** (Optional-feature) — Where `<caller has Permission.X / flag set>`, the `<component>` shall `<behavior>`.
|
||||
- **REQ-005** (Unwanted-behavior) — If `<undesired condition>`, then the `<component>` shall `<safe response / ErrorCode>`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- One measurable criterion per REQ-NNN: numbers, limits, status codes — not adjectives. -->
|
||||
|
||||
- **REQ-001** — <measurable>.
|
||||
- **REQ-002** — <measurable>.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- <The nearest tempting scope creep, named and excluded.>
|
||||
|
||||
## API / Contract Stub
|
||||
|
||||
<Inline stub or link to .specify/features/<name>/api-contract.yaml. Name new paths/methods/status codes and the @RequirePermission on each mutating endpoint.>
|
||||
|
||||
## Data Model Changes
|
||||
|
||||
<Schema delta + next free Flyway V<n> (verify on disk) + rollback note. "none" if not applicable.>
|
||||
|
||||
## Security Considerations
|
||||
|
||||
<STRIDE categories touched (+ ASTRIDE if an AI agent/tool is involved). Link a threat-model.md if the attack surface is non-trivial.>
|
||||
|
||||
## Open Questions
|
||||
|
||||
<!-- Each item BLOCKS implementation until resolved. -->
|
||||
|
||||
- [ ] <question> — owner: <name>
|
||||
|
||||
## Traceability
|
||||
|
||||
| REQ-ID | Task ID(s) | Test ID(s) | Status |
|
||||
|---|---|---|---|
|
||||
| REQ-001 | | | Planned |
|
||||
|
||||
<!-- Mirror these rows into .specify/rtm.md. -->
|
||||
|
||||
## Persona Review Results
|
||||
|
||||
| Persona | Status | Key Findings | Resolved |
|
||||
|---|---|---|---|
|
||||
| Requirements Engineer | PENDING | | |
|
||||
| Developer | PENDING | | |
|
||||
| Security | PENDING | | |
|
||||
| DevOps | PENDING | | |
|
||||
| UI/UX | PENDING | | |
|
||||
| Architect | PENDING | | |
|
||||
206
.gitea/workflows/sdd-gate.yml
Normal file
206
.gitea/workflows/sdd-gate.yml
Normal file
@@ -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<<EOF"
|
||||
echo "### ⚠️ Constitution changed — Sync Impact review"
|
||||
echo ""
|
||||
echo "\`.specify/constitution.md\` was modified in this PR. Per its §6 Sync Impact rule, re-read and reconcile every file below, and confirm the semantic version bump:"
|
||||
echo ""
|
||||
while IFS= read -r line; do echo "- \`${line#./}\`"; done < /tmp/refs.txt
|
||||
echo "EOF"
|
||||
} >> "$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"; }
|
||||
Reference in New Issue
Block a user