diff --git a/.env.example b/.env.example index b9feede..c5b0623 100644 --- a/.env.example +++ b/.env.example @@ -53,4 +53,4 @@ # Observability # ----------------------------------------------------------------------------- # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 -# OTEL_SERVICE_NAME= +# OTEL_SERVICE_NAME=stoopid-commons diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml new file mode 100644 index 0000000..4abc977 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -0,0 +1,78 @@ +name: Bug Report +description: Report a bug or issue +title: "[Bug]: " +type: "Bug" +projects: + - "conversadocs/21" +body: + - type: markdown + attributes: + value: | + ## Description + + - type: textarea + id: description + attributes: + label: Description + description: Describe the bug in detail. + validations: + required: true + + - type: markdown + attributes: + value: | + ## Steps to Reproduce + + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: Provide the steps needed to reproduce the bug. + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See error + validations: + required: true + + - type: markdown + attributes: + value: | + ## Expected Behavior + + - type: textarea + id: expected_behavior + attributes: + label: Expected Behavior + description: What should happen instead? + validations: + required: true + + - type: markdown + attributes: + value: | + ## Screenshots + + - type: input + id: screenshots + attributes: + label: Screenshots + description: You can upload screenshots by dragging and dropping them into this field. + placeholder: "Attach screenshots or videos" + validations: + required: false + + - type: markdown + attributes: + value: | + ## Additional Context + + - type: textarea + id: context + attributes: + label: Additional Context + description: Any other information that might be helpful. + placeholder: "Add additional context here..." + validations: + required: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 478bc79..0752201 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,33 +16,11 @@ permissions: contents: read jobs: - # =========================================================================== - # Template guard — short-circuits the entire workflow if .template-repo - # marker file exists. Delete .template-repo to enable real CI runs. - # =========================================================================== - guard: - name: Template guard - runs-on: ubuntu-latest - outputs: - is_template: ${{ steps.check.outputs.is_template }} - steps: - - uses: actions/checkout@v6 - - id: check - run: | - if [ -f .template-repo ]; then - echo "::notice::This is the unconfigured template repo. Skipping CI." - echo "is_template=true" >> "$GITHUB_OUTPUT" - else - echo "is_template=false" >> "$GITHUB_OUTPUT" - fi - # =========================================================================== # Path filter — determines which language jobs need to run # =========================================================================== changes: name: Detect changes - needs: guard - if: needs.guard.outputs.is_template != 'true' runs-on: ubuntu-latest outputs: python: ${{ steps.filter.outputs.python }} @@ -279,16 +257,11 @@ jobs: ci-passed: name: CI passed runs-on: ubuntu-latest - needs: [ guard, changes, python, typescript, rust, terraform, docker ] + needs: [ changes, python, typescript, rust, terraform, docker ] if: always() steps: - name: Verify all required jobs succeeded or were skipped run: | - # If the template guard fired, allow the workflow to pass. - if [ "${{ needs.guard.outputs.is_template }}" = "true" ]; then - echo "Template guard active. CI skipped intentionally." - exit 0 - fi # Any required job that ran must have succeeded. # Skipped jobs (because their language wasn't changed) are fine. results='${{ toJSON(needs) }}' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2e4c8f6..cf9268c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -18,30 +18,8 @@ permissions: contents: read jobs: - # =========================================================================== - # Template guard — short-circuits the entire workflow if .template-repo - # marker file exists. Delete .template-repo to enable real CI runs. - # =========================================================================== - guard: - name: Template guard - runs-on: ubuntu-latest - outputs: - is_template: ${{ steps.check.outputs.is_template }} - steps: - - uses: actions/checkout@v6 - - id: check - run: | - if [ -f .template-repo ]; then - echo "::notice::This is the unconfigured template repo. Skipping CodeQL." - echo "is_template=true" >> "$GITHUB_OUTPUT" - else - echo "is_template=false" >> "$GITHUB_OUTPUT" - fi - analyze: name: Analyze (${{ matrix.language }}) - needs: guard - if: needs.guard.outputs.is_template != 'true' runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8e7898c..4a1a2c3 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -20,33 +20,11 @@ env: COVERAGE_THRESHOLD: "80" jobs: - # =========================================================================== - # Template guard — short-circuits the entire workflow if .template-repo - # marker file exists. Delete .template-repo to enable real CI runs. - # =========================================================================== - guard: - name: Template guard - runs-on: ubuntu-latest - outputs: - is_template: ${{ steps.check.outputs.is_template }} - steps: - - uses: actions/checkout@v6 - - id: check - run: | - if [ -f .template-repo ]; then - echo "::notice::This is the unconfigured template repo. Skipping coverage." - echo "is_template=true" >> "$GITHUB_OUTPUT" - else - echo "is_template=false" >> "$GITHUB_OUTPUT" - fi - # =========================================================================== # Path filter — same approach as ci.yml # =========================================================================== changes: name: Detect changes - needs: guard - if: needs.guard.outputs.is_template != 'true' runs-on: ubuntu-latest outputs: python: ${{ steps.filter.outputs.python }} @@ -274,15 +252,11 @@ jobs: coverage-passed: name: Coverage passed runs-on: ubuntu-latest - needs: [ guard, changes, python, typescript, rust ] + needs: [ changes, python, typescript, rust ] if: always() steps: - name: Verify all coverage jobs succeeded or were skipped run: | - if [ "${{ needs.guard.outputs.is_template }}" = "true" ]; then - echo "Template guard active. Coverage skipped intentionally." - exit 0 - fi results='${{ toJSON(needs) }}' echo "Job results:" echo "$results" | jq . diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5c9c89f..21edd78 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -34,33 +34,11 @@ env: PLATFORMS: linux/amd64,linux/arm64 jobs: - # =========================================================================== - # Template guard — short-circuits the entire workflow if .template-repo - # marker file exists. Delete .template-repo to enable real Docker builds. - # =========================================================================== - guard: - name: Template guard - runs-on: ubuntu-latest - outputs: - is_template: ${{ steps.check.outputs.is_template }} - steps: - - uses: actions/checkout@v6 - - id: check - run: | - if [ -f .template-repo ]; then - echo "::notice::This is the unconfigured template repo. Skipping Docker build." - echo "is_template=true" >> "$GITHUB_OUTPUT" - else - echo "is_template=false" >> "$GITHUB_OUTPUT" - fi - # =========================================================================== # Build (and conditionally push) the image # =========================================================================== build: name: Build & Push - needs: guard - if: needs.guard.outputs.is_template != 'true' runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8bac7a0..329c7f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,34 +13,12 @@ permissions: contents: read jobs: - # =========================================================================== - # Template guard — short-circuits the entire workflow if .template-repo - # marker file exists. Delete .template-repo to enable real release runs. - # =========================================================================== - guard: - name: Template guard - runs-on: ubuntu-latest - outputs: - is_template: ${{ steps.check.outputs.is_template }} - steps: - - uses: actions/checkout@v6 - - id: check - run: | - if [ -f .template-repo ]; then - echo "::notice::This is the unconfigured template repo. Skipping release." - echo "is_template=true" >> "$GITHUB_OUTPUT" - else - echo "is_template=false" >> "$GITHUB_OUTPUT" - fi - # =========================================================================== # release-please: opens/updates a release PR based on Conventional Commits. # When that PR is merged, this job creates a tag and GitHub release. # =========================================================================== release-please: name: Release Please - needs: guard - if: needs.guard.outputs.is_template != 'true' runs-on: ubuntu-latest permissions: diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e18ee07..528e27a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,6 @@ { - ".": "0.0.0" + "packages/python/stoopid-logging": "0.0.0", + "packages/ts/stoopid-logging": "0.0.0", + "packages/rust/stoopid-logging": "0.0.0", + "charts/stoopid-service-base": "0.0.0" } diff --git a/.template-repo b/.template-repo deleted file mode 100644 index edcdde2..0000000 --- a/.template-repo +++ /dev/null @@ -1,16 +0,0 @@ -This file marks the repository as the unconfigured template. - -While this file exists at the repository root, the following workflows -short-circuit and exit successfully without doing real work: - - - .github/workflows/ci.yml - - .github/workflows/codeql.yml - - .github/workflows/coverage.yml - - .github/workflows/release.yml - - .github/workflows/docker.yml - -Commit Lint (.github/workflows/commitlint.yml) still runs on this -repository so contributions to the template itself are validated. - -DELETE THIS FILE as part of template setup. See CONTRIBUTING.md -"Template Setup" section, step 1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e7395f..68793ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,11 +8,6 @@ For security-related concerns and how to report vulnerabilities, see [`SECURITY.md`](./SECURITY.md). For expected behavior in project spaces, see [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md). -> **Template setup:** This repository was created from a template and requires -> one-time configuration before it is fully functional. See -> [Template Setup](#template-setup) below. Delete this blockquote and the -> entire `Template Setup` section once setup is complete. - ## Table of Contents - [Template Setup](#template-setup) _(delete after setup)_ @@ -28,142 +23,6 @@ For security-related concerns and how to report vulnerabilities, see - [Troubleshooting](#troubleshooting) - [Getting Help](#getting-help) -## Template Setup - -> **Delete this entire section once setup is complete.** All inline -> `> **Setup:**` blockquotes throughout this document should also be removed -> as their corresponding tasks are addressed. - -This checklist covers everything required to make a freshly-instantiated -repository functional. Work through it in order; later steps depend on -earlier ones. - -### 1. Delete the template marker - -Remove `.template-repo` from the repository root. While this file exists, -the CI, CodeQL, Coverage, Release, and Docker workflows short-circuit and -exit successfully without running. This prevents the unconfigured template -from generating noise or failing runs. - -```sh -rm .template-repo -git add .template-repo -git commit -m "chore: remove template marker" -``` - -The Commit Lint workflow continues to run regardless — it validates PR -titles whether the repository is the template or an instantiated project. - -### 2. Replace placeholders - -Search and replace across the repository: - -- `` — the organization or team name -- `` — the project's short name (used in `release-please-config.json` and elsewhere) -- `` — the security contact email - -Files known to contain placeholders: - -- `SECURITY.md` -- `CONTRIBUTING.md` (this file) -- `release-please-config.json` - -### 3. Configure GitHub repository settings - -Under the repository's **Settings** tab: - -- **General → Pull Requests:** - - Allow merge commits: **disabled** - - Allow squash merging: **enabled** - - Default commit message: **"Pull request title"** (this is critical — release-please reads merge commit messages, which under squash-merge come from the PR title) - - Allow rebase merging: **disabled** - - Always suggest updating pull request branches: **enabled** - - Automatically delete head branches: **enabled** -- **Code security and analysis:** enable - - Dependabot alerts - - Dependabot security updates - - Secret scanning - - Push protection - - Code scanning (CodeQL is configured via workflow but must be enabled) -- **Actions → General:** confirm workflow permissions allow read/write where needed (Settings → Actions → General → Workflow permissions) - -### 4. Configure branch protection - -Under **Settings → Branches**, add a branch protection rule for `main`: - -- Require a pull request before merging -- Require approvals: **1** -- Require review from Code Owners -- Require status checks to pass before merging: - - `CI passed` - - `Coverage passed` - - `Commit Lint / PR title` - - `CodeQL / Analyze (...)` for each language in use - - `Docker / Build & Push` (only if Dockerfile present) -- Require branches to be up to date before merging -- Require conversation resolution before merging -- Do not allow bypassing the above settings - -### 5. Configure CODEOWNERS - -Edit `.github/CODEOWNERS` to assign owners for the codebase. At minimum, -include a global fallback (`* @/`). - -### 6. Configure container registry (if applicable) - -This repository defaults to `ghcr.io` and requires no setup. If publishing -to ECR or another registry instead: - -- Add repository **Variables** (Settings → Secrets and variables → Actions → Variables): - - `REGISTRY` — e.g. `123456789.dkr.ecr.us-east-1.amazonaws.com` - - `IMAGE_NAME` — e.g. `my-project` (omit to use `/`) - - `AWS_REGION` — e.g. `us-east-1` (ECR only) -- Add repository **Secret** (ECR only): - - `AWS_ROLE_TO_ASSUME` — IAM role ARN configured for OIDC trust with this repository -- Configure AWS-side OIDC trust: - - Create an IAM identity provider for `token.actions.githubusercontent.com` - - Create an IAM role with a trust policy scoped to this GitHub org/repo - - Grant the role ECR push permissions on the target repository - - Ensure the target ECR repository exists (ECR does not auto-create) - -### 7. Choose project-specific options - -- **Release type:** Edit `release-please-config.json` and change `release-type` from `simple` to `node`, `python`, or `rust` if this project has a single canonical version file. Leave as `simple` for polyglot or version-less projects. -- **Local dev orchestrator:** Update `make dev` in the `Makefile` if neither Tilt nor Docker Compose is appropriate. -- **Python type checker:** If using Python, decide between Pyright and mypy and add the chosen tool to `pyproject.toml` dev dependencies. The CI workflow auto-detects which is installed. - -### 8. Create `.env.example` - -If this project requires environment variables, create `.env.example` at the -repository root with placeholder values only. Real secrets must come from an -external secret store; see [`SECURITY.md`](./SECURITY.md#secrets-management). - -### 9. Remove unused language scaffolding - -Delete files and configuration for languages this project does not use: - -- TypeScript: remove `package.json`, `pnpm-lock.yaml`, `tsconfig.json`, the `npm` block from `.github/dependabot.yml` -- Python: remove `pyproject.toml`, `uv.lock`, the `uv` block from `.github/dependabot.yml` -- Rust: remove `Cargo.toml`, `Cargo.lock`, the `cargo` block from `.github/dependabot.yml` -- Terraform: remove all `.tf` files and the `terraform` block from `.github/dependabot.yml` - -The CI workflow (`ci.yml`) auto-detects which languages are present via path -filters; unused language jobs simply skip. Removing scaffolding keeps the -repository clean and avoids confusion. - -### 10. Verify - -Open a test PR with a trivial change. Confirm: - -- All required CI checks run and pass -- The PR title is validated by commitlint -- Coverage report posts as a sticky comment (if applicable) -- CodeQL findings (if any) appear in the Security tab - -If everything looks correct, **delete the [Template Setup](#template-setup) -section, this blockquote, the entry in the Table of Contents, and any -remaining `> **Setup:**` blockquotes throughout this document.** - ## Getting Started ### Prerequisites @@ -205,11 +64,6 @@ any local state required to run the project. ### Local Environment -> **Setup:** If this project requires environment variables, create -> `.env.example` at the repository root with placeholder values only. -> Real secrets must come from an external secret store; see -> [`SECURITY.md`](./SECURITY.md#secrets-management). - Copy the example environment file: ```sh @@ -342,10 +196,6 @@ The old name is removed; there is no deprecation period. ### Review requirements -> **Setup:** Confirm `.github/CODEOWNERS` is populated with appropriate -> owners. At minimum, include a global fallback (`* @/`). -> Branch protection on `main` should require Code Owner review. - - One approval from a code owner is required to merge. - Code owners are defined in `.github/CODEOWNERS`. - All required CI checks must pass. @@ -392,10 +242,6 @@ A change is ready to merge when: ## Per-Language Conventions -> **Setup:** Delete subsections below for languages this project does not use. -> CI auto-detects language presence via path filters; subsections kept here -> should match what the project actually contains. - ### TypeScript / JavaScript - **Package manager:** pnpm diff --git a/README.md b/README.md index 96c8acb..9840983 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,259 @@ -# stoopid-template +# stoopid-commons -A template for Stoopid stuff. +Generic, reusable infrastructure for polyglot microservice systems. Open source under MIT license. ## Overview -This section should provide a brief overview of what this application or service will do. Include only high-level -details. +`stoopid-commons` is a collection of language-spanning utilities and patterns developed at Stoopid Company for building polyglot microservice architectures. It contains: + +- **Per-language structured logging packages** — Python (structlog), TypeScript (pino), Rust (tracing). All produce JSON output with a common schema (`timestamp`, `level`, `service`, `correlation_id`, `span_id`, `message`, plus structured fields), so logs from any service in any language can be aggregated and queried uniformly. +- **Helm chart base** — a parameterized base chart that services extend with their own `values.yaml`. Captures common k8s deployment patterns (deployments, services, config maps, ingress, HPA, OTel sidecar) so individual services don't re-author manifests from scratch. +- **OpenTelemetry conventions** — span naming, attribute conventions, and helper packages per language for consistent instrumentation across services. +- **Common utilities** — correlation ID generation and propagation, retry policies with backoff, circuit breaker patterns, and other generic helpers per language. +- **Build tooling** — shared Makefile patterns, pre-commit configurations, and CI workflow templates that any cell or service can extend. + +This repo is consumed as published packages, not as a source dependency. Other projects pull these via npm, PyPI, crates.io, and Helm chart repositories. + +It does not contain anything specific to any particular product or architecture. Project-specific contracts and types live in their own repos. ## Setup -This section should provide instructions on how someone would setup this application or service to run on their local -machine. This should include: +You only need to set up `stoopid-commons` locally if you're contributing to it. To use the published packages in your own projects, follow the consumer instructions in the **Usage** section below. + +This repo is polyglot — Python, TypeScript, and Rust toolchains are all needed to build and publish all packages. Contributors working on only one language can install only that language's tooling. + +### macOS + +Install Homebrew if not present, then: + +```bash +brew install git make +brew install python@3.12 +brew install node +brew install rustup-init && rustup-init -y +brew install kubectl helm +brew install minikube +brew install pre-commit +``` + +After Rust install, restart your shell or `source "$HOME/.cargo/env"`. + +Clone and bootstrap: + +```bash +git clone https://github.com/stoopidco/stoopid-commons.git +cd stoopid-commons +make setup +``` + +### Linux (Ubuntu/Debian) + +```bash +sudo apt update +sudo apt install -y git make build-essential curl + +# Python 3.12 via deadsnakes if not on 24.04+ +sudo add-apt-repository ppa:deadsnakes/ppa +sudo apt install -y python3.12 python3.12-venv python3.12-dev + +# Node.js via NodeSource +curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - +sudo apt install -y nodejs + +# Rust +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +source "$HOME/.cargo/env" + +# Kubernetes tooling +sudo snap install kubectl --classic +sudo snap install helm --classic +curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 +sudo install minikube-linux-amd64 /usr/local/bin/minikube + +# pre-commit +pip install --user pre-commit +``` + +Clone and bootstrap: + +```bash +git clone https://github.com/stoopidco/stoopid-commons.git +cd stoopid-commons +make setup +``` -- instructions for macOS -- instructions for Windows -- instructions for WSL2 setup on Windows if needed -- instructions for Linux +### Windows + +Native Windows is not supported. Use WSL2. + +### WSL2 on Windows + +Install WSL2 with Ubuntu from PowerShell as Administrator: + +```powershell +wsl --install -d Ubuntu-22.04 +``` + +Reboot when prompted. Launch Ubuntu from the Start menu and complete the initial user setup. Then follow the **Linux (Ubuntu/Debian)** instructions above inside the WSL2 environment. + +For Docker Desktop integration with WSL2, install Docker Desktop on Windows and enable WSL2 integration under Settings → Resources → WSL Integration. + +Clone the repo into your WSL2 home directory (not `/mnt/c/`) for filesystem performance: + +```bash +cd ~ +git clone https://github.com/stoopidco/stoopid-commons.git +cd stoopid-commons +make setup +``` ## Usage -This section should provide instructions for how to use the application or service once setup is complete. +### Consuming packages in your project + +`stoopid-commons` packages are published to public registries. Add them as normal dependencies — you do not need to clone this repo. + +**Python (logging):** + +```bash +pip install stoopid-logging +``` + +```python +from stoopid_logging import get_logger + +log = get_logger(service="my-service") +log.info("processing started", record_id=123) +``` + +**TypeScript / Node (logging):** + +```bash +npm install @stoopid/logging +``` + +```typescript +import { getLogger } from "@stoopid/logging"; + +const log = getLogger({ service: "my-service" }); +log.info({ recordId: 123 }, "processing started"); +``` + +**Rust (logging):** + +```toml +[dependencies] +stoopid-logging = "0.1" +``` + +```rust +use stoopid_logging::init_logger; + +init_logger("my-service"); +tracing::info!(record_id = 123, "processing started"); +``` + +**Helm chart base:** + +```bash +helm repo add stoopid https://stoopidco.github.io/stoopid-commons-charts +helm repo update +``` + +In your service's `Chart.yaml`: + +```yaml +dependencies: + - name: stoopid-service-base + version: "0.1.x" + repository: "https://stoopidco.github.io/stoopid-commons-charts" +``` + +In your service's `values.yaml`, override only what differs from the base. + +See the `examples/` directory in this repo for end-to-end usage examples per package. + +### Local development workflows + +If you're contributing to `stoopid-commons` itself: + +```bash +make build # Build all packages +make test # Run all tests +make lint # Lint all code +make fmt # Format all code +make help # Show all available targets +``` + +To test changes locally before publishing: + +```bash +make publish-local PACKAGE=stoopid-logging-python +``` + +This publishes to a local registry that other repos on your machine can consume. See `CONTRIBUTING.md` for local registry setup details. ## Troubleshooting -This section should provide instructions for how to debug common issues that are not related to the application or -service but can also guide users towards filing a bug report if one is found. +### `make setup` fails on dependency installation + +Confirm your toolchain versions match the requirements: + +- Python ≥ 3.12 +- Node.js ≥ 20 +- Rust ≥ 1.75 (stable) + +Run `make doctor` to check versions and report missing tools. + +### Package not found when consuming + +If `pip install stoopid-logging` or `npm install @stoopid/logging` fails: + +- Verify the package name spelling +- Check for network connectivity to the registry +- For corporate networks, verify proxy/VPN configuration allows access to PyPI/npm +- Confirm the version constraint in your dependency file matches a published version + +### Pre-commit hooks failing + +```bash +pre-commit clean +pre-commit install --install-hooks +``` + +If a specific hook is failing without obvious cause, run it directly: `pre-commit run --all-files`. + +### Helm chart base not pulling + +```bash +helm repo update +helm dependency update # in your service directory +``` + +If still failing, check the chart repository URL is correct and accessible. + +### Cannot publish package locally + +Verify your local registry is running. See `CONTRIBUTING.md` under "Local Package Registry" for Verdaccio (npm) and devpi (Python) setup. + +### Filing an issue or bug report + +Issues are welcome. File at https://github.com/stoopidco/stoopid-commons/issues with the `bug` label. Include: + +- Your operating system and version +- Output of `make doctor` or relevant tool versions (`python --version`, `node --version`, `rustc --version`) +- The exact command that failed +- Full output of the failure +- Whether the issue reproduces in a clean clone of the repo + +For security issues, see `SECURITY.md` — do not file public issues for security disclosures. + +For feature requests or discussion, use the `enhancement` or `discussion` label. + +## License + +MIT. See `LICENSE`. + +## Contributing + +External contributions welcome. See `CONTRIBUTING.md` for development guidelines, code style, and the PR process. See `CODE_OF_CONDUCT.md` for community standards. diff --git a/SECURITY.MD b/SECURITY.MD index a20da94..bdba3b8 100644 --- a/SECURITY.MD +++ b/SECURITY.MD @@ -3,7 +3,7 @@ This document describes how security is handled in this project, how to report vulnerabilities, and what conventions contributors must follow. -> **Template note:** Replace all ``, ``, and +> **Template note:** Replace all `Stoopid Company`, `stoopid-commons`, and > `` placeholders before publishing. Remove this note once > instantiated. @@ -166,7 +166,7 @@ Every incident requires a blameless post-mortem documenting: timeline, root caus ## Compliance and Audit Logs, audit trails, and evidence relevant to compliance obligations are -retained per `` policy. Contributors should not alter or delete audit +retained per `Stoopid Company` policy. Contributors should not alter or delete audit data without explicit security approval. ## Questions diff --git a/docs/README.md b/docs/README.md index 8fd5ed7..1120b47 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,22 +1,12 @@ -# - -> **Template note:** This is the technical specification for the project. It -> is the bridge between business need and technical reality — the document a -> new engineer reads to understand _what_ this project is and _why_ it exists, -> and the document a business stakeholder reads to understand _what_ is -> actually being built. Keep it current; a stale tech spec is worse than no -> tech spec. -> -> Replace `` and all other placeholders below before publishing. -> Delete this blockquote once instantiated. - -**Status:** -**Last reviewed:** -**Owner:** +# stoopid-commons + +**Status:** draft +**Last reviewed:** 2026-05-02 +**Owner:** Jason Anton ## Table of Contents -- [\](#project_name) +- [stoopid-commons](#stoopid-commons) - [Table of Contents](#table-of-contents) - [Overview](#overview) - [Goals and Non-goals](#goals-and-non-goals) @@ -35,99 +25,114 @@ ## Overview - +`stoopid-commons` is a collection of language-spanning utilities and patterns developed at Stoopid Company for building polyglot microservice systems. It produces published packages — for Python, TypeScript, and Rust — and a parameterized Helm chart base that any project can consume to get consistent structured logging, observability conventions, k8s deployment patterns, and common utilities. The repo is open source under MIT license. Its primary internal user is the TrAICE (True AI Computing Environment) platform and the products built on it, but it is intentionally generic and contains nothing specific to TrAICE or any other Stoopid product. - +The problem it solves is the inconsistency that emerges when polyglot microservices are built without shared infrastructure: logs in different formats per language, k8s manifests re-authored from scratch per service, ad-hoc retry and correlation patterns, and divergent OpenTelemetry conventions. These differences are individually small but in aggregate make cross-service debugging painful and onboarding slow. Solving it now matters because Stoopid is about to start building TrAICE, a multi-cell architecture where this divergence would compound quickly; establishing the shared foundation before the cells are written is significantly cheaper than retrofitting consistency later. ## Goals and Non-goals ### Goals -What this project will do. Each goal should be specific enough to be testable. - -- -- -- +- Publish per-language structured logging packages (Python via structlog, TypeScript via pino, Rust via tracing) that produce JSON output conforming to a single shared schema, so logs from any service in any language can be aggregated and queried uniformly. +- Publish a parameterized Helm chart base (`stoopid-service-base`) that services extend via `values.yaml`, eliminating the need for services to author k8s manifests from scratch for common deployment patterns. +- Publish per-language OpenTelemetry helper packages with shared span naming and attribute conventions, so traces are consistent across services. +- Publish per-language utility packages for correlation ID generation and propagation, retry with exponential backoff, and circuit breaker patterns. +- Provide reusable build tooling (Makefile patterns, pre-commit configurations, GitHub Actions workflow templates) that any cell or service can extend. +- All packages versioned via semver, with versions enforced by conventional-commit-driven CI automation. Breaking changes require an explicit `BREAKING CHANGE:` marker. +- Maintain the repo as a real public artifact: working examples, accept external issues, respond to security disclosures. ### Non-goals -What this project will explicitly **not** do. This section is as important as -the goals section — it prevents scope creep and sets expectations. - -Use the `(deferred)` suffix for items that are out of scope _for now_ but may -be revisited. Items without the suffix are out of scope permanently. - -- -- -- +- Anything specific to the TrAICE platform or products built on it (e.g., VICI). Platform-specific contracts, schemas, and types live in a separate private repo. +- Application-level frameworks. This repo provides building blocks, not opinionated frameworks (no service generator, no scaffolded application templates beyond the Helm chart base). +- Service mesh, ingress controller, or other cluster-level infrastructure. The Helm base assumes a working cluster with standard primitives. +- Language support beyond Python, TypeScript, and Rust (deferred). Adding Go, Java, or other languages is out of scope for v1 but may be revisited if needed. +- A central logging or tracing backend. This repo defines conventions for emitting logs and traces; backends (Loki, Tempo, Grafana Cloud, etc.) are the consumer's choice. +- Cross-language RPC or schema definitions. Those concerns belong in project-specific repos (e.g., the TrAICE platform's contracts repo). +- A static documentation site (deferred). Markdown READMEs and per-package docs are sufficient until external usage warrants a site. +- Backward compatibility with non-current versions of language toolchains (Python <3.12, Node <20, Rust <1.75). ## Current State -What is actually built and working today. This section drifts faster than any -other; review it whenever a meaningful capability lands or is removed. +Nothing is built yet. This document is being written as the foundation for the initial implementation. + +- **Capability:** none yet +- **Known gaps:** all packages and chart base need to be authored from scratch +- **Known issues:** n/a -- -- -- -- +The repo template has been created. Next steps are filling out the cell-level documentation (`README.md`, `CONTRIBUTING.md`, this spec, ADRs), establishing the build and CI tooling, then implementing the first package (logging) as the proof-of-concept for the multi-language publishing pipeline. ## Solution - +`stoopid-commons` is a polyglot monorepo. Each language has its own subdirectory under `packages/` (e.g., `packages/python/`, `packages/ts/`, `packages/rust/`) containing the language-specific package implementations. A shared `charts/` directory holds the Helm chart base. Build tooling (`scripts/`, `Makefile`, `.github/`) is shared at the repo root. + +CI is responsible for: validating each package on every PR (lint, type check, unit tests, integration tests where applicable), generating any cross-language artifacts that need to stay synchronized (such as JSON schema definitions for the logging format), and publishing packages to public registries on merge to `main`. Conventional commits drive semver bumps; CI tags releases and updates per-package CHANGELOGs automatically. - +Consumers do not clone this repo. They install published packages from PyPI, npm, crates.io, and the chart repository at pinned versions. Local development against in-flight changes uses a local registry (Verdaccio for npm, devpi for Python) per the workflow in `CONTRIBUTING.md`. + +A diagram of the repo structure and publishing flow belongs in `diagrams/`. To be added in the next pass. ### Key components -- **** — . -- **** — . +- **logging packages** — Per-language structured logging wrappers producing a common JSON schema. `packages/python/stoopid-logging/`, `packages/ts/stoopid-logging/`, `packages/rust/stoopid-logging/`. +- **otel packages** — Per-language OpenTelemetry helpers with shared conventions. `packages/python/stoopid-otel/`, `packages/ts/stoopid-otel/`, `packages/rust/stoopid-otel/`. +- **utils packages** — Per-language utilities (correlation IDs, retry, backoff, circuit breaker). `packages/python/stoopid-utils/`, `packages/ts/stoopid-utils/`, `packages/rust/stoopid-utils/`. +- **service-base Helm chart** — Parameterized chart that services extend via `values.yaml`. `charts/stoopid-service-base/`. +- **build tooling** — Shared Makefile, pre-commit config, GitHub Actions workflow templates. `Makefile`, `.pre-commit-config.yaml`, `.github/workflows/`. +- **examples** — End-to-end usage examples per package. `examples/`. ### Significant design decisions -Significant decisions live in [`adrs/`](./adrs/) as ADRs. Reference them inline -where relevant rather than restating them here. Examples: +Significant decisions live in [`adrs/`](./adrs/) as ADRs. Reference them inline where relevant rather than restating them here. ADRs to be authored as decisions are made; initial set will include: -- See [ADR-0001](./adrs/0001-example.md) for . -- See [ADR-0002](./adrs/0002-example.md) for . +- ADR-0001: Polyglot monorepo with per-language `packages/` subdirectory structure +- ADR-0002: Conventional commits + semver enforced via CI automation +- ADR-0003: `main`-only branching with tags as deploy targets +- ADR-0004: Common JSON schema for structured logging across all language packages +- ADR-0005: Parameterized Helm chart base over per-service manifest authoring +- ADR-0006: Local package registry workflow (Verdaccio + devpi) for in-flight development ## Constraints and Assumptions -Things outside the team's control that shape the solution. Keep this honest — -unstated assumptions cause the worst kinds of failure. - ### Technical -- -- +- Must support consumption from Python ≥ 3.12, Node.js ≥ 20, Rust ≥ 1.75. +- Helm chart base targets Kubernetes ≥ 1.28 with Helm ≥ 3.12. +- Logging packages must produce output compatible with standard log aggregators (Loki, Elasticsearch, CloudWatch Logs) without custom parsers. +- OpenTelemetry conventions must conform to the OTel semantic conventions specification — divergence breaks interop with any standard OTel backend. +- Build and test must complete in CI in under 10 minutes for the full matrix to keep PR feedback fast. +- All published packages must be reproducibly buildable from a tagged commit. ### Regulatory and organizational -- -- +- MIT license — no copyleft obligations on consumers. +- Publishing to public registries means once published, a version cannot be unpublished or modified. Versioning discipline is mandatory. +- Security disclosures handled per `SECURITY.md`. Response SLA: acknowledge within 5 business days, fix or workaround within 30 days for high-severity issues. +- No PII or proprietary information may be committed. The repo is public from day one. ### Assumptions -- -- +- Python's structlog, Node's pino, and Rust's tracing libraries will continue to be the canonical structured logging libraries for their respective ecosystems for the foreseeable future. If any of them is deprecated or supplanted, the corresponding `stoopid-logging` package will need to be re-implemented. +- The OpenTelemetry semantic conventions will continue to evolve in a backward-compatible manner. Breaking changes in OTel would require a major version bump of the otel packages. +- GitHub will continue to host this repo and its CI. Migration to another platform would require rewriting `.github/workflows/`. +- ghcr.io and the public language registries (PyPI, npm, crates.io) will remain available for publishing. +- Internal Stoopid projects (notably TrAICE cells and the products built on them) will be the primary consumers initially. External adoption is welcome but not assumed. ## Open Questions -Append-only. When a question is answered, either: - -1. Move the resolution into the relevant section above and delete the question, or -2. If the resolution involves a significant design decision, capture it in an ADR and link from here. - -- -- +- Should we publish the Helm chart base via a GitHub Pages-hosted chart repository, or use the OCI artifact pattern via ghcr.io? — Jason, 2026-05-02 +- Do we want a CLI tool for scaffolding new services that consume `stoopid-commons` packages, or is a copy-paste-able example sufficient? — Jason, 2026-05-02 +- Should the logging schema include OTel trace and span IDs as first-class fields, or only as a generic `context` object? — Jason, 2026-05-02 +- What's the minimum viable test matrix for CI — do we test against multiple Python/Node/Rust versions, or only the minimum supported? — Jason, 2026-05-02 +- Do we want a `stoopid-config` package for shared configuration loading patterns (env vars, config files, secrets), or is that scope creep? — Jason, 2026-05-02 ## Glossary -Project-specific terms. Generic industry terms do not belong here. When this -list grows past ~20 entries, split it into `docs/glossary.md`. - -- **** — -- **** — +- **Cell** — A top-level repo organizing related services and packages by function. Concept comes from the TrAICE platform; `stoopid-commons` is generic infrastructure that cells (TrAICE or otherwise) can consume. +- **Conventional commits** — A commit message format (`feat:`, `fix:`, `BREAKING CHANGE:`, etc.) that enables automated semver bumping and CHANGELOG generation. +- **Helm chart base** — A parameterized Helm chart that other charts extend via `dependencies` and override via `values.yaml`. Single source of truth for common k8s deployment patterns. +- **Polyglot monorepo** — A single repo containing code in multiple programming languages, with shared tooling and coordinated releases. +- **Service base** — Specifically `stoopid-service-base`, the Helm chart that services in any cell extend for their k8s deployment. +- **Stoopid Company** — The organization owning this repo and the broader TrAICE platform. +- **TrAICE** — True AI Computing Environment. The cognitive architecture platform Stoopid is building. `stoopid-commons` provides generic infrastructure used by TrAICE cells, but TrAICE itself is not part of this repo. +- **VICI** — Virtually Intelligent Chat Interface. A chat product built on top of TrAICE. Mentioned here only for context; not relevant to `stoopid-commons` consumers. diff --git a/docs/adrs/0001-polyglot-monorepo-layout.md b/docs/adrs/0001-polyglot-monorepo-layout.md new file mode 100644 index 0000000..f72c396 --- /dev/null +++ b/docs/adrs/0001-polyglot-monorepo-layout.md @@ -0,0 +1,99 @@ +# ADR-0001: Polyglot monorepo with per-language `packages/` subdirectories + +**Status:** accepted +**Date:** 2026-05-02 +**Deciders:** Jason Anton + +## Context + +Stoopid is about to start building TrAICE, a multi-cell architecture whose +services span Python, TypeScript, and Rust. Without shared infrastructure, each +language ecosystem will diverge in logging format, OpenTelemetry conventions, +retry semantics, correlation propagation, and Kubernetes deployment patterns. +Those differences are individually small but in aggregate make cross-service +debugging painful and onboarding slow. + +The shared infrastructure is naturally cross-cutting: a logging schema bump +must land in Python, TypeScript, and Rust simultaneously, or the cross-language +log aggregation it exists to enable breaks. The same applies to OTel attribute +conventions, correlation ID propagation rules, and the JSON schema definitions +for the logging packages. + +Three structural options exist: + +1. One repo per language (three or more repos). +2. One repo per package (nine-plus repos at v1). +3. One monorepo with per-language subtrees. + +Option 1 makes cross-language schema changes a multi-PR coordination problem. +Option 2 multiplies that further and adds release-coordination tax. Option 3 +keeps cross-cutting changes atomic at the cost of CI matrix breadth. + +## Decision + +`stoopid-commons` is a single repository with the following structure: + +``` +packages/ + python// + ts// + rust// +charts/ + stoopid-service-base/ +scripts/ +.github/ +Makefile +``` + +Each language sub-tree owns its own lockfile and build configuration. The +`Makefile` detects the presence of language manifests (`pyproject.toml`, +`package.json`, `Cargo.toml`) and dispatches to language-native tooling. The +`charts/` directory holds the parameterized Helm chart base. Cross-language +artifacts (e.g. JSON schema definitions referenced by all three logging +packages) live at the root or under `schemas/`. + +## Consequences + +### Positive + +- Cross-language changes (logging schema, OTel attribute conventions, common + utility semantics) are atomic in a single PR. +- Single CI configuration, single CONTRIBUTING/SECURITY/CODEOWNERS, single + pre-commit configuration, single dev-tooling baseline. +- The `Makefile` becomes the one entry point for `bootstrap`, `build`, `test`, + `lint`, `format`, and `check` regardless of language. +- New language packages do not require new repos, branch protection rules, or + release pipelines. + +### Negative + +- The CI matrix grows multiplicatively as packages multiply, which presses on + the under-10-minute CI budget. +- Contributors need at least cursory familiarity with all three toolchains to + navigate the repo even if they only modify one language. +- `release-please` configuration must be aware of multiple per-package + versioned roots within the same repo. + +### Neutral + +- No attempt is made at a unified cross-language build (no Bazel, no Nx). + Each language stays on its native tooling. +- The repo can later add or remove language sub-trees without affecting the + others, subject to ADR-0002's versioning discipline. + +## Alternatives Considered + +- **One repo per language** — rejected. Cross-language schema sync becomes a + manual coordination problem, and each repo carries duplicate scaffolding + (CI, pre-commit, contributor docs). +- **One repo per package** — rejected. Combinatorial explosion of repos for a + small team; coordinated releases across packages become impractical. +- **Bazel/Nx-style unified build** — rejected. The tooling tax (build + configuration in a non-native language, contributor onboarding cost) exceeds + the benefit at this scale; the per-language tooling is mature enough that + unification offers little. + +## References + +- [docs/README.md](../README.md) — project specification, "Solution" section. +- `Makefile` — language detection logic. diff --git a/docs/adrs/0002-conventional-commits-and-semver.md b/docs/adrs/0002-conventional-commits-and-semver.md new file mode 100644 index 0000000..9ed35d7 --- /dev/null +++ b/docs/adrs/0002-conventional-commits-and-semver.md @@ -0,0 +1,94 @@ +# ADR-0002: Conventional commits + semver enforced via CI automation + +**Status:** accepted +**Date:** 2026-05-02 +**Deciders:** Jason Anton + +## Context + +`stoopid-commons` publishes packages to public registries: PyPI for Python, +npm for TypeScript, crates.io for Rust, and a Helm chart repository for +charts. Once published, a version cannot be unpublished or rewritten — the +public registry retention policies are deliberate and irreversible. +Versioning mistakes are permanent. + +At v1 the repo will publish on the order of nine packages (three logging, +three OTel helper, three utility) plus the Helm chart base. Manual version +bumps across that surface area are error-prone. Manual CHANGELOG curation is +worse. Consumers — including the TrAICE platform pinned at exact versions — +need to trust that minor and patch bumps are non-breaking, or the value of +SemVer evaporates. + +Two disciplines are required: + +1. A machine-checkable commit format that encodes intent (feature vs fix vs + breaking change). +2. Tooling that maps commits to per-package version bumps and CHANGELOG + entries deterministically. + +## Decision + +The repo adopts Conventional Commits 1.0 as the commit-message format, and +[release-please](https://github.com/googleapis/release-please) as the release +automation: + +- Every commit on `main` follows Conventional Commits format. The PR title + also follows the format because squash-merge uses the PR title as the + commit subject. +- `commitlint` (in CI) and `conventional-pre-commit` (locally) reject + non-conforming messages. +- `release-please` (configured in `release-please-config.json`) computes + per-package SemVer bumps from commit history, generates CHANGELOG entries, + opens a release PR, and tags releases on merge. +- A `BREAKING CHANGE:` footer or a `!` after the type/scope triggers a major + bump on the next release. +- `feat:` produces a minor bump; `fix:` and `perf:` produce patch bumps; + `chore:`, `ci:`, `build:`, `style:`, `test:`, `refactor:`, `docs:` do not + appear in CHANGELOGs and do not bump versions. + +## Consequences + +### Positive + +- Version bumps are deterministic and auditable straight from `git log`. +- CHANGELOGs are free. +- The same convention applies uniformly across three languages and the Helm + chart, which removes per-language process drift. +- Release PRs are reviewable artifacts: a reviewer sees the bump and the + changelog before publication. + +### Negative + +- Contributors must learn the commit format. Mitigated by `make commit`, + pre-commit validation, and PR-title linting that catches mistakes early. +- A bad commit message that lands on `main` cannot be retroactively fixed + without rewriting public history (which is forbidden — see ADR-0003). The + solution is a follow-up commit and accurate CHANGELOG editing in the + release PR. + +### Neutral + +- The CI test-matrix breadth (which Python, Node, and Rust versions to run + against) is a separate decision and is deferred to a later ADR. This ADR + commits to the version-management mechanism only. +- `release-please` is not the only tool that can do this; the choice is + defensible and reversible if a better fit appears. + +## Alternatives Considered + +- **Manual versioning per package** — rejected. Coordinating SemVer across + nine-plus packages by hand will be skipped under deadline pressure, leading + to version mistakes that cannot be corrected after publication. +- **`semantic-release`** — considered. Mature and capable, but less native to + multi-language monorepos with multiple versioned roots than `release-please`. +- **Calendar versioning (CalVer)** — rejected. Library consumers expect + SemVer; CalVer offers no signal about breaking-change risk for a downstream + upgrade decision. + +## References + +- [docs/README.md](../README.md) — "Solution" and "Constraints and + Assumptions" sections. +- `release-please-config.json` — release automation configuration. +- `.pre-commit-config.yaml` — local commit message validation. +- [Conventional Commits 1.0](https://www.conventionalcommits.org/en/v1.0.0/). diff --git a/docs/adrs/0003-trunk-based-tags-as-deploy-targets.md b/docs/adrs/0003-trunk-based-tags-as-deploy-targets.md new file mode 100644 index 0000000..52cc091 --- /dev/null +++ b/docs/adrs/0003-trunk-based-tags-as-deploy-targets.md @@ -0,0 +1,95 @@ +# ADR-0003: `main`-only branching with tags as deploy targets + +**Status:** accepted +**Date:** 2026-05-02 +**Deciders:** Jason Anton + +## Context + +`stoopid-commons` is a library and chart repository. Its consumers — TrAICE +cells today, external adopters tomorrow — depend on it by version pin, never +by branch. The repo is small enough, and its CI gating is strong enough +(linting, tests, type checks, coverage threshold, secret scanning, CodeQL), +that a single trunk with required-PR-review and required-CI is sufficient +gating for `main` to be continuously shippable. + +Long-lived release branches (`develop`, `release/*`, `next`) carry costs: + +- Backports double the surface area for any fix that must reach an older + major. +- Version skew between branches drifts and must be reconciled. +- Tag-pinned consumers care about *tags*, not branches; branch ceremony does + not buy them anything. +- For a v0/v1 library with no production consumers yet, none of those costs + is justified. + +Tags are the natural deploy target because release-please (see ADR-0002) +already creates them, they are immutable by convention, and they uniquely +identify "what version is what code." + +## Decision + +The repository uses one long-lived branch — `main` — with the following +rules: + +- All work happens on short-lived feature branches off `main`. +- Branches merge to `main` via pull request after code-owner review and CI + pass. +- Squash-merge is the default; the squash commit message uses the PR title + to preserve Conventional Commits format. +- Releases are git tags created by release-please on merges that contain + releasable commits. +- **Tags, not branches, are the deploy target.** Consumers pin to versions + (i.e. tags); the `main` branch HEAD is not itself a deployable artifact. +- Force-push to `main` is forbidden. The local `make push` target enforces + no-divergence on `main`. Force-push to feature branches is allowed before + review. +- No `develop`, `release/*`, or `next` branches exist by default. + +If a future scenario demands a back-patch to an older major (e.g., a major +external consumer pinned to an unsupported major needs a security fix), the +exception is to create a long-lived `release/N.x` branch *only at that +moment*, not preemptively. + +## Consequences + +### Positive + +- Linear, auditable history. `git log main` is the project's canonical + timeline. +- No backport overhead by default; fixes go forward only. +- Tags are the single source of truth for "what version corresponds to what + code." +- Lower cognitive load for contributors: there is one branch to be aware of. + +### Negative + +- Hotfixes for older majors require a deliberate exception (a long-lived + `release/N.x` branch created when the need arises). +- The first time a back-patch is required, the team will incur a one-time + cost to set up the back-patch branch and re-run release tooling against + it. + +### Neutral + +- The deploy target for downstream consumers (Helm chart base versions + consumed by TrAICE, package versions installed from PyPI/npm/crates.io) is + always a tag. +- This decision is consistent with ADR-0002: release-please creates the tags; + this ADR records that the tags are what consumers pin to. + +## Alternatives Considered + +- **GitFlow (`develop` + `release/*` + `main`)** — rejected. Adds ceremony + per release without buying anything for a library repo with strong CI on + trunk. +- **Trunk + per-minor `release/X.Y` branches preemptively** — rejected. Not + yet justified by demand; the cost-of-introduction is low if a back-patch + becomes necessary. + +## References + +- [docs/README.md](../README.md) — "Solution" section. +- `Makefile` — `push` target's main-divergence check. +- [CONTRIBUTING.md](../../CONTRIBUTING.md) — "Branching and Workflow". +- ADR-0002 — release-please owns tag creation. diff --git a/docs/adrs/0004-shared-logging-json-schema.md b/docs/adrs/0004-shared-logging-json-schema.md new file mode 100644 index 0000000..ae7b085 --- /dev/null +++ b/docs/adrs/0004-shared-logging-json-schema.md @@ -0,0 +1,116 @@ +# ADR-0004: Common JSON schema for structured logging across all language packages + +**Status:** accepted +**Date:** 2026-05-02 +**Deciders:** Jason Anton + +## Context + +The primary motivating problem for `stoopid-commons` is consistency across +polyglot services: a TrAICE cell composed of Python, TypeScript, and Rust +services should not produce three different log shapes that downstream +aggregators must reconcile. structlog (Python), pino (TypeScript), and the +`tracing` crate (Rust) each emit JSON natively, but their default field +names and types diverge. Without a shared contract, queries fragment along +language lines (`level` vs `severity`, `time` vs `timestamp`, `msg` vs +`message`), correlation across services becomes manual, and every backend +(Loki, Elasticsearch, CloudWatch, Grafana Cloud) needs custom parsing per +emitter. + +A shared schema imposes a contract: every logging package emits objects that +conform to it. The schema is the single source of truth; per-language +packages are implementations of it. CI validates emitted output against the +schema, so divergence is caught before publication, not at log-aggregation +time. + +This ADR records the existence and required-fields baseline of the schema. +Some specific field-placement questions (notably whether OpenTelemetry trace +and span identifiers belong as top-level fields or nested under a context +object) are deliberately left unresolved here so they can be decided +explicitly with their own tradeoff analysis. + +## Decision + +`stoopid-commons` defines a single JSON schema that every language's +structured-logging package emits against. The schema lives in the +repository (path determined during implementation, expected +`schemas/log-event.schema.json`) and is the contract. + +**Required top-level fields (baseline):** + +- `timestamp` — RFC 3339 / ISO 8601 UTC string. +- `level` — lowercase string, one of `debug`, `info`, `warn`, `error`, + `fatal`. +- `message` — human-readable event description. +- `service` — service name (typically from environment, e.g. + `OTEL_SERVICE_NAME`). +- `version` — service version. + +**Structured context:** an object field carrying caller-supplied structured +fields (the conventional "extra" / "bindings" / fields passed by the +emitter). + +**Validation:** each language package's CI suite runs an emit-and-validate +test that produces sample output and validates it against the schema. A +schema change requires synchronized updates to all three packages in the +same PR (atomic cross-language change — see ADR-0001). + +**Explicitly deferred:** placement of OpenTelemetry trace and span +identifiers (top-level fields versus nested under the context object) is +deferred to a separate decision. The open question is tracked in +[docs/README.md](../README.md). Logging packages may emit them at whichever +location the follow-up decision specifies; this ADR does not pin it. + +## Consequences + +### Positive + +- Cross-service log queries are uniform across languages: one query shape + works whether the log came from a Python, TypeScript, or Rust service. +- Backend-agnostic: any standard log aggregator (Loki, Elasticsearch, + CloudWatch Logs, Grafana Cloud) can consume the output without custom + parsers. +- Schema is testable: CI catches schema drift before a package is + published. +- New language packages added later (out of scope for v1) inherit the + contract for free. + +### Negative + +- Adding or changing a required field is a coordinated breaking change + across all three packages, with a corresponding major version bump on + each package. +- Each language's idiomatic logging style bends slightly to fit the schema + (e.g., structlog's bound-context model, pino's child-logger pattern, and + the `tracing` crate's span-fields model must all serialize to the same + shape). + +### Neutral + +- The schema is OpenTelemetry-compatible by construction (matching field + semantics where they overlap) but is not a wholesale adoption of the + OTel logs data model. A future ADR may revisit OTel logs adoption once + it is generally available across all three SDKs. +- Trace/span ID placement is left open by design and is the subject of a + separate decision. + +## Alternatives Considered + +- **Per-language schemas** — rejected. Defeats the cross-language + consistency that motivates the project. +- **Wholesale adoption of the OpenTelemetry logs data model** — + considered. Deferred until OTel logs are stable and well-supported in + all three language SDKs. The chosen schema is OTel-compatible to keep + this option open. +- **Loose convention without a machine-checkable schema** — rejected. + Conventions drift without enforcement; the whole point of doing this in + shared packages is to remove drift. + +## References + +- [docs/README.md](../README.md) — "Solution", "Key Components", and + "Open Questions" sections. +- ADR-0001 — atomic cross-language changes are possible because of the + monorepo structure. +- ADR-0002 — schema-bumping discipline rides on the SemVer enforcement. +- [OpenTelemetry semantic conventions](https://opentelemetry.io/docs/specs/semconv/). diff --git a/docs/adrs/0005-parameterized-helm-chart-base.md b/docs/adrs/0005-parameterized-helm-chart-base.md new file mode 100644 index 0000000..ae061e9 --- /dev/null +++ b/docs/adrs/0005-parameterized-helm-chart-base.md @@ -0,0 +1,116 @@ +# ADR-0005: Parameterized Helm chart base over per-service manifest authoring + +**Status:** accepted +**Date:** 2026-05-02 +**Deciders:** Jason Anton + +## Context + +Every service deployed in a Stoopid Kubernetes cluster needs roughly the +same set of primitives: a `Deployment` with sane probe defaults, a +`Service`, a `HorizontalPodAutoscaler`, a `ServiceMonitor` for Prometheus, +a `NetworkPolicy`, a `PodDisruptionBudget`, ConfigMap and Secret wiring, +sidecar slots for OpenTelemetry, and a consistent label and annotation +scheme. Authoring these per service produces: + +- Inconsistent probe defaults (some services miss readiness probes; + others have wrong startup-probe budgets). +- Divergent label schemas (different keys for `app`, `service`, + `version`), which break standard selectors and dashboards. +- Security-context drift (some services run as root, others not). +- One-off bugs in manifest templating that are caught only in production. +- Per-service redundancy that grows as the fleet grows. + +A shared parameterized chart base eliminates that duplication. Services +override behavior via `values.yaml` rather than re-authoring manifests. +Bug fixes and security tightening (e.g. a CVE-driven default change) +become one-place changes that propagate by chart version bump. + +The Helm ecosystem already supports this pattern via library/parent +charts; the repo's `charts/stoopid-service-base` directory is reserved +for it. + +## Decision + +`stoopid-commons` ships a parameterized Helm chart base named +`stoopid-service-base` under `charts/stoopid-service-base/`. Services +consume it via Helm dependency in their own `Chart.yaml` and override +behavior through `values.yaml`. + +The base owns and templates: + +- `Deployment` shape, including container spec, env wiring, and resource + defaults. +- Probe defaults (liveness, readiness, startup) with sane budgets that + consumers can override. +- A standard label schema, including OpenTelemetry resource attributes + (`service.name`, `service.version`). +- Security context defaults (non-root, read-only root filesystem, + dropped capabilities). +- `Service`. +- `ServiceMonitor` template (consumers opt in via values). +- Common annotations (e.g. for Prometheus scraping, OpenTelemetry + instrumentation hints). + +Consumers extend the base with their own templates only when they need +behavior the base does not cover. Forking the base is discouraged; if a +behavior is widely needed, it should be promoted into the base via a +chart version bump. + +Distribution mechanism (GitHub Pages chart repository versus OCI +artifacts in ghcr.io) is **deliberately deferred** and is tracked as an +open question in [docs/README.md](../README.md). This ADR records the +architectural choice (parameterized base over per-service authoring), +not the transport. + +## Consequences + +### Positive + +- New service Kubernetes setup becomes a `values.yaml` file. The + developer-velocity gain is large. +- Probe, label, and security-context drift becomes near-zero across + the fleet. +- One place to fix a CVE-driven security default. A chart bump + propagates the fix. +- The label schema (especially OTel resource attributes) is enforced + by template, which makes Grafana dashboards and Loki/Tempo + correlation work without per-service tuning. + +### Negative + +- Base chart upgrades are coordinated migrations across all consuming + services. SemVer discipline matters for chart versions just as much + as for package versions. +- Values surface area must be designed carefully — too narrow and + consumers fork; too wide and the chart becomes unmaintainable. +- Consumers must learn how Helm parent/library charts compose; the + cognitive load is non-zero, even if smaller than per-service + authoring. + +### Neutral + +- Distribution mechanism (chart repo via GitHub Pages versus OCI via + ghcr.io) is deferred and does not affect the architectural choice + recorded here. +- Cluster-level infrastructure (service mesh, ingress controller) is + out of scope per `docs/README.md` non-goals; the base assumes those + exist as cluster primitives. + +## Alternatives Considered + +- **Kustomize base + overlays** — rejected. Less ergonomic than Helm + for value-driven configuration, and Stoopid's ecosystem is already + Helm-centric. +- **Per-service hand-authored manifests** — rejected. This is the + status-quo problem the chart base solves. +- **Cookiecutter / scaffold tool that generates per-service + manifests** — rejected. Drift returns the moment the scaffold ages; + generated artifacts are not propagated by chart version bump. + +## References + +- [docs/README.md](../README.md) — "Solution", "Key Components", and + "Open Questions" sections. +- ADR-0002 — chart versions follow the same SemVer + release-please + discipline as language packages. diff --git a/docs/adrs/0006-local-package-registry-workflow.md b/docs/adrs/0006-local-package-registry-workflow.md new file mode 100644 index 0000000..2512201 --- /dev/null +++ b/docs/adrs/0006-local-package-registry-workflow.md @@ -0,0 +1,121 @@ +# ADR-0006: Local package registry workflow for in-flight development + +**Status:** accepted +**Date:** 2026-05-02 +**Deciders:** Jason Anton + +## Context + +TrAICE cells and other consumers pull `stoopid-commons` packages from +public registries (PyPI, npm, crates.io) by version pin. During in-flight +development of a `stoopid-commons` change — say a new field in the +logging schema — a consumer needs to validate against the unreleased +package *before* it ships. Two naïve approaches fail: + +1. **Publish a real version to public registries to test it.** Public + registries are append-only and global. Pre-release tags pollute + version history with dev noise that cannot be removed. Mistakes are + permanent (see ADR-0002). +2. **Use path-based local installs (e.g. `pip install -e ../local-path`, + npm `file:` deps).** Works on one developer machine, but does not + reproduce in containers or CI. It also does not validate the + *published* package layout — the `pyproject.toml` packaging metadata, + the npm `files` whitelist, the included assets — and so it routinely + passes locally and breaks at consumer install time. + +A useful in-flight workflow needs to (a) avoid touching public +registries, (b) reproduce the exact install path that production will +use, and (c) be cheap to set up and tear down. + +The Python and TypeScript ecosystems both have mature local-registry +options: + +- **devpi** for Python — a local PyPI mirror and dev-publishing index. +- **Verdaccio** for npm — a local npm proxy/registry. + +Both run as a local Docker container, expose a registry URL, and accept +real `twine`/`uv publish` and `npm publish`/`pnpm publish` against them. + +Rust is asymmetric: there is no widely-deployed open-source equivalent +to devpi or Verdaccio that is as turnkey. The pragmatic Rust idiom is +Cargo path dependencies for same-machine work and Cargo git +dependencies for cross-repo work, both of which are first-class in +Cargo and reproduce the published package layout (`Cargo.toml` / +`[lib]` / `[bin]` configuration) more faithfully than the Python or +TypeScript path-install equivalents. + +## Decision + +The canonical in-flight-dev workflow is: + +- **Python:** local **devpi** instance. Developers run a docker-compose + snippet documented in [CONTRIBUTING.md](../../CONTRIBUTING.md), + publish dev-tagged versions (e.g. `0.5.0-dev.`) to it via + `uv publish` against the devpi index URL, and have the consumer set + the appropriate index URL to pull from devpi. +- **TypeScript:** local **Verdaccio** instance. Same pattern: publish + via `pnpm publish --registry http://localhost:4873`, consume by + setting `npmrc` registry to the local instance. +- **Rust:** Cargo path dependencies for same-machine dev (most common), + Cargo git dependencies (against a feature branch) for cross-repo dev. + No local Cargo registry is required. + +Promotion to the public registries follows the normal release-please +flow (see ADR-0002): the dev-tagged versions stay local, and a real +SemVer release is cut by merging to `main` with a Conventional Commit +that triggers the appropriate bump. The dev artifacts are discarded +when the local registry container is torn down. + +## Consequences + +### Positive + +- Zero risk of accidental public publishes during dev. Pre-release + noise stays off PyPI/npm/crates.io. +- Consumers exercise the exact install path that production will use, + so packaging-metadata mistakes (missing files, wrong entry points) + are caught before publication. +- Local registries are trivial to tear down (`docker compose down`). +- Cargo path/git deps are first-class and well-understood by Rust + developers. + +### Negative + +- Python and TypeScript developers run an additional local container. + Mitigated by a docker-compose snippet documented in + [CONTRIBUTING.md](../../CONTRIBUTING.md). +- Asymmetry between Rust (path/git deps) and Python/TypeScript (local + registry). The asymmetry is documented but real, and contributors + switching languages must remember it. + +### Neutral + +- This decision does not preclude adopting a self-hosted Cargo + registry (kellnr, shipyard) later if Rust path/git deps prove + insufficient — for example, if cross-org Rust consumers proliferate + and want a Verdaccio-equivalent dev path. +- It also does not preclude using GitHub Packages or another private + registry for Python or TypeScript if needs change. + +## Alternatives Considered + +- **Path/file installs only (e.g. `pip install -e`, npm `file:`)** — + rejected for Python and TypeScript. Does not validate published + packaging metadata; ships hidden bugs to first real consumer at + install time. +- **Pre-release tags published to public registries during dev** — + rejected. Public registries are append-only; dev-tagged versions + pollute version history permanently and cannot be un-published. +- **Tarball drops in PR comments / shared-drive artifacts** — + rejected. Not reproducible; not scriptable in CI; high friction. +- **Self-hosted Cargo registry (kellnr, shipyard) for Rust at v1** — + considered, deferred. Cargo path/git deps cover the v1 use cases + without operational overhead. Revisit if usage demands it. + +## References + +- [docs/README.md](../README.md) — "Solution" section, local-registry + workflow. +- [CONTRIBUTING.md](../../CONTRIBUTING.md) — local-development + workflow (to be filled in with docker-compose snippet). +- ADR-0002 — public-registry publication discipline. diff --git a/docs/adrs/0007-package-naming-convention.md b/docs/adrs/0007-package-naming-convention.md new file mode 100644 index 0000000..c0ffc9a --- /dev/null +++ b/docs/adrs/0007-package-naming-convention.md @@ -0,0 +1,123 @@ +# ADR-0007: Package naming convention across registries + +**Status:** accepted +**Date:** 2026-05-02 +**Deciders:** Jason Anton + +## Context + +`stoopid-commons` publishes packages to four public, append-only namespaces: +PyPI (Python), npm (TypeScript), crates.io (Rust), and a Helm chart +repository (charts). Package names on those registries are effectively +permanent: a name reserved at first publish cannot be cleanly renamed +without abandoning the original and re-onboarding every consumer. Naming +must therefore be decided once, deliberately, and applied consistently +across all four registries. + +Each ecosystem has its own naming conventions: + +- **PyPI** — bare names with hyphens (`stoopid-logging`). Distribution + name vs. import name can differ; the import name uses underscores + (`stoopid_logging`). +- **npm** — bare names (`stoopid-logging`) or scoped names + (`@stoopid/logging`). Scopes provide namespace protection but introduce + asymmetry with the other registries. +- **crates.io** — bare names with hyphens (`stoopid-logging`). No + namespace concept. +- **Helm** — chart names with hyphens (`stoopid-service-base`). No + namespace concept beyond the chart repository itself. + +Three plausible conventions exist: + +1. **Bare `stoopid-` everywhere.** Simple, consistent, easy to + discover by search. +2. **Scoped npm + bare elsewhere.** Idiomatic on npm, asymmetric across + languages. +3. **Always-prefixed `stoopid-commons-`.** Self-documenting that + the artifact ships from this repo, but verbose and treats the *repo + name* as part of every *artifact name*. + +Three of the planned first-target packages share a common name across +languages: a Python `stoopid-logging`, a TypeScript `stoopid-logging`, +and a Rust `stoopid-logging`. Each lives on a different registry, so the +bare-name collision only matters for git-tag disambiguation, which is +handled separately (see Decision). + +## Decision + +Use bare `stoopid-` package names across PyPI, npm, and crates.io. +Helm charts use `stoopid-` (e.g. `stoopid-service-base`). + +- The published distribution name is `stoopid-` on every registry. +- For Python, the corresponding import name is `stoopid_` per PEP 8 + module conventions. +- npm packages are **not** scoped (`@stoopid/` is rejected). +- The repo-name prefix `stoopid-commons-` is **not** used as a package + prefix; "commons" describes the *repo*, not the *artifacts*. +- Identical published names across languages (e.g. three different + `stoopid-logging` packages on three different registries) are + disambiguated in git tags by a language suffix on the release-please + `component`: `stoopid-logging-py`, `stoopid-logging-ts`, + `stoopid-logging-rs`. The suffix appears in the tag, not in the + published package name. + +Before first publish to a registry, package authors verify that the +chosen name is available; if not, the fallback is to publish as +`@stoopid/` on npm (acceptable degraded path) or to negotiate a +different name and update this ADR. Do not silently rename without an +ADR change. + +## Consequences + +### Positive + +- Consistent and discoverable: searching "stoopid-" on any of the three + language registries surfaces the full set. +- Easy to document and remember; no per-registry rule for contributors + to learn. +- Trivial mapping from package directory (`packages/python/stoopid-logging/`) + to published name (`stoopid-logging`). + +### Negative + +- Bare names assume availability on each registry. Each first-publish + must verify availability; collisions force a per-package fallback. +- npm without a scope provides no namespace squat-protection. Mitigated + by publishing the names early (before any external attention) and by + the documented fallback to `@stoopid/` if a collision actually + occurs. +- Identical published names across languages require explicit git-tag + disambiguation via the `component` field in + `release-please-config.json` — without it, three packages would all + produce tags like `stoopid-logging-v0.1.0` and collide. + +### Neutral + +- Helm chart naming follows the same convention; no extra rule needed. +- This decision is independent of the chart distribution mechanism (the + GitHub Pages vs OCI question tracked in `docs/README.md` open + questions). + +## Alternatives Considered + +- **Scoped npm (`@stoopid/`) + bare elsewhere** — rejected. + Adds asymmetry between languages for marginal namespace-protection + benefit on a single registry. The fallback path remains available if + a real collision forces it. +- **Always-prefixed `stoopid-commons-`** — rejected. Verbose, + duplicates information already implied by the repo, and conflates the + repository name with the artifact name. Consumers should not need to + know which repo a package came from to use it. +- **Per-language idiomatic naming (e.g. `stoopid_logging` Python + distribution name)** — rejected. PEP 8 governs *import* names, not + *distribution* names; the distribution name is a marketing and + discovery surface, and consistency across languages outweighs + per-ecosystem idiom. + +## References + +- ADR-0002 — publication discipline; once published, names are + permanent. +- `release-please-config.json` — `package-name` and language-suffixed + `component` per package. +- [docs/README.md](../README.md) — Solution section and goals. diff --git a/docs/adrs/README.md b/docs/adrs/README.md index 10cdeed..5b4826b 100644 --- a/docs/adrs/README.md +++ b/docs/adrs/README.md @@ -90,4 +90,10 @@ review and edit before committing. -- _(no ADRs yet)_ +- [ADR-0001 — Polyglot monorepo layout](0001-polyglot-monorepo-layout.md) +- [ADR-0002 — Conventional commits and semver](0002-conventional-commits-and-semver.md) +- [ADR-0003 — Trunk-based, tags as deploy targets](0003-trunk-based-tags-as-deploy-targets.md) +- [ADR-0004 — Shared logging JSON schema](0004-shared-logging-json-schema.md) +- [ADR-0005 — Parameterized Helm chart base](0005-parameterized-helm-chart-base.md) +- [ADR-0006 — Local package registry workflow](0006-local-package-registry-workflow.md) +- [ADR-0007 — Package naming convention](0007-package-naming-convention.md) diff --git a/release-please-config.json b/release-please-config.json index b059819..0b2e1be 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,12 +1,12 @@ { "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "release-type": "simple", - "include-component-in-tag": false, + "include-component-in-tag": true, "include-v-in-tag": true, "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": true, "draft": false, "prerelease": false, + "separate-pull-requests": true, "changelog-sections": [ { "type": "feat", "section": "Features", "hidden": false }, { "type": "fix", "section": "Bug Fixes", "hidden": false }, @@ -21,8 +21,28 @@ { "type": "style", "section": "Styles", "hidden": true } ], "packages": { - ".": { - "package-name": "", + "packages/python/stoopid-logging": { + "release-type": "python", + "package-name": "stoopid-logging", + "component": "stoopid-logging-py", + "changelog-path": "CHANGELOG.md" + }, + "packages/ts/stoopid-logging": { + "release-type": "node", + "package-name": "stoopid-logging", + "component": "stoopid-logging-ts", + "changelog-path": "CHANGELOG.md" + }, + "packages/rust/stoopid-logging": { + "release-type": "rust", + "package-name": "stoopid-logging", + "component": "stoopid-logging-rs", + "changelog-path": "CHANGELOG.md" + }, + "charts/stoopid-service-base": { + "release-type": "helm", + "package-name": "stoopid-service-base", + "component": "stoopid-service-base", "changelog-path": "CHANGELOG.md" } }