From 49cf3bb7c93967cd48d09b67133b1327e4404ac3 Mon Sep 17 00:00:00 2001 From: Dean Harel Date: Wed, 10 Jun 2026 23:19:38 +0300 Subject: [PATCH] chore: group spec artifacts under spec/ and scrub internal detail from public docs --- AGENTS.md | 35 +++++++++++-------------- README.md | 22 ++++++++-------- package.json | 2 +- scripts/publish.ts | 20 +++++++------- openapi.json => spec/openapi.json | 0 openapi.yaml => spec/openapi.yaml | 0 provenance.json => spec/provenance.json | 0 7 files changed, 39 insertions(+), 40 deletions(-) rename openapi.json => spec/openapi.json (100%) rename openapi.yaml => spec/openapi.yaml (100%) rename provenance.json => spec/provenance.json (100%) diff --git a/AGENTS.md b/AGENTS.md index fef9527..6bb7254 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,47 +1,44 @@ # UNIPaaS/openapi Canonical, versioned OpenAPI spec for the UNIPaaS Platform API. Source of truth for docs, SDKs, and -the future MCP server. The spec is generated in `platform-api` and published here by that repo's CI, -authenticating as the `openapi-publisher` GitHub App; do not hand-edit `openapi.json`. +the future MCP server. The spec is generated upstream from the UNIPaaS Platform API and published +here automatically by the producer's CI; do not hand-edit the spec. ## Layout -- `openapi.json` / `openapi.yaml` - latest published spec, both formats (HEAD = current); the - producer hands over JSON, `publish.ts` derives the YAML. Do not hand-edit either. -- `provenance.json` - the last publish's deploy `sequence`/`sha`/`tag`; the ordering guard drops +- `spec/openapi.json` / `spec/openapi.yaml` - latest published spec, both formats (HEAD = current); + the producer hands over JSON, `publish.ts` derives the YAML. Do not hand-edit either. +- `spec/provenance.json` - the last publish's deploy `sequence`/`sha`/`tag`; the ordering guard drops publishes from an older deploy than this. Generated on publish; do not hand-edit. - `CHANGELOG.md` - generated diff between releases. - `.spectral.yaml` - shared lint ruleset. - `scripts/publish.ts` - change-detect + ordering guard, commits latest + cuts a release; invoked by - platform-api CI as `npm run publish:spec` after a production deploy. + the producer's CI as `npm run publish:spec` after a production deploy. - `scripts/release-tag.ts` - computes the provenance tag; pure, unit-tested. Scripts are strict TypeScript run with `tsx` (no build step); `tsc --noEmit` is the typecheck gate. ## Publishing -platform-api's `publish-openapi` CI job generates the spec on a production deploy and runs -`npm run publish:spec -- --spec --sha --date --sequence `. The hub -owns the logic: it canonically compares the spec to HEAD (no-op if unchanged), drops publishes from an -older deploy than the last recorded `sequence` (so HEAD mirrors what's live; a rollback is a newer -deploy and passes), writes both formats, and cuts an idempotent release. Auth is a short-lived -installation token from the `openapi-publisher` GitHub App (App id `4020207`, key in 1Password), -minted in the producer job; releases are attributed to `openapi-publisher[bot]`. +The producer's CI generates the spec on a production deploy and hands it to +`npm run publish:spec -- --spec --sha --date --sequence `. The hub owns +the logic: it canonically compares the spec to HEAD (no-op if unchanged), drops publishes from an +older deploy than the last recorded `sequence` (so HEAD mirrors what is live; a rollback is a newer +deploy and passes), writes both formats plus provenance, and cuts an idempotent release. The producer +just generates and hands over; it holds no publishing logic. ## Versioning -Two separate concepts. `info.version` inside the spec is a hand-curated human label owned by -platform-api. The release tag `v+.` is the provenance identity and -the thing consumers pin. +Two separate concepts. `info.version` inside the spec is a curated human label. The release tag +`v+.` is the provenance identity and the thing consumers pin. ## Gates / commands - `npm install` - install the dev toolchain (tsx, typescript, spectral). - `npm run typecheck` - strict type-check (`tsc --noEmit`). - `npm test` - run the unit tests (`tsx --test`). -- `npm run lint:spec` - lint `openapi.json` against `.spectral.yaml`. +- `npm run lint:spec` - lint `spec/openapi.json` against `.spectral.yaml`. ## Conventions -Conventional Commits, no attribution trailers. SSH remotes only -(`git@github.com-unipaas:UNIPaaS/openapi.git`). +Conventional Commits, no attribution trailers. SSH remotes only (`git@github.com:UNIPaaS/openapi.git`). diff --git a/README.md b/README.md index 8faa7ef..877ed80 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,16 @@ The canonical, versioned OpenAPI spec for the UNIPaaS Platform API. This repo is the source of truth that downstream consumers (the developer docs, generated SDKs, the OpenAPI->MCP server) pull -from. It is not where the spec is authored: the spec is generated from `platform-api` and published -here by that repo's CI. +from. The spec is not authored here: it is generated from the UNIPaaS Platform API and published to +this repo automatically. Do not hand-edit it. ## What is here -- `openapi.json` / `openapi.yaml` - the latest published spec in both formats (HEAD always serves - current). JSON for tooling, YAML for human-readable diffs and language-agnostic codegen. +- `spec/openapi.json` / `spec/openapi.yaml` - the latest published spec in both formats (HEAD always + serves current). JSON for tooling, YAML for human-readable diffs and language-agnostic codegen. - Releases - each publish cuts a GitHub Release with the immutable `openapi.json` and `openapi.yaml` - assets, tagged - `v+.` so every spec traces back to the platform-api commit that - built it. + assets, tagged `v+.` so every spec traces back to the source + commit that built it. - `CHANGELOG.md` - human-readable diff between releases (generated). - `.spectral.yaml` - the lint ruleset shared by producer and consumers. @@ -24,11 +23,12 @@ Pin a specific version: Or always-latest from HEAD: - curl -fsSL https://raw.githubusercontent.com/UNIPaaS/openapi/main/openapi.json -o openapi.json + curl -fsSL https://raw.githubusercontent.com/UNIPaaS/openapi/main/spec/openapi.json -o openapi.json This repo is public, so reads need no authentication. -## How it is published +## Versioning -`platform-api` CI generates the spec on a successful production deploy, compares it to the last -release, and on a real change runs `scripts/publish.ts` here to commit latest and cut a release. +`info.version` inside the spec is a curated human label. The release tag +`v+.` is the provenance identity and the thing consumers pin: it is +monotonic and traces each published spec back to the exact source commit that produced it. diff --git a/package.json b/package.json index e49a3f8..3aafafb 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "test": "tsx --test scripts/*.test.ts", "typecheck": "tsc --noEmit", - "lint:spec": "spectral lint openapi.json --ruleset .spectral.yaml", + "lint:spec": "spectral lint spec/openapi.json --ruleset .spectral.yaml", "publish:spec": "tsx scripts/publish.ts" }, "dependencies": { diff --git a/scripts/publish.ts b/scripts/publish.ts index 43363b7..5c66b6a 100644 --- a/scripts/publish.ts +++ b/scripts/publish.ts @@ -1,7 +1,7 @@ -// Usage: npm run publish:spec -- --spec --sha --date --sequence +// Usage: npm run publish:spec -- --spec --sha --date --sequence // The hub's publish entrypoint. Producers stay dumb: they generate a spec and hand it over; ordering, // change-detection, idempotency, the tag scheme, and release self-heal all live here. `--sequence` is -// a monotonic deploy key (GitLab's CI_PIPELINE_ID) so HEAD mirrors the most recent production deploy. +// a monotonic deploy key (the producer's CI pipeline id) so HEAD mirrors the most recent production deploy. // Run inside a clone of this repo with gh authenticated (contents:write). Decision logic is in // publish-logic.ts (unit-tested); this file is the I/O around it. import fs from 'node:fs'; @@ -11,7 +11,9 @@ import { releaseTag } from './release-tag'; import { classify, type OpenApiSpec, type Provenance } from './publish-logic'; const REPO = 'UNIPaaS/openapi'; -const PROVENANCE_FILE = 'provenance.json'; +const SPEC_JSON = 'spec/openapi.json'; +const SPEC_YAML = 'spec/openapi.yaml'; +const PROVENANCE_FILE = 'spec/provenance.json'; function arg(name: string): string { const i = process.argv.indexOf(`--${name}`); @@ -37,14 +39,14 @@ const releaseExists = (tag: string): boolean => { const readJson = (path: string): T => JSON.parse(fs.readFileSync(path, 'utf8')) as T; const notes = (version: string, sha: string, isoDate: string): string => - [`Spec version \`${version}\`.`, `From platform-api ${sha.slice(0, 7)} on ${isoDate.slice(0, 10)}.`].join('\n'); + [`Spec version \`${version}\`.`, `From source commit ${sha.slice(0, 7)} on ${isoDate.slice(0, 10)}.`].join('\n'); // Create the release from the committed assets. Idempotent: skip if the tag is already released. const ensureRelease = (tag: string, body: string): void => { if (releaseExists(tag)) { console.log(`release ${tag} already exists; nothing to do`); return; } - run('gh', ['release', 'create', tag, 'openapi.json', 'openapi.yaml', '--repo', REPO, '--title', tag, '--notes', body]); + run('gh', ['release', 'create', tag, SPEC_JSON, SPEC_YAML, '--repo', REPO, '--title', tag, '--notes', body]); console.log(`released ${tag}`); }; @@ -57,7 +59,7 @@ if (!Number.isInteger(sequence) || sequence < 0) { } const incoming = readJson(specPath); -const current = fs.existsSync('openapi.json') ? readJson('openapi.json') : null; +const current = fs.existsSync(SPEC_JSON) ? readJson(SPEC_JSON) : null; const provenance = fs.existsSync(PROVENANCE_FILE) ? readJson(PROVENANCE_FILE) : null; const lastSequence = provenance ? provenance.sequence : -1; @@ -71,10 +73,10 @@ if (stage === 'stale') { if (stage === 'changed') { const tag = releaseTag({ infoVersion: incoming.info.version, isoDate, sha }); // Write both formats: JSON for tooling, YAML for human-readable diffs and language-agnostic codegen. - fs.copyFileSync(specPath, 'openapi.json'); - fs.writeFileSync('openapi.yaml', toYaml(incoming, { lineWidth: -1, noRefs: true })); + fs.copyFileSync(specPath, SPEC_JSON); + fs.writeFileSync(SPEC_YAML, toYaml(incoming, { lineWidth: -1, noRefs: true })); fs.writeFileSync(PROVENANCE_FILE, `${JSON.stringify({ sequence, sha, timestamp: isoDate, tag } satisfies Provenance, null, 2)}\n`); - run('git', ['add', 'openapi.json', 'openapi.yaml', PROVENANCE_FILE]); + run('git', ['add', SPEC_JSON, SPEC_YAML, PROVENANCE_FILE]); if (capture('git', ['status', '--porcelain']).trim()) { run('git', ['commit', '-m', `chore: publish spec ${tag}`]); run('git', ['push', 'origin', 'HEAD']); diff --git a/openapi.json b/spec/openapi.json similarity index 100% rename from openapi.json rename to spec/openapi.json diff --git a/openapi.yaml b/spec/openapi.yaml similarity index 100% rename from openapi.yaml rename to spec/openapi.yaml diff --git a/provenance.json b/spec/provenance.json similarity index 100% rename from provenance.json rename to spec/provenance.json