diff --git a/.claude/hooks/block-ai-tells.sh b/.claude/hooks/block-ai-tells.sh new file mode 100755 index 0000000..b5f43d5 --- /dev/null +++ b/.claude/hooks/block-ai-tells.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Claude Code PreToolUse hook (matcher: Bash). +# Blocks git commit / gh issue|pr create|comment commands whose message, title, +# or body contains an AI tell or emoji. Advisory layer; the git hooks enforce. +set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=/dev/null +source "$DIR/lib.sh" + +PY="$(command -v python3 || echo /usr/bin/python3)" +input="$(cat)" +cmd="$(printf '%s' "$input" | "$PY" -c 'import json,sys +try: print(json.load(sys.stdin).get("tool_input",{}).get("command","")) +except Exception: print("")')" + +case "$cmd" in + *"git commit"*|*"gh issue create"*|*"gh pr create"*|*"gh issue comment"*|*"gh pr comment"*) ;; + *"gh api"*comment*) ;; + *) exit 0 ;; +esac + +tells="$(printf '%s' "$cmd" | gov_find_text_tells)" +emoji="$(printf '%s' "$cmd" | gov_find_emoji)" +if [ -n "$tells$emoji" ]; then + reason="Blocked: AI-tell or emoji in a commit/issue/PR command. Remove attribution trailers (Co-Authored-By, Generated with), emojis, and session/AI references, then retry. Matched: ${tells} ${emoji}" + "$PY" -c 'import json,sys +print(json.dumps({"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":sys.argv[1]}}))' "$reason" +fi +exit 0 diff --git a/.claude/hooks/check-push-readiness.sh b/.claude/hooks/check-push-readiness.sh new file mode 100755 index 0000000..4656e6d --- /dev/null +++ b/.claude/hooks/check-push-readiness.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Claude Code PreToolUse hook (matcher: Bash). +# On `git push`, gives fast feedback: blocks if there are uncommitted changes or +# if a quick test pass fails for the detected ecosystem. The git pre-push hook is +# the authoritative gate; this just shortens the feedback loop. +set -euo pipefail +PY="$(command -v python3 || echo /usr/bin/python3)" +input="$(cat)" +cmd="$(printf '%s' "$input" | "$PY" -c 'import json,sys +try: print(json.load(sys.stdin).get("tool_input",{}).get("command","")) +except Exception: print("")')" + +case "$cmd" in *"git push"*) ;; *) exit 0 ;; esac + +deny() { + "$PY" -c 'import json,sys +print(json.dumps({"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":sys.argv[1]}}))' "$1" + exit 0 +} + +if ! git diff --quiet --exit-code 2>/dev/null; then + deny "Uncommitted changes present. Commit or stash before pushing." +fi +if [ -f go.mod ]; then + if ! go test -short ./... >/dev/null 2>&1; then + deny "Quick test pass failed (go test -short ./...). Fix before pushing." + fi +fi +exit 0 diff --git a/.claude/hooks/commit-msg b/.claude/hooks/commit-msg new file mode 100755 index 0000000..e437859 --- /dev/null +++ b/.claude/hooks/commit-msg @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Git commit-msg hook (enforcement). Requires a Conventional Commit title and +# rejects AI-tells or emoji anywhere in the message. +set -euo pipefail +ROOT="$(git rev-parse --show-toplevel)" +# shellcheck source=/dev/null +source "$ROOT/.claude/hooks/lib.sh" + +msg_file="$1" +title="$(head -1 "$msg_file")" + +if ! printf '%s' "$title" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9._-]+\))?!?: .+'; then + echo "commit-msg: title must be a Conventional Commit: type(scope)?: description" >&2 + echo " valid types: feat fix docs style refactor perf test build ci chore revert" >&2 + echo " got: $title" >&2 + exit 1 +fi + +tells="$(gov_find_text_tells < "$msg_file")" +emoji="$(gov_find_emoji < "$msg_file")" +if [ -n "$tells$emoji" ]; then + echo "commit-msg: AI-tell or emoji not allowed in commit message:" >&2 + [ -n "$tells" ] && echo "$tells" >&2 + [ -n "$emoji" ] && echo "$emoji" >&2 + exit 1 +fi +exit 0 diff --git a/.claude/hooks/forbidden-text.txt b/.claude/hooks/forbidden-text.txt new file mode 100644 index 0000000..15f57a4 --- /dev/null +++ b/.claude/hooks/forbidden-text.txt @@ -0,0 +1,116 @@ +# AI-tell text patterns blocked in commit messages, issue/PR titles and bodies, +# comments, and source files. Extended-regex (grep -E), matched case-insensitively. +# One pattern per line. Lines beginning with # and blank lines are ignored. +# Emoji are blocked separately by unicode range (see lib.sh), not listed here. +# +# Goal: HUMAN WRITE, not CLAUDE WRITE. Every human-facing artifact should read +# as if a person wrote it. The ACTIVE patterns below are reliable generated-text +# fingerprints: attribution trailers, assistant self-reference, working-session +# framing, and the multi-word filler/cliche phrasing that betrays an LLM. They +# are multi-word on purpose, to avoid false positives on real source and docs. +# +# Single common buzzwords (robust, comprehensive, leverage, ...) live in the +# commented OPT-IN block at the bottom: they read as generated but also appear +# in legitimate human writing, so enable them only if you want them enforced. + +# --- Attribution trailers / tool fingerprints --- +Co-Authored-By +Co-authored-by +Generated with +Generated by (Claude|AI|GPT|Copilot|Codex|an AI|a language model) +written by (Claude|an AI|AI) +Claude Code +Claude Opus +Claude Sonnet +@anthropic-ai +anthropic\.com +openai\.com + +# --- Assistant self-reference --- +as an AI +as an AI( language)? model +as a large language model +as an AI assistant +I am an AI +I'?m an AI +I am a language model +I do not have (personal|the ability) +I cannot (browse|access|provide real-time) +my (training data|knowledge cutoff|last update) +knowledge cutoff +as of my last (update|training) + +# --- Working-session / process framing --- +in this( CI)? session +in (this|our) (conversation|chat|thread|session|exchange) +as (previously )?discussed +as (you )?requested +as (mentioned|noted|stated|described) (earlier|above|previously|below) +per (your|our) (request|discussion|conversation) +in the previous (turn|message|response|step) +based on (your|the) (request|previous) + +# --- LLM hedging / filler cliches --- +It('?s| is) important to (note|remember|understand|keep in mind) +It('?s| is) worth (noting|mentioning|highlighting) +I hope this helps +Hope (this|that) helps +Let me know if (you|there|that|anything) +feel free to (reach out|ask|let me know|modify|adjust|customize) +If you have any (questions|concerns|feedback) +(do not|don'?t) hesitate to +without further ado +let'?s (dive|jump) (in|into|right) +(delve|delving) into +rest assured +that being said +needless to say +it goes without saying +when it comes to +at the end of the day +in today'?s (fast-paced|digital|modern|ever-) +in the (realm|world) of +a testament to +plays a (crucial|vital|key|pivotal|significant) role +it'?s not just about +the key (takeaway|thing to remember) + +# --- AI structure / boilerplate openers --- +Here('?s| is) (a |an )?(breakdown|summary|quick overview|rundown|high-level overview) +In (summary|conclusion|essence), +To (summarize|conclude|sum up), +Certainly! +Absolutely! +Sure(,| thing)! +Great question +I'?d be happy to +Below is (a|an|the) +Let'?s get started +Let'?s break (it|this) down + +# --- OPT-IN: single-word AI buzzwords (disabled by default) --- +# These read as generated but also appear in legitimate docs, so they would +# false-positive on real source/READMEs. Uncomment any you want enforced. +# seamless(ly)? +# robust +# comprehensive +# leverage +# utilize +# delve +# intricate +# pivotal +# realm +# tapestry +# multifaceted +# nuanced +# underscore(s|d)? +# showcase(s|d)? +# elevate +# unleash +# supercharge +# cutting-edge +# state-of-the-art +# paradigm shift +# holistic +# synergy +# game-?chang(er|ing) diff --git a/.claude/hooks/install.sh b/.claude/hooks/install.sh new file mode 100755 index 0000000..2bf0b79 --- /dev/null +++ b/.claude/hooks/install.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Install the governance hooks into the current git repository. +# Copies the Claude Code hooks + shared lib into .claude/hooks/ and the git +# enforcement hooks into .git/hooks/. Run from anywhere inside the repo. +set -euo pipefail +ROOT="$(git rev-parse --show-toplevel)" +SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEST="$ROOT/.claude/hooks" + +mkdir -p "$DEST" +# Skip the copy when the source already IS the destination (e.g. a repo that +# vendored the hooks under .claude/hooks); copying onto itself errors. +if [ "$SRC" != "$DEST" ]; then + cp "$SRC/lib.sh" "$SRC/forbidden-text.txt" \ + "$SRC/block-ai-tells.sh" "$SRC/validate-conventional-commit.sh" "$SRC/check-push-readiness.sh" \ + "$DEST/" +fi +chmod +x "$DEST/"*.sh + +for h in pre-commit commit-msg pre-push; do + cp "$SRC/$h" "$ROOT/.git/hooks/$h" + chmod +x "$ROOT/.git/hooks/$h" +done + +echo "Installed:" +echo " Claude Code hooks -> $ROOT/.claude/hooks/ (wire them in .claude/settings.json)" +echo " git hooks -> $ROOT/.git/hooks/ (pre-commit, commit-msg, pre-push)" +echo "Add the PreToolUse entries from .claude/settings.json (see governance settings.template.json)." diff --git a/.claude/hooks/lib.sh b/.claude/hooks/lib.sh new file mode 100755 index 0000000..b5cfa23 --- /dev/null +++ b/.claude/hooks/lib.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Shared helpers for the project governance hooks. +# Works with BSD (macOS) and GNU tooling. Emoji detection uses perl (BSD grep +# lacks -P); text-pattern detection uses grep -E. + +GOV_HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOV_PATTERNS_FILE="${GOV_PATTERNS_FILE:-$GOV_HOOKS_DIR/forbidden-text.txt}" + +# Build one extended-regex alternation from the patterns file. +gov_text_regex() { + grep -vE '^[[:space:]]*(#|$)' "$GOV_PATTERNS_FILE" 2>/dev/null | paste -sd '|' - +} + +# Read stdin; print offending lines for forbidden AI-tell phrases (empty if none). +gov_find_text_tells() { + local re + re="$(gov_text_regex)" + [ -z "$re" ] && return 0 + grep -inE "$re" || true +} + +# Read stdin; print offending lines containing emoji/pictographs (empty if none). +gov_find_emoji() { + perl -CSD -ne 'print "$.: $_" if /[\x{1F000}-\x{1FAFF}\x{2600}-\x{27BF}\x{2B00}-\x{2BFF}\x{FE00}-\x{FE0F}\x{1F1E6}-\x{1F1FF}]/' +} diff --git a/.claude/hooks/pre-commit b/.claude/hooks/pre-commit new file mode 100755 index 0000000..21f2472 --- /dev/null +++ b/.claude/hooks/pre-commit @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Git pre-commit hook (enforcement). +# - Emoji are banned in every staged text file EXCEPT CI workflow files, where +# they are allowed in job/step names. +# - AI-tell phrases are banned too, EXCEPT in files whose purpose is to document +# the rule (CLAUDE.md, the governance hooks, the pattern list), which +# legitimately contain those phrases as examples. +set -euo pipefail +ROOT="$(git rev-parse --show-toplevel)" +# shellcheck source=/dev/null +source "$ROOT/.claude/hooks/lib.sh" + +is_binary() { + case "$1" in + *.png|*.jpg|*.jpeg|*.gif|*.pdf|*.ico|*.svg|*.woff|*.woff2|*.ttf|*.eot|*.zip|*.gz|*.jar|*.keystore) return 0 ;; + *) return 1 ;; + esac +} + +# Paths exempt from the AI-tell TEXT scan (still subject to the emoji scan). +tells_exempt() { + case "$1" in + CLAUDE.md|*/CLAUDE.md) return 0 ;; + .claude/*|*/.claude/*) return 0 ;; + governance/*|*/governance/*) return 0 ;; + .github/workflows/*|*workflows/*.yml|*workflows/*.yaml) return 0 ;; + *forbidden-text.txt) return 0 ;; + *) return 1 ;; + esac +} + +# Paths where emoji ARE allowed: Markdown docs (Bugs5382 standard), GitHub +# config (CI workflow job/step names and release-drafter category titles), and +# the hub's governance/github tree that holds that GitHub config before sync. +emoji_exempt() { + case "$1" in + .github/*|*/.github/*) return 0 ;; + *.md) return 0 ;; + NOTES.txt|*/NOTES.txt) return 0 ;; + governance/github/*) return 0 ;; + *) return 1 ;; + esac +} + +fail=0 +while IFS= read -r f; do + [ -f "$f" ] || continue + case "$f" in .git/*) continue ;; esac + is_binary "$f" && continue + + if ! emoji_exempt "$f"; then + emoji="$(gov_find_emoji < "$f")" + if [ -n "$emoji" ]; then echo "pre-commit: emoji not allowed in $f:" >&2; echo "$emoji" >&2; fail=1; fi + fi + + if ! tells_exempt "$f"; then + tells="$(gov_find_text_tells < "$f")" + if [ -n "$tells" ]; then echo "pre-commit: AI-tell not allowed in $f:" >&2; echo "$tells" >&2; fail=1; fi + fi +done < <(git diff --cached --name-only --diff-filter=ACM) + +[ "$fail" -eq 0 ] || { echo "pre-commit: blocked. Remove the above before committing." >&2; exit 1; } +exit 0 diff --git a/.claude/hooks/pre-push b/.claude/hooks/pre-push new file mode 100755 index 0000000..00c3e76 --- /dev/null +++ b/.claude/hooks/pre-push @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Git pre-push hook (enforcement). Detects the ecosystem from manifest files and +# runs format/lint/test gates before anything is pushed. Verified and clean +# before GitHub. Runs only the tooling that is actually installed. +set -euo pipefail +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" + +run() { echo "pre-push: $*"; "$@"; } + +if [ -f go.mod ]; then + unformatted="$(gofmt -l . 2>/dev/null | grep -v '^vendor/' || true)" + if [ -n "$unformatted" ]; then + echo "pre-push: not gofmt-clean (run: gofmt -w .):" >&2; echo "$unformatted" >&2; exit 1 + fi + run go vet ./... || { echo "pre-push: go vet failed" >&2; exit 1; } + if command -v golangci-lint >/dev/null 2>&1; then + run golangci-lint run ./... || { echo "pre-push: golangci-lint failed" >&2; exit 1; } + else + echo "pre-push: golangci-lint not installed; skipping (CI will run it)" + fi + run go test ./... || { echo "pre-push: tests failed" >&2; exit 1; } +fi + +if [ -f package.json ]; then + if [ ! -d node_modules ]; then + echo "pre-push: node_modules not installed; skipping npm lint/test (CI runs them)" + else + has() { node -e "process.exit(require('./package.json').scripts?.['$1']?0:1)" 2>/dev/null; } + if has lint; then run npm run --silent lint || { echo "pre-push: npm lint failed" >&2; exit 1; }; fi + if has test; then run npm test --silent || { echo "pre-push: npm test failed" >&2; exit 1; }; fi + fi +fi + +if [ -f pyproject.toml ]; then + if command -v ruff >/dev/null 2>&1; then + run ruff check . || { echo "pre-push: ruff failed" >&2; exit 1; } + fi + if command -v pytest >/dev/null 2>&1; then + run pytest -q || { echo "pre-push: pytest failed" >&2; exit 1; } + fi +fi + +echo "pre-push: checks passed" +exit 0 diff --git a/.claude/hooks/validate-conventional-commit.sh b/.claude/hooks/validate-conventional-commit.sh new file mode 100755 index 0000000..6dec8fa --- /dev/null +++ b/.claude/hooks/validate-conventional-commit.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Claude Code PreToolUse hook (matcher: Bash). +# Validates Conventional Commit format on git commit -m and gh issue|pr create +# titles. Advisory layer; the commit-msg git hook enforces. +set -euo pipefail +PY="$(command -v python3 || echo /usr/bin/python3)" +input="$(cat)" + +# Extract the command and, from it, the relevant title/message via shlex so we +# survive quoting. Emits the candidate title on stdout (empty if not applicable). +title="$(printf '%s' "$input" | "$PY" -c ' +import json,sys,shlex +try: + cmd=json.load(sys.stdin).get("tool_input",{}).get("command","") +except Exception: + print(""); sys.exit() +try: + toks=shlex.split(cmd) +except Exception: + print(""); sys.exit() +is_commit="git" in toks and "commit" in toks +is_create=("gh" in toks) and ("create" in toks) +if not (is_commit or is_create): + print(""); sys.exit() +flags=({"-m","--message"} if is_commit else {"-t","--title"}) +val="" +for i,t in enumerate(toks): + if t in flags and i+1/-` (for example `feat/12-add-listener`). +3. Commit using Conventional Commits (`type(scope): description`). No attribution + trailers, no emoji in source or commit messages (emoji are fine in Markdown). +4. Open a PR with a Conventional Commit title. The autolabeler sets the category label + from the title; fill the PR template, reference the issue (`Closes #N`), and add a + closing summary before merge. +5. PRs merge by squash. On merge, release-drafter drafts the next notes and the changelog + updates on `main`; the maintainer publishes releases manually. + +Keep one concern per PR, even small ones. When editing GitHub Actions workflows, a job id must be a +plain identifier (a letter or `_`, then alphanumerics/`-`/`_`); put emoji and display text in the +job's `name:`. The Actionlint check enforces this. + +## Local setup + +Install the governance hooks once per clone: `bash .claude/hooks/install.sh`. They +enforce Conventional Commits and the no-tell/no-emoji rules before you push; CI enforces +the same. See `CLAUDE.md` for the full working agreement. diff --git a/.github/DISCUSSION_TEMPLATE/ideas.yml b/.github/DISCUSSION_TEMPLATE/ideas.yml new file mode 100644 index 0000000..9a8a2f5 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/ideas.yml @@ -0,0 +1,17 @@ +title: "[Idea] " +labels: [enhancement] +body: + - type: textarea + id: idea + attributes: + label: Idea + description: The problem or opportunity, and the outcome you imagine. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Other approaches you weighed, and why this one. + validations: + required: false diff --git a/.github/DISCUSSION_TEMPLATE/q-a.yml b/.github/DISCUSSION_TEMPLATE/q-a.yml new file mode 100644 index 0000000..7401939 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/q-a.yml @@ -0,0 +1,21 @@ +title: "[Q&A] " +labels: [question] +body: + - type: markdown + attributes: + value: | + For usage help and questions. For a bug or a feature request, open an issue instead. + - type: textarea + id: question + attributes: + label: Question + description: What are you trying to do, and what have you tried so far? + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Which version of api are you on? + validations: + required: false diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..5fb451d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# Enable a Sponsor button by uncommenting and filling one of these. +# github: [CryptOS-PKI] +# custom: ["https://example.com/sponsor"] diff --git a/.github/ISSUE_TEMPLATE/01-feature.yml b/.github/ISSUE_TEMPLATE/01-feature.yml new file mode 100644 index 0000000..65360f3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-feature.yml @@ -0,0 +1,38 @@ +name: Feature / task +description: A unit of new work or an enhancement. +title: "feat(): " +labels: ["enhancement"] +body: + - type: textarea + id: summary + attributes: + label: Summary + description: What should change and why. State the intended outcome. + validations: + required: true + - type: textarea + id: acceptance + attributes: + label: Acceptance criteria + description: What must be true for this to be done. Use checkboxes where helpful. + validations: + required: true + - type: textarea + id: tasks + attributes: + label: Tasks + description: >- + For sequential / multi-step work, list the ordered units of work. Convert each to a + sub-issue of this one (this issue becomes the parent). One task per line. + value: | + - [ ] Task 1 + - [ ] Task 2 + validations: + required: false + - type: textarea + id: notes + attributes: + label: Notes + description: Constraints, dependencies, or links to related issues. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/02-bug.yml b/.github/ISSUE_TEMPLATE/02-bug.yml new file mode 100644 index 0000000..32d1e7c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-bug.yml @@ -0,0 +1,25 @@ +name: Bug report +description: A defect in existing behavior. +title: "fix(): " +labels: ["bug"] +body: + - type: textarea + id: what-happened + attributes: + label: What happened + description: The observed behavior, with steps to reproduce. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: Version, OS/runtime, and any relevant logs. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml deleted file mode 100644 index ce00434..0000000 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Bug report -description: Report a defect in the API definitions or generated stubs -title: "fix: " -labels: ["bug"] -assignees: ["Bugs5382"] -body: - - type: textarea - id: what-happened - attributes: - label: What happened - description: The actual behavior, with any error output. - validations: - required: true - - type: textarea - id: expected - attributes: - label: Expected behavior - validations: - required: true - - type: textarea - id: repro - attributes: - label: Steps to reproduce - description: Minimal steps โ€” the RPC/message and the call that triggers it. - validations: - required: true - - type: textarea - id: environment - attributes: - label: Environment - description: api module version/commit, which consumer (cryptos / manager / web), Go or TypeScript stubs, and protoc/buf version if regenerating. - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/chore.yml b/.github/ISSUE_TEMPLATE/chore.yml deleted file mode 100644 index 0edcc08..0000000 --- a/.github/ISSUE_TEMPLATE/chore.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Chore (build / CI / docs / refactor) -description: Tooling, build, CI, docs, refactor, or other non-feature work -title: "chore: " -labels: ["chore"] -assignees: ["Bugs5382"] -body: - - type: textarea - id: what - attributes: - label: What - description: What needs doing? - validations: - required: true - - type: textarea - id: why - attributes: - label: Why - description: Why it's worth doing now. - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml deleted file mode 100644 index c46529e..0000000 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Feature / enhancement -description: Propose a new feature or enhancement -title: "feat: " -labels: ["enhancement"] -assignees: ["Bugs5382"] -body: - - type: textarea - id: what - attributes: - label: What - description: What should be built or changed? - validations: - required: true - - type: textarea - id: why - attributes: - label: Why - description: The problem this solves or the value it adds. - validations: - required: true - - type: textarea - id: acceptance - attributes: - label: Acceptance criteria - description: How do we know it's done? (tests, behavior, checks) - validations: - required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..3d1a294 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,28 @@ + + +## What and why + + + +Closes # + +## Verification + + + +- [ ] Lint clean +- [ ] Tests pass +- [ ] Build succeeds +- [ ] Documentation updated (if behavior or API changed) + +## How this was verified + + + +--- + +**Before merging:** add a closing comment summarizing what was actually done in this PR +(not just the checked boxes). diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..03ed44c --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Reporting a vulnerability + +Report security issues privately through GitHub Security Advisories on this repository +(the **Security** tab -> **Report a vulnerability**). Please do not open a public issue +for a suspected vulnerability. + +Expect an acknowledgement within a few business days; we will coordinate a fix and a +disclosure timeline with you. + +## Supported versions + +Security fixes target the latest released major version of `api`. diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000..6554735 --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,7 @@ +# Support + +- Usage help and questions: open a **Discussion** (Q&A) on this repository. +- Bugs and feature requests: open an **issue** from the templates. +- Security issues: see [SECURITY.md](SECURITY.md) โ€” do not file these publicly. + +Maintained by @CryptOS-PKI; response times are best-effort. diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..436ad2b --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,43 @@ +# Canonical label set for every Bugs5382 repo. job-label-sync.yaml reconciles a +# repo to exactly this set (it also deletes labels not listed here). Aligned to +# the release-drafter categories + version-resolver and the conventional-commit +# prefixes the autolabeler maps from PR titles. +- name: breaking + color: "b60205" + description: "Breaking change. Major version bump." +- name: enhancement + color: "a2eeef" + description: "New feature (feat). Minor version bump." +- name: fix + color: "0e8a16" + description: "Bug fix (fix). Patch." +- name: bug + color: "d73a4a" + description: "Something isn't working." +- name: performance + color: "fbca04" + description: "Performance improvement (perf)." +- name: refactor + color: "c5def5" + description: "Internal change / refactor." +- name: changed + color: "bfdadc" + description: "Behavior change." +- name: deprecated + color: "fef2c0" + description: "Deprecated functionality." +- name: removed + color: "e11d21" + description: "Removed functionality." +- name: security + color: "ee0701" + description: "Security fix." +- name: documentation + color: "0075ca" + description: "Documentation only (docs)." +- name: dependencies + color: "0366d6" + description: "Dependency updates." +- name: skip-changelog + color: "ededed" + description: "Excluded from release notes (chore/ci/test/style)." diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index bfe05e8..eacc159 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,71 +1,79 @@ -# Release Drafter configuration. -# -# Categories are keyed on this project's type labels (enhancement / bug / -# chore / documentation); the autolabeler classifies Conventional-Commit PR -# titles into them so the draft is categorized without manual labelling. -# -# Scope labels (phase-1/2/3, scaffolding) are deliberately NOT category or -# exclude labels โ€” they are ignored by the changelog. Listing them under -# exclude-labels would drop nearly every PR; they simply don't drive a -# category. -name-template: "v$RESOLVED_VERSION" -tag-template: "v$RESOLVED_VERSION" +# Canonical release-drafter config for every Bugs5382 repo (libraries/extensions +# use release-drafter; only apps would use release-please, which we do not ship). +# The autolabeler derives the categorizing label from the conventional-commit PR +# title, so labeling is near-zero-effort; job-label-checker is the backstop. +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' template: | # What Changed ๐Ÿ‘€ $CHANGES + # Extra + **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION categories: + - title: ๐Ÿ’ฅ Breaking Changes + labels: [breaking] - title: ๐Ÿš€ Features - labels: - - enhancement + labels: [enhancement] - title: ๐Ÿ› Bug Fixes - labels: - - bug + labels: [bug, fix] + - title: โšก Performance + labels: [performance] + - title: โš ๏ธ Changes + labels: [refactor, changed] + - title: โ›”๏ธ Deprecated + labels: [deprecated] + - title: ๐Ÿ—‘ Removed + labels: [removed] + - title: ๐Ÿ” Security + labels: [security] - title: ๐Ÿ“„ Documentation - labels: - - documentation - - title: ๐Ÿ”ง Maintenance - labels: - - chore + labels: [documentation] - title: ๐Ÿงฉ Dependency Updates - labels: - - dependencies + labels: [dependencies] collapse-after: 5 -change-template: "- $TITLE @$AUTHOR (#$NUMBER)" +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' change-title-escapes: '\<*_&' +# Bot authors render as `@name[bot]`, which GitHub markdown does not autolink +# (the bracketed login is not a valid mention). Rewrite the common ones to a +# real link so the credit is clickable instead of plain text. +replacers: + - search: '/@github-actions\[bot\]/g' + replace: '[@github-actions](https://github.com/apps/github-actions)' version-resolver: major: - labels: - - breaking + labels: [breaking] minor: - labels: - - enhancement + labels: [enhancement] patch: - labels: - - bug - - chore - - documentation - - dependencies + labels: [fix, bug, performance, refactor, changed, deprecated, removed, security, documentation, dependencies] default: patch -exclude-labels: - - skip-changelog -# Classify Conventional-Commit PR titles into the type labels above. The -# breaking rule matches the CC "!" marker (e.g. feat!: / fix(x)!:). autolabeler: - label: breaking title: - - '/^[a-z]+(\(.+\))?!:/i' + - '/^[a-z]+(\(.+\))?!:/' + - '/BREAKING[ -]CHANGE/' - label: enhancement - title: - - '/^feat(\(.+\))?!?:/i' - - label: bug - title: - - '/^fix(\(.+\))?!?:/i' + title: ['/^feat(\(.+\))?:/'] + - label: fix + title: ['/^fix(\(.+\))?:/'] + - label: performance + title: ['/^perf(\(.+\))?:/'] + - label: refactor + title: ['/^refactor(\(.+\))?:/'] - label: documentation + title: ['/^docs(\(.+\))?:/'] + - label: dependencies title: - - '/^docs(\(.+\))?!?:/i' - - label: chore + - '/^build(\(.+\))?:/' + - '/^chore\(deps.*\):/' + - label: security + title: ['/^security(\(.+\))?:/'] + - label: skip-changelog title: - - '/^(chore|build|ci|refactor|perf|test|style|revert)(\(.+\))?!?:/i' + - '/^(ci|test|style)(\(.+\))?:/' + - '/^chore(?!\(deps)(\(.+\))?:/' +exclude-labels: + - skip-changelog diff --git a/.github/workflows/action-lint.yaml b/.github/workflows/action-lint.yaml new file mode 100644 index 0000000..4db8697 --- /dev/null +++ b/.github/workflows/action-lint.yaml @@ -0,0 +1,32 @@ +name: Actionlint +# Validate the GitHub Actions workflow files. The other PR checks don't verify +# that a workflow is actually runnable, so a malformed one (e.g. an invalid job +# id) can merge green and only fail at startup on main. actionlint catches that +# class at PR time. shellcheck integration is disabled (-shellcheck=) to keep +# the gate focused on workflow validity; drop that flag to also lint run: shell. +on: + pull_request: + branches: [main] + push: + branches: [main] + paths: ['.github/workflows/**'] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +permissions: + contents: read +jobs: + actionlint: + name: ๐Ÿงน Actionlint + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ“ฅ Checkout + uses: actions/checkout@v4 + - name: ๐Ÿน Setup Go + uses: actions/setup-go@v5 + with: + go-version: "stable" + - name: ๐Ÿงน Run actionlint + run: | + go install github.com/rhysd/actionlint/cmd/actionlint@latest + "$(go env GOPATH)/bin/actionlint" -color -shellcheck= diff --git a/.github/workflows/job-gitleaks.yaml b/.github/workflows/job-gitleaks.yaml new file mode 100644 index 0000000..357e2f6 --- /dev/null +++ b/.github/workflows/job-gitleaks.yaml @@ -0,0 +1,35 @@ +name: Gitleaks +# Secret scan on EVERY push and PR -- no paths-ignore, because a secret can land +# in any file, docs included. Hard failure: the scan exits non-zero on a finding. +# +# Runs the gitleaks CLI directly rather than gitleaks/gitleaks-action@v2: that +# action requires a paid GITLEAKS_LICENSE for organization-owned repos, while the +# CLI is free everywhere. This matches the `gitleaks detect` the repos already +# run locally via `task lint`. +on: + push: + pull_request: + branches: [main] +permissions: + contents: read +jobs: + gitleaks: + name: ๐Ÿ”’ Gitleaks (secret scan) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Install gitleaks + run: | + VERSION="8.30.0" + curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/gitleaks_${VERSION}_linux_x64.tar.gz" \ + | tar -xz -C /usr/local/bin gitleaks + gitleaks version + - name: Run Gitleaks + run: | + # Honor a repo-local .gitleaks.toml if present; scan the full tree. + config_arg="" + [ -f .gitleaks.toml ] && config_arg="--config .gitleaks.toml" + gitleaks detect --source . --redact --verbose --no-banner $config_arg diff --git a/.github/workflows/job-golic.yaml b/.github/workflows/job-golic.yaml new file mode 100644 index 0000000..93f3f8b --- /dev/null +++ b/.github/workflows/job-golic.yaml @@ -0,0 +1,37 @@ +name: GoLic +# Verify every source file carries the MIT license header. golic (Bugs5382's own +# tool) is run through go-task (`task license`) -- the standard runner for every +# project. `golic inject --dry -x` fails the check if a header is missing; it never +# modifies files. Docs (**.md) are skipped -- they carry no header. +on: + pull_request: + branches: [main] + push: + branches: [main] + paths-ignore: ['**.md'] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +permissions: + contents: read +jobs: + golic: + name: ๐Ÿ” License headers + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: 'stable' + - name: Install Task + uses: arduino/setup-task@v2 + with: + version: 3.x + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Verify MIT headers + run: | + export PATH="${PATH}:$(go env GOPATH)/bin" + task license diff --git a/.github/workflows/job-label-checker.yaml b/.github/workflows/job-label-checker.yaml new file mode 100644 index 0000000..f55bfdd --- /dev/null +++ b/.github/workflows/job-label-checker.yaml @@ -0,0 +1,92 @@ +name: Label Checker +# Every PR must carry a categorizing label. This workflow derives the label from +# the conventional-commit PR title and applies it, then confirms it is present. +# +# Labeling and the check live in ONE workflow on purpose. The label is applied +# with the default GITHUB_TOKEN, and GitHub does not fire new workflow events +# (e.g. `labeled`) from GITHUB_TOKEN actions, so a separate checker keyed on the +# `labeled` event would never re-run after the label was added and would stay +# failed on a fresh PR. Running the check as a job that `needs` the autolabel job +# guarantees the label is applied in the same run. +# +# The label is applied deterministically with `gh` rather than relying on +# release-drafter's autolabeler, which does not reliably apply labels in this +# configuration. The release-drafter.yml autolabeler block is still used at draft +# time; this job is what guarantees a categorizing label on every PR. +on: + pull_request: + types: [opened, reopened, edited, synchronize] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true +permissions: + contents: read + pull-requests: write +jobs: + autolabel: + name: Autolabel + runs-on: ubuntu-latest + steps: + # Map the conventional-commit prefix in the PR title to a categorizing + # label and apply it. Best-effort (continue-on-error): the + # categorizing-label job below is the authoritative check. + - name: Apply label from PR title + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + TITLE: ${{ github.event.pull_request.title }} + run: | + # Derive the label from the conventional-commit type/prefix. + label="" + case "$TITLE" in + *"!:"*|*"BREAKING CHANGE"*|*"BREAKING-CHANGE"*) label="breaking" ;; + feat\(*\)!:*|feat!:*) label="breaking" ;; + feat:*|feat\(*\):*) label="enhancement" ;; + fix:*|fix\(*\):*) label="fix" ;; + perf:*|perf\(*\):*) label="performance" ;; + refactor:*|refactor\(*\):*) label="refactor" ;; + docs:*|docs\(*\):*) label="documentation" ;; + security:*|security\(*\):*) label="security" ;; + build:*|build\(*\):*|chore\(deps*\):*) label="dependencies" ;; + ci:*|ci\(*\):*|test:*|test\(*\):*|style:*|style\(*\):*|chore:*|chore\(*\):*) label="skip-changelog" ;; + *) echo "PR title is not a recognized conventional-commit type; leaving labels alone."; exit 0 ;; + esac + echo "Applying label: $label" + gh api -X POST "repos/$REPO/issues/$PR/labels" -f "labels[]=$label" >/dev/null + + categorizing-label: + name: Categorizing label present + needs: [autolabel] + # Run even if autolabel was skipped/failed; the label may already be present + # from the PR title. + if: always() + runs-on: ubuntu-latest + permissions: + pull-requests: read + steps: + # Poll the LIVE labels via the API rather than the trigger payload. The + # autolabel job above applies the label with the default GITHUB_TOKEN; that + # write takes a moment to be visible, and the `opened` event payload this + # run was triggered with never carries it. Retrying against the API closes + # that race so a fresh PR passes on the first run instead of needing a rerun. + - name: Verify a categorizing label is present + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + allowed="breaking enhancement bug fix performance refactor changed deprecated removed security documentation dependencies skip-changelog" + for attempt in 1 2 3 4 5 6; do + labels="$(gh api "repos/$REPO/issues/$PR/labels" --jq '.[].name' 2>/dev/null || true)" + for l in $labels; do + for a in $allowed; do + if [ "$l" = "$a" ]; then echo "Found categorizing label: $l"; exit 0; fi + done + done + echo "No categorizing label yet (attempt $attempt/6); waiting for the autolabeler..." + sleep 10 + done + echo "No categorizing label present. Add one of: $allowed" + exit 1 diff --git a/.github/workflows/job-label-sync.yaml b/.github/workflows/job-label-sync.yaml new file mode 100644 index 0000000..d37d770 --- /dev/null +++ b/.github/workflows/job-label-sync.yaml @@ -0,0 +1,23 @@ +name: Label Sync +# Reconciles the repo's labels to the canonical set (.github/labels.yml): +# creates/updates the canonical labels AND deletes any label not in the file, +# so every repo carries exactly the standard set (no stale GitHub defaults or +# leftover release labels). +on: + workflow_dispatch: + push: + branches: [main] + paths: ['.github/labels.yml'] +permissions: + issues: write +jobs: + labeler: + name: Sync labels + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: crazy-max/ghaction-github-labeler@v5 + with: + yaml-file: .github/labels.yml + skip-delete: false + dry-run: false diff --git a/.github/workflows/job-license-check.yaml b/.github/workflows/job-license-check.yaml new file mode 100644 index 0000000..aa47776 --- /dev/null +++ b/.github/workflows/job-license-check.yaml @@ -0,0 +1,66 @@ +name: License Check +# Fail if a 3rd-party dependency carries a license outside the allowlist. +# Ecosystem-aware: each job early-exits if its manifest is absent. hashFiles() +# is NOT valid in a job-level `if` (no workspace before checkout), so the +# manifest check lives in a step. No SAST/vuln scanning (deliberately out of scope). +on: + pull_request: + branches: [main] + push: + branches: [main] +permissions: + contents: read +jobs: + npm: + name: npm dependency licenses + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Detect package.json + id: manifest + run: | + if [ -f package.json ]; then echo "found=true" >> "$GITHUB_OUTPUT"; else echo "found=false (skipping)"; echo "found=false" >> "$GITHUB_OUTPUT"; fi + + - name: Setup Node + if: steps.manifest.outputs.found == 'true' + uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Install dependencies + if: steps.manifest.outputs.found == 'true' + run: npm install --ignore-scripts --no-audit --no-fund + + - name: Check dependency licenses + if: steps.manifest.outputs.found == 'true' + run: | + npx --yes license-checker-rseidelsohn \ + --production --excludePrivatePackages \ + --onlyAllow "MIT;ISC;Apache-2.0;BSD-2-Clause;BSD-3-Clause;0BSD;CC0-1.0;Unlicense;BlueOak-1.0.0;Python-2.0;MPL-2.0;CC-BY-4.0" + + go: + name: Go dependency licenses + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Detect go.mod + id: manifest + run: | + if [ -f go.mod ]; then echo "found=true" >> "$GITHUB_OUTPUT"; else echo "found=false (skipping)"; echo "found=false" >> "$GITHUB_OUTPUT"; fi + + - name: Setup Go + if: steps.manifest.outputs.found == 'true' + uses: actions/setup-go@v5 + with: + go-version: 'stable' + + - name: Check dependency licenses + if: steps.manifest.outputs.found == 'true' + run: | + go install github.com/google/go-licenses@latest + "$(go env GOPATH)/bin/go-licenses" check ./... \ + --allowed_licenses=MIT,ISC,Apache-2.0,BSD-2-Clause,BSD-3-Clause,0BSD,CC0-1.0,Unlicense,MPL-2.0,CC-BY-4.0 diff --git a/.github/workflows/job-pr-checks.yaml b/.github/workflows/job-pr-checks.yaml new file mode 100644 index 0000000..491e0a7 --- /dev/null +++ b/.github/workflows/job-pr-checks.yaml @@ -0,0 +1,70 @@ +name: PR Checks +# Authoritative CI enforcement of what the local hooks check (hooks are advisory +# and may not be installed): conventional PR title, no AI-tells/emoji in the +# diff, and a PR body that follows the template + references an issue. +on: + pull_request: + types: [opened, edited, synchronize, reopened] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true +permissions: + contents: read + pull-requests: read +jobs: + pr-title: + name: PR Title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + pr-hygiene: + name: PR Hygiene + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Scan changed files + env: + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha }} + run: | + # Reuse the exact rules the local pre-commit hook uses. + source .claude/hooks/lib.sh + fail=0 + while IFS= read -r f; do + [ -f "$f" ] || continue + case "$f" in + .git/*) continue ;; + *.png|*.jpg|*.jpeg|*.gif|*.pdf|*.ico|*.svg|*.woff|*.woff2|*.ttf|*.eot|*.zip|*.gz) continue ;; + esac + # Emoji allowed in Markdown and .github; blocked elsewhere. + case "$f" in + .github/*|*.md|NOTES.txt|*/NOTES.txt) : ;; + *) e="$(gov_find_emoji < "$f")"; [ -n "$e" ] && { echo "emoji not allowed in $f:"; echo "$e"; fail=1; } ;; + esac + # AI-tells blocked except in files that document the rule. + case "$f" in + CLAUDE.md|*/CLAUDE.md|.claude/*|*/.claude/*|.github/workflows/*|*forbidden-text.txt) : ;; + *) t="$(gov_find_text_tells < "$f")"; [ -n "$t" ] && { echo "AI-tell not allowed in $f:"; echo "$t"; fail=1; } ;; + esac + done < <(git diff --name-only --diff-filter=ACM "$BASE" "$HEAD") + [ "$fail" -eq 0 ] || { echo "pr-hygiene: remove the above before merging."; exit 1; } + + pr-body: + name: PR Body + runs-on: ubuntu-latest + steps: + - name: Check body + env: + BODY: ${{ github.event.pull_request.body }} + run: | + fail=0 + printf '%s' "$BODY" | grep -qiE '(closes|fixes|resolves|refs) #[0-9]+' \ + || { echo "PR body must reference an issue (e.g. 'Closes #12')."; fail=1; } + printf '%s' "$BODY" | grep -qiE '##[[:space:]]*what' \ + || { echo "PR body must follow the template (a 'What and why' section)."; fail=1; } + [ "$fail" -eq 0 ] || exit 1 diff --git a/.gitignore b/.gitignore index a839607..0354f13 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ tools/ # OS .DS_Store + +# Local design notes (non-tracked). +plan/ diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..878a35e --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,12 @@ +title = "Gitleaks Configuration" + +[extend] +useDefault = true + +[allowlist] +description = "global allow list" +paths = [ + '''README.md''', + '''.idea''', + '''.vscode''', +] diff --git a/.licignore b/.licignore index be4d6f5..d193bfd 100644 --- a/.licignore +++ b/.licignore @@ -10,3 +10,7 @@ # Re-deny generated stubs (regenerated from .proto on every build). go/**/*.pb.go + +# Proto headers are hand-maintained in the .proto sources (golic v0.2.0 does not +# round-trip the post-syntax block comment); exclude them from the golic check. +*.proto diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..70e2f37 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,40 @@ +# AGENTS.md - api + +Guide for AI agents working in this repository. Pair with `CLAUDE.md` (the working agreement and +hook-enforced rules). Keep this file current when the build, layout, or public API changes. + +## What this is + +Shared .proto definitions and generated gRPC stubs for CryptOS-PKI. + + + +## Using api + + + +## Layout + + + +- `src/` - +- `/` - + +## Build, test, lint + + + +- Build: `` +- Test: `` (note any service/fixture the integration tests require) +- Lint: `` +- License headers / docs: `` + +## Conventions and gotchas + +- See `CLAUDE.md` for the branch/commit/PR rules; they are enforced by the git hooks in + `.claude/hooks` (run `bash .claude/hooks/install.sh` once per clone). +- diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6a73c65 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,92 @@ +# CLAUDE.md - api + +Working agreement for this repository. It was scaffolded from `Bugs5382/project-template`; +the governance below is shared across all repos created that way. + +## Enforced by hooks (run `bash .claude/hooks/install.sh` once per clone) + +- Conventional Commits on commits, issue titles, and PR titles. +- No AI tells in commits/issues/PRs/comments/source; no emoji in source or commit messages (emoji + are allowed in Markdown docs and CI workflow files). +- Pre-push: the ecosystem's format/lint/test gate must pass (Go: gofmt/vet/golangci-lint/test; + npm: lint/test scripts; Python: ruff/pytest). + +## Conventions + +- Branching: never commit to `main`. Work on a feature/working branch; open a PR. +- Commits: Conventional Commits (`type(scope): description`). The operator (@CryptOS-PKI) is the + author of record on every commit. +- Voice: human-authored. No attribution trailers (`Co-Authored-By`, `Generated with`), no robot + glyphs/emoji, no session framing. +- Local design notes live in a non-tracked `plan/` folder; delete a note when its work is done. +- GitHub Actions: a job id must be a plain identifier (a letter or `_`, then alphanumerics/`-`/`_`); + put emoji and display text in the job's `name:`, never the job key. The Actionlint check + (`.github/workflows/action-lint.yaml`) enforces this, so a malformed workflow fails at PR time + instead of silently at startup on `main`. + +## Engineering discipline + +- Root-cause before fixing: confirm the actual cause with evidence before changing code; do not + patch symptoms. +- Map every reference before removing a feature: trace its wiring across the tree first, preserve + adjacent behavior that only looks related, and defer-and-flag an entangled piece rather than + guessing it. +- Verify with evidence, not assertions: run the real check for what changed (lint, a full + template/build render, `actionlint` for workflows) before calling it done. Green CI is necessary, + not sufficient. +- One concern per branch/PR, even tiny ones โ€” it keeps reviews and the drafted changelog clean. +- When PRs interact, state an explicit merge ORDER rather than opening them and walking away: + anything a *tag* triggers needs its inputs on `main` first; a new *gate* (check) needs the + violations it catches fixed first; a workflow that builds from committed content needs that + content merged first. +- Semver framing: `breaking` only means breaking against a *released* version; removing something + that was never shipped is not a breaking change. +- Cross-repo reconciliation goes through a neutral drop-zone outside both repos โ€” never a repo + inside a repo, and no accidental gitlinks/submodules. + +## Workflow + +Issue (from a template; free-form issues are disabled) -> for sequential / multi-step work, a parent +issue with ordered **sub-issues** -> put it on the active **milestone** -> branch +`/-` -> code (comments cite the issue) -> PR with a Conventional Commit title +(the autolabeler sets the category label from the title), the template body, and a **closing +summary** before merge -> **squash** merge. The operator (@CryptOS-PKI) is the assignee. + +On merge, release-drafter drafts the next notes by label and `CHANGELOG.md` updates on `main` via the +changelog action -- **nothing tags automatically**. When the first push to main resolves the version, +rename the milestone to that version. The maintainer then **manually publishes the GitHub Release**, +which creates the tag with the finalized changelog (and triggers the publish where the repo ships a +package). + +Keep public artifacts (issues, PRs, commit messages) free of references to local-only design notes. + +## Releasing + +On every push to `main` the **Release Manager** workflow (`.github/workflows/job-version-bump.yaml`) +runs: release-drafter anticipates the next version, the manifest version is bumped (package +ecosystems only), `CHANGELOG.md` is updated via the changelog action, and a +`chore(pre-release): vX [skip ci]` commit is pushed back to `main`. **Nothing tags automatically.** +The maintainer then publishes the GitHub Release by hand, which creates the `vX.Y.Z` tag with the +finalized changelog and triggers the publish workflow where the repo ships a package. + +The Release Manager pushes to `main` through the release GitHub App, which the branch ruleset +(`governance/rulesets/branch-default.json`) lists as a bypass actor โ€” without that bypass the +`[skip ci]` commit would be rejected by the PR-required rule. + +**GitHub Action repos release differently** โ€” a composite/JS action has no package manifest and is +**not** published to a registry. The maintainer manually tags `vX.Y.Z`, publishes the GitHub +Release, and repoints the floating major tag `@vN` (e.g. `git tag -fa v1 -m "v1 -> v1.2.3" v1.2.3 && +git push origin v1 --force`) so consumers pinned to `owner/repo@vN` pick up the release; publishing +to the GitHub Marketplace is an optional manual step. The **first release is `v1.0.0` by hand** โ€” +release-drafter would otherwise draft `v0.1.0` on the first run. See `ecosystems/action/README.md` +for the full action-release sequence. + +### Docs site and first-release gotchas + +- A tag-gated GitHub Pages deploy (publishing only on `vX.Y.Z`) fails at the Deploy step unless the + auto-created `github-pages` environment allows tag refs. Once, alongside enabling Pages + (Settings -> Pages -> Source = GitHub Actions), add a tag policy, then re-run the failed Deploy + job (no need to re-cut the tag): + `gh api -X POST repos/CryptOS-PKI/api/environments/github-pages/deployment-branch-policies -f name='v*' -f type=tag` +- Docusaurus MDX 3: avoid the `## Heading {#custom-id}` explicit-id syntax (it fails to compile); + rely on the auto-generated slugs. diff --git a/Taskfile.yml b/Taskfile.yml index 1a992de..321737a 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -37,8 +37,14 @@ tasks: fi license: - desc: Inject Apache 2.0 license headers via golic + desc: Verify every source file carries the Apache 2.0 license header (CI; never writes). cmds: + - go install github.com/Bugs5382/golic/cmd/golic@v0.2.0 + - golic inject -t apache2 -c "2026 Shane" --dry -x + license:fix: + desc: Inject the Apache 2.0 header into any source file missing it. + cmds: + - go install github.com/Bugs5382/golic/cmd/golic@v0.2.0 - golic inject -t apache2 -c "2026 Shane" test: