diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f928b9d..205bc47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,13 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: '20' + node-version: "20" - name: Install dependencies run: npm ci @@ -39,7 +41,24 @@ jobs: run: | npx spectral lint schemas/core/openapi.yaml --format github-actions --fail-severity warn + - name: Check for contract changes + id: contract + run: | + BASE_REF="${{ github.event.pull_request.base.sha || github.event.before }}" + if [ -z "$BASE_REF" ] || ! git cat-file -e "$BASE_REF" 2>/dev/null; then + echo "changed=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + CHANGED=$(git diff --name-only "$BASE_REF" HEAD -- '*.tsp') + if [ -n "$CHANGED" ]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No contract files changed — skipping version bump check" + fi + - name: Check version bump + if: steps.contract.outputs.changed == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b98a6df --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,106 @@ +# HyperFleet API Spec + +TypeSpec sources that generate the HyperFleet core OpenAPI specification. Published as the `hyperfleet` npm package for provider-specific repos (e.g., `hyperfleet-api-spec-gcp`). Also consumed as a Go module via `schemas/schemas.go` (`embed.FS`). + +## Critical path + +```bash +npm install # Install TypeSpec compiler + deps +./build-schema.sh # Build core OpenAPI spec +npx spectral lint schemas/core/openapi.yaml --fail-severity warn # Lint (matches CI strictness) +``` + +**IMPORTANT:** `schemas/core/openapi.yaml` is committed. CI runs `git diff --exit-code schemas/` — if you change TypeSpec sources, rebuild and commit the schema in the same PR. + +**IMPORTANT:** Every PR must bump the version in `main.tsp` (the `version` field inside the `@info` decorator). CI compares against the latest release tag and blocks if unchanged or lower. + +When bumping version: +1. Edit the `version` field inside the `@info` decorator in `main.tsp` +2. Run `./build-schema.sh` (auto-syncs `package.json` version — never edit `package.json` manually) +3. Update `CHANGELOG.md`: add new `## [X.Y.Z] - YYYY-MM-DD` section, update comparison links + +## Source of truth + +| Topic | File | +|-------|------| +| Build process | `build-schema.sh` | +| CI checks | `.github/workflows/ci.yml` | +| Release automation | `.github/workflows/release.yml`, `RELEASING.md` | +| Contributing guide | `CONTRIBUTING.md` | +| Changelog format | `CHANGELOG.md` (Keep a Changelog) | +| Spectral rules | `.spectral.yaml` | +| TypeSpec config | `tspconfig.yaml` | +| Go module embed | `schemas/schemas.go` | + +## Architecture: shared vs core + +``` +shared/ → Cross-provider models and services (npm package) +core/ → Core-only models and internal-only services +main.tsp → Entry point (imports shared + core) +schemas/core/ → Generated output (committed) +``` + +**Where to put new code:** + +| What | Where | Why | +|------|-------|-----| +| Models used by all providers | `shared/models/{resource}/model.tsp` | Published as npm package | +| Endpoints for external clients | `shared/services/{resource}.tsp` | Shared across provider contracts | +| Internal-only endpoints (adapters) | `core/services/{resource}-internal.tsp` | Core contract only | +| Core-specific model overrides | `core/models/{resource}/model.tsp` | Not shared | +| Provider-specific models | Separate repo (e.g., `hyperfleet-api-spec-gcp`) | Own contract | + +**Directory naming:** `shared/models/` uses plural names (`clusters/`, `nodepools/`, `statuses/`) except `resource/` and `common/`. `core/models/` uses singular names (`cluster/`, `nodepool/`). Follow existing convention per directory. + +## TypeSpec conventions + +**IMPORTANT: Required decorators on every interface:** `@useAuth(HyperFleet.BearerAuth)`, `@tag("ResourceName")`. Every operation must have `@operationId("operationName")` and `@summary("...")`. Missing `@useAuth` causes the generated spec to omit security requirements — this was a real bug (commit `89b9f9b`). + +**Service file boilerplate:** +```tsp +import "@typespec/http"; +import "@typespec/openapi"; +import "@typespec/openapi3"; +// ... model imports ... + +using Http; +using OpenAPI; + +namespace HyperFleet; + +@tag("Resources") +@route("/resources") +@useAuth(HyperFleet.BearerAuth) +interface Resources { + @get + @summary("List resources") + @operationId("getResources") + getResources(...QueryParams): Body | Error | BadRequestResponse; +} +``` + +**Model files** do not declare a namespace or `using` statements — just imports and model definitions. + +**Naming:** +- Resources: `Cluster`, `NodePool`, `Resource` (singular) +- Create payloads: `ClusterCreateRequest`, `ResourceCreateRequest` +- Patch payloads: `ClusterPatchRequest`, `ResourcePatchRequest` +- Lists: `ClusterList`, `ResourceList` + +**Import order:** TypeSpec library imports first, then relative model/service imports. + +**Example files:** Each resource has `example_*.tsp` files for `@example` decorators. Example files in `shared/models/` are imported from their resource's `model.tsp`. Example files in `core/models/` are imported from `main.tsp`. Example files do not declare a namespace. + +## Boundaries + +- **IMPORTANT:** Never edit files in `schemas/` or `tsp-output-core/` directly — they are generated +- `package.json` version is auto-synced by `build-schema.sh` — do not edit manually +- New service files must be imported in `main.tsp` or they won't compile into the schema + +## Gotchas + +- `@typespec/rest` and `@typespec/versioning` are in `package.json` but not imported in any source file — they may be transitive requirements. Don't remove without testing. +- Spectral linting in CI uses `--fail-severity warn` — all warnings are treated as errors. +- The `go.mod` at repo root exists so downstream Go services can `go get` this module and read schemas via `embed.FS`. Don't remove it. +- The `BearerAuth` model in `main.tsp` uses lowercase `"bearer"` as a workaround for `kin-openapi` library requirements. diff --git a/CLAUDE.md b/CLAUDE.md index fc6237d..43c994c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,234 +1 @@ -# HyperFleet API Spec - AI Agent Context - -This repository generates the HyperFleet core OpenAPI specification from TypeSpec definitions. The GCP-specific contract lives in [hyperfleet-api-spec-gcp](https://github.com/openshift-hyperfleet/hyperfleet-api-spec-gcp), which imports shared models from this repo as the `hyperfleet` npm package. - -## Quick Reference - -**Build commands:** -```bash -npm run build # Generate core OpenAPI spec -./build-schema.sh # Same, via script directly -``` - -**Validation workflow:** -```bash -npm install # Install dependencies -./build-schema.sh # Build core OpenAPI 3.0 -ls -l schemas/core/openapi.yaml # Confirm output exists -``` - -## Key Concepts - -### Repository Layout - -``` -shared/ # Cross-provider models and services (published as the `hyperfleet` npm package) -core/ # Core-only models and internal services -main.tsp # Entry point — imports shared + core -schemas/core/ # Generated output (committed) -``` - -**When adding new models:** -- Cross-provider models → `shared/models/` -- Core-only models → `core/models/` -- GCP-specific → separate repo (`hyperfleet-api-spec-gcp`) - -### Public vs Internal APIs - -Status endpoints are split: -- `shared/services/statuses.tsp` - GET operations (external clients) -- `core/services/statuses-internal.tsp` - PUT operations (internal adapters only) and resource force-delete - -The split allows generating different API contracts per audience. Only `statuses.tsp` is imported by default. - -## Code Style - -### TypeSpec Conventions - -**Imports first, namespace second** (applies to service and model files; example `const` files do not declare a namespace): -```typescript -import "@typespec/http"; -import "../models/common/model.tsp"; - -namespace HyperFleet; -``` - -**Use decorators for HTTP semantics:** -```typescript -@route("/clusters") -interface Clusters { - @get list(): Cluster[] | Error; - @post create(@body cluster: ClusterInput): Cluster | Error; -} -``` - -**Model naming:** -- Resources: `Cluster`, `NodePool` (singular) -- Inputs: `ClusterInput`, `NodePoolInput` -- Provider-specific: `GCPClusterSpec`, `AWSClusterSpec` - -### File Organization - -``` -shared/models/{resource}/ - ├── model.tsp # Shared model definitions - └── interfaces.tsp # Optional: shared interfaces - -shared/services/ - └── {resource}.tsp # Shared service endpoints - -core/models/{resource}/ - └── model.tsp # Core-specific models - -core/services/ - └── {resource}-internal.tsp # Internal-only endpoints -``` - -## Boundaries - -**DO NOT:** -- Modify generated files in `schemas/` or `tsp-output-core/` directly -- Add dependencies without checking TypeSpec version compatibility -- Auto-generate documentation - it degrades agent performance per research -- Commit `node_modules/` or build artifacts - -**DO:** -- Run `./build-schema.sh` and commit `schemas/core/openapi.yaml` with your changes -- Keep TypeSpec files focused (one resource per service file) -- Use semantic versioning for releases (automated on merge to main) - -## Common Tasks - -### Add a new endpoint to existing service - -```typescript -// shared/services/clusters.tsp -namespace HyperFleet; - -@route("/clusters") -interface Clusters { - // ... existing endpoints ... - - @get - @route("/{id}/health") - getHealth(@path id: string): HealthStatus | Error; -} -``` - -### Add a new resource - -1. Create model: -```typescript -// shared/models/health/model.tsp -import "@typespec/http"; - -model HealthStatus { - id: string; - state: "healthy" | "degraded" | "critical"; - lastChecked: utcDateTime; -} -``` - -2. Create service: -```typescript -// shared/services/health.tsp -import "@typespec/http"; -import "../models/health/model.tsp"; -import "../models/common/model.tsp"; - -namespace HyperFleet; - -@route("/health") -interface Health { - @get check(): HealthStatus | Error; -} -``` - -3. Import in `main.tsp`: -```typescript -import "./shared/services/health.tsp"; -``` - -4. Build: `npm run build` - -### Add provider-specific fields - -Provider-specific models live in the provider's own repository (e.g., `hyperfleet-api-spec-gcp`). See that repo for examples of how to extend core shared models. - -## Version Bump and Changelog - -When bumping the version in `main.tsp`, always update `CHANGELOG.md`: - -1. Keep `## [Unreleased]` at the top, then add a new version section as `## [X.Y.Z] - YYYY-MM-DD` -2. List changes under appropriate headings (`Added`, `Changed`, `Fixed`, `Removed`) -3. Update the comparison links at the bottom of the file -4. Follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format - -## Validation Checklist - -Before submitting changes: - -- [ ] Dependencies installed: `npm install` -- [ ] Core schema builds: `./build-schema.sh` -- [ ] Schema file generated: `ls schemas/core/openapi.yaml` -- [ ] No TypeSpec compilation errors (check output) -- [ ] Schema passes linting: `spectral lint schemas/core/openapi.yaml` -- [ ] Changes committed including schema update -- [ ] PR description references related issue - -## Build System Details - -**The build-schema.sh script:** -1. Extracts the version from `main.tsp` and syncs it into `package.json` -2. Runs `node_modules/.bin/tsp compile main.tsp --output-dir tsp-output-core` -3. Moves output to `schemas/core/openapi.yaml` - -**Output locations:** -- TypeSpec temp: `tsp-output-core/schema/openapi.yaml` (auto-deleted) -- Final: `schemas/core/openapi.yaml` (committed) - -**Version sync:** `package.json` version is kept in lockstep with `main.tsp` automatically on every build. The CI consistency check (`git diff --exit-code`) enforces that both are committed together. - -## VS Code Extension Notes - -The TypeSpec extension may show false errors for models resolved only at compile time. Both the CLI and "Emit from TypeSpec" command work correctly. - -## Dependencies - -All TypeSpec libraries use version `^1.6.0` for consistency: -- `@typespec/compiler` - Core compiler -- `@typespec/http` - HTTP semantics -- `@typespec/openapi` - OpenAPI decorators -- `@typespec/openapi3` - OpenAPI 3.0 emitter -- `@typespec/rest` - REST conventions -- `@typespec/versioning` - API versioning support - -**Adding new TypeSpec libraries:** -```bash -npm install --save-dev @typespec/library-name@^1.6.0 -``` - -Match the version range to existing dependencies. - -## Release Process - -Releases are **fully automated** via GitHub Actions (`.github/workflows/release.yml`). - -On every push to `main`, the release workflow: -1. Extracts the version from the `@info` decorator in `main.tsp` -2. Skips if a tag for that version already exists -3. Builds the core OpenAPI schema -4. Creates an annotated Git tag (`vX.Y.Z`) -5. Publishes a GitHub Release with `core-openapi.yaml` attached - -The CI workflow (`.github/workflows/ci.yml`) enforces that the version in `main.tsp` is bumped from the latest release tag before a PR can be merged. - -To release a new version, simply bump the version in `main.tsp` and merge to `main`. - -## Architecture Context - -This repository is part of the HyperFleet project. For broader context: -- Architecture repo: https://github.com/openshift-hyperfleet/architecture -- Main API implementation: https://github.com/openshift-hyperfleet/hyperfleet-api - -The API implementation consumes the generated OpenAPI specs for validation and documentation. +@AGENTS.md