diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..39f1a41 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,67 @@ +# EditorConfig — https://editorconfig.org +# Canonical eco-wide template (.shared-templates/editorconfig.tmpl). + +root = true + +# Default for everything. +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +# Go uses tabs by convention. +[*.go] +indent_style = tab +indent_size = 4 + +# Python — PEP 8. +[*.py] +indent_size = 4 + +# TypeScript / JavaScript — 2 spaces, ecosystem default. +[*.{ts,tsx,js,jsx,mjs,cjs}] +indent_size = 2 + +# Web assets. +[*.{html,css,scss}] +indent_size = 2 + +# YAML — 2 spaces (ecosystem standard, GitHub Actions, k8s, etc.). +[*.{yml,yaml}] +indent_size = 2 + +# JSON / JSONC. +[*.{json,jsonc}] +indent_size = 2 + +# TOML. +[*.toml] +indent_size = 2 + +# Markdown — 2 spaces, preserve trailing whitespace (used for line breaks). +[*.md] +trim_trailing_whitespace = false +indent_size = 2 + +# Shell scripts. +[*.{sh,bash,zsh,fish}] +indent_size = 4 + +# Makefiles must use tabs. +[{Makefile,*.mk}] +indent_style = tab + +# Dockerfiles. +[Dockerfile*] +indent_size = 4 + +# GitHub Actions workflows — 2 spaces. +[.github/**/*.{yml,yaml}] +indent_size = 2 + +# Config files. +[*.{cfg,ini,conf}] +indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3342e8f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,86 @@ +# Canonical eco-wide .gitattributes template (.shared-templates/gitattributes.tmpl). +# Auto-detect text files and normalise line endings to LF. + +* text=auto eol=lf + +# --- Source code ----------------------------------------------------------- +*.go text eol=lf diff=golang +*.py text eol=lf diff=python +*.ts text eol=lf +*.tsx text eol=lf +*.js text eol=lf +*.jsx text eol=lf +*.mjs text eol=lf +*.cjs text eol=lf +*.rs text eol=lf diff=rust + +# --- Shell + config -------------------------------------------------------- +*.sh text eol=lf +*.bash text eol=lf +*.toml text eol=lf +*.yaml text eol=lf +*.yml text eol=lf +*.json text eol=lf linguist-language=JSON +*.jsonc text eol=lf linguist-language=JSON +*.cff text eol=lf + +# --- Documentation --------------------------------------------------------- +*.md text eol=lf diff=markdown +*.txt text eol=lf + +# --- Build / packaging ---------------------------------------------------- +Makefile text eol=lf +*.mk text eol=lf +Dockerfile* text eol=lf +docker-compose*.yml text eol=lf +.github/**/*.yml text eol=lf +.github/**/*.yaml text eol=lf + +# --- Generated artefacts (mark as such for diffs and language stats) ------ +go.mod text eol=lf linguist-generated +go.sum text eol=lf linguist-generated +*.pb.go linguist-generated +*_generated.go linguist-generated +package-lock.json linguist-generated +pnpm-lock.yaml linguist-generated +yarn.lock linguist-generated + +# --- Vendored / external sources ------------------------------------------ +vendor/** linguist-vendored +node_modules/** linguist-vendored +testdata/** linguist-vendored +benchmarks/data/** linguist-vendored + +# --- Binary files (do not text-normalise) --------------------------------- +*.exe binary +*.dll binary +*.so binary +*.dylib binary +*.a binary +*.o binary +*.db binary +*.sqlite binary +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.svg text eol=lf +*.pdf binary +*.zip binary +*.tar.gz binary +*.tgz binary +*.whl binary + +# --- Source archive hygiene (excluded from `git archive`) ----------------- +.github export-ignore +.shared-templates export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.editorconfig export-ignore +.golangci.yml export-ignore +.goreleaser.yml export-ignore +.goreleaser.yaml export-ignore +testdata/ export-ignore +benchmarks/ export-ignore +e2e/ export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..067feb6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,81 @@ + + +## Summary + + + +## Changes + + + +- + +## Privacy & redaction impact + + + +## Agent compatibility + + + +## Testing + + + +```text +$ go test -race -count=1 ./... +... +$ golangci-lint run ./... +... +``` + +## Checklist + +- [ ] Commits follow [Conventional Commits](https://www.conventionalcommits.org/) + (`feat:`, `fix:`, `perf:`, `refactor:`, `docs:`, `test:`, etc.) +- [ ] `go build ./...` passes +- [ ] `golangci-lint run ./...` passes (no new lint findings, no + `nolint:…` without justification) +- [ ] `go test -race -count=1 ./...` passes locally +- [ ] e2e impact considered (relevant `e2e/` test added, updated, or + verified) — n/a if change is contained to non-runtime code +- [ ] Public APIs in `cmd/trace/cli/...` have godoc comments +- [ ] `CHANGELOG.md` updated under `## [Unreleased]` if user-visible +- [ ] No regression in `redact/` tests +- [ ] No secrets, tokens, or PII added to the repo (test fixtures use + synthetic values only) +- [ ] No `Co-authored-by:` trailers (this is solo-developer work) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68fd46c..8abb3cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,57 +1,161 @@ +# Canonical CI workflow for hawk-eco Go repos. +# Source of truth: .shared-templates/workflows/go-ci.yml.tmpl +# +# Two deployment models: +# +# 1. NOW — render this template inline into each repo's +# .github/workflows/ci.yml. Every repo has identical content. +# +# 2. LATER — once GrayCodeAI/.github exists as a central repo, move this +# file to GrayCodeAI/.github/.github/workflows/go-ci.yml with +# `on: workflow_call:`. Each repo's ci.yml becomes a 5-line caller: +# +# name: CI +# on: { push: { branches: [main] }, pull_request: } +# jobs: +# ci: +# uses: GrayCodeAI/.github/.github/workflows/go-ci.yml@main + name: CI + on: push: branches: [main, dev] pull_request: branches: [main, dev] +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + GO_VERSION: "1.26.1" + jobs: - build: + # ------------------------------------------------------------------------- + # Format + vet — fastest, fail fast. + # ------------------------------------------------------------------------- + fmt-vet: + name: fmt + vet runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.26.2' - - run: go build ./... + go-version: ${{ env.GO_VERSION }} + cache: true + - name: gofumpt diff + run: | + go install mvdan.cc/gofumpt@latest + out=$(gofumpt -l .) + if [ -n "$out" ]; then + echo "::error::gofumpt would reformat the following files:" + echo "$out" + exit 1 + fi + - name: go vet + run: go vet ./... + # ------------------------------------------------------------------------- + # Lint — golangci-lint covers most static checks. + # ------------------------------------------------------------------------- lint: + name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.26.2' + go-version: ${{ env.GO_VERSION }} + cache: true - uses: golangci/golangci-lint-action@v7 with: - version: v2.11.3 + version: v2.1.0 + install-mode: goinstall + verify: false + args: --timeout=5m + # ------------------------------------------------------------------------- + # Tests with race detector + coverage upload. + # ------------------------------------------------------------------------- test: + name: test (race + cover) runs-on: ubuntu-latest - continue-on-error: true steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.26.2' - - name: Setup Keyring + go-version: ${{ env.GO_VERSION }} + cache: true + - name: Tidy check run: | - sudo apt-get update && sudo apt-get install -y gnome-keyring - echo 'testpass' | gnome-keyring-daemon --unlock - - run: go test -race -count=1 -timeout=120s ./... - - name: Coverage - run: | - go test -coverprofile=coverage.out ./... 2>/dev/null || true - go tool cover -func=coverage.out | tail -1 || true + go mod tidy + if ! git diff --quiet; then + echo "::error::go.mod / go.sum out of date — run 'go mod tidy' and commit" + git diff + exit 1 + fi + - name: Test + run: go test ./... -race -count=1 -coverprofile=coverage.out -covermode=atomic -timeout=180s + - name: Coverage summary + run: go tool cover -func=coverage.out | tail -1 + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: coverage + path: coverage.out + # ------------------------------------------------------------------------- + # Security scan — vulnerability database + (optional) gosec. + # ------------------------------------------------------------------------- security: + name: security + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + - name: govulncheck + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + govulncheck ./... + - name: gosec (advisory) + continue-on-error: true + run: | + go install github.com/securego/gosec/v2/cmd/gosec@latest + gosec -exclude=G104,G301,G302,G304,G306 ./... + + # ------------------------------------------------------------------------- + # Cross-platform build matrix — only for repos that produce a binary. + # Repos that are pure libraries can keep this job (it'll just `go build ./...`) + # or remove it locally. + # ------------------------------------------------------------------------- + build: + name: build (${{ matrix.goos }}/${{ matrix.goarch }}) runs-on: ubuntu-latest + needs: [fmt-vet, lint, test] + strategy: + fail-fast: false + matrix: + goos: [linux, darwin, windows] + goarch: [amd64, arm64] + exclude: + - goos: windows + goarch: arm64 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.26.2' - - name: Install govulncheck - run: go install golang.org/x/vuln/cmd/govulncheck@latest - - name: Run govulncheck - run: govulncheck ./... || true + go-version: ${{ env.GO_VERSION }} + cache: true + - name: Build + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: "0" + run: go build ./... diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..639f55f --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,43 @@ +# Canonical release-please workflow for hawk-eco repos. +# Opens / updates a release PR on every push to main; on merge of that PR, +# tags the new release. The tag triggers goreleaser (separate workflow). +# +# Source of truth: .shared-templates/release-please.yml.tmpl at the eco root. + +name: release-please + +on: + push: + branches: [main] + +permissions: + contents: write + pull-requests: write + issues: write + +concurrency: + group: release-please-${{ github.ref }} + cancel-in-progress: false + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - name: Run release-please + id: release + uses: googleapis/release-please-action@v4 + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Summary + if: always() + run: | + if [[ "${{ steps.release.outputs.release_created }}" == "true" ]]; then + echo "Released ${{ steps.release.outputs.tag_name }}." >> $GITHUB_STEP_SUMMARY + elif [[ "${{ steps.release.outputs.pr }}" != "" ]]; then + echo "Updated release PR: ${{ steps.release.outputs.pr }}" >> $GITHUB_STEP_SUMMARY + else + echo "No release-relevant changes detected." >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95490f6..0ab66ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,136 +1,41 @@ -name: Release +# Canonical release workflow for hawk-eco Go binary repos. +# Triggered by release-please when it pushes a v* tag. +# Source of truth: .shared-templates/workflows/go-release.yml.tmpl + +name: release on: - workflow_dispatch: - inputs: - version: - description: "Release tag (semver with v prefix, e.g. v1.2.3)" - required: true - type: string push: - tags: - - "v*" + tags: ["v*"] permissions: contents: write - -env: - RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version || github.ref_name }} + packages: write + id-token: write # for cosign keyless signing if enabled later jobs: - release: + goreleaser: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 - ref: ${{ env.RELEASE_TAG }} - - - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4 - - - name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - name: Checkout + uses: actions/checkout@v4 with: - cache: false - go-version-file: go.mod + fetch-depth: 0 # goreleaser needs full history for changelog - - name: Generate Homebrew Tap token - id: app-token - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + - name: Set up Go + uses: actions/setup-go@v5 with: - app-id: ${{ secrets.HOMEBREW_TAP_APP_ID }} - private-key: ${{ secrets.HOMEBREW_TAP_APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - repositories: | - homebrew-tap - scoop-bucket - - - name: Detect release type - id: release-type - run: | - VERSION="${RELEASE_TAG#v}" - if [[ "$VERSION" == *-* ]]; then - echo "prerelease=true" >> "$GITHUB_OUTPUT" - else - echo "prerelease=false" >> "$GITHUB_OUTPUT" - fi - - - name: Extract release notes from CHANGELOG.md - if: steps.release-type.outputs.prerelease == 'false' - run: | - VERSION="${RELEASE_TAG#v}" - awk -v ver="$VERSION" 'BEGIN{header="^## \\[" ver "\\]"} $0 ~ header{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md > "$RUNNER_TEMP/release_notes.md" - if [ ! -s "$RUNNER_TEMP/release_notes.md" ]; then - echo "::error::No changelog entry found for version ${VERSION} in CHANGELOG.md" - exit 1 - fi - - - name: Generate nightly release notes - if: steps.release-type.outputs.prerelease == 'true' - run: | - LAST_NIGHTLY=$(git tag -l 'v*-nightly.*' --sort=-creatordate | grep -v "^${RELEASE_TAG}$" | head -1) - if [ -n "$LAST_NIGHTLY" ]; then - BASE_TAG="$LAST_NIGHTLY" - else - BASE_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude '*-*' HEAD 2>/dev/null || echo "") - fi - echo "## Nightly Build (${RELEASE_TAG})" > "$RUNNER_TEMP/release_notes.md" - echo "" >> "$RUNNER_TEMP/release_notes.md" - if [ -n "$BASE_TAG" ]; then - echo "Changes since ${BASE_TAG}:" >> "$RUNNER_TEMP/release_notes.md" - echo "" >> "$RUNNER_TEMP/release_notes.md" - git log --oneline "${BASE_TAG}..HEAD" --no-merges >> "$RUNNER_TEMP/release_notes.md" - fi + go-version: "1.26.1" + cache: true - name: Run GoReleaser - uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7 + uses: goreleaser/goreleaser-action@v6 with: - distribution: goreleaser-pro - version: latest - args: release --clean --release-notes=${{ runner.temp }}/release_notes.md + distribution: goreleaser + version: "~> v2" + args: release --clean env: - GORELEASER_CURRENT_TAG: ${{ env.RELEASE_TAG }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAP_GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} - MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} - MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} - MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} - MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} - MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} - POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} - POSTHOG_ENDPOINT: ${{ vars.POSTHOG_ENDPOINT }} - DISCORD_WEBHOOK_ID: ${{ secrets.DISCORD_WEBHOOK_ID }} - DISCORD_WEBHOOK_TOKEN: ${{ secrets.DISCORD_WEBHOOK_TOKEN }} - - notify-slack: - runs-on: ubuntu-latest - needs: [release] - if: ${{ always() && needs.release.result == 'failure' }} - steps: - - name: Notify Slack of release failure - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 - with: - webhook: ${{ secrets.E2E_SLACK_WEBHOOK_URL }} - webhook-type: incoming-webhook - payload: | - { - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": ":red_circle: *Release Failed* for `${{ env.RELEASE_TAG }}`\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View run details>" - } - }, - { - "type": "context", - "elements": [ - { - "type": "mrkdwn", - "text": "Tag: `${{ env.RELEASE_TAG }}` by ${{ github.actor }}" - } - ] - } - ] - } + # Optional secrets used by some repos' goreleaser configs: + HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..2be9c43 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.2.0" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 5426253..44c65bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,26 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). This project ad --- +## [Unreleased] + +### Changed +- **Version re-baselined to `0.2.0`** in + `cmd/trace/cli/versioninfo/versioninfo.go`. Aligns trace with the rest + of the hawk-eco ecosystem (`hawk`, `tok`, `eyrie`, `yaad`, `sight`, + `inspect`). + +### Added — Production hygiene (top-50 OSS parity) +- `.gitattributes` — LF line-ending normalization, binary detection, + GitHub linguist hints (collapse `go.sum` in PR diffs, mark `docs/**` + and the symlinked `AGENTS.md` as documentation). +- `.editorconfig` — consistent formatting across editors (UTF-8, LF + newlines, 2-space YAML, tabs for Go, final newline + trim trailing + whitespace). +- `.github/PULL_REQUEST_TEMPLATE.md` — Summary / Changes / Privacy & + redaction impact / Agent compatibility / Testing / Checklist. The + privacy/redaction section is specific to trace because every change + in `redact/` can leak PII or secrets if regressed. + ## [0.1.0] - 2026-05-03 ### Added diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index c8c5f74..0b126e5 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,56 +1,60 @@ # Code of Conduct -We are committed to providing a welcoming, respectful, and harassment-free environment for everyone. +## Our pledge ---- +We — the maintainers and contributors of the trace project — pledge to +make participation in our community a harassment-free experience for everyone, +regardless of age, body size, visible or invisible disability, ethnicity, sex +characteristics, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. -## Scope +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. -This applies to all Trace community spaces: +## Our standards -- GitHub (repos, issues, PRs, discussions) -- Discord -- Events and public representation +Examples of behaviour that contributes to a positive environment: ---- +- Demonstrating empathy and kindness toward other people. +- Being respectful of differing opinions, viewpoints, and experiences. +- Giving and gracefully accepting constructive feedback. +- Accepting responsibility, apologising to those affected by mistakes, and + learning from the experience. +- Focusing on what is best not just for us as individuals, but for the + overall community. -## Expected Behavior +Examples of unacceptable behaviour: -- Be respectful and professional -- Use inclusive language -- Give and accept constructive feedback gracefully -- Own your mistakes +- The use of sexualised language or imagery, and sexual attention or advances. +- Trolling, insulting or derogatory comments, and personal or political + attacks. +- Public or private harassment. +- Publishing others' private information, such as a physical or email + address, without their explicit permission. +- Other conduct which could reasonably be considered inappropriate in a + professional setting. -## Unacceptable Behavior - -- Harassment, discrimination, or personal attacks -- Unwelcome sexual attention or imagery -- Sharing private information without consent -- Sustained disruption of discussions -- Any behavior inappropriate in a professional setting - ---- - -## Reporting - -Contact **[conduct@graycode.ai](mailto:conduct@graycode.ai)**. +## Enforcement -All reports are confidential. Include: -- Who was involved -- What happened and when -- Any supporting context +Community leaders are responsible for clarifying and enforcing our standards +of acceptable behaviour, and will take appropriate and fair corrective +action in response to any behaviour they deem inappropriate, threatening, +offensive, or harmful. ---- +Instances of abusive, harassing, or otherwise unacceptable behaviour may be +reported to the maintainers via the contact in `SECURITY.md` or by opening a +confidential GitHub Security Advisory at +. All +complaints will be reviewed and investigated promptly and fairly. -## Enforcement +All community leaders are obligated to respect the privacy and security of +the reporter of any incident. -| Level | For | Action | -|---|---|---| -| Correction | First minor violation | Private notice | -| Warning | Pattern of minor violations | Formal warning, no-contact period | -| Temp Ban | Serious violation | 30-90 day ban from all spaces | -| Permanent Ban | Repeated or severe violations | Permanent removal | +## Attribution ---- +This Code of Conduct is adapted from the +[Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html). -Adapted from the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). +For answers to common questions about this code of conduct, see the +Contributor Covenant FAQ at . diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ec05e8..aa7984a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,125 +1,114 @@ -# Contributing +# Contributing to trace + +Thanks for your interest! This guide covers the conventions used across the +hawk-eco. The eco-wide standards (versioning, release tooling, repo layout) +are defined in . + +## Quick start + +1. Fork the repo and create a feature branch off `main`: + ```bash + git checkout -b feat/short-description + ``` +2. Make your changes in small, focused commits. +3. Run the full local check before pushing: + ```bash + make ci + ``` +4. Open a pull request. CI will re-run the same checks plus security + scanning, race-detector tests, and (where applicable) integration tests. + +## Build & test + +This repo uses the standardised hawk-eco Makefile targets. Run `make help` +for the full list. The most common targets: + +| Target | What it does | +| ------------------- | ------------------------------------------------ | +| `make build` | Build the binary / verify the library compiles | +| `make test` | Run unit tests | +| `make test-race` | Run unit tests with the race detector | +| `make cover` | Generate a coverage report | +| `make lint` | Run the linter (`golangci-lint` / `ruff`) | +| `make fmt` | Format source files | +| `make vet` | Run `go vet` / `mypy` | +| `make security` | Run `govulncheck` / `pip-audit` | +| `make ci` | Run everything CI runs (the gate before pushing) | + +## Commit message convention + +We use [Conventional Commits](https://www.conventionalcommits.org/). This +isn't cosmetic — release-please reads commit messages to bump the `VERSION` +file and generate the CHANGELOG, so getting them right matters. -Thanks for your interest in Trace! We welcome contributions of all kinds. - ---- - -## Before You Start - -1. **Open an issue** describing your idea or the bug you found -2. **Wait for maintainer feedback** — we may have context that saves you time -3. **Get the green light**, then start coding - -This avoids wasted effort on work that doesn't align with the project direction. - ---- - -## Getting Started - -### Prerequisites - -- Go 1.26+ -- [mise](https://mise.jdx.dev/) — `curl https://mise.run | sh` - -### Setup - -```bash -git clone https://github.com/GrayCodeAI/trace.git -cd trace -mise trust && mise install -go mod download -mise run build -mise run test ``` +(): -### Good First Issues - -Look for issues labeled `good-first-issue`. Great starting points: + -- Documentation improvements -- Test coverage -- Small bug fixes + +``` ---- +**Types:** -## Development Workflow +- `feat:` — a new feature (triggers a minor version bump) +- `fix:` — a bug fix (triggers a patch version bump) +- `perf:` — performance improvement +- `refactor:` — code restructure with no behaviour change +- `docs:` — documentation only +- `test:` — adding or fixing tests +- `build:` — build system or dependencies +- `ci:` — CI configuration +- `chore:` — anything else (no release effect) +- `revert:` — reverts a previous commit -```bash -# Create a branch -git checkout -b feature/your-feature +**Breaking changes:** add `!` after the type/scope or include `BREAKING +CHANGE:` in the footer. This triggers a major version bump. -# Make changes, then verify -mise run fmt # Format -mise run lint # Lint -mise run test # Test +Examples: -# Commit -git commit -m "Add: description of your change" ``` - -### Code Style - -- Standard Go idioms -- All errors handled explicitly -- `gofmt` and `golangci-lint` must pass -- See [CLAUDE.md](CLAUDE.md) for architecture patterns - -### Testing - -```bash -mise run test # Unit tests -mise run test:integration # Integration tests -mise run test:ci # Full CI suite +feat(client): add streaming retry with exponential backoff +fix: handle empty response body in chat handler +refactor!: rename ClientV1 to Client (BREAKING CHANGE) ``` ---- - -## Pull Requests - -### Checklist - -- [ ] Related issue exists and is approved -- [ ] `mise run lint` passes -- [ ] `mise run test` passes -- [ ] New code has tests -- [ ] PR description explains *what* and *why* - -### Process - -1. Push your branch -2. Open a PR against `main` -3. Link the related issue -4. Address review feedback -5. Maintainer merges - ---- - -## Security - -Found a vulnerability? **Do not** open a public issue. +## Pull request checklist -Email [security@graycode.ai](mailto:security@graycode.ai) instead. See [SECURITY.md](SECURITY.md). +Before requesting review: ---- +- [ ] `make ci` passes locally. +- [ ] New behaviour has tests; bug fixes have a regression test. +- [ ] `CHANGELOG.md` entries are **not** edited manually — release-please + generates them from your commit messages. +- [ ] The `VERSION` file is **not** edited manually — release-please bumps + it on release. +- [ ] Public API changes have updated doc comments. +- [ ] No secrets, API keys, or PII in code, comments, tests, or fixtures. -## Community +## Code review etiquette -- [GitHub Issues](https://github.com/GrayCodeAI/trace/issues) — bugs & features -- [Discord](https://discord.gg/ZESTkgxGF) — questions & discussion +- Reviewers focus on correctness, design, and tests; formatting is + enforced by tooling, not humans. +- Authors respond to every comment (resolved, addressed, or politely + declined with rationale) — no silent dismissals. +- Squash-merge by default; the PR title becomes the commit (so it must + be a valid Conventional Commit message). +- One approving review from a CODEOWNERS-listed reviewer is required. -Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before participating. +## Reporting bugs ---- +Open an issue using the bug-report template. Include the `trace` +version (`trace --version` for binaries, `trace.Version` for +libraries — see this repo's `VERSION` file), reproduction steps, expected +behaviour, and actual behaviour. -## Resources +## Reporting security issues -| Document | Purpose | -|---|---| -| [README.md](README.md) | Setup & usage | -| [CLAUDE.md](CLAUDE.md) | Architecture & internals | -| [AGENTS.md](AGENTS.md) | Agent integration guide | -| [SECURITY.md](SECURITY.md) | Vulnerability reporting | +**Do not open a public issue.** See [SECURITY.md](./SECURITY.md) for +private reporting channels. ---- +## License -Thank you for helping make Trace better. +By contributing, you agree that your contributions will be licensed under +the same license as this repo (see [LICENSE](./LICENSE)). diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..42d221e --- /dev/null +++ b/Makefile @@ -0,0 +1,126 @@ +# Canonical hawk-eco Makefile for Go binary repos. +# Source of truth: .shared-templates/Makefile.binary.tmpl at the eco root. +# Placeholders rendered per repo: trace, ./cmd/trace. + +# --------------------------------------------------------------------------- +# Project metadata +# --------------------------------------------------------------------------- +NAME := trace +MAIN_PKG := ./cmd/trace + +# --------------------------------------------------------------------------- +# Versioning — sourced from VERSION file; falls back to git describe. +# See https://github.com/GrayCodeAI/hawk/blob/main/VERSIONING.md. +# --------------------------------------------------------------------------- +VERSION ?= $(shell cat VERSION 2>/dev/null | head -n1 | tr -d '[:space:]' || git describe --tags --always --dirty 2>/dev/null || echo "dev") +COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "none") +DATE := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ') + +LDFLAGS := -s -w \ + -X main.Version=$(VERSION) \ + -X main.Commit=$(COMMIT) \ + -X main.BuildDate=$(DATE) + +# --------------------------------------------------------------------------- +# Tooling — pinned, install if missing. +# --------------------------------------------------------------------------- +GOBIN_DIR := $(shell go env GOPATH)/bin +GOLANGCI := $(GOBIN_DIR)/golangci-lint +GOFUMPT := $(GOBIN_DIR)/gofumpt +GOIMPORTS := $(GOBIN_DIR)/goimports +GOVULNCHECK := $(GOBIN_DIR)/govulncheck +GORELEASER := $(GOBIN_DIR)/goreleaser + +# --------------------------------------------------------------------------- +# Phony declarations (alphabetical). +# --------------------------------------------------------------------------- +.PHONY: all bench build ci clean cover fmt help install lint lint-fix \ + release security test test-10x test-race tidy version vet + +# --------------------------------------------------------------------------- +# Default target. +# --------------------------------------------------------------------------- +all: lint test build ## Default — lint, test, build. + +# --------------------------------------------------------------------------- +# Build / install / release. +# --------------------------------------------------------------------------- +build: ## Build the binary into bin/$(NAME). + CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o bin/$(NAME) $(MAIN_PKG) + +install: ## Install the binary to $GOBIN. + CGO_ENABLED=0 go install -ldflags="$(LDFLAGS)" $(MAIN_PKG) + +release: ## Cut a release via goreleaser (requires a clean tree + tag). + @command -v $(GORELEASER) >/dev/null 2>&1 || (echo "install: go install github.com/goreleaser/goreleaser/v2@latest" && exit 1) + $(GORELEASER) release --clean + +# --------------------------------------------------------------------------- +# Tests. +# --------------------------------------------------------------------------- +test: ## Run unit tests. + go test ./... -count=1 -timeout=120s + +test-race: ## Run unit tests with the race detector. + go test ./... -race -count=1 -timeout=180s + +test-10x: ## Run tests 10 times to surface flakes. + go test ./... -race -count=10 -timeout=600s + +cover: ## Generate a coverage report (coverage.out + coverage.html). + go test ./... -race -coverprofile=coverage.out -covermode=atomic -timeout=180s + @go tool cover -func=coverage.out | grep "^total:" + @go tool cover -html=coverage.out -o coverage.html + @echo "Coverage report: coverage.html" + +bench: ## Run benchmarks. + go test ./... -bench=. -benchmem -count=3 -timeout=300s + +# --------------------------------------------------------------------------- +# Quality gates. +# --------------------------------------------------------------------------- +fmt: ## Format source files (gofumpt + goimports). + @command -v $(GOFUMPT) >/dev/null 2>&1 || (echo "install: go install mvdan.cc/gofumpt@latest" && exit 1) + @command -v $(GOIMPORTS) >/dev/null 2>&1 || (echo "install: go install golang.org/x/tools/cmd/goimports@latest" && exit 1) + $(GOFUMPT) -w . + $(GOIMPORTS) -w . + +vet: ## Run go vet. + go vet ./... + +lint: ## Run golangci-lint. + @command -v $(GOLANGCI) >/dev/null 2>&1 || (echo "install: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest" && exit 1) + $(GOLANGCI) run ./... --timeout=5m + +lint-fix: ## Run golangci-lint with --fix. + @command -v $(GOLANGCI) >/dev/null 2>&1 || (echo "install: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest" && exit 1) + $(GOLANGCI) run ./... --fix --timeout=5m + +security: ## Run govulncheck. + @command -v $(GOVULNCHECK) >/dev/null 2>&1 || (echo "install: go install golang.org/x/vuln/cmd/govulncheck@latest" && exit 1) + $(GOVULNCHECK) ./... + +tidy: ## Tidy go.mod / go.sum. + go mod tidy + go mod verify + +# --------------------------------------------------------------------------- +# Composite gate used by CI and pre-push. +# --------------------------------------------------------------------------- +ci: tidy fmt vet lint test-race security ## Run everything CI runs. + @echo "All CI checks passed." + +# --------------------------------------------------------------------------- +# Misc. +# --------------------------------------------------------------------------- +version: ## Print the version that will be embedded. + @echo "Version: $(VERSION)" + @echo "Commit: $(COMMIT)" + @echo "Date: $(DATE)" + +clean: ## Remove build artefacts. + rm -rf bin/ dist/ coverage.out coverage.html + go clean -testcache + +help: ## Show this help. + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' diff --git a/SECURITY.md b/SECURITY.md index cb993ff..df6a581 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,54 +1,71 @@ -# Security Policy +# Security Policy — trace -We take security seriously. Thank you for helping keep Trace and its users safe. +## Supported versions ---- +We support the latest minor version on each `0.x` line, and the latest two +minor versions once `1.x` ships. Older versions receive critical-severity +fixes only on a best-effort basis. -## Reporting a Vulnerability +The current canonical version is the contents of the [`VERSION`](./VERSION) +file at the repo root. See [`VERSIONING.md`](https://github.com/GrayCodeAI/hawk/blob/main/VERSIONING.md) +for the eco-wide versioning scheme. -**Do not report security issues through public GitHub issues.** +## Reporting a vulnerability -Email **[security@graycode.ai](mailto:security@graycode.ai)** with: +**Do not open a public GitHub issue for security vulnerabilities.** Instead: -1. Clear description of the vulnerability -2. Impact assessment -3. Steps to reproduce -4. Affected versions (if known) -5. Suggested fix (optional) +1. Open a private [GitHub Security Advisory](https://github.com/GrayCodeAI/trace/security/advisories/new), **or** +2. Email `security@graycode.ai` with the details below. -### Response Timeline +Include in your report: -| Stage | Timeframe | -|---|---| -| Acknowledgment | Within 48 hours | -| Status update | Within 7 days | -| Resolution target | Within 90 days for critical issues | +- A description of the vulnerability and the affected component. +- Steps to reproduce, ideally with a minimal proof-of-concept. +- The version (`VERSION` file or git SHA) you tested against. +- The potential impact and any suggested mitigation. -All reports are kept confidential. +**Response targets:** ---- +- Initial acknowledgement: within **48 hours**. +- Triage and severity assessment: within **5 business days**. +- Coordinated fix and disclosure: within **30 days** for high/critical, **90 + days** for medium/low (per industry-standard responsible disclosure). -## Scope +## Disclosure policy -**In scope:** -- The Trace CLI (`trace` binary) -- Official GrayCode AI repositories -- Trace services at graycode.ai +We follow [coordinated vulnerability disclosure](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure): -**Out of scope:** -- Third-party dependency issues (report upstream) -- Social engineering -- Denial of service attacks -- Issues requiring physical device access +- Reporters receive credit in the advisory and CHANGELOG (unless they opt + out). +- We request that reporters refrain from public disclosure until a fix has + been released or the disclosure deadline above has elapsed. +- We will not pursue legal action against good-faith researchers acting + within this policy. ---- +## Security practices in this repo -## Security Advisories +- **Dependency monitoring:** automated via Dependabot (see + `.github/dependabot.yml`). +- **Static analysis:** `golangci-lint` / `ruff` / `mypy` enforced in CI. +- **Vulnerability scanning:** `govulncheck` (Go) / `pip-audit` (Python) run + on every CI build. +- **Lockfiles:** `go.sum` / `pnpm-lock.yaml` / `pyproject.toml` are pinned + and committed. +- **Reproducible builds:** release artefacts ship with SHA-256 checksums via + goreleaser. +- **No secrets in source:** API keys are configuration, not constants. Pre- + commit hooks block accidental secret commits. -Advisories are issued for vulnerabilities exploitable by remote or non-local actors. +## Scope -Local-only issues (ReDoS in local execution, resource exhaustion requiring local access) are treated as bug reports — use [GitHub Issues](https://github.com/GrayCodeAI/trace/issues). +This policy covers the code in this repository and the release artefacts +published from it. It does not cover: ---- +- Third-party dependencies (report to upstream). +- LLM provider services that trace integrates with (report to the + provider). +- Local filesystem misuse where an attacker already has shell access (out of + threat model). -Thank you for responsible disclosure. +For trace-specific threat-model notes, see the README and any docs in +this repo. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..0ea3a94 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.2.0 diff --git a/cmd/trace/cli/versioninfo/versioninfo.go b/cmd/trace/cli/versioninfo/versioninfo.go index ec05c25..fa67341 100644 --- a/cmd/trace/cli/versioninfo/versioninfo.go +++ b/cmd/trace/cli/versioninfo/versioninfo.go @@ -1,7 +1,23 @@ +// Package versioninfo exposes the trace CLI version metadata. +// +// The Version and Commit variables are set at build time via ldflags by +// goreleaser: +// +// -X github.com/GrayCodeAI/trace/cmd/trace/cli/versioninfo.Version={{.Version}} +// -X github.com/GrayCodeAI/trace/cmd/trace/cli/versioninfo.Commit={{.ShortCommit}} +// +// The VERSION file at the repo root is the canonical source of truth used by +// release tooling (release-please) and CI; goreleaser derives the version +// from the matching git tag at release time. +// +// The default "dev" value applies only to local builds without ldflags, so +// developers can immediately see when they're running an unreleased binary. package versioninfo -// Version and Commit are set at build time via ldflags. var ( - Version = "0.1.0" - Commit = "unknown" + // Version is the semantic version. Set via ldflags at release time. + Version = "dev" + + // Commit is the git commit short SHA. Set via ldflags at release time. + Commit = "none" ) diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..ba5700d --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,112 @@ +# Canonical lefthook config for hawk-eco Go repos. +# Source of truth: .shared-templates/lefthook.yml.tmpl +# +# Install lefthook: +# brew install lefthook (macOS) +# go install github.com/evilmartians/lefthook@latest +# npm install -g lefthook (cross-platform) +# +# Activate hooks in this repo (one time): +# lefthook install +# +# Skip hooks for a single commit (use sparingly): +# LEFTHOOK=0 git commit -m "..." + +# --------------------------------------------------------------------------- +# pre-commit — runs before commit creation, on staged files only. +# --------------------------------------------------------------------------- +pre-commit: + parallel: true + commands: + + fmt: + glob: "*.go" + run: | + if ! command -v gofumpt >/dev/null 2>&1; then + echo "lefthook: gofumpt not installed (go install mvdan.cc/gofumpt@latest)"; exit 1 + fi + gofumpt -w {staged_files} + stage_fixed: true + + imports: + glob: "*.go" + run: | + if ! command -v goimports >/dev/null 2>&1; then + echo "lefthook: goimports not installed (go install golang.org/x/tools/cmd/goimports@latest)"; exit 1 + fi + goimports -w {staged_files} + stage_fixed: true + + lint: + glob: "*.go" + run: | + if ! command -v golangci-lint >/dev/null 2>&1; then + echo "lefthook: golangci-lint not installed — skipping (install: https://golangci-lint.run/usage/install/)" + exit 0 + fi + golangci-lint run --new-from-rev=HEAD~1 --fix {staged_files} + stage_fixed: true + + yaml-lint: + glob: "*.{yml,yaml}" + run: | + # Quick syntax check via Python's yaml module (already on most systems). + for f in {staged_files}; do + python3 -c "import sys, yaml; yaml.safe_load(open(sys.argv[1]))" "$f" || exit 1 + done + + forbidden-strings: + run: | + # Catch obvious credential-shaped strings in staged additions. + bad=$(git diff --cached --diff-filter=AM -U0 -- {staged_files} \ + | grep -E '^\+' \ + | grep -Ei '(aws_secret|password\s*=|api[_-]?key\s*=|BEGIN [A-Z]+ PRIVATE KEY)' \ + | grep -v 'example\|placeholder\|TODO\|x-release-please' || true) + if [ -n "$bad" ]; then + echo "lefthook: possible secret in staged changes:" + echo "$bad" + echo "If this is a false positive, bypass with: LEFTHOOK=0 git commit" + exit 1 + fi + +# --------------------------------------------------------------------------- +# pre-push — heavier checks, runs only on push (not every commit). +# --------------------------------------------------------------------------- +pre-push: + commands: + + test: + run: go test ./... -count=1 -timeout=60s + + vet: + run: go vet ./... + + govulncheck: + run: | + if ! command -v govulncheck >/dev/null 2>&1; then + echo "lefthook: govulncheck not installed — skipping" + exit 0 + fi + govulncheck ./... + +# --------------------------------------------------------------------------- +# commit-msg — validate Conventional Commits (release-please depends on it). +# --------------------------------------------------------------------------- +commit-msg: + commands: + + conventional-commit: + run: | + msg=$(head -n1 "{1}") + # Allow merge commits, revert commits, and release-please bot commits to bypass. + case "$msg" in + "Merge "*|"Revert "*|"chore(main): release"*) exit 0 ;; + esac + # Conventional commits regex. + if ! echo "$msg" | grep -qE '^(feat|fix|perf|refactor|test|docs|build|ci|chore|revert|style)(\([a-z0-9 _-]+\))?!?: .{1,72}$'; then + echo "lefthook: commit message does not follow Conventional Commits." + echo " format: (): " + echo " example: feat(client): add streaming retry" + echo " full guide: https://www.conventionalcommits.org/" + exit 1 + fi diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..29251f0 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "release-type": "go", + "package-name": "trace", + "include-v-in-tag": true, + "include-component-in-tag": false, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance" }, + { "type": "refactor", "section": "Refactoring" }, + { "type": "revert", "section": "Reverts" }, + { "type": "docs", "section": "Documentation", "hidden": false }, + { "type": "test", "section": "Tests", "hidden": false }, + { "type": "build", "section": "Build", "hidden": true }, + { "type": "ci", "section": "CI", "hidden": true }, + { "type": "chore", "section": "Chores", "hidden": true }, + { "type": "style", "section": "Style", "hidden": true } + ], + "extra-files": [{"type":"version-txt","path":"VERSION"}] + } + } +}