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/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..9ae9e32 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,115 @@ +name: Bug report +description: Something is broken or behaving unexpectedly. +title: "bug: " +labels: ["bug", "triage"] + +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to file a bug report. Please fill in as much + of the form as you can — the more we know, the faster we can fix it. + + Before submitting: + - Search [existing issues](https://github.com/GrayCodeAI/hawk-sdk-go/issues) to avoid duplicates. + - If this is a security issue, please **do not** file a public issue. See `SECURITY.md`. + + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear, concise description of the bug. + placeholder: When I call hawksdk.Client., I expected X but got Y. + validations: + required: true + + - type: dropdown + id: surface + attributes: + label: Surface + description: Which SDK surface is affected? + options: + - "Client (Health / Chat / Sessions / Stats / DeleteSession)" + - "ChatStream / SSE" + - "Retry / backoff" + - "Errors (typed APIError, categories)" + - "Agent / Tool / Workflow orchestration" + - "Build / module" + validations: + required: true + + - type: textarea + id: reproduce + attributes: + label: Steps to reproduce + description: Minimal Go snippet that reliably reproduces the problem. + render: go + placeholder: | + c := hawksdk.New() + resp, err := c.Chat(context.Background(), hawksdk.ChatRequest{Message: "..."}) + // ^ wrong shape / panic / hang / etc. + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What did you expect to happen instead? + validations: + required: true + + - type: input + id: sdk-version + attributes: + label: hawk-sdk-go version + description: Output of `hawksdk.Version` or the git SHA you built from. + placeholder: "0.2.0" + validations: + required: true + + - type: input + id: daemon-version + attributes: + label: hawk daemon version + description: Output of `hawk version` (the daemon you're hitting). + placeholder: "0.2.0" + validations: + required: true + + - type: input + id: go-version + attributes: + label: Go version + description: Output of `go version`. + placeholder: "go version go1.26.1 darwin/arm64" + validations: + required: true + + - type: input + id: os + attributes: + label: Operating system + description: e.g. macOS 14.5 (arm64), Ubuntu 24.04 (amd64), Windows 11 (amd64). + placeholder: "macOS 14.5 (arm64)" + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Logs / output + description: | + Paste any relevant output, including the full error wrapping chain. + **Redact API tokens, session IDs, and any private data first.** + render: shell + + - type: checkboxes + id: confirm + attributes: + label: Confirmation + options: + - label: I searched existing issues and did not find a duplicate. + required: true + - label: I redacted any secrets, tokens, or private data from logs. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..63f3212 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/GrayCodeAI/hawk-sdk-go/security/advisories/new + about: Please report security issues privately via a GitHub Security Advisory. See SECURITY.md. + - name: Question / discussion + url: https://github.com/GrayCodeAI/hawk-sdk-go/discussions + about: Have a question or want to discuss an idea? Open a discussion instead of an issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..11956ae --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,70 @@ +name: Feature request +description: Suggest an improvement or a new SDK capability. +title: "feat: " +labels: ["enhancement", "triage"] + +body: + - type: markdown + attributes: + value: | + Thanks for proposing a feature. hawk-sdk-go is a thin Go client for + the local hawk daemon. Every feature is evaluated against whether it + serves **a single developer** running their own hawk daemon — i.e. + it improves ergonomics, lowers latency, or simplifies integration. + + Before submitting: + - Search [existing issues](https://github.com/GrayCodeAI/hawk-sdk-go/issues) to avoid duplicates. + - For new daemon endpoints, the daemon side must land first. + + - type: dropdown + id: kind + attributes: + label: Kind of feature + description: What flavour of change is this? + options: + - "New Client method (wraps a daemon endpoint)" + - "Streaming / SSE handling" + - "Retry / backoff / resilience" + - "Typed errors / error categories" + - "Agent / Tool / Workflow orchestration" + - "Configuration (options / transport)" + - "Tooling / CI / docs" + validations: + required: true + + - type: textarea + id: problem + attributes: + label: What problem are you trying to solve? + description: Describe the user problem first. Solutions can come later. + placeholder: When I call , I have to write boilerplate Y because the SDK doesn't expose X. + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: How would you like the SDK to behave? Snippet of API you'd want. + render: go + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: | + What did you try? What do other SDKs (openai-go, anthropic-sdk-go, + sashabaranov/go-openai, langchaingo) do? Why isn't that enough? + + - type: checkboxes + id: principles + attributes: + label: Solo-developer fit + description: hawk-sdk-go avoids enterprise scope. Confirm this feature respects that. + options: + - label: Works with zero configuration (sensible defaults). + - label: Does not introduce a third-party network dependency. + - label: Does not break the wire-compatibility with existing daemon versions. + - label: Has an escape hatch (override via option, transport, or env). diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ca6bae1 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,72 @@ + + +## Summary + + + +## Changes + + + +- + +## API impact + + + +## Daemon compatibility + + + +## Testing + + + +```text +$ make test +... +$ make lint +... +``` + +## Checklist + +- [ ] Commits follow [Conventional Commits](https://www.conventionalcommits.org/) + (`feat:`, `fix:`, `perf:`, `refactor:`, `docs:`, `test:`, etc.) +- [ ] `make build` (or `go build ./...`) passes +- [ ] `make lint` passes (no new lint findings, no `nolint:…` without justification) +- [ ] `make test` passes locally with `-race` enabled +- [ ] New or changed code has tests (table-driven where appropriate) +- [ ] Public APIs have godoc comments and runnable examples where helpful +- [ ] `CHANGELOG.md` updated under `## [Unreleased]` if user-visible +- [ ] `version.go` bumped if this is a release-eligible change +- [ ] Every new outbound HTTP request sets `User-Agent: hawk-sdk-go/` + via the `userAgent()` helper +- [ ] No secrets, tokens, or PII added to the repo +- [ ] No `Co-authored-by:` trailers (this is solo-developer work) diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..534aa94 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,27 @@ +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + labels: + - dependencies + - go + commit-message: + prefix: "chore(deps)" + include: scope + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 3 + labels: + - dependencies + - github-actions + commit-message: + prefix: "chore(ci)" + include: scope diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8abb3cb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +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: + # ------------------------------------------------------------------------- + # 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: ${{ 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: ${{ env.GO_VERSION }} + cache: true + - uses: golangci/golangci-lint-action@v7 + with: + 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 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + - name: Tidy check + run: | + 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: ${{ 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/.gitignore b/.gitignore new file mode 100644 index 0000000..7d6a0d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Test artifacts +*.test +*.out +coverage.html +coverage.txt + +# Build output +bin/ +dist/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Go build/mod caches +.gocache/ +.gomodcache/ + +# Go workspace (local-only) +go.work +go.work.sum diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..760c36a --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,43 @@ +version: "2" + +linters: + default: none + enable: + - errcheck + - govet + - ineffassign + - staticcheck + - unused + - misspell + - gocritic + - unconvert + - whitespace + - bodyclose + - noctx + +linters-settings: + errcheck: + check-type-assertions: true + check-blank: false + gocritic: + enabled-tags: + - diagnostic + - performance + disabled-checks: + - hugeParam + - rangeValCopy + - appendAssign + staticcheck: + checks: ["all", "-SA1019"] + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + exclude-dirs: + - vendor + exclude-rules: + - path: _test\.go + linters: + - errcheck + - gocritic + - noctx 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 new file mode 100644 index 0000000..be5ad50 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,70 @@ +# Changelog + +All notable changes to `hawk-sdk-go` are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- **`const Version = "0.2.0"`** in `version.go`, exposed as the package-level + source of truth for the SDK version. Aligns hawk-sdk-go with the rest of + the hawk-eco ecosystem (`hawk`, `tok`, `eyrie`, `yaad`, `trace`, `sight`, + `inspect`). +- **`User-Agent: hawk-sdk-go/` header** on every outbound HTTP + request — `Health`, `Chat`, `ChatStream`, `Sessions`, `Session`, + `Messages`, `DeleteSession`, `Stats`. Lets daemon operators identify + SDK clients in logs and reject misbehaving versions cleanly. +- **OSS standard files** (this is the first PR to add them): + - `README.md` — install, usage, API surface, versioning, contributing. + - `LICENSE` — MIT. + - `CONTRIBUTING.md` — quick start, branch flow (this repo branches + from `main`), conventional commits, code standards, testing. + - `SECURITY.md` — vulnerability reporting via GitHub Security + Advisories. + - `CODE_OF_CONDUCT.md` — Contributor Covenant 2.1. + - `.gitignore` — Go artifacts, IDE files, OS files, coverage output. + - `.gitattributes` — LF normalization, binary detection, GitHub + linguist hint to collapse `go.sum` in PR diffs. + - `.github/workflows/ci.yml` — race tests + coverage upload + lint + (golangci-lint v2) + multi-platform build matrix + (linux/darwin/windows × amd64/arm64) + govulncheck. + - `.github/dependabot.yml` — weekly `gomod` + `github-actions` + updates. + - `.github/PULL_REQUEST_TEMPLATE.md` — Summary / Changes / API impact + / Daemon compatibility / Testing / Checklist. + - `.github/ISSUE_TEMPLATE/bug_report.yml` — structured bug report. + - `.github/ISSUE_TEMPLATE/feature_request.yml` — feature request + with solo-dev fit checks. + - `.github/ISSUE_TEMPLATE/config.yml` — routes security to + advisories, questions to discussions, blocks blank issues. + +### Changed +- `gofmt -w` on `client.go` and `retry.go` to remove pre-existing + trailing-blank-line drift in files I touched. +- Test fixture in `client_test.go` updated to mock daemon version + `"0.2.0"` (was `"0.3.0"`) for ecosystem consistency. The test does + not assert on this value; the change is cosmetic. + +### Production-hardening pass already on this branch (commit `1028420`) +- Strict `golangci-lint` v2 config with `errcheck`, `staticcheck`, + `gocritic` (diagnostic + performance), `unused`, `ineffassign`, + `misspell`, `noctx`, `bodyclose`, `unconvert`, `whitespace`. +- All `errcheck` issues fixed (`resp.Body.Close` deferred and + error-path closes). +- Initial `Makefile`, `.editorconfig`. + +## [0.0.1] — 2026-05-13 + +### Added +- Initial Go SDK for the hawk daemon API (commits `66e1e1a`, `d04480f`): + - `Client` with `Health`, `Chat`, `ChatStream`, `Sessions`, + `Session`, `Messages`, `DeleteSession`, `Stats`. + - Typed errors with categories (`ErrCategoryRateLimited`, + `ErrCategoryUnauthorized`, `ErrCategoryNotFound`, + `ErrCategoryServer`, `ErrCategoryTransport`). + - Retry with exponential backoff, full jitter, and `Retry-After` + honouring on 429. + - SSE stream helpers. + - `Agent`, `Tool`, `Workflow` orchestration primitives. diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..19ce65b --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,19 @@ +# CODEOWNERS for hawk-sdk-go +* @GrayCodeAI/maintainers + +# Public API surface — bump SDK version (VERSION file) when these change +/client.go @GrayCodeAI/sdk-team +/agent.go @GrayCodeAI/sdk-team +/workflow.go @GrayCodeAI/sdk-team +/tools.go @GrayCodeAI/sdk-team +/types.go @GrayCodeAI/sdk-team +/errors.go @GrayCodeAI/sdk-team +/version.go @GrayCodeAI/maintainers +/VERSION @GrayCodeAI/maintainers + +# CI / release +/.github/ @GrayCodeAI/devops-team +/Makefile @GrayCodeAI/devops-team + +# Documentation +*.md @GrayCodeAI/docs-team diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ce73cb9 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,60 @@ +# Code of Conduct + +## Our pledge + +We — the maintainers and contributors of the hawk-sdk-go 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. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our standards + +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. + +Examples of unacceptable behaviour: + +- 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. + +## Enforcement + +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. + +All community leaders are obligated to respect the privacy and security of +the reporter of any incident. + +## 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). + +For answers to common questions about this code of conduct, see the +Contributor Covenant FAQ at . diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a69bc1b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,114 @@ +# Contributing to hawk-sdk-go + +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. + +``` +(): + + + + +``` + +**Types:** + +- `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 + +**Breaking changes:** add `!` after the type/scope or include `BREAKING +CHANGE:` in the footer. This triggers a major version bump. + +Examples: + +``` +feat(client): add streaming retry with exponential backoff +fix: handle empty response body in chat handler +refactor!: rename ClientV1 to Client (BREAKING CHANGE) +``` + +## Pull request checklist + +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. + +## Code review etiquette + +- 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. + +## Reporting bugs + +Open an issue using the bug-report template. Include the `hawk-sdk-go` +version (`hawk-sdk-go --version` for binaries, `hawk-sdk-go.Version` for +libraries — see this repo's `VERSION` file), reproduction steps, expected +behaviour, and actual behaviour. + +## Reporting security issues + +**Do not open a public issue.** See [SECURITY.md](./SECURITY.md) for +private reporting channels. + +## License + +By contributing, you agree that your contributions will be licensed under +the same license as this repo (see [LICENSE](./LICENSE)). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0354720 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 GrayCode AI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..807b338 --- /dev/null +++ b/Makefile @@ -0,0 +1,112 @@ +# Canonical hawk-eco Makefile for Go library repos. +# Source of truth: .shared-templates/Makefile.library.tmpl at the eco root. +# Placeholders rendered per repo: hawk-sdk-go. + +# --------------------------------------------------------------------------- +# Project metadata +# --------------------------------------------------------------------------- +NAME := hawk-sdk-go + +# --------------------------------------------------------------------------- +# 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') + +# --------------------------------------------------------------------------- +# 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 + +# --------------------------------------------------------------------------- +# Phony declarations (alphabetical). +# --------------------------------------------------------------------------- +.PHONY: all bench build ci clean cover fmt help lint lint-fix \ + security test test-10x test-race tidy version vet + +# --------------------------------------------------------------------------- +# Default target. +# --------------------------------------------------------------------------- +all: lint test build ## Default — lint, test, build. + +# --------------------------------------------------------------------------- +# Build (verify the library compiles). +# --------------------------------------------------------------------------- +build: ## Verify the library compiles. + CGO_ENABLED=0 go build ./... + +# --------------------------------------------------------------------------- +# 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 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/README.md b/README.md new file mode 100644 index 0000000..80f7bb4 --- /dev/null +++ b/README.md @@ -0,0 +1,142 @@ +# hawk-sdk-go + +Go SDK for the [hawk](https://github.com/GrayCodeAI/hawk) daemon API. + +`hawk-sdk-go` is a small, dependency-free client for the local hawk daemon +HTTP API. It exposes idiomatic Go types for chat, streaming, sessions, +messages, and aggregated stats; supports automatic retry with exponential +backoff and `Retry-After` honouring; and provides typed error categories +for clean error handling. + +It is built for **solo developers** running their own hawk daemon locally. +Nothing in this SDK calls a third-party service or phones home. + +## Install + +```bash +go get github.com/GrayCodeAI/hawk-sdk-go +``` + +## Usage + +### Health check + +```go +package main + +import ( + "context" + "fmt" + "log" + + hawksdk "github.com/GrayCodeAI/hawk-sdk-go" +) + +func main() { + c := hawksdk.New() + h, err := c.Health(context.Background()) + if err != nil { + log.Fatal(err) + } + fmt.Printf("daemon ok — version=%s sessions=%d uptime=%s\n", + h.Version, h.Sessions, h.Uptime) +} +``` + +### Chat + +```go +resp, err := c.Chat(ctx, hawksdk.ChatRequest{ + Message: "summarise this commit", + Model: "claude-opus-4-20250514", +}) +if err != nil { + return err +} +fmt.Println(resp.Response) +``` + +### Streaming + +```go +stream, err := c.ChatStream(ctx, hawksdk.ChatRequest{Message: "..."}) +if err != nil { + return err +} +defer stream.Close() + +for { + chunk, err := stream.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + fmt.Print(chunk.Delta) +} +``` + +### Custom base URL & retry + +```go +c := hawksdk.New( + hawksdk.WithBaseURL("http://hawk.local:4590"), + hawksdk.WithRetry(hawksdk.DefaultRetryConfig()), +) +``` + +### Typed errors + +```go +resp, err := c.Chat(ctx, req) +if err != nil { + var apiErr *hawksdk.APIError + if errors.As(err, &apiErr) { + switch apiErr.Category { + case hawksdk.ErrCategoryRateLimited: + // back off + case hawksdk.ErrCategoryUnauthorized: + // re-auth + case hawksdk.ErrCategoryNotFound: + // ... + } + } + return err +} +``` + +## API surface + +- `New(opts ...ClientOption) *Client` — constructor with `WithBaseURL`, + `WithHTTPClient`, `WithRetry`. +- `Client.Health(ctx) (*HealthResponse, error)` — daemon connectivity check. +- `Client.Chat(ctx, ChatRequest) (*ChatResponse, error)` — non-streaming. +- `Client.ChatStream(ctx, ChatRequest) (*StreamReader, error)` — SSE. +- `Client.Sessions(ctx, *ListOptions) (*PaginatedResponse[SessionSummary], error)` — list. +- `Client.Session(ctx, id) (*SessionDetail, error)` — get one. +- `Client.Messages(ctx, sessionID, *ListOptions) (*PaginatedResponse[Message], error)` — list messages. +- `Client.DeleteSession(ctx, id) error` — delete. +- `Client.Stats(ctx) (*StatsResponse, error)` — aggregated stats. +- `Agent` / `Tool` / `Workflow` — higher-level orchestration helpers. + +The full reference is on [pkg.go.dev](https://pkg.go.dev/github.com/GrayCodeAI/hawk-sdk-go). + +## Versioning & compatibility + +This SDK adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The `Version` constant is reported in the `User-Agent` header +(`hawk-sdk-go/0.2.0`) so daemon operators can identify SDK clients in logs. + +The SDK targets daemon API version `v1`. Breaking changes to the daemon API +will be tracked in `CHANGELOG.md` with a clear migration path. + +## Contributing + +See [`CONTRIBUTING.md`](CONTRIBUTING.md). All commits must follow +[Conventional Commits](https://www.conventionalcommits.org/) and must not +include `Co-authored-by:` trailers. + +## License + +[MIT](LICENSE) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ebe7032 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,71 @@ +# Security Policy — hawk-sdk-go + +## 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. + +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. + +## Reporting a vulnerability + +**Do not open a public GitHub issue for security vulnerabilities.** Instead: + +1. Open a private [GitHub Security Advisory](https://github.com/GrayCodeAI/hawk-sdk-go/security/advisories/new), **or** +2. Email `security@graycode.ai` with the details below. + +Include in your report: + +- 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. + +**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). + +## Disclosure policy + +We follow [coordinated vulnerability disclosure](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure): + +- 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 + +- **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. + +## Scope + +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 hawk-sdk-go integrates with (report to the + provider). +- Local filesystem misuse where an attacker already has shell access (out of + threat model). + +For hawk-sdk-go-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/client.go b/client.go index 96f91ce..403b15a 100644 --- a/client.go +++ b/client.go @@ -75,6 +75,7 @@ func (c *Client) ChatStream(ctx context.Context, req ChatRequest) (*StreamReader } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Accept", "text/event-stream") + httpReq.Header.Set("User-Agent", userAgent()) resp, err := c.httpClient.Do(httpReq) if err != nil { @@ -82,7 +83,7 @@ func (c *Client) ChatStream(ctx context.Context, req ChatRequest) (*StreamReader } if resp.StatusCode != http.StatusOK { - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() return nil, parseAPIError(resp) } @@ -124,12 +125,13 @@ func (c *Client) DeleteSession(ctx context.Context, id string) error { if err != nil { return fmt.Errorf("hawk-sdk: create request: %w", err) } + req.Header.Set("User-Agent", userAgent()) resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("hawk-sdk: delete request: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK { return parseAPIError(resp) @@ -157,12 +159,13 @@ func (c *Client) get(ctx context.Context, path string, params url.Values, out in return fmt.Errorf("hawk-sdk: create request: %w", err) } req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", userAgent()) resp, err := c.doWithRetry(ctx, req, nil) if err != nil { return fmt.Errorf("hawk-sdk: request failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return parseAPIError(resp) @@ -186,12 +189,13 @@ func (c *Client) post(ctx context.Context, path string, body interface{}, out in } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", userAgent()) resp, err := c.doWithRetry(ctx, req, data) if err != nil { return fmt.Errorf("hawk-sdk: request failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return parseAPIError(resp) @@ -216,4 +220,3 @@ func paginationParams(opts *ListOptions) url.Values { } return params } - diff --git a/client_test.go b/client_test.go index fd1b55d..2614c31 100644 --- a/client_test.go +++ b/client_test.go @@ -21,7 +21,7 @@ func TestHealth(t *testing.T) { } json.NewEncoder(w).Encode(HealthResponse{ Status: "ok", - Version: "0.3.0", + Version: "0.2.0", Uptime: "1h30m", Sessions: 2, StartedAt: "2024-01-01T00:00:00Z", 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..3ab4bd0 --- /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": "hawk-sdk-go", + "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"}] + } + } +} diff --git a/retry.go b/retry.go index 5fc09d4..60fc3d0 100644 --- a/retry.go +++ b/retry.go @@ -140,7 +140,7 @@ func (c *Client) doWithRetry(ctx context.Context, req *http.Request, body []byte } // Drain body before retry to allow connection reuse. - resp.Body.Close() + _ = resp.Body.Close() if sleepErr := sleepWithContext(ctx, backoff); sleepErr != nil { return nil, sleepErr @@ -155,4 +155,3 @@ func (c *Client) doWithRetry(ctx context.Context, req *http.Request, body []byte } return nil, lastErr } - diff --git a/version.go b/version.go new file mode 100644 index 0000000..2adff1e --- /dev/null +++ b/version.go @@ -0,0 +1,24 @@ +package hawksdk + +import ( + _ "embed" + "strings" +) + +// versionFile is the canonical version, read at compile time from the VERSION +// file at the repo root. The VERSION file is the single source of truth used +// by release tooling (release-please, goreleaser, CI). +// +//go:embed VERSION +var versionFile string + +// Version of the hawk-sdk-go library. Used in the User-Agent header on +// outbound HTTP requests so a misbehaving SDK can be identified by daemon +// logs and operators. +// +// Source of truth: the VERSION file at the repo root. Do not edit this +// constant directly — bump VERSION instead (or let release-please do it). +var Version = strings.TrimSpace(versionFile) + +// userAgent returns the User-Agent string for outbound HTTP requests. +func userAgent() string { return "hawk-sdk-go/" + Version }