Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 73 additions & 11 deletions .github/workflows/release-cli-finalize.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ jobs:
permissions:
contents: write
steps:
- name: Check out the current release finalizer
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: Download the verified public release
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
Expand Down Expand Up @@ -182,17 +187,74 @@ jobs:
release_flags=(--latest=false)
if [[ "$RELEASE_DIST_TAG" == "next" ]]; then
release_flags+=(--prerelease)
elif [[ "$RELEASE_DIST_TAG" != "latest" ]]; then
elif [[ "$RELEASE_DIST_TAG" == "latest" ]]; then
release_flags+=(--prerelease=false)
else
echo "Unsupported CLI release dist-tag: $RELEASE_DIST_TAG" >&2
exit 1
fi
gh release create "$RELEASE_TAG" \
"$RELEASE_DIRECTORY/$RELEASE_TARBALL_NAME" \
"$RELEASE_DIRECTORY/$RELEASE_TARBALL_NAME.sha256" \
"$RELEASE_DIRECTORY/$RELEASE_TARBALL_NAME.files.json" \
"$RELEASE_DIRECTORY/release.json" \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
"${release_flags[@]}" \
--title "Maka CLI $RELEASE_VERSION" \
--notes-file "$RELEASE_DIRECTORY/release-notes.md"

release_endpoint="repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG"
release_json="$RUNNER_TEMP/github-release.json"
release_assets=(
"$RELEASE_DIRECTORY/$RELEASE_TARBALL_NAME"
"$RELEASE_DIRECTORY/$RELEASE_TARBALL_NAME.sha256"
"$RELEASE_DIRECTORY/$RELEASE_TARBALL_NAME.files.json"
"$RELEASE_DIRECTORY/release.json"
)

if ! gh api "$release_endpoint" > "$release_json" 2>/dev/null; then
if ! gh release create "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
--draft \
"${release_flags[@]}" \
--title "Maka CLI $RELEASE_VERSION" \
--notes-file "$RELEASE_DIRECTORY/release-notes.md"; then
echo "GitHub Release creation did not confirm success; inspecting remote state" >&2
fi
gh api "$release_endpoint" > "$release_json"
fi

release_draft="$(RELEASE_JSON="$release_json" node -e '
const fs = require("node:fs");
const release = JSON.parse(fs.readFileSync(process.env.RELEASE_JSON, "utf8"));
if (typeof release.draft !== "boolean") throw new Error("GitHub Release draft state is invalid");
process.stdout.write(String(release.draft));
')"
if [[ "$release_draft" == "true" ]]; then
gh release edit "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
--draft=true \
"${release_flags[@]}" \
--title "Maka CLI $RELEASE_VERSION" \
--notes-file "$RELEASE_DIRECTORY/release-notes.md"
gh release upload "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--clobber \
"${release_assets[@]}"
gh release edit "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
--draft=false \
"${release_flags[@]}" \
--title "Maka CLI $RELEASE_VERSION" \
--notes-file "$RELEASE_DIRECTORY/release-notes.md"
fi

gh api "$release_endpoint" > "$release_json"
node scripts/release-cli-publication.mjs validate-github-release \
"$RELEASE_DIRECTORY" \
"$release_json"

latest_json="$RUNNER_TEMP/latest-release.json"
if gh api "repos/$GITHUB_REPOSITORY/releases/latest" > "$latest_json" 2>/dev/null; then
LATEST_JSON="$latest_json" node -e '
const fs = require("node:fs");
const latest = JSON.parse(fs.readFileSync(process.env.LATEST_JSON, "utf8"));
if (latest.tag_name === process.env.RELEASE_TAG) {
throw new Error("CLI release must not become the repository GitHub Latest release");
}
'
fi
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ The app distinguishes configured, send-ready, and experimental connection states

## Terminal entry points

For the public npm package, see the [CLI installation and usage guide](./packages/cli/README.md).
The commands below run the development CLI from a source checkout.

Build the workspaces first:

```sh
Expand Down
3 changes: 3 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ Maka 不内置共享模型账号。第一次打开时:

## 使用终端入口

公共 npm 包的安装和使用方式请查看 [CLI 中文指南](./packages/cli/README.zh-CN.md)。下面的命令
用于从源码 checkout 运行开发版 CLI。

先构建 workspace:

```sh
Expand Down
4 changes: 4 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ This page is the authority map for Maka documentation. Code and contract tests r
- [Frontend CSS governance](./frontend-css-governance.md) ([中文](./frontend-css-governance.zh-CN.md))
- [Windows support baseline](./windows-support.md)

### Release operations

- [CLI npm release](./cli-npm-release.md) ([中文](./cli-npm-release.zh-CN.md))

### Security and privacy

