diff --git a/.changeset/remove-built-in-hosted-provider.md b/.changeset/remove-built-in-hosted-provider.md index de11b0d9..9de205e1 100644 --- a/.changeset/remove-built-in-hosted-provider.md +++ b/.changeset/remove-built-in-hosted-provider.md @@ -1,5 +1,5 @@ --- -"@pymodel/pythinker-code": major +"@pymodel/pythinker-code": minor --- Remove the built-in hosted provider and its sign-in, usage, feedback, model aliases, and SDK methods; configure a supported provider with its own API key or OAuth instead. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8f6dfa0c..1035226a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -643,8 +643,48 @@ jobs: RELEASE_TAG: ${{ needs.release.outputs.pythinker_release_tag }} run: node apps/pythinker-code/scripts/native/produce-manifest.mjs dist-native-release "$RELEASE_TAG" + # Unlike the desktop release, which uploads into a draft and refuses to + # touch a published one, this release is already live: changesets creates + # it when it publishes to npm. `--clobber` therefore replaced the assets + # of a shipped version on any re-run of this job — and a re-run after a + # source change would put different bytes behind a version number users + # already have. A version has to identify one exact build forever. + # + # These assets are one set, not a bag of files: manifest.json pins a + # sha256 for every zip, and install.sh / install.ps1 verify against it. + # So the rule is all or nothing. A release that already carries the whole + # set is finished and is left alone; an empty one gets everything. A + # partial set stops the job, because filling in the gaps would pair zips + # from one build with checksums from another, and a rebuild is not + # guaranteed to be byte-identical even at the same commit. Sorting that + # out is a decision for a person: either what is published is the + # release, or it needs a new patch version. - name: Upload assets to GitHub Release + shell: bash env: GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ needs.release.outputs.pythinker_release_tag }} - run: gh release upload "$RELEASE_TAG" dist-native-release/* --clobber + run: | + set -euo pipefail + existing="$(gh release view "$RELEASE_TAG" --json assets --jq '.assets[].name')" + present=() + absent=() + for path in dist-native-release/*; do + name="$(basename "$path")" + if printf '%s\n' "$existing" | grep -qxF "$name"; then + present+=("$name") + else + absent+=("$path") + fi + done + if [ "${#absent[@]}" -eq 0 ]; then + echo "::notice::All ${#present[@]} assets are already on ${RELEASE_TAG}; nothing to upload." + exit 0 + fi + if [ "${#present[@]}" -gt 0 ]; then + echo "::error::${RELEASE_TAG} already carries ${#present[@]} of these assets: ${present[*]}" + echo "::error::Adding the rest would mix two builds behind one version - manifest.json pins a sha256 per zip." + echo "::error::Resolve by hand: keep what is published, or cut the next patch and release that instead." + exit 1 + fi + gh release upload "$RELEASE_TAG" "${absent[@]}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 72a4d638..dfdbd3e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,21 +82,47 @@ This repo uses [changesets](https://github.com/changesets/changesets) to manage ### Bump levels -| Level | Use for | Example | -| --- | --- | --- | -| `patch` | A fix, or a small addition to something that already exists | `2.1.2` → `2.1.3` | -| `minor` | A capability a user could not reach before | `2.1.3` → `2.2.0` | -| `major` | A break: something that worked stops working, or works differently | `2.2.0` → `3.0.0` | +Pythinker Code follows semantic versioning. The commit type you would have written decides the level: + +| Commit type | Level | Use for | Example | +| --- | --- | --- | --- | +| `fix`, `perf`, `security` | `patch` | A fix, a speed-up, or a small addition to something that already exists | `1.2.0` → `1.2.1` | +| `feat` | `minor` | A capability a user could not reach before | `1.2.8` → `1.3.0` | +| a break | `major` | Something that worked stops working, or works differently | `1.x` → `2.0.0` | + +Each number counts on its own and none of them roll over at nine. Ten fixes on top of `1.2.0` land on `1.2.10`, not `1.3.0`; the minor moves only when a feature ships, and the major only when something breaks: + +```text +1.2.9 + fix → 1.2.10 +1.9.9 + fix → 1.9.10 +1.9.9 + feature → 1.10.0 +1.x + break → 2.0.0 +``` Prefer one changeset per pull request. A pull request that needs several is usually carrying several separate releases, and once they are versioned together the changelog can no longer say which change each entry came from. -A `major` needs a maintainer's sign-off: the `changeset-policy` workflow fails a pull request that adds a major changeset, or edits an existing one up to `major`, unless it carries the `breaking-change-approved` label. A pinned install keeps working, but every consumer who upgrades has to deal with the break, and an npm publish cannot be taken back — so it is a decision, never a side effect of a large branch. +A `major` needs a maintainer's sign-off: the `changeset-policy` workflow fails a pull request that adds a major changeset, or edits an existing one up to `major`, unless it carries the `breaking-change-approved` label. A pinned install keeps working, but every consumer who upgrades has to deal with the break, and an npm publish cannot be taken back — so it is a decision, never a side effect of a large branch. Nothing else can reach a major: no amount of counting gets there on its own. + +### A released version is one exact build, forever + +Once a version exists, everything published under it is frozen. This matters most for the desktop app and the native CLI bundles, where a version number is what an updater resolves to a specific set of bytes it has already advertised. + +Never do any of these: + +- Overwrite the assets on a published release +- Move a tag that has shipped +- Rebuild binaries and publish them under a version that already exists +- Replace `latest.yml` / `latest-mac.yml` / `manifest.json` for a version whose binaries are already out + +To correct a bad build, cut the next patch instead: fix the problem, bump, build from that exact commit, sign and notarize, and publish fresh artifacts under the new number. The updater then advertises the new version, and anyone who already installed the old one keeps a build that still matches what it claims to be. + +CI enforces this rather than trusting the rule. The desktop release uploads into a draft and refuses to touch a release that is already published. The native CLI job treats its assets as one set — `manifest.json` pins a sha256 for every zip — so it uploads all of them or none: a release that already has the full set is left alone, and a partial set stops the job rather than pairing zips from one build with checksums from another. ### Release cadence Changesets keeps a `ci: release packages` pull request open on `main` and rewrites it as changesets land. Merging it cuts exactly one release, so how often it is merged is what decides the version sequence: -- Merged per change, versions follow each change: `2.1.2`, `2.1.3`, `2.1.4`, `2.2.0`. +- Merged per change, versions follow each change: `1.2.1`, `1.2.2`, `1.2.3`, `1.3.0`. - Left to accumulate, a backlog collapses into one bump and the numbers in between never exist. The repository variable `AUTO_MERGE_RELEASE_PR` chooses between the two. Set to `true`, the release pull request merges itself once its required checks pass, giving one release per change. Unset or `false`, a maintainer merges it when a release is wanted.