diff --git a/.github/scripts/bump-upstream-pin.mjs b/.github/scripts/bump-upstream-pin.mjs index cb34ec9..04688d3 100644 --- a/.github/scripts/bump-upstream-pin.mjs +++ b/.github/scripts/bump-upstream-pin.mjs @@ -1,7 +1,8 @@ #!/usr/bin/env node /** - * Upstream pins — move one of `.objectstack-sha` / `.cloud-sha` and write the - * pull-request body that lists what changed upstream since the previous pin. + * Upstream pins — check both `.objectstack-sha` and `.cloud-sha`, and move the + * objectstack pin, writing the pull-request body that lists what changed + * upstream since the previous pin. The cloud pin is moved by hand (below). * * ## Why this exists * @@ -12,18 +13,29 @@ * pass misses. A pin turns it into a bounded one: each file at the repository * root holds the one upstream commit the site was last aligned to, and every * alignment is a review of the upstream commits and changed paths since that - * commit. This script computes that range; `bump-upstream-docs.yml` runs it - * and opens the pull request whose body it writes. + * commit. For objectstack this script computes that range; + * `bump-upstream-docs.yml` runs it and opens the pull request whose body it + * writes. * * ## When a pin moves * * objectstack on a published `@objectstack/cli` release — the commit its * release tag `@objectstack/cli@VERSION` names. With no `--sha`, - * the release is npm's `latest` dist-tag. - * cloud after a successful cloud production deploy — the commit that - * deploy serves, never a merge to cloud's `main`. There is no - * default: the deploy supplies `--sha`, and the run refuses - * without one. + * the release is npm's `latest` dist-tag. This script moves it. + * cloud BY HAND, when someone refreshes the docs to match production — + * not on every cloud release (maintainer ruling, + * docs.objectos.ai#433). The person writes the full SHA of the + * commit production serves (cloud's `v*` tag, or its + * deploy-cloud-prod run; the health header's 8-character + * `x-objectstack-build-sha` only identifies it) and pastes + * `git log OLD..NEW -- CHANGELOG.md .changeset/` from a local + * cloud checkout into the pull-request body. No workflow, dispatch + * or token moves it, and this script refuses `--upstream cloud`: + * cloud is private and this repository holds no token to read it. + * AGENTS.md "Upstream pins" has the steps. + * + * `--check` validates both pin files the same way, by format alone; it reads + * neither upstream, so a hand-edited `.cloud-sha` passes it like any other. * * ## The pin file * @@ -31,8 +43,8 @@ * accepted value is the placeholder `unpinned`, for an upstream that has never * been pinned (`.cloud-sha` ships with it, because cloud is a private * repository this site's maintainers could not read when the pins were added). - * A range cannot start at the placeholder, so the first bump of an unpinned - * upstream only records the commit and says so; the next bump is the first + * A range cannot start at the placeholder, so the first move of an unpinned + * pin only records the commit and lists nothing; the next one is the first * bounded diff. A missing file, an empty one, an abbreviated SHA or a branch * name is refused — a pin that cannot be resolved to one commit bounds nothing. * @@ -41,8 +53,8 @@ * - the new commit is not a descendant of the current pin — the range * `OLD..NEW` would silently drop whatever `OLD` has that `NEW` lacks, and * the review would read a diff that is not the change; - * - the upstream cannot be read — cloud is private and needs `UPSTREAM_TOKEN`; - * - a cloud bump with no `--sha`. + * - the upstream cannot be read; + * - a bump of an upstream whose pin is moved by hand (cloud). * * A bump to the commit already pinned is not an error: it writes nothing, * reports `changed=false`, and the workflow opens no pull request. @@ -62,7 +74,7 @@ * * node .github/scripts/bump-upstream-pin.mjs --check * Validate both pin files. Exit 1 naming each bad one. - * node .github/scripts/bump-upstream-pin.mjs --upstream objectstack|cloud \ + * node .github/scripts/bump-upstream-pin.mjs --upstream objectstack \ * [--sha SHA] --body FILE [--git-dir DIR] * Resolve, fetch (blobless) into DIR, write the new pin and the body. * Writes `changed`, `old`, `new`, `short`, `label` and `title` to $GITHUB_OUTPUT @@ -79,19 +91,25 @@ import { spawnSync } from 'node:child_process'; const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); -/** The two upstreams. `paths` is what the alignment review reads. */ +/** + * The two upstreams. `paths` is what the alignment review reads. `byHand` marks + * a pin this script checks but never moves (docs.objectos.ai#433): its paths are + * the ones the person moving it lists with `git log` in the pull-request body. + */ const UPSTREAMS = { objectstack: { repo: 'objectstack-ai/objectstack', pinFile: '.objectstack-sha', paths: ['content/docs', 'docs/releases', 'CHANGELOG.md'], release: '@objectstack/cli', + byHand: false, }, cloud: { repo: 'objectstack-ai/cloud', pinFile: '.cloud-sha', paths: ['CHANGELOG.md', '.changeset'], release: null, + byHand: true, }, }; @@ -124,6 +142,17 @@ function parseUpstream(name) { return UPSTREAMS[name]; } +/** An upstream this script may move: declared, and not moved by hand. */ +function bumpable(name) { + const up = parseUpstream(name); + if (up.byHand) { + throw new Refusal( + `${up.pinFile} is moved by hand, not by this script (docs.objectos.ai#433): write the full SHA production serves and paste \`git log OLD..NEW -- ${up.paths.join(' ')}\` from a local checkout into the pull-request body — see AGENTS.md "Upstream pins"`, + ); + } + return up; +} + /** A `--sha` / dispatch-payload value: absent, or a full SHA. Never a ref name. */ function parseShaInput(value) { const v = (value ?? '').trim(); @@ -265,7 +294,7 @@ function readPinFile(root, up) { async function bump(opts) { const name = opts.upstream; - const up = parseUpstream(name); + const up = bumpable(name); const oldPin = readPinFile(opts.root, up); const url = `https://github.com/${up.repo}.git`; const env = gitEnv(process.env.UPSTREAM_TOKEN); @@ -274,9 +303,6 @@ async function bump(opts) { let source = 'supplied by the run that requested this bump'; let target = null; if (!newSha) { - if (!up.release) { - throw new Refusal(`a ${name} bump needs --sha: the commit its production deploy serves (there is no default)`); - } const version = await latestRelease(up.release); const tag = `${up.release}@${version}`; const ls = git(['ls-remote', url, `refs/tags/${tag}`, `refs/tags/${tag}^{}`], { env }); @@ -294,8 +320,7 @@ async function bump(opts) { const wants = kind === 'range' ? [newSha, oldPin.sha] : [newSha]; const fetched = git(['fetch', '--quiet', '--filter=blob:none', url, ...wants], { gitDir, env, allowFail: true }); if (fetched.status !== 0) { - const hint = name === 'cloud' && !process.env.UPSTREAM_TOKEN ? ' — cloud is private: set UPSTREAM_TOKEN' : ''; - throw new Refusal(`could not fetch ${up.repo} at ${wants.join(', ')}${hint}: ${(fetched.stderr || '').trim().split('\n').pop()}`); + throw new Refusal(`could not fetch ${up.repo} at ${wants.join(', ')}: ${(fetched.stderr || '').trim().split('\n').pop()}`); } git(['cat-file', '-e', `${newSha}^{commit}`], { gitDir }); @@ -433,6 +458,10 @@ function selfTest() { for (const bad of ['', 'objectui', 'toString', '__proto__', undefined]) { expectRefusal(`upstream: ${JSON.stringify(bad)}`, () => parseUpstream(bad), 'unknown upstream'); } + // Only objectstack is bumped here; the cloud pin is moved by hand (#433), and the refusal says how. + if (bumpable('objectstack') !== UPSTREAMS.objectstack) fail('bump: objectstack is not bumpable'); + expectRefusal('bump: cloud', () => bumpable('cloud'), 'moved by hand'); + expectRefusal('bump: cloud names the git log to paste', () => bumpable('cloud'), 'git log OLD..NEW -- CHANGELOG.md .changeset'); // The pin a missing file refuses is the real reader, against a real (temporary) tree. const empty = mkdtempSync(join(tmpdir(), 'pin-selftest-')); @@ -489,7 +518,7 @@ function selfTest() { process.exitCode = 1; return; } - console.log(`✓ self-test: ${total} pin/input case(s), upstream names, tag resolution, plan, body bounds and token carriage`); + console.log(`✓ self-test: ${total} pin/input case(s), upstream names, the hand-moved cloud pin, tag resolution, plan, body bounds and token carriage`); } /* ------------------------------------------------------------------ main -- */ diff --git a/.github/workflows/bump-upstream-docs.yml b/.github/workflows/bump-upstream-docs.yml index 60cd70b..510561f 100644 --- a/.github/workflows/bump-upstream-docs.yml +++ b/.github/workflows/bump-upstream-docs.yml @@ -1,45 +1,45 @@ name: Bump upstream docs pin -# Moves ONE upstream pin — `.objectstack-sha` or `.cloud-sha` — and opens a -# pull request whose body lists the upstream commits and changed paths since -# the previous pin. That body is the alignment review's input: the site is -# checked against a bounded diff instead of a periodic re-read of both corpora. -# The rules for when each pin moves, and what the body lists, are in the header -# of `.github/scripts/bump-upstream-pin.mjs`; AGENTS.md has the short form. +# Moves the objectstack pin, `.objectstack-sha`, and opens a pull request whose +# body lists the upstream commits and changed paths since the previous pin. +# That body is the alignment review's input: the site is checked against a +# bounded diff instead of a periodic re-read of the corpus. The rules are in the +# header of `.github/scripts/bump-upstream-pin.mjs`; AGENTS.md has the short form. # # objectstack on a published `@objectstack/cli` release. With no `sha`, the # run pins the commit tagged with npm's `latest` release. -# cloud after a successful cloud production deploy, to the commit that -# deploy serves. `sha` is required; the run refuses without it. +# +# The cloud pin, `.cloud-sha`, is NOT moved here: it is moved by hand when the +# docs are refreshed to match production (maintainer ruling, +# docs.objectos.ai#433), and the helper refuses `upstream: cloud`. AGENTS.md +# "Upstream pins" has the steps. This workflow still checks its format. # # Triggers: `workflow_dispatch` by hand, or `repository_dispatch` of type -# `bump-upstream-docs` with `client_payload: { upstream, sha }` — the shape a -# deploy or release workflow elsewhere sends. On a pull request that touches the -# pins, the helper or this file, the `pins` job runs the helper's self-test and -# checks both pin files. +# `bump-upstream-docs` with `client_payload: { upstream: objectstack, sha }` — +# the shape a release workflow elsewhere sends. On a pull request that touches +# the pins, the helper or this file, the `pins` job runs the helper's self-test +# and checks both pin files by format alone; it reads neither upstream. # # ⛔ One secret, `UPSTREAM_BUMP_TOKEN`, and the bump refuses to start without -# it. It is used for two things `GITHUB_TOKEN` cannot do here: -# - read `objectstack-ai/cloud`, a private repository (contents: read there); -# - push the bump branch and open the pull request (contents: write and -# pull-requests: write here). A pull request opened with `GITHUB_TOKEN` -# starts no `pull_request` workflows — GitHub suppresses them to prevent -# recursive runs — so CI would never run on the bump, and whether this -# repository lets Actions open pull requests at all is a repository setting. +# it. It pushes the bump branch and opens the pull request (contents: write and +# pull-requests: write here), which `GITHUB_TOKEN` cannot do usefully: a pull +# request it opens starts no `pull_request` workflows — GitHub suppresses them +# to prevent recursive runs — so CI would never run on the bump, and whether +# this repository lets Actions open pull requests at all is a repository +# setting. on: workflow_dispatch: inputs: upstream: - description: Which pin to move + description: Which pin to move (the cloud pin is moved by hand, see AGENTS.md) type: choice - options: [objectstack, cloud] + options: [objectstack] required: true sha: description: >- - Full 40-character commit SHA to pin. Required for cloud (the commit its - production deploy serves). Leave empty for objectstack to pin npm's - latest @objectstack/cli release. + Full 40-character commit SHA to pin. Leave empty to pin npm's latest + @objectstack/cli release. type: string required: false repository_dispatch: @@ -96,7 +96,7 @@ jobs: UPSTREAM_BUMP_TOKEN: ${{ secrets.UPSTREAM_BUMP_TOKEN }} run: | if [ -z "$UPSTREAM_BUMP_TOKEN" ]; then - echo "::error title=NOT CONFIGURED::The repository secret UPSTREAM_BUMP_TOKEN is not set. It needs contents: read on objectstack-ai/cloud and contents: write + pull-requests: write on this repository. GITHUB_TOKEN is not a substitute: it cannot read cloud, and a pull request it opens runs no CI." + echo "::error title=NOT CONFIGURED::The repository secret UPSTREAM_BUMP_TOKEN is not set. It needs contents: write + pull-requests: write on this repository. GITHUB_TOKEN is not a substitute: a pull request it opens runs no CI. (The cloud pin needs no token: it is moved by hand, see AGENTS.md.)" exit 1 fi diff --git a/AGENTS.md b/AGENTS.md index 033e8d0..70b0235 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -103,7 +103,17 @@ node .github/scripts/check-translations.mjs --worklist # what the next pass wil ## Upstream pins -`.objectstack-sha` and `.cloud-sha` at the repository root each hold one full commit SHA: the commit of `objectstack-ai/objectstack` and of `objectstack-ai/cloud` this site was last aligned to. The objectstack pin moves when an `@objectstack/cli` release is published, to the commit its release tag names; the cloud pin moves after a successful cloud production deploy, to the commit that deploy serves — never on a merge to cloud's `main`. ⛔ Don't edit a pin by hand: `.github/workflows/bump-upstream-docs.yml` (run by hand, or a `repository_dispatch` of type `bump-upstream-docs` with `{ upstream, sha }`) moves one pin and opens a PR whose body lists the upstream commits and changed paths since the previous pin — objectstack: `content/docs`, `docs/releases`, `CHANGELOG.md`; cloud: `CHANGELOG.md`, `.changeset/`. That list is the alignment review's input: update the English pages it affects before the pin lands. `.cloud-sha` holds the placeholder `unpinned` until its first bump, which records a commit and lists nothing. The rules live in the header of `.github/scripts/bump-upstream-pin.mjs`. +`.objectstack-sha` and `.cloud-sha` at the repository root each hold one full commit SHA: the commit of `objectstack-ai/objectstack` and of `objectstack-ai/cloud` this site was last aligned to. What changed upstream between the old pin and the new one is the alignment review's input: update the English pages it affects before the pin lands. + +**The objectstack pin** moves when an `@objectstack/cli` release is published, to the commit its release tag names. ⛔ Don't edit it by hand: `.github/workflows/bump-upstream-docs.yml` (run by hand, or a `repository_dispatch` of type `bump-upstream-docs` with `{ upstream: "objectstack", sha }`) moves it and opens a PR whose body lists the upstream commits and changed paths since the previous pin in `content/docs`, `docs/releases` and `CHANGELOG.md`. Opening that PR needs the repository secret `UPSTREAM_BUMP_TOKEN`. + +**The cloud pin moves by hand** (maintainer ruling, #433): when someone refreshes the docs to match production, not on every cloud release. No workflow, dispatch or token moves it. To move it: + +1. Take the full 40-character SHA of the commit production serves: `git rev-parse vX.Y.Z^{commit}` on cloud's `v*` release tag, or the commit of cloud's `deploy-cloud-prod` run. The `x-objectstack-build-sha` header of `https://cloud.objectos.ai/api/v1/health` has only the first 8 characters: it tells you which commit, but it is not a pin. +2. Write that SHA to `.cloud-sha` as the file's only line. +3. In the PR body, paste the output of `git log .. -- CHANGELOG.md .changeset/`, run in a local cloud checkout. That list is the alignment review's input. The first hand update, from the placeholder `unpinned`, has no previous commit, so it lists nothing. + +On a PR that touches a pin, the `pins` job checks the format only (a full lowercase SHA, or `unpinned`) and reads neither upstream. The rules live in the header of `.github/scripts/bump-upstream-pin.mjs`. ## Commands