diff --git a/CHANGELOG.md b/CHANGELOG.md index 97581ccb..9c1332d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,13 @@ # Changelog -## Unreleased +## v0.10.0 - 2026-08-17 -Source manifests are prepared as the coordinated `0.10.0-next.0` candidate. -This is local release-preparation evidence only: the candidate is not claimed -as published, the npm `next` channel must be verified independently, and the -documented public stable release remains `0.9.0` until registry evidence says -otherwise. +Source manifests are prepared as the coordinated `0.10.0` release for all 18 +public packages, including the first publications of +`@opentag/control-protocol` and `@opentag/delivery-contract`. Source state is +release-preparation evidence only: npm registry metadata remains +authoritative, and `0.9.0` remains the published stable release until the +`0.10.0` publication and promotion complete on the registry. OpenTag now applies a zero-config verified completion tier to GitHub-backed runs. When a run ships a pull request and the repository has no explicit @@ -36,6 +37,20 @@ executor-success semantics. local runtime, the daemon config field `daemon.defaultGitHubCompletion`, and the `OPENTAG_GITHUB_DEFAULT_COMPLETION` environment variable (`governed` default, `compat` preserves legacy executor-success semantics). +- `@opentag/control-protocol`, a new public package carrying the canonical + Control V1 schemas, types, canonical-JSON digest helpers, completion + handling, and credential-safety utilities. Core keeps a compatibility + re-export, and Client depends on the protocol package directly. +- `@opentag/delivery-contract`, a new public package carrying the canonical + delivery-observation fixtures and receipt contracts for the unified + delivery vertical. +- An optional self-hosted Control Plane (private `@opentag/control-plane` OCI + image, not an npm package) with a Node/Hono/PostgreSQL runtime, a same-origin + React operator console, tenant-scoped identity with durable login throttling, + runner and target management, fenced hosted-run coordination, governed + permissions, GitHub ingress that stays disabled without an explicit master + secret, management audit, recurring jobs, and a Docker Compose self-hosting + profile. ### Compatibility and migration diff --git a/README.md b/README.md index e492786c..4b07ab9a 100644 --- a/README.md +++ b/README.md @@ -326,8 +326,8 @@ opentag-dev setup ## Packages -Package source candidate: `v0.10.0-next.0`. This source state is prepared for -local release validation only; it is not evidence that the candidate was +Package source release: `v0.10.0`. This source state is prepared for +local release validation only; it is not evidence that the release was published. npm dist-tags remain authoritative for public channel versions, and `0.9.0` remains the documented stable release until registry evidence says otherwise. The coordinated package family contains 18 public packages under diff --git a/README.zh-CN.md b/README.zh-CN.md index 1cca4848..a687b0e1 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -309,8 +309,8 @@ opentag-dev setup ## 软件包 -当前源码候选版本:`v0.10.0-next.0`。此源码状态只表示已准备本地发布验证, -不代表候选版本已经发布;npm dist-tag 仍是公开通道版本的权威来源,在注册表 +当前源码发布版本:`v0.10.0`。此源码状态只表示已准备本地发布验证, +不代表该版本已经发布;npm dist-tag 仍是公开通道版本的权威来源,在注册表 证据表明发生变化前,`0.9.0` 仍是文档所述的稳定版本。OpenTag 在 `@opentag` scope 下协调发布 18 个公开软件包。 diff --git a/docs/live-e2e-smoke-harness.md b/docs/live-e2e-smoke-harness.md index cf6ec15e..3f991bc9 100644 --- a/docs/live-e2e-smoke-harness.md +++ b/docs/live-e2e-smoke-harness.md @@ -55,7 +55,7 @@ smoke_root="$(mktemp -d)" set -euo pipefail cd "$smoke_root" npm init -y >/dev/null - npm install --no-audit --no-fund @opentag/cli@0.9.0 + npm install --no-audit --no-fund @opentag/cli@0.10.0 ./node_modules/.bin/opentag --version ) ``` diff --git a/docs/npm-prerelease.md b/docs/npm-prerelease.md index 0c4c6da1..31a9e179 100644 --- a/docs/npm-prerelease.md +++ b/docs/npm-prerelease.md @@ -1,16 +1,20 @@ # npm Prerelease Candidate Procedure -**Historical candidate-only procedure.** It does not authorize the next OpenTag -release. If a future release is explicitly approved to publish a stable version -directly to `latest`, do not use this document: assign the stable version and -update the stable release runbook in a separately authorized release change. - -This document defines the release contract for the coordinated +**Historical candidate-only procedure, superseded.** It does not authorize the +next OpenTag release. If a future release is explicitly approved to publish a +stable version directly to `latest`, do not use this document: assign the +stable version and update the stable release runbook in a separately +authorized release change. That authorization happened for `0.10.0`: the +source family was reassigned to the stable `0.10.0` version and the release +procedure moved to [`npm-release.md`](npm-release.md). The `0.10.0-next.0` +candidate described here was never published. + +This document defined the release contract for the coordinated `0.10.0-next.0` package family. It prepares an operator-controlled candidate; it does not authorize publication and it is not evidence that any npm or Git operation has occurred. -The stable `0.9.0` procedure remains in [`npm-release.md`](npm-release.md). +The stable procedure remains in [`npm-release.md`](npm-release.md). Registry metadata is authoritative for public availability. Source manifests, local tarballs, passing tests, and this guide cannot establish that a candidate is published. diff --git a/docs/npm-release.md b/docs/npm-release.md index d729ade6..150db945 100644 --- a/docs/npm-release.md +++ b/docs/npm-release.md @@ -7,18 +7,27 @@ version. ## Target release ```text -0.9.0 +0.10.0 ``` The release is first published on the npm `next` dist-tag, tested from the registry, then promoted to `latest`. The exact commit that produced the npm -artifacts must also receive the matching `v0.9.0` git tag and GitHub Release. - -The public release set contains 16 packages, including -`@opentag/governance`. The complete family has a coordinated `0.8.0` release. -Publishing `0.9.0` with `--tag next` must leave each package's existing -`latest` pointer unchanged until the complete registry, ACP, governance, -factory, and live-platform gate passes. +artifacts must also receive the matching `v0.10.0` git tag and GitHub Release. + +The public release set contains 18 packages, including the first publications +of `@opentag/control-protocol` and `@opentag/delivery-contract`. The other 16 +packages have a coordinated `0.9.0` release. Publishing `0.10.0` with +`--tag next` must leave each existing package's `latest` pointer unchanged +until the complete registry, ACP, governance, factory, and live-platform gate +passes. + +First-publication exception: `@opentag/control-protocol` and +`@opentag/delivery-contract` have never been published, so they have no +`0.9.0` baseline. Their first `--tag next` publish creates only the `next` +dist-tag, so they have no pre-promotion `latest` value to snapshot or restore. +The snapshot, promotion, and rollback loops below encode this exception +explicitly; do not "fix" a missing first-publication `latest` tag before +promotion. ## Public package discovery and order @@ -45,7 +54,7 @@ plan. ## Release gate Start from the intended release commit with a clean working tree. Confirm that -all 16 public manifests use `0.9.0` and that the frozen lockfile is current, +all 18 public manifests use `0.10.0` and that the frozen lockfile is current, then run the verification ladder in this order: ```bash @@ -116,7 +125,7 @@ authority, then confirm npm access and publish from that same commit: set -euo pipefail test -z "$(git status --porcelain)" - release_state_dir=".omx/releases/0.9.0" + release_state_dir=".omx/releases/0.10.0" release_commit_file="$release_state_dir/release-commit-sha" current_release_commit="$(git rev-parse HEAD)" mkdir -p "$release_state_dir" @@ -141,8 +150,8 @@ authority, then confirm npm access and publish from that same commit: ``` The publish command uses the same automatic publication set and topological -order as `release:check`. A coordinated release is incomplete until all 16 -packages exist at `0.9.0`; do not promote a partial package family. Preserve +order as `release:check`. A coordinated release is incomplete until all 18 +packages exist at `0.10.0`; do not promote a partial package family. Preserve the exact `release-commit-sha` file with the release record. Every later lock, tag, rollback, and release check reads that authority instead of recapturing the current `HEAD`. @@ -175,14 +184,14 @@ smoke_root="$(mktemp -d)" cd "$smoke_root" npm init --yes >/dev/null - npm install --no-audit --no-fund @opentag/cli@0.9.0 - test "$("$smoke_root/node_modules/.bin/opentag" --version)" = "0.9.0" + npm install --no-audit --no-fund @opentag/cli@0.10.0 + test "$("$smoke_root/node_modules/.bin/opentag" --version)" = "0.10.0" "$smoke_root/node_modules/.bin/opentag" --help npm audit --prefix "$smoke_root" --omit=dev --audit-level=high ) ``` -The version must be `0.9.0`. With isolated config and state directories, run +The version must be `0.10.0`. With isolated config and state directories, run the setup, doctor, and foreground-start path for one platform that has real test credentials: @@ -220,12 +229,12 @@ Also verify every package and its canary tag before promotion: ( set -euo pipefail - test "$("$smoke_root/node_modules/.bin/opentag" --version)" = "0.9.0" + test "$("$smoke_root/node_modules/.bin/opentag" --version)" = "0.10.0" for manifest in packages/*/package.json; do [ "$(jq -r '.publishConfig.access // ""' "$manifest")" = "public" ] || continue package="$(jq -r '.name' "$manifest")" - test "$(npm view "$package@0.9.0" version)" = "0.9.0" - test "$(npm view "$package" dist-tags.next)" = "0.9.0" + test "$(npm view "$package@0.10.0" version)" = "0.10.0" + test "$(npm view "$package" dist-tags.next)" = "0.10.0" npm view "$package" dist-tags --json done ) @@ -242,7 +251,7 @@ window: ( set -euo pipefail - release_state_dir=".omx/releases/0.9.0" + release_state_dir=".omx/releases/0.10.0" release_commit_file="$release_state_dir/release-commit-sha" lock_owner_file="$release_state_dir/npm-dist-tags.lock-sha" test -f "$release_commit_file" @@ -293,7 +302,7 @@ retry cannot replace the original rollback authority: ( set -euo pipefail - release_state_dir=".omx/releases/0.9.0" + release_state_dir=".omx/releases/0.10.0" release_commit_file="$release_state_dir/release-commit-sha" lock_owner_file="$release_state_dir/npm-dist-tags.lock-sha" test -f "$release_commit_file" @@ -314,24 +323,37 @@ retry cannot replace the original rollback authority: [ "$(jq -r '.publishConfig.access // ""' "$manifest")" = "public" ] || continue package="$(jq -r '.name' "$manifest")" dist_tags_json="$(npm view "$package" dist-tags --json)" - previous_latest="$(jq -er '.latest | select(type == "string" and length > 0)' <<<"$dist_tags_json")" - test "$previous_latest" = "0.8.0" + case "$package" in + "@opentag/control-protocol"|"@opentag/delivery-contract") + previous_latest="$(jq -r '.latest // "absent"' <<<"$dist_tags_json")" + case "$previous_latest" in + absent|"0.10.0") ;; + *) echo "Refusing unexpected first-publication $package latest=$previous_latest" >&2; exit 1 ;; + esac + ;; + *) + previous_latest="$(jq -er '.latest | select(type == "string" and length > 0)' <<<"$dist_tags_json")" + test "$previous_latest" = "0.9.0" + ;; + esac printf '%s\t%s\n' "$package" "$previous_latest" >>"$snapshot_tmp" done - test "$(wc -l <"$snapshot_tmp" | tr -d ' ')" = "16" - test "$(cut -f1 "$snapshot_tmp" | sort -u | wc -l | tr -d ' ')" = "16" - test "$(cut -f2 "$snapshot_tmp" | sort -u)" = "0.8.0" + test "$(wc -l <"$snapshot_tmp" | tr -d ' ')" = "18" + test "$(cut -f1 "$snapshot_tmp" | sort -u | wc -l | tr -d ' ')" = "18" + test "$(awk -F '\t' '$1 != "@opentag/control-protocol" && $1 != "@opentag/delivery-contract" { print $2 }' "$snapshot_tmp" | sort -u)" = "0.9.0" mv -n "$snapshot_tmp" "$rollback_file" test ! -e "$snapshot_tmp" ) ``` Keep that exact file until the release is complete. It records the actual -pre-promotion `latest` target for every package and fails unless the coordinated -family is still on the known `0.8.0` baseline. A registry lookup failure or a -missing/unexpected tag stops the release; it is never interpreted as rollback -authority. Back the snapshot up outside the ephemeral shell session before -changing any dist-tag. +pre-promotion `latest` target for every package and fails unless the 16 +previously published packages are still on the known `0.9.0` baseline; the +never-published `@opentag/control-protocol` and `@opentag/delivery-contract` +record `absent` (or `0.10.0` on a partial-promotion retry). A registry lookup +failure or a missing/unexpected tag on any other package stops the release; it +is never interpreted as rollback authority. Back the snapshot up outside the +ephemeral shell session before changing any dist-tag. The following promotion loop is retryable. Every first attempt and retry must reuse the original `rollback_file`; never rerun the snapshot block after any @@ -341,7 +363,7 @@ package has been promoted: ( set -euo pipefail - release_state_dir=".omx/releases/0.9.0" + release_state_dir=".omx/releases/0.10.0" release_commit_file="$release_state_dir/release-commit-sha" lock_owner_file="$release_state_dir/npm-dist-tags.lock-sha" test -f "$release_commit_file" @@ -351,70 +373,83 @@ package has been promoted: lock_commit="$(<"$lock_owner_file")" test "$(gh api repos/amplifthq/opentag/git/ref/heads/release-lock/npm-dist-tags --jq '.object.sha')" = "$lock_commit" test "$(gh api "repos/amplifthq/opentag/git/commits/$lock_commit" --jq '.parents[0].sha')" = "$release_commit" - rollback_file=".omx/releases/0.9.0/pre-promotion-latest.tsv" + rollback_file=".omx/releases/0.10.0/pre-promotion-latest.tsv" test -f "$rollback_file" - test "$(wc -l <"$rollback_file" | tr -d ' ')" = "16" - test "$(cut -f1 "$rollback_file" | sort -u | wc -l | tr -d ' ')" = "16" - test "$(cut -f2 "$rollback_file" | sort -u)" = "0.8.0" + test "$(wc -l <"$rollback_file" | tr -d ' ')" = "18" + test "$(cut -f1 "$rollback_file" | sort -u | wc -l | tr -d ' ')" = "18" + test "$(awk -F '\t' '$1 != "@opentag/control-protocol" && $1 != "@opentag/delivery-contract" { print $2 }' "$rollback_file" | sort -u)" = "0.9.0" for manifest in packages/*/package.json; do [ "$(jq -r '.publishConfig.access // ""' "$manifest")" = "public" ] || continue package="$(jq -r '.name' "$manifest")" previous_latest="$(awk -F '\t' -v package="$package" '$1 == package { print $2 }' "$rollback_file")" - test "$previous_latest" = "0.8.0" current_tags_json="$(npm view "$package" dist-tags --json)" - current_latest="$(jq -er '.latest | select(type == "string" and length > 0)' <<<"$current_tags_json")" current_next="$(jq -er '.next | select(type == "string" and length > 0)' <<<"$current_tags_json")" - test "$current_next" = "0.9.0" - case "$current_latest" in - "$previous_latest") npm dist-tag add "$package@0.9.0" latest ;; - "0.9.0") ;; - *) echo "Refusing to replace drifted $package latest=$current_latest" >&2; exit 1 ;; + test "$current_next" = "0.10.0" + case "$package" in + "@opentag/control-protocol"|"@opentag/delivery-contract") + case "$previous_latest" in absent|"0.10.0") ;; *) echo "Refusing corrupt snapshot for $package" >&2; exit 1 ;; esac + current_latest="$(jq -r '.latest // "absent"' <<<"$current_tags_json")" + case "$current_latest" in + absent) npm dist-tag add "$package@0.10.0" latest ;; + "0.10.0") ;; + *) echo "Refusing to replace drifted $package latest=$current_latest" >&2; exit 1 ;; + esac + ;; + *) + test "$previous_latest" = "0.9.0" + current_latest="$(jq -er '.latest | select(type == "string" and length > 0)' <<<"$current_tags_json")" + case "$current_latest" in + "$previous_latest") npm dist-tag add "$package@0.10.0" latest ;; + "0.10.0") ;; + *) echo "Refusing to replace drifted $package latest=$current_latest" >&2; exit 1 ;; + esac + ;; esac - test "$(npm view "$package" dist-tags.latest)" = "0.9.0" - test "$(npm view "$package" dist-tags.next)" = "0.9.0" + test "$(npm view "$package" dist-tags.latest)" = "0.10.0" + test "$(npm view "$package" dist-tags.next)" = "0.10.0" done ) ``` Rerun the package loop from the registry-verification section and confirm both -`next` and `latest` point at `0.9.0` for all 16 packages. +`next` and `latest` point at `0.10.0` for all 18 packages. ## Create the matching source release Create the source tag from the exact clean commit used for `release:publish`. -Copy the `v0.9.0` section of `CHANGELOG.md` into a temporary release-notes file, +Copy the `v0.10.0` section of `CHANGELOG.md` into a temporary release-notes file, then run: ```bash ( set -euo pipefail - release_commit_file=".omx/releases/0.9.0/release-commit-sha" + release_commit_file=".omx/releases/0.10.0/release-commit-sha" test -f "$release_commit_file" release_commit="$(<"$release_commit_file")" test "$(git rev-parse HEAD)" = "$release_commit" test -z "$(git status --porcelain)" release_tag_probe="$(mktemp)" - if ! gh api --include --silent repos/amplifthq/opentag/git/ref/tags/v0.9.0 >"$release_tag_probe"; then + if ! gh api --include --silent repos/amplifthq/opentag/git/ref/tags/v0.10.0 >"$release_tag_probe"; then : # Inspect the HTTP status below; only a confirmed 404 permits tag creation. fi release_tag_status="$(awk 'toupper($1) ~ /^HTTP\// {status=$2} END {print status}' "$release_tag_probe")" rm -f "$release_tag_probe" case "$release_tag_status" in 200) - release_tag_ref="$(gh api repos/amplifthq/opentag/git/ref/tags/v0.9.0)" + release_tag_ref="$(gh api repos/amplifthq/opentag/git/ref/tags/v0.10.0)" ;; 404) - if git show-ref --verify --quiet refs/tags/v0.9.0; then - test "$(git cat-file -t refs/tags/v0.9.0)" = "tag" - test "$(git rev-parse 'v0.9.0^{}')" = "$release_commit" + if git show-ref --verify --quiet refs/tags/v0.10.0; then + test "$(git cat-file -t refs/tags/v0.10.0)" = "tag" + test "$(git rev-parse 'v0.10.0^{}')" = "$release_commit" else - git tag -a v0.9.0 "$release_commit" -m "OpenTag v0.9.0" + git tag -a v0.10.0 "$release_commit" -m "OpenTag v0.10.0" fi - git push origin refs/tags/v0.9.0 - release_tag_ref="$(gh api repos/amplifthq/opentag/git/ref/tags/v0.9.0)" + git push origin refs/tags/v0.10.0 + release_tag_ref="$(gh api repos/amplifthq/opentag/git/ref/tags/v0.10.0)" ;; *) echo "Refusing tag creation after upstream lookup returned HTTP ${release_tag_status:-unavailable}" >&2 @@ -425,19 +460,19 @@ then run: release_tag_object="$(jq -r '.object.sha' <<<"$release_tag_ref")" test "$(gh api "repos/amplifthq/opentag/git/tags/$release_tag_object" --jq '.object.sha')" = "$release_commit" existing_release_state="$(gh api --paginate 'repos/amplifthq/opentag/releases?per_page=100' \ - --jq '.[] | select(.tag_name == "v0.9.0") | [.tag_name, .draft, .prerelease, (.published_at != null)] | @tsv')" + --jq '.[] | select(.tag_name == "v0.10.0") | [.tag_name, .draft, .prerelease, (.published_at != null)] | @tsv')" case "$existing_release_state" in "") - gh release create v0.9.0 \ + gh release create v0.10.0 \ --verify-tag \ - --title "OpenTag v0.9.0" \ - --notes-file /tmp/opentag-v0.9.0-release-notes.md + --title "OpenTag v0.10.0" \ + --notes-file /tmp/opentag-v0.10.0-release-notes.md ;; - $'v0.9.0\tfalse\tfalse\ttrue') ;; - *) echo "Refusing conflicting draft, prerelease, unpublished, or duplicate v0.9.0 GitHub Release state" >&2; exit 1 ;; + $'v0.10.0\tfalse\tfalse\ttrue') ;; + *) echo "Refusing conflicting draft, prerelease, unpublished, or duplicate v0.10.0 GitHub Release state" >&2; exit 1 ;; esac - release_state="$(gh api repos/amplifthq/opentag/releases/tags/v0.9.0)" - test "$(jq -r '.tag_name' <<<"$release_state")" = "v0.9.0" + release_state="$(gh api repos/amplifthq/opentag/releases/tags/v0.10.0)" + test "$(jq -r '.tag_name' <<<"$release_state")" = "v0.10.0" test "$(jq -r '.draft' <<<"$release_state")" = "false" test "$(jq -r '.prerelease' <<<"$release_state")" = "false" jq -er '.published_at | select(type == "string" and length > 0)' <<<"$release_state" >/dev/null @@ -450,11 +485,11 @@ the pushed remote tag must also be annotated and target that commit. The paginated release lookup must succeed before an absent release is created, so a network or API failure is never misread as authoritative absence. An existing draft, prerelease, unpublished object, or duplicate match is conflicting state: -stop and inspect it rather than treating it as the completed `v0.9.0` release. +stop and inspect it rather than treating it as the completed `v0.10.0` release. Verify that the GitHub Release tag resolves to the same commit that produced the npm tarballs. The release is not complete until npm, git, and GitHub all -identify version `0.9.0`. After that verification—or after a completed and +identify version `0.10.0`. After that verification—or after a completed and verified rollback—release the exclusive window only when it still points at your release commit: @@ -462,7 +497,7 @@ your release commit: ( set -euo pipefail - release_state_dir=".omx/releases/0.9.0" + release_state_dir=".omx/releases/0.10.0" release_commit_file="$release_state_dir/release-commit-sha" lock_owner_file="$release_state_dir/npm-dist-tags.lock-sha" test -f "$release_commit_file" @@ -485,15 +520,15 @@ Do not unpublish immutable package versions during rollback. `latest` tag unchanged and stop the rollout. Preserve `next` for diagnosis or move it to the corrected version. - If `latest` promotion fails partway, first finish or retry only the idempotent - promotion loop with the original snapshot. If 0.9.0 itself must be withdrawn, - restore each package's recorded pre-promotion target and leave 0.9.0 on `next` + promotion loop with the original snapshot. If 0.10.0 itself must be withdrawn, + restore each package's recorded pre-promotion target and leave 0.10.0 on `next` for diagnosis: ```bash ( set -euo pipefail - release_state_dir=".omx/releases/0.9.0" + release_state_dir=".omx/releases/0.10.0" release_commit_file="$release_state_dir/release-commit-sha" lock_owner_file="$release_state_dir/npm-dist-tags.lock-sha" test -f "$release_commit_file" @@ -503,32 +538,56 @@ Do not unpublish immutable package versions during rollback. lock_commit="$(<"$lock_owner_file")" test "$(gh api repos/amplifthq/opentag/git/ref/heads/release-lock/npm-dist-tags --jq '.object.sha')" = "$lock_commit" test "$(gh api "repos/amplifthq/opentag/git/commits/$lock_commit" --jq '.parents[0].sha')" = "$release_commit" - rollback_file=".omx/releases/0.9.0/pre-promotion-latest.tsv" + rollback_file=".omx/releases/0.10.0/pre-promotion-latest.tsv" test -f "$rollback_file" - test "$(wc -l <"$rollback_file" | tr -d ' ')" = "16" - test "$(cut -f1 "$rollback_file" | sort -u | wc -l | tr -d ' ')" = "16" - test "$(cut -f2 "$rollback_file" | sort -u)" = "0.8.0" + test "$(wc -l <"$rollback_file" | tr -d ' ')" = "18" + test "$(cut -f1 "$rollback_file" | sort -u | wc -l | tr -d ' ')" = "18" + test "$(awk -F '\t' '$1 != "@opentag/control-protocol" && $1 != "@opentag/delivery-contract" { print $2 }' "$rollback_file" | sort -u)" = "0.9.0" for manifest in packages/*/package.json; do [ "$(jq -r '.publishConfig.access // ""' "$manifest")" = "public" ] || continue package="$(jq -r '.name' "$manifest")" + case "$package" in + "@opentag/control-protocol"|"@opentag/delivery-contract") + previous_latest="$(awk -F '\t' -v package="$package" '$1 == package { print $2 }' "$rollback_file")" + case "$previous_latest" in + absent|"0.10.0") ;; + *) echo "Refusing corrupt snapshot for $package" >&2; exit 1 ;; + esac + current_tags_json="$(npm view "$package" dist-tags --json)" + test "$(jq -er '.next | select(type == "string" and length > 0)' <<<"$current_tags_json")" = "0.10.0" + current_latest="$(jq -r '.latest // "absent"' <<<"$current_tags_json")" + case "$previous_latest:$current_latest" in + absent:absent|absent:0.10.0|0.10.0:0.10.0) ;; + *) echo "Refusing unexpected $package latest=$current_latest" >&2; exit 1 ;; + esac + # First publication: there is no previous `latest` to restore, and npm + # refuses to delete a package's `latest` tag. After validation, leave + # the tags unchanged (`latest` stays absent or 0.10.0) and record the + # withdrawal in the incident note. + continue + ;; + esac previous_latest="$(awk -F '\t' -v package="$package" '$1 == package { print $2 }' "$rollback_file")" - test "$previous_latest" = "0.8.0" + test "$previous_latest" = "0.9.0" test "$(npm view "$package@$previous_latest" version)" = "$previous_latest" current_tags_json="$(npm view "$package" dist-tags --json)" current_latest="$(jq -er '.latest | select(type == "string" and length > 0)' <<<"$current_tags_json")" current_next="$(jq -er '.next | select(type == "string" and length > 0)' <<<"$current_tags_json")" - test "$current_next" = "0.9.0" + test "$current_next" = "0.10.0" case "$current_latest" in "$previous_latest") ;; - "0.9.0") npm dist-tag add "$package@$previous_latest" latest ;; + "0.10.0") npm dist-tag add "$package@$previous_latest" latest ;; *) echo "Refusing to replace drifted $package latest=$current_latest" >&2; exit 1 ;; esac test "$(npm view "$package" dist-tags.latest)" = "$previous_latest" - test "$(npm view "$package" dist-tags.next)" = "0.9.0" + test "$(npm view "$package" dist-tags.next)" = "0.10.0" done ) ``` Verify all dist-tags after rollback, publish a clear incident note, then release -the exclusive window with the guarded command above. A later fix must use a new -version; never overwrite `0.9.0`. +the exclusive window with the guarded command above. The incident note must +state that `@opentag/control-protocol` and `@opentag/delivery-contract` keep +`latest` absent or at `0.10.0` (both are valid rollback outcomes, depending on +whether promotion reached them) because a first publication has no earlier +stable target. A later fix must use a new version; never overwrite `0.10.0`. diff --git a/docs/versioning.md b/docs/versioning.md index 13a32ec9..61e4b117 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -34,8 +34,8 @@ Private runnable apps are not published: ## Pre-1.0 Policy -The source package family is prepared as the `0.10.0-next.0` prerelease -candidate. Source manifests are preparation evidence only: npm dist-tags remain +The source package family is prepared as the coordinated `0.10.0` release. +Source manifests are preparation evidence only: npm dist-tags remain authoritative for published channel versions, and `0.9.0` remains the documented stable release until registry evidence says otherwise. The public API is still settling, so all releases remain in the `0.x` line until the @@ -52,12 +52,15 @@ The `0.6.0` release moves all built-in coding agents onto Generic ACP, adds Curs The `0.7.0` release completes the Phase 1 completion-governance vertical slice with durable work threads and contracts, deterministic evidence-backed assessments, GitHub PR/check/merge evidence ingestion, replay-safe reassessment, CLI explanations and bounded waivers, and a 16-package publication set that includes `@opentag/governance`. The `0.8.0` release completes the first provider-live recipe-driven factory loop with access identity, bounded human escalation, explainable multi-runner routing, immutable recipes and WorkThread-only workstreams, restart-safe batch admission, authoritative accepted-outcome metrics, and a real GitHub issue-to-merge-to-receipt proof while keeping planning external. The `0.9.0` release adds an authorized, read-only Slack-to-Linear project backlog query with bounded pagination, deterministic ordering, live OAuth token use, exact channel authorization, a query-only credential boundary, and an isolated best-effort Events API lane that does not change control or mutation delivery semantics. -The `0.10.0-next.0` source candidate adds canonical, evidence-backed completion -authority for governed runs. Its prerelease procedure is intentionally -`next`-only, fail-closed, and immutable; see -[`npm-prerelease.md`](npm-prerelease.md). Preparing this source candidate does -not assert that npm publication, provider-live validation, or stable promotion -has occurred. +The `0.10.0` release adds canonical, evidence-backed completion authority for +governed runs, first-publishes the new `@opentag/control-protocol` and +`@opentag/delivery-contract` packages (growing the public set from 16 to 18), +and introduces an optional self-hosted Control Plane distributed as a private +OCI image. Its release procedure is documented in +[`npm-release.md`](npm-release.md); the earlier `0.10.0-next.0` candidate +procedure in [`npm-prerelease.md`](npm-prerelease.md) was superseded before +publication. Preparing this source state does not assert that npm publication, +provider-live validation, or stable promotion has occurred. For each npm release: diff --git a/packages/cli/package.json b/packages/cli/package.json index 64187166..bfbb1f43 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/cli", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "OpenTag command line interface.", "type": "module", "engines": { diff --git a/packages/cli/test/docs-contract.test.ts b/packages/cli/test/docs-contract.test.ts index d70eeba9..531e11b4 100644 --- a/packages/cli/test/docs-contract.test.ts +++ b/packages/cli/test/docs-contract.test.ts @@ -7,7 +7,7 @@ function repoFile(path: string): string { } describe("platform setup docs contract", () => { - it("keeps the 0.10.0-next.0 candidate procedure fail-closed and next-only", () => { + it("keeps the superseded 0.10.0-next.0 candidate procedure fail-closed and next-only", () => { const prereleaseGuide = repoFile("docs/npm-prerelease.md"); const normalizedPrereleaseGuide = prereleaseGuide.replace(/\s+/g, " "); const versioningGuide = repoFile("docs/versioning.md"); @@ -46,16 +46,19 @@ describe("platform setup docs contract", () => { "stable promotion is outside this procedure" ); expect(prereleaseGuide).not.toContain("npm publish --access public --tag latest"); - expect(versioningGuide).toContain("`0.10.0-next.0` prerelease"); + expect(normalizedPrereleaseGuide).toContain( + "The `0.10.0-next.0` candidate described here was never published" + ); + expect(versioningGuide).toContain("coordinated `0.10.0` release"); expect(readme).toContain("[npm prerelease candidate guide](docs/npm-prerelease.md)"); expect(readmeZh).toContain("[npm prerelease 候选发布指南](docs/npm-prerelease.md)"); }); - it("keeps the 0.9.0 release procedure explicit and concurrency-safe", () => { + it("keeps the 0.10.0 release procedure explicit and concurrency-safe", () => { const releaseGuide = repoFile("docs/npm-release.md"); const liveGuide = repoFile("docs/live-e2e-smoke-harness.md"); - expect(liveGuide).toContain("npm install --no-audit --no-fund @opentag/cli@0.9.0"); + expect(liveGuide).toContain("npm install --no-audit --no-fund @opentag/cli@0.10.0"); expect(liveGuide).toContain('smoke_root="$(mktemp -d)"\n(\n set -euo pipefail'); expect(releaseGuide).toContain("refs/heads/release-lock/npm-dist-tags"); expect(releaseGuide).toContain("npm-dist-tags.lock-sha"); @@ -66,33 +69,42 @@ describe("platform setup docs contract", () => { expect(releaseGuide).toContain('= "$lock_commit"'); expect(releaseGuide).toContain('current_latest="$(jq -er'); expect(releaseGuide).toContain('current_next="$(jq -er'); - expect(releaseGuide).toContain('test "$current_next" = "0.9.0"'); - expect(releaseGuide.match(/test "\$previous_latest" = "0\.8\.0"/gu)).toHaveLength(3); - expect(releaseGuide.match(/cut -f2 "\$(?:snapshot_tmp|rollback_file)" \| sort -u\)" = "0\.8\.0"/gu)).toHaveLength(3); - expect(releaseGuide).not.toContain('"0.7.0"'); - expect(releaseGuide.match(/test "\$\("\$smoke_root\/node_modules\/\.bin\/opentag" --version\)" = "0\.9\.0"/gu)) + expect(releaseGuide).toContain('test "$current_next" = "0.10.0"'); + expect(releaseGuide).toContain("First-publication exception"); + expect( + releaseGuide.match(/"@opentag\/control-protocol"\|"@opentag\/delivery-contract"\)/gu) + ).toHaveLength(3); + expect(releaseGuide).toContain("no previous `latest` to restore"); + expect(releaseGuide.match(/test "\$previous_latest" = "0\.9\.0"/gu)).toHaveLength(3); + expect( + releaseGuide.match( + /awk -F '\\t' '\$1 != "@opentag\/control-protocol" && \$1 != "@opentag\/delivery-contract" \{ print \$2 \}' "\$(?:snapshot_tmp|rollback_file)" \| sort -u\)" = "0\.9\.0"/gu + ) + ).toHaveLength(3); + expect(releaseGuide).not.toContain('"0.8.0"'); + expect(releaseGuide.match(/test "\$\("\$smoke_root\/node_modules\/\.bin\/opentag" --version\)" = "0\.10\.0"/gu)) .toHaveLength(2); expect(releaseGuide).toContain('smoke_root="$(mktemp -d)"\n(\n set -euo pipefail'); expect(releaseGuide).toContain( 'Also verify every package and its canary tag before promotion:\n\n```bash\n(\n set -euo pipefail' ); - expect(releaseGuide).toContain('git tag -a v0.9.0 "$release_commit" -m "OpenTag v0.9.0"'); + expect(releaseGuide).toContain('git tag -a v0.10.0 "$release_commit" -m "OpenTag v0.10.0"'); expect(releaseGuide).toContain( - "if ! gh api --include --silent repos/amplifthq/opentag/git/ref/tags/v0.9.0" + "if ! gh api --include --silent repos/amplifthq/opentag/git/ref/tags/v0.10.0" ); expect(releaseGuide).toContain("only a confirmed 404 permits tag creation"); expect(releaseGuide).toContain("release_tag_status=\"$(awk 'toupper($1) ~ /^HTTP\\//"); expect(releaseGuide).toContain('case "$release_tag_status" in\n 200)'); - expect(releaseGuide).toContain(" 404)\n if git show-ref --verify --quiet refs/tags/v0.9.0; then"); + expect(releaseGuide).toContain(" 404)\n if git show-ref --verify --quiet refs/tags/v0.10.0; then"); expect(releaseGuide).toContain("Refusing tag creation after upstream lookup returned HTTP"); - expect(releaseGuide).toContain("git show-ref --verify --quiet refs/tags/v0.9.0"); - expect(releaseGuide).toContain("git cat-file -t refs/tags/v0.9.0"); - expect(releaseGuide).toContain('git rev-parse \'v0.9.0^{}\''); - expect(releaseGuide).toContain("git push origin refs/tags/v0.9.0"); + expect(releaseGuide).toContain("git show-ref --verify --quiet refs/tags/v0.10.0"); + expect(releaseGuide).toContain("git cat-file -t refs/tags/v0.10.0"); + expect(releaseGuide).toContain('git rev-parse \'v0.10.0^{}\''); + expect(releaseGuide).toContain("git push origin refs/tags/v0.10.0"); expect(releaseGuide).toContain('git/tags/$release_tag_object'); expect(releaseGuide).toContain("gh api --paginate 'repos/amplifthq/opentag/releases?per_page=100'"); expect(releaseGuide).toContain('case "$existing_release_state" in'); - expect(releaseGuide).toContain("$'v0.9.0\\tfalse\\tfalse\\ttrue'"); + expect(releaseGuide).toContain("$'v0.10.0\\tfalse\\tfalse\\ttrue'"); expect(releaseGuide).toContain("'.draft'"); expect(releaseGuide).toContain("'.prerelease'"); expect(releaseGuide).toContain('.published_at | select(type == "string" and length > 0)'); diff --git a/packages/client/package.json b/packages/client/package.json index b1737790..09fc12c2 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/client", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "HTTP client SDK for creating, claiming, and updating OpenTag dispatcher runs.", "type": "module", "engines": { diff --git a/packages/control-protocol/package.json b/packages/control-protocol/package.json index 4c057707..6fd6eb82 100644 --- a/packages/control-protocol/package.json +++ b/packages/control-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/control-protocol", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "Canonical OpenTag Control V1 schemas, types, and digest helpers.", "type": "module", "engines": { diff --git a/packages/core/package.json b/packages/core/package.json index eb4ca258..eda1aaf2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/core", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "Core OpenTag protocol schemas, types, JSON Schema, and mention parsing.", "type": "module", "engines": { diff --git a/packages/delivery-contract/package.json b/packages/delivery-contract/package.json index debd9fa6..0f11dbb0 100644 --- a/packages/delivery-contract/package.json +++ b/packages/delivery-contract/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/delivery-contract", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "Canonical OpenTag side-effect intent and delivery observation contracts.", "type": "module", "engines": { diff --git a/packages/discord/package.json b/packages/discord/package.json index 0d87b6a5..e4bdb298 100644 --- a/packages/discord/package.json +++ b/packages/discord/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/discord", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "Discord interactions normalization and callback rendering for OpenTag.", "type": "module", "engines": { diff --git a/packages/dispatcher/package.json b/packages/dispatcher/package.json index c78944d5..a8825b9e 100644 --- a/packages/dispatcher/package.json +++ b/packages/dispatcher/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/dispatcher", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "Embeddable OpenTag dispatcher Hono app and durable delivery coordination.", "type": "module", "engines": { diff --git a/packages/github/package.json b/packages/github/package.json index 67fb0858..2598c6f9 100644 --- a/packages/github/package.json +++ b/packages/github/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/github", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "GitHub event normalization and callback rendering for OpenTag.", "type": "module", "engines": { diff --git a/packages/gitlab/package.json b/packages/gitlab/package.json index 7cbcd91b..fc917f02 100644 --- a/packages/gitlab/package.json +++ b/packages/gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/gitlab", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "GitLab webhook normalization and callback rendering for OpenTag.", "type": "module", "engines": { diff --git a/packages/governance/package.json b/packages/governance/package.json index 12bb50f5..46343c4a 100644 --- a/packages/governance/package.json +++ b/packages/governance/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/governance", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "Deterministic execution governance for OpenTag work loops.", "type": "module", "engines": { diff --git a/packages/lark/package.json b/packages/lark/package.json index bea5fda4..f87e40c9 100644 --- a/packages/lark/package.json +++ b/packages/lark/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/lark", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "Lark/Feishu message normalization and callback helpers for OpenTag.", "type": "module", "engines": { diff --git a/packages/linear/package.json b/packages/linear/package.json index cd484fd1..605bbaad 100644 --- a/packages/linear/package.json +++ b/packages/linear/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/linear", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "Linear webhook normalization, callback rendering, and issue mutation helpers for OpenTag.", "type": "module", "engines": { diff --git a/packages/local-runtime/package.json b/packages/local-runtime/package.json index 82df1b2b..c51f34b0 100644 --- a/packages/local-runtime/package.json +++ b/packages/local-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/local-runtime", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "Local OpenTag dispatcher, daemon, and diagnostics runtime helpers.", "type": "module", "engines": { diff --git a/packages/runner/package.json b/packages/runner/package.json index 8d952367..2d5b1881 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/runner", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "Executor contracts and built-in runner adapters for OpenTag.", "type": "module", "engines": { diff --git a/packages/slack/package.json b/packages/slack/package.json index 197a21c5..3b19ae1b 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/slack", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "Slack app mention normalization and callback helpers for OpenTag.", "type": "module", "engines": { diff --git a/packages/store/package.json b/packages/store/package.json index 5e3d3f56..b1990267 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/store", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "SQLite and Drizzle persistence primitives for OpenTag runs and leases.", "type": "module", "engines": { diff --git a/packages/teams/package.json b/packages/teams/package.json index f60116fd..237912da 100644 --- a/packages/teams/package.json +++ b/packages/teams/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/teams", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "Microsoft Teams activity normalization and callback rendering for OpenTag.", "type": "module", "engines": { "node": ">=20" }, diff --git a/packages/telegram/package.json b/packages/telegram/package.json index 0a283b77..0770ade6 100644 --- a/packages/telegram/package.json +++ b/packages/telegram/package.json @@ -1,6 +1,6 @@ { "name": "@opentag/telegram", - "version": "0.10.0-next.0", + "version": "0.10.0", "description": "Telegram message normalization and callback helpers for OpenTag.", "type": "module", "engines": { diff --git a/scripts/dev/run-slack-linear-backlog-live-test.ts b/scripts/dev/run-slack-linear-backlog-live-test.ts index 87241fbf..4e3a289b 100644 --- a/scripts/dev/run-slack-linear-backlog-live-test.ts +++ b/scripts/dev/run-slack-linear-backlog-live-test.ts @@ -329,7 +329,7 @@ async function waitForSlackReply(input: { async function main(): Promise { const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); - const expectedVersion = optionalEnv("OPENTAG_SLACK_LINEAR_EXPECTED_CLI_VERSION") ?? "0.9.0"; + const expectedVersion = optionalEnv("OPENTAG_SLACK_LINEAR_EXPECTED_CLI_VERSION") ?? "0.10.0"; const configSourcePath = resolve(requiredEnv("OPENTAG_CONFIG_PATH")); const botToken = requiredEnv("OPENTAG_SLACK_BOT_TOKEN"); const appToken = requiredEnv("OPENTAG_SLACK_APP_TOKEN");