- [Workspace privacy context](./workspace-privacy-context.md)
Expand Down
234 changes: 234 additions & 0 deletions docs/cli-npm-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
# Maka CLI npm release operations

[简体中文](./cli-npm-release.zh-CN.md)

This runbook is the operational authority for publishing `maka-agent`. The package version is
independent of the Desktop release line. Every public version must come from the exact tarball
validated by the Stage workflow.

## Release invariants

- Dispatch release workflows only from `main`.
- Publish prereleases under `next` and stable versions under `latest`.
- Use the Git tag `cli-v<version>`; CLI releases never replace the Desktop GitHub Latest release.
- Do not run `npm publish`. GitHub Actions may only run `npm stage publish`; a human package
maintainer approves the staged package with npm 2FA.
- Do not rebuild between validation, staging, approval, and finalization.
- Never reuse a public version. Fixes require a new prerelease, patch, minor, or major version.

The two workflow boundaries are:

1. [Stage CLI npm release](../.github/workflows/release-cli-stage.yml) builds and validates one
immutable tarball, records its source identity, enters the protected `npm-release` Environment,
and submits it to npm staging through OIDC.
2. [Finalize CLI npm release](../.github/workflows/release-cli-finalize.yml) accepts only the exact
successful Stage run and attempt, verifies the public registry bytes, signature, provenance, and
dist-tag, then creates the exact Git tag and a non-Latest GitHub Release.

## One-time control-plane configuration

### GitHub Environment

Create an Environment named `npm-release` with:

- `main` as the only allowed deployment branch;
- the active CLI release maintainer as a required reviewer;
- self-review allowed while one person is the sole release maintainer;
- administrator bypass disabled where repository policy permits it;
- no environment secrets or variables.

Repository administration permission is required to configure the Environment. The workflow itself
uses GitHub OIDC and does not read an npm token.

### npm Trusted Publisher

In the `maka-agent` package settings, configure one GitHub Actions trusted publisher:

| Field | Value |
| --- | --- |
| Organization or user | `maka-agent` |
| Repository | `maka-agent` |
| Workflow filename | `release-cli-stage.yml` |
| Environment name | `npm-release` |
| Allowed actions | `npm stage publish` only |

The workflow filename is case-sensitive and contains no `.github/workflows/` prefix. Keep
`npm publish` disabled for this trust relationship.

After the first OIDC Stage succeeds, set package publishing access to **Require two-factor
authentication and disallow tokens**, then revoke obsolete publish tokens. Do not remove the human
package owner or recovery access as part of that change.

## Prepare a release

1. Merge all intended package, documentation, and release changes to `main`.
2. Set `packages/cli/package.json` to the unused target version and merge that change. The release
tool maps prerelease versions to `next` and stable versions to `latest`.
3. Confirm the target version is absent from both public and staged package state:

```sh
version=0.1.0-beta.1
npm view "maka-agent@$version" version --registry https://registry.npmjs.org/
npm stage list maka-agent --registry https://registry.npmjs.org/
```

The first command should report that the target version is not present. Resolve any existing
stage instead of submitting the same version again.
4. Confirm the `npm-release` Environment and Trusted Publisher still match the values above and the
approving npm account has 2FA enabled.

## Stage the candidate

1. Open **Actions → Stage CLI npm release → Run workflow**.
2. Select `main` and enter the exact version from `packages/cli/package.json`.
3. Wait for the reusable package validation jobs to pass. They build one tarball and validate the
installed CLI on Linux x64, macOS arm64, and Windows x64, plus real Harbor and Pier Docker cells
on Linux x64.
4. Review and approve the `npm-release` Environment deployment.
5. Record the successful Stage workflow run ID, run attempt, source commit, version, and staged
artifact checksum from the run summary and `cli-staged-release-<attempt>` artifact.

Do not approve anything on npm if the Stage workflow did not finish successfully.

## Inspect and approve on npm

Use Node.js 22.14.0 or newer and npm 11.15.0 or newer for the inspection and approval commands below. The Stage workflow uses its own reviewed toolchain: the Node.js version pinned in the workflow and the exact npm version pinned in the repository's `packageManager`.

```sh
npm stage list maka-agent --registry https://registry.npmjs.org/
stage_id=replace-with-reviewed-stage-id
npm stage view "$stage_id" --registry https://registry.npmjs.org/
npm stage download "$stage_id" --registry https://registry.npmjs.org/
```

Before approval:

- require the package name, version, dist-tag, provenance, and source repository to match the Stage
run;
- compare the downloaded staged tarball's SHA-256 with the workflow artifact's `.tgz.sha256`;
- inspect the file inventory and the packaged `README.md`;
- confirm the tarball belongs to the recorded Stage run and source commit.

Approve only that stage ID. npm requires 2FA and makes the package public as part of approval:

```sh
npm stage approve "$stage_id" --registry https://registry.npmjs.org/
```

The same review and approval can be performed from the package's **Staged Packages** page on
npmjs.com.

## Finalize the public release

After npm reports the version as public:

1. Open **Actions → Finalize CLI npm release → Run workflow** on `main`.
2. Enter the successful Stage run ID, its exact run attempt, and the version.
3. Let the inspection job verify the public tarball bytes, checksum, inventory, dist-tag, npm
signature, and Trusted Publishing provenance.
4. Review and approve the `npm-release` Environment deployment for the Git tag and GitHub Release.
5. Confirm the workflow created `cli-v<version>` at the Stage source commit. A prerelease must be
marked prerelease; no CLI release may become the repository's GitHub Latest release.

Check the resulting registry state:

```sh
version=0.1.0-beta.1
npm view "maka-agent@$version" version dist.tarball dist.integrity --json
npm view maka-agent dist-tags --json
```

Finally, install the exact public version on each release platform and complete one real TUI/model
turn. On the supported Eval host, complete at least one real experiment cell and inspect score,
usage, cost, and artifacts.

## Failure recovery

### Before npm staging

If validation or Environment approval fails before `npm stage publish`, fix the problem on `main`
and start a new Stage run. No npm version has been consumed.

### Stage workflow failed but npm contains a stage

The submission is the Stage workflow's final business step, so a lost response can leave npm with a
stage even when the workflow is not successful. Do not approve that orphan: Finalize accepts only a
successful Stage run attempt.

Inspect it, then reject the exact stage ID with 2FA before starting a new Stage run:

```sh
stage_id=replace-with-reviewed-stage-id
npm stage view "$stage_id" --registry https://registry.npmjs.org/
npm stage reject "$stage_id" --registry https://registry.npmjs.org/
```

Never reject a stage based only on version text; bind the action to the inspected stage ID.

### Stage succeeded but review found a problem

Reject the stage, fix the problem on `main`, and stage again. Do not approve a candidate merely to
clear the staging area.

### npm approval succeeded but Finalize failed

The npm version is already immutable. Do not publish or approve it again. Preserve the Stage run ID,
attempt, version, and artifacts. If the package bytes and provenance are valid, fix the current
Finalize verifier on `main` and rerun Finalize against that same successful Stage identity.

Finalize is idempotent across partial GitHub Release creation: it resumes an exact draft and accepts
an already-published release only after verifying its metadata and asset digests. If an existing
`cli-v<version>` tag points anywhere other than the recorded Stage source commit, or an existing
published release differs from the verified candidate, stop and investigate. Do not move or delete
it to make the workflow pass.

### The public version is defective

First move the affected dist-tag back to a previously verified version:

```sh
known_good=0.1.0-beta.1
npm dist-tag add "maka-agent@$known_good" next
# For a stable release incident, use latest instead of next.
```

Then deprecate only the defective version and direct users to the recovered dist-tag, which already
points to the verified version:

```sh
bad_version=0.1.0-beta.2
recovery_tag=next
# For a stable release incident, use latest instead of next.
npm deprecate "maka-agent@$bad_version" "Known issue; install maka-agent@$recovery_tag."
```

Verify the tags, fix the defect, and release a new version through the complete Stage and Finalize
flow. Do not use `npm unpublish` as routine rollback: removing immutable dependency bytes can break
existing installations and does not restore the reviewed release chain.

## Ownership and emergency recovery

- GitHub repository admins own the `npm-release` Environment configuration. The release maintainer
owns dispatch, Environment review, staged-package inspection, npm 2FA approval, and final
acceptance.
- npm package owners own Trusted Publisher, publishing-access, maintainer, and dist-tag recovery.
- Keep at least one 2FA-protected human owner while trusted publishing is active. Before removing the
current direct owner, add the intended npm organization publishing team and another direct human
recovery maintainer, then verify both paths.
- The workflows must not gain a long-lived npm token. If OIDC, the Environment, or the trust
relationship is broken, pause releases and repair that control plane instead of bypassing staging
with `npm publish`.
- If an npm account is lost, use its account recovery methods or another verified package owner.
Until a second owner is established, recovery depends on the current owner's npm recovery
credentials; treat completing that ownership follow-up as operational debt.
- If repository or npm publisher settings change unexpectedly, remove or disable the trust
relationship, preserve workflow and npm audit evidence, restore the reviewed configuration, and
use a new version for any candidate whose integrity is uncertain.

## References

- [npm staged publishing](https://docs.npmjs.com/staged-publishing/)
- [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/)
- [npm dist-tags](https://docs.npmjs.com/cli/dist-tag/)
- [npm deprecation](https://docs.npmjs.com/cli/v11/commands/npm-deprecate/)
- [GitHub deployment environments](https://docs.github.com/en/actions/reference/workflows-and-actions/deployments-and-environments)
Loading
Loading