mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-08 09:49:34 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4863e9507 | ||
|
|
c211b12798 | ||
|
|
c1dd401ce9 |
No files matched your search
@@ -1,11 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions and setup help
|
||||
url: https://github.com/Gitlawb/zero/discussions/categories/q-a
|
||||
url: https://github.com/Twigpine/zero/discussions/categories/q-a
|
||||
about: Ask in Discussions Q&A. Include zero --version, your OS, provider/model, and zero doctor output for setup problems.
|
||||
- name: Feature ideas
|
||||
url: https://github.com/Gitlawb/zero/discussions/categories/ideas
|
||||
url: https://github.com/Twigpine/zero/discussions/categories/ideas
|
||||
about: Propose and discuss ideas in Discussions before opening a PR (Zero is in controlled development, see CONTRIBUTING.md).
|
||||
- name: Security vulnerability
|
||||
url: https://github.com/Gitlawb/zero/security/advisories/new
|
||||
url: https://github.com/Twigpine/zero/security/advisories/new
|
||||
about: Report security issues privately via GitHub Security Advisories (see SECURITY.md). Do not file a public issue.
|
||||
@@ -5,7 +5,7 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
New feature ideas start in [Discussions Ideas](https://github.com/Gitlawb/zero/discussions/categories/ideas),
|
||||
New feature ideas start in [Discussions Ideas](https://github.com/Twigpine/zero/discussions/categories/ideas),
|
||||
not here. This form is for the step after that: an idea that gained traction in Discussions,
|
||||
or one a maintainer asked to have tracked as an issue. Please link the discussion below.
|
||||
Do not begin implementation until the issue has been reviewed and marked with the
|
||||
|
||||
@@ -100,19 +100,21 @@ jobs:
|
||||
gh release create "$tag" --repo "$GITHUB_REPOSITORY" --title "$tag" --generate-notes 2>/dev/null || true
|
||||
gh release upload "$tag" dist/release/* --repo "$GITHUB_REPOSITORY" --clobber
|
||||
|
||||
# Publishes @gitlawb/zero to npm: first the five platform payloads (versions
|
||||
# of the SAME package at suffixed versions like X.Y.Z-linux-x64, carrying the
|
||||
# native binary + vendored helpers), then the wrapper at X.Y.Z whose
|
||||
# optionalDependencies alias those platform versions. Platform versions MUST
|
||||
# publish first — the wrapper pins them exactly, so the reverse order would
|
||||
# open a window where installs resolve aliases that 404. See
|
||||
# docs/NPM_PACKAGING.md for the full contract.
|
||||
# Publishes the npm package named in package.json (@twigpine/zero): first
|
||||
# the five platform payloads (versions of the SAME package at suffixed
|
||||
# versions like X.Y.Z-linux-x64, carrying the native binary + vendored
|
||||
# helpers), then the wrapper at X.Y.Z whose optionalDependencies alias those
|
||||
# platform versions. Platform versions MUST publish first — the wrapper pins
|
||||
# them exactly, so the reverse order would open a window where installs
|
||||
# resolve aliases that 404. See docs/NPM_PACKAGING.md for the full contract.
|
||||
#
|
||||
# Auth is npm OIDC trusted publishing: no NPM_TOKEN secret. The package's
|
||||
# npmjs.com settings must list this repo + workflow file as a trusted
|
||||
# publisher, and provenance is then generated automatically. Note npm only
|
||||
# allows configuring a trusted publisher on an EXISTING package, so the very
|
||||
# first @gitlawb/zero publish is a one-time manual `npm publish` bootstrap.
|
||||
# npmjs.com settings must list this repo + workflow file + the npm-publish
|
||||
# environment as a trusted publisher, and provenance is then generated
|
||||
# automatically. npm checks provenance against package.json `repository`,
|
||||
# so that must name this repository. Note npm only allows configuring a
|
||||
# trusted publisher on an EXISTING package, so a new package name starts
|
||||
# with a one-time manual `npm publish` of an empty 0.0.0 placeholder.
|
||||
# Because the platform payloads are versions of the same package, they reuse
|
||||
# that single trusted-publisher configuration.
|
||||
publish-npm:
|
||||
@@ -125,7 +127,7 @@ jobs:
|
||||
# maintainer approves it in the Actions UI before anything reaches npm.
|
||||
environment:
|
||||
name: npm-publish
|
||||
url: https://www.npmjs.com/package/@gitlawb/zero
|
||||
url: https://www.npmjs.com/package/@twigpine/zero
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # OIDC token exchange for npm trusted publishing + provenance
|
||||
@@ -201,47 +203,58 @@ jobs:
|
||||
- name: Assemble npm packages
|
||||
run: node scripts/npm/build-platform-packages.mjs --artifacts-dir dist/artifacts --out-dir dist/npm
|
||||
|
||||
# Platform versions are semver PRERELEASES of the wrapper version
|
||||
# (X.Y.Z-linux-x64), so they must never publish under the default
|
||||
# `latest` dist-tag — that would make `npm install @gitlawb/zero`
|
||||
# deliver a platform payload as the CLI. Publish them under a dedicated
|
||||
# tag and assert `latest` survived before the wrapper goes out.
|
||||
- name: Publish platform packages to npm
|
||||
# Platform payloads first under the `platform` dist-tag, then the wrapper,
|
||||
# with the `latest` dist-tag checked before and after (platform versions
|
||||
# are semver PRERELEASES of the wrapper version and must never become
|
||||
# `latest`). Versions already on the registry are skipped, so "Re-run
|
||||
# failed jobs" is safe after a partial publish.
|
||||
- name: Publish npm packages
|
||||
shell: bash
|
||||
run: |
|
||||
version="$(node -p "require('./package.json').version")"
|
||||
for dir in dist/npm/platforms/*/; do
|
||||
echo "publishing ${dir}"
|
||||
(cd "$dir" && npm publish --access public --tag platform)
|
||||
done
|
||||
# Tolerate a failed lookup (registry lag, or no latest tag yet on a
|
||||
# bootstrap publish): under set -e a bare failure here would abort
|
||||
# AFTER the platform payloads went out, leaving no wrapper and a
|
||||
# re-run that fails on republishing existing versions. An empty
|
||||
# value falls through the guard below.
|
||||
latest="$(npm view @gitlawb/zero dist-tags.latest || true)"
|
||||
case "$latest" in
|
||||
"${version}-"*)
|
||||
echo "::error::dist-tag latest points at platform version ${latest} — a platform publish clobbered it. Fix the dist-tag before the wrapper publish." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "dist-tag latest is ${latest} (unchanged by platform publishes)."
|
||||
run: scripts/npm/publish-packages.sh dist/npm
|
||||
|
||||
- name: Publish wrapper to npm
|
||||
# One final release under the pre-rename name @gitlawb/zero. Zero 0.9.0's
|
||||
# `zero upgrade` runs `npm install -g @gitlawb/zero@latest`, and people
|
||||
# reinstall the old name from shell history and old tutorials, so this
|
||||
# release is published once more under it: the same verified artifacts,
|
||||
# assembled as @gitlawb/zero, which the binary and wrapper recognise and
|
||||
# answer with the switch steps (docs/NPM_PACKAGING.md). Runs after the
|
||||
# @twigpine/zero publish, so the package it points at already exists.
|
||||
#
|
||||
# The gate keeps this to a single release: it is owed only while the old
|
||||
# name's latest is still the last pre-rename version, or is this version
|
||||
# (a re-run after it went out). After this release, `npm deprecate` the
|
||||
# old package and delete these two steps.
|
||||
- name: Check whether the final @gitlawb/zero release is owed
|
||||
id: legacy-name
|
||||
shell: bash
|
||||
env:
|
||||
LAST_PRE_RENAME_VERSION: 0.9.0
|
||||
run: |
|
||||
version="$(node -p "require('./package.json').version")"
|
||||
(cd dist/npm/wrapper && npm publish --access public)
|
||||
# Confirm the wrapper owns `latest` (retry: registry metadata can lag
|
||||
# a publish by a few seconds).
|
||||
for attempt in 1 2 3 4 5; do
|
||||
latest="$(npm view @gitlawb/zero dist-tags.latest || true)"
|
||||
if [ "$latest" = "$version" ]; then
|
||||
echo "dist-tag latest is ${version}."
|
||||
exit 0
|
||||
latest=""
|
||||
for _ in 1 2 3; do
|
||||
latest="$(npm view @gitlawb/zero dist-tags.latest 2>/dev/null || true)"
|
||||
if [ -n "$latest" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
sleep 10
|
||||
done
|
||||
echo "::error::dist-tag latest is ${latest}, expected ${version} after the wrapper publish." >&2
|
||||
exit 1
|
||||
if [ -z "$latest" ]; then
|
||||
echo "::error::could not read the latest dist-tag of @gitlawb/zero from the registry" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$latest" = "$LAST_PRE_RENAME_VERSION" ] || [ "$latest" = "$version" ]; then
|
||||
owed=true
|
||||
else
|
||||
owed=false
|
||||
fi
|
||||
echo "@gitlawb/zero latest is ${latest}; final release under the old name owed: ${owed}"
|
||||
echo "owed=${owed}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Publish the final @gitlawb/zero release
|
||||
if: ${{ steps.legacy-name.outputs.owed == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
node scripts/npm/build-platform-packages.mjs \
|
||||
--artifacts-dir dist/artifacts --out-dir dist/npm-legacy --package-name @gitlawb/zero
|
||||
scripts/npm/publish-packages.sh dist/npm-legacy
|
||||
+5
-5
@@ -9,9 +9,9 @@ reports, feature requests, reproduction details, logs, and discussion on existin
|
||||
issues.
|
||||
|
||||
Questions and setup help belong in
|
||||
[GitHub Discussions](https://github.com/Gitlawb/zero/discussions) (see
|
||||
[GitHub Discussions](https://github.com/Twigpine/zero/discussions) (see
|
||||
[SUPPORT.md](SUPPORT.md)), not in the issue tracker. Feature ideas start in the
|
||||
[Ideas](https://github.com/Gitlawb/zero/discussions/categories/ideas) category;
|
||||
[Ideas](https://github.com/Twigpine/zero/discussions/categories/ideas) category;
|
||||
an idea that gains traction there is the path to an approved issue and,
|
||||
eventually, an accepted PR.
|
||||
|
||||
@@ -41,7 +41,7 @@ There are three primary ways to contribute:
|
||||
|
||||
1. Open a bug report with clear reproduction steps.
|
||||
2. Propose a feature in
|
||||
[Discussions Ideas](https://github.com/Gitlawb/zero/discussions/categories/ideas),
|
||||
[Discussions Ideas](https://github.com/Twigpine/zero/discussions/categories/ideas),
|
||||
describing the problem, use case, and expected behavior. A feature-request
|
||||
issue is filed only after the idea gains traction there or a maintainer asks
|
||||
for one.
|
||||
@@ -193,7 +193,7 @@ Clear bug reports are the most useful way to help the project during this phase.
|
||||
## Submitting a Feature Request
|
||||
|
||||
Feature requests start as a thread in
|
||||
[Discussions Ideas](https://github.com/Gitlawb/zero/discussions/categories/ideas),
|
||||
[Discussions Ideas](https://github.com/Twigpine/zero/discussions/categories/ideas),
|
||||
not as an issue. The feature-request issue form exists for the step AFTER that:
|
||||
when an idea has gained traction in Discussions or a maintainer has asked for an
|
||||
issue to track it. Whether in the discussion or the eventual issue, please
|
||||
@@ -222,7 +222,7 @@ contribution phase.
|
||||
|
||||
General questions, setup help, and "is this a bug or my config" uncertainty all
|
||||
go to
|
||||
[Discussions Q&A](https://github.com/Gitlawb/zero/discussions/categories/q-a).
|
||||
[Discussions Q&A](https://github.com/Twigpine/zero/discussions/categories/q-a).
|
||||
If you are unsure whether a proposed code change is appropriate, raise it in
|
||||
Discussions Ideas or on the relevant issue first and ask. Please keep discussion
|
||||
focused, specific, and respectful of maintainer time.
|
||||
@@ -116,7 +116,7 @@ zero exec --output-format stream-json < turns.jsonl
|
||||
### npm
|
||||
|
||||
```bash
|
||||
npm install -g @gitlawb/zero
|
||||
npm install -g @twigpine/zero
|
||||
zero
|
||||
```
|
||||
|
||||
@@ -130,18 +130,30 @@ matching GitHub Release whenever it is missing. Windows on ARM runs the x64
|
||||
build under emulation. See [docs/NPM_PACKAGING.md](docs/NPM_PACKAGING.md) for
|
||||
how the package is put together.
|
||||
|
||||
Installed Zero as `@gitlawb/zero`? The package moved to `@twigpine/zero` in
|
||||
0.10.0 and the old name gets no further releases. Both packages provide the
|
||||
`zero` command, so quit any running `zero`, then remove the old package first:
|
||||
|
||||
```bash
|
||||
npm uninstall -g @gitlawb/zero
|
||||
npm install -g @twigpine/zero
|
||||
```
|
||||
|
||||
Your config, sessions, and logins are kept; they live outside the package. For
|
||||
Bun, pnpm, and yarn, see [docs/INSTALL.md](docs/INSTALL.md#moving-from-gitlawbzero).
|
||||
|
||||
### Install scripts
|
||||
|
||||
Linux/macOS:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.sh | bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Twigpine/zero/main/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.ps1 | iex
|
||||
irm https://raw.githubusercontent.com/Twigpine/zero/main/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
### From source
|
||||
@@ -149,7 +161,7 @@ irm https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.ps1 | ie
|
||||
Source builds require Go 1.26.9+.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Gitlawb/zero.git
|
||||
git clone https://github.com/Twigpine/zero.git
|
||||
cd zero
|
||||
go run ./cmd/zero
|
||||
```
|
||||
@@ -526,19 +538,19 @@ go run ./cmd/zero-release build --goos windows --goarch amd64 --output dist/zero
|
||||
Real-time chat happens on the [Discord server](https://discord.gg/CaQDS6wdFn).
|
||||
|
||||
Questions, setup help, ideas, and sharing all live in
|
||||
[GitHub Discussions](https://github.com/Gitlawb/zero/discussions):
|
||||
[GitHub Discussions](https://github.com/Twigpine/zero/discussions):
|
||||
|
||||
| Category | Use it for |
|
||||
|---|---|
|
||||
| [Q&A](https://github.com/Gitlawb/zero/discussions/categories/q-a) | Setup help, provider/model configuration, "how do I" questions |
|
||||
| [Ideas](https://github.com/Gitlawb/zero/discussions/categories/ideas) | Feature proposals and design discussion before any PR |
|
||||
| [Show and tell](https://github.com/Gitlawb/zero/discussions/categories/show-and-tell) | Your skills, plugins, MCP setups, themes, and workflows |
|
||||
| [Announcements](https://github.com/Gitlawb/zero/discussions/categories/announcements) | Releases and project news from the maintainers |
|
||||
| [Q&A](https://github.com/Twigpine/zero/discussions/categories/q-a) | Setup help, provider/model configuration, "how do I" questions |
|
||||
| [Ideas](https://github.com/Twigpine/zero/discussions/categories/ideas) | Feature proposals and design discussion before any PR |
|
||||
| [Show and tell](https://github.com/Twigpine/zero/discussions/categories/show-and-tell) | Your skills, plugins, MCP setups, themes, and workflows |
|
||||
| [Announcements](https://github.com/Twigpine/zero/discussions/categories/announcements) | Releases and project news from the maintainers |
|
||||
|
||||
For a good Q&A answer fast, include `zero --version`, your OS and install
|
||||
method, the provider/model in use, and `zero doctor` output. See
|
||||
[SUPPORT.md](SUPPORT.md). Bugs belong in
|
||||
[issues](https://github.com/Gitlawb/zero/issues/new/choose); security reports
|
||||
[issues](https://github.com/Twigpine/zero/issues/new/choose); security reports
|
||||
follow [SECURITY.md](SECURITY.md), never a public thread.
|
||||
|
||||
## Contributing
|
||||
|
||||
+18
-9
@@ -105,24 +105,33 @@ zero exec --output-format stream-json < turns.jsonl
|
||||
### npm
|
||||
|
||||
```bash
|
||||
npm install -g @gitlawb/zero
|
||||
npm install -g @twigpine/zero
|
||||
zero
|
||||
```
|
||||
|
||||
npm 包是一个小型包装器,其平台构建(Linux 和 macOS 的 x64/arm64、Windows 的 x64,包含浏览器/终端控制辅助工具)作为可选依赖直接从 npm registry 安装——没有安装脚本,也不会从 npm 之外下载任何内容。Bun、pnpm 和 yarn 的行为完全一致,无需任何信任或批准步骤。跳过可选依赖的安装(`--omit=optional`)也能工作:只要二进制文件缺失,包装器就会从对应的 GitHub Release 下载。Windows on ARM 通过模拟运行 x64 构建。详见 [docs/NPM_PACKAGING.md](docs/NPM_PACKAGING.md)。
|
||||
|
||||
之前通过 `@gitlawb/zero` 安装过 Zero?从 0.10.0 起,npm 包已迁移到 `@twigpine/zero`,旧包名不再发布新版本。两个包都提供 `zero` 命令,所以请先退出正在运行的 `zero`,再卸载旧包:
|
||||
|
||||
```bash
|
||||
npm uninstall -g @gitlawb/zero
|
||||
npm install -g @twigpine/zero
|
||||
```
|
||||
|
||||
配置、会话和登录信息都会保留,它们存放在包之外。Bun、pnpm 和 yarn 的命令见 [docs/INSTALL.md](docs/INSTALL.md#moving-from-gitlawbzero)。
|
||||
|
||||
### 安装脚本
|
||||
|
||||
Linux/macOS:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.sh | bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Twigpine/zero/main/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.ps1 | iex
|
||||
irm https://raw.githubusercontent.com/Twigpine/zero/main/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
### 从源码构建
|
||||
@@ -130,7 +139,7 @@ irm https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.ps1 | ie
|
||||
源码构建需要 Go 1.26.9+。
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Gitlawb/zero.git
|
||||
git clone https://github.com/Twigpine/zero.git
|
||||
cd zero
|
||||
go run ./cmd/zero
|
||||
```
|
||||
@@ -358,14 +367,14 @@ go run ./cmd/zero-release build --goos windows --goarch amd64 --output dist/zero
|
||||
实时交流在 [Discord 服务器](https://discord.gg/CaQDS6wdFn) 进行。
|
||||
|
||||
提问、安装帮助、想法和分享都在
|
||||
[GitHub Discussions](https://github.com/Gitlawb/zero/discussions):
|
||||
[GitHub Discussions](https://github.com/Twigpine/zero/discussions):
|
||||
|
||||
| 分类 | 用途 |
|
||||
|---|---|
|
||||
| [Q&A](https://github.com/Gitlawb/zero/discussions/categories/q-a) | 安装帮助、提供商/模型配置、"如何做"类问题 |
|
||||
| [Ideas](https://github.com/Gitlawb/zero/discussions/categories/ideas) | 功能提议和 PR 之前的设计讨论 |
|
||||
| [Show and tell](https://github.com/Gitlawb/zero/discussions/categories/show-and-tell) | 你的技能、插件、MCP 配置、主题和工作流 |
|
||||
| [Announcements](https://github.com/Gitlawb/zero/discussions/categories/announcements) | 维护者发布的版本和项目动态 |
|
||||
| [Q&A](https://github.com/Twigpine/zero/discussions/categories/q-a) | 安装帮助、提供商/模型配置、"如何做"类问题 |
|
||||
| [Ideas](https://github.com/Twigpine/zero/discussions/categories/ideas) | 功能提议和 PR 之前的设计讨论 |
|
||||
| [Show and tell](https://github.com/Twigpine/zero/discussions/categories/show-and-tell) | 你的技能、插件、MCP 配置、主题和工作流 |
|
||||
| [Announcements](https://github.com/Twigpine/zero/discussions/categories/announcements) | 维护者发布的版本和项目动态 |
|
||||
|
||||
## 贡献
|
||||
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ name: ZERO
|
||||
description: >-
|
||||
Run the ZERO coding agent headlessly in a GitHub workflow. ZERO is
|
||||
model- and provider-agnostic: you supply the provider and API key as inputs.
|
||||
author: Gitlawb
|
||||
author: Twigpine
|
||||
|
||||
branding:
|
||||
icon: terminal
|
||||
@@ -110,7 +110,7 @@ inputs:
|
||||
zero-repo:
|
||||
description: Repository to install the ZERO release from (owner/repo).
|
||||
required: false
|
||||
default: Gitlawb/zero
|
||||
default: Twigpine/zero
|
||||
|
||||
outputs:
|
||||
exit-code:
|
||||
|
||||
+63
-13
@@ -1,11 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { existsSync, readFileSync, writeSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
|
||||
const CURRENT_PACKAGE_NAME = '@twigpine/zero';
|
||||
// The npm package moved from @gitlawb/zero to @twigpine/zero in 0.10.0. That
|
||||
// release was also published once under the old name, so 0.9.0's `zero
|
||||
// upgrade` and anyone still installing the old name land on a working build
|
||||
// that says where the package went (docs/NPM_PACKAGING.md). Nothing is
|
||||
// published under the old name after that.
|
||||
const LEGACY_PACKAGE_NAME = '@gitlawb/zero';
|
||||
|
||||
// The npm name this wrapper was published under, read from its own
|
||||
// package.json so the name lives in one place. The platform payload alias and
|
||||
// every reinstall hint below derive from it.
|
||||
function readPackageName(root) {
|
||||
try {
|
||||
const { name } = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
||||
if (typeof name === 'string' && name !== '') return name;
|
||||
} catch {
|
||||
// An unreadable manifest must not stop the CLI from launching.
|
||||
}
|
||||
return CURRENT_PACKAGE_NAME;
|
||||
}
|
||||
|
||||
const packageName = readPackageName(packageRoot);
|
||||
|
||||
// Same note as legacyPackageSwitchMessage in internal/update/installmethod.go.
|
||||
// Uninstalling first is the only safe order: both packages own the `zero` bin,
|
||||
// so npm refuses to install the new one over the old (EEXIST), and forcing it
|
||||
// lets a later uninstall of the old package delete the shared bin.
|
||||
function legacyPackageNotice() {
|
||||
return (
|
||||
`[zero] Zero has moved to ${CURRENT_PACKAGE_NAME} on npm, and ${LEGACY_PACKAGE_NAME} gets no further releases.\n` +
|
||||
'To keep getting updates, quit zero and run:\n' +
|
||||
` npm uninstall -g ${LEGACY_PACKAGE_NAME}\n` +
|
||||
` npm install -g ${CURRENT_PACKAGE_NAME}\n` +
|
||||
'Uninstall first: both packages provide the `zero` command. With bun, pnpm or yarn, use its global remove and add commands instead.\n'
|
||||
);
|
||||
}
|
||||
|
||||
// Printed when the wrapper exits rather than before zero starts, because the
|
||||
// TUI takes over the screen and would hide anything printed first. Only on a
|
||||
// terminal, so scripts that capture stderr see nothing new. `zero update` and
|
||||
// `zero upgrade` print the same note themselves.
|
||||
if (packageName === LEGACY_PACKAGE_NAME && !['update', 'upgrade'].includes(process.argv[2])) {
|
||||
process.on('exit', () => {
|
||||
if (process.stderr.isTTY || process.env.ZERO_WRAPPER_SIMULATE_TTY === '1') {
|
||||
// Synchronous: an exit handler cannot wait for an asynchronous write.
|
||||
writeSync(2, legacyPackageNotice());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function zeroBinaryName(platform = process.platform) {
|
||||
return platform === 'win32' ? 'zero.exe' : 'zero';
|
||||
}
|
||||
@@ -81,10 +133,10 @@ function bunRecoveryParagraph() {
|
||||
return (
|
||||
'You installed with Bun, which does not run dependency lifecycle scripts\n' +
|
||||
'by default. Trust the package to run the blocked postinstall:\n' +
|
||||
' bun pm trust @gitlawb/zero (project install)\n' +
|
||||
' bun pm -g trust @gitlawb/zero (global install)\n' +
|
||||
` bun pm trust ${packageName} (project install)\n` +
|
||||
` bun pm -g trust ${packageName} (global install)\n` +
|
||||
'On Bun versions without `bun pm trust`, add\n' +
|
||||
' "trustedDependencies": ["@gitlawb/zero"]\n' +
|
||||
` "trustedDependencies": ["${packageName}"]\n` +
|
||||
'to your project package.json and reinstall.\n' +
|
||||
'\n'
|
||||
);
|
||||
@@ -93,14 +145,14 @@ function bunRecoveryParagraph() {
|
||||
function buildFromSourceParagraph() {
|
||||
return (
|
||||
'If this platform has no prebuilt binary, build from source:\n' +
|
||||
'https://github.com/Gitlawb/zero (go run ./cmd/zero, requires Go 1.26+).'
|
||||
'https://github.com/Twigpine/zero (go run ./cmd/zero, requires Go 1.26+).'
|
||||
);
|
||||
}
|
||||
|
||||
function missingBinaryContextParagraph() {
|
||||
return (
|
||||
'Normally npm installs it as an optional dependency of @gitlawb/zero\n' +
|
||||
`(@gitlawb/zero-${process.platform}-${process.arch}), and the wrapper can\n` +
|
||||
`Normally npm installs it as an optional dependency of ${packageName}\n` +
|
||||
`(${packageName}-${process.platform}-${process.arch}), and the wrapper can\n` +
|
||||
'also download it from the GitHub Release when it is missing.'
|
||||
);
|
||||
}
|
||||
@@ -113,7 +165,7 @@ function missingNativeRecoveryParagraphs(postinstallScript) {
|
||||
'\n' +
|
||||
'Things to try:\n' +
|
||||
' - reinstall without omitting optional dependencies:\n' +
|
||||
' npm install -g @gitlawb/zero\n' +
|
||||
` npm install -g ${packageName}\n` +
|
||||
' - run the downloader manually (needs write access to the package dir):\n' +
|
||||
` node "${postinstallScript}"\n` +
|
||||
'\n' +
|
||||
@@ -182,12 +234,12 @@ function missingNativeDoctorTextReport(postinstallScript) {
|
||||
);
|
||||
}
|
||||
|
||||
// The platform payload is a version of @gitlawb/zero itself, installed under
|
||||
// an npm: alias (see docs/NPM_PACKAGING.md). The alias name is derived from
|
||||
// The platform payload is a version of this same package, installed under an
|
||||
// npm: alias (see docs/NPM_PACKAGING.md). The alias name is derived from
|
||||
// process.platform/process.arch, so an unsupported platform simply fails to
|
||||
// resolve and we fall through to the downloader.
|
||||
function platformPackageBinary() {
|
||||
const aliasedName = `@gitlawb/zero-${process.platform}-${process.arch}`;
|
||||
const aliasedName = `${packageName}-${process.platform}-${process.arch}`;
|
||||
let manifestPath;
|
||||
try {
|
||||
manifestPath = createRequire(import.meta.url).resolve(`${aliasedName}/package.json`);
|
||||
@@ -227,8 +279,6 @@ function downloadMissingBinary(packageRoot) {
|
||||
});
|
||||
}
|
||||
|
||||
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
|
||||
function resolveNativeBinary() {
|
||||
const fromPlatformPackage = platformPackageBinary();
|
||||
if (fromPlatformPackage) return fromPlatformPackage;
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Gitlawb/zero@v1
|
||||
- uses: Twigpine/zero@v1
|
||||
with:
|
||||
prompt: ${{ inputs.task }}
|
||||
provider: openai
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
| `github-token` | no | `${{ github.token }}` | Token used to post a PR comment. Requires `pull-requests: write`. |
|
||||
| `working-directory` | no | `${{ github.workspace }}` | Directory to run ZERO in. |
|
||||
| `zero-version` | no | (action ref → `latest`) | ZERO release version/tag to install, e.g. `v1.2.3` or `latest`. |
|
||||
| `zero-repo` | no | `Gitlawb/zero` | Repository to install the ZERO release from. |
|
||||
| `zero-repo` | no | `Twigpine/zero` | Repository to install the ZERO release from. |
|
||||
|
||||
## Outputs
|
||||
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Gitlawb/zero@v1
|
||||
- uses: Twigpine/zero@v1
|
||||
with:
|
||||
prompt: |
|
||||
Investigate this issue and propose a fix.
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Gitlawb/zero@v1
|
||||
- uses: Twigpine/zero@v1
|
||||
id: zero
|
||||
with:
|
||||
prompt-file: .github/zero/upgrade-deps.md
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
- **Least-privilege tokens.** Grant only the permissions the workflow needs
|
||||
(`contents: write` to edit files, `pull-requests: write` to comment). The
|
||||
default `GITHUB_TOKEN` is scoped to the repository.
|
||||
- **Pin the action.** Reference a tag (`Gitlawb/zero@v1`) or a commit SHA so a
|
||||
- **Pin the action.** Reference a tag (`Twigpine/zero@v1`) or a commit SHA so a
|
||||
workflow run uses a known ZERO version. `zero-version` lets you pin the
|
||||
installed binary independently of the action ref.
|
||||
- **Linux and macOS runners** are supported; Windows runners are rejected with a
|
||||
|
||||
+51
-16
@@ -5,11 +5,11 @@ blessed one.
|
||||
|
||||
| Method | Command | Self-update |
|
||||
| --- | --- | --- |
|
||||
| Install script (Linux, macOS) | `curl -fsSL https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.sh \| bash` | `zero upgrade` |
|
||||
| Install script (Windows) | `irm https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.ps1 \| iex` | `zero upgrade` |
|
||||
| npm | `npm install -g @gitlawb/zero` | `zero upgrade` (runs npm) |
|
||||
| Release archive | download from [Releases](https://github.com/Gitlawb/zero/releases) | `zero upgrade` |
|
||||
| mise | `mise use -g ubi:Gitlawb/zero` | `mise upgrade` |
|
||||
| Install script (Linux, macOS) | `curl -fsSL https://raw.githubusercontent.com/Twigpine/zero/main/scripts/install.sh \| bash` | `zero upgrade` |
|
||||
| Install script (Windows) | `irm https://raw.githubusercontent.com/Twigpine/zero/main/scripts/install.ps1 \| iex` | `zero upgrade` |
|
||||
| npm | `npm install -g @twigpine/zero` | `zero upgrade` (runs npm) |
|
||||
| Release archive | download from [Releases](https://github.com/Twigpine/zero/releases) | `zero upgrade` |
|
||||
| mise | `mise use -g ubi:Twigpine/zero` | `mise upgrade` |
|
||||
| `go install` | `go install github.com/Gitlawb/zero/cmd/zero@latest` | rerun the command |
|
||||
| Source | `go build -o zero ./cmd/zero` | rebuild |
|
||||
|
||||
@@ -29,7 +29,7 @@ Release and verify its checksum.
|
||||
## npm
|
||||
|
||||
```bash
|
||||
npm install -g @gitlawb/zero
|
||||
npm install -g @twigpine/zero
|
||||
zero
|
||||
```
|
||||
|
||||
@@ -56,15 +56,47 @@ cannot write to keeps failing — rerun the install or the command below with
|
||||
sufficient permissions). To trigger the fetch manually:
|
||||
|
||||
```bash
|
||||
node "$(npm root -g)/@gitlawb/zero/scripts/postinstall.mjs"
|
||||
node "$(npm root -g)/@twigpine/zero/scripts/postinstall.mjs"
|
||||
```
|
||||
|
||||
### Moving from @gitlawb/zero
|
||||
|
||||
Zero was published as `@gitlawb/zero` up to 0.9.0. From 0.10.0 it is
|
||||
`@twigpine/zero`, and the old name gets no further releases.
|
||||
|
||||
Both packages provide the `zero` command, so npm refuses to install the new
|
||||
one while the old one is installed (`EEXIST`). Forcing it is worse: a later
|
||||
uninstall of the old package removes the shared `zero` command. Quit any
|
||||
running `zero` (on Windows a running `zero.exe` cannot be removed), then
|
||||
uninstall the old package before installing the new one:
|
||||
|
||||
```bash
|
||||
npm uninstall -g @gitlawb/zero
|
||||
npm install -g @twigpine/zero
|
||||
```
|
||||
|
||||
With another package manager, the same two steps:
|
||||
|
||||
```bash
|
||||
bun remove -g @gitlawb/zero && bun add -g @twigpine/zero
|
||||
pnpm remove -g @gitlawb/zero && pnpm add -g @twigpine/zero
|
||||
yarn global remove @gitlawb/zero && yarn global add @twigpine/zero
|
||||
```
|
||||
|
||||
Config, sessions, logins, and plugins live in your user config and home
|
||||
directories, not in the package, so they carry over unchanged.
|
||||
|
||||
0.10.0 was also published once as `@gitlawb/zero`, so an old install that
|
||||
updates lands on a working build. That build prints these steps when `zero`
|
||||
exits in a terminal, and `zero upgrade` and `zero update --check` print them
|
||||
too.
|
||||
|
||||
## Linux And macOS Script
|
||||
|
||||
Install the latest release:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.sh | bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Twigpine/zero/main/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
From a checkout:
|
||||
@@ -89,7 +121,7 @@ scripts/install.sh --install-dir "$HOME/bin"
|
||||
|
||||
Defaults:
|
||||
|
||||
- Repository: `Gitlawb/zero`
|
||||
- Repository: `Twigpine/zero`
|
||||
- Version: latest GitHub release
|
||||
- Install path: `~/.local/bin/zero`
|
||||
|
||||
@@ -100,7 +132,7 @@ Requirements: Bash, `curl` or `wget`, `tar`, and `shasum` or `sha256sum`.
|
||||
Install the latest release:
|
||||
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.ps1 | iex
|
||||
irm https://raw.githubusercontent.com/Twigpine/zero/main/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
From a checkout:
|
||||
@@ -123,7 +155,7 @@ powershell -ExecutionPolicy Bypass -File scripts/install.ps1 -InstallDir "$env:U
|
||||
|
||||
Defaults:
|
||||
|
||||
- Repository: `Gitlawb/zero`
|
||||
- Repository: `Twigpine/zero`
|
||||
- Version: latest GitHub release
|
||||
- Install path: `%LOCALAPPDATA%\zero\bin\zero.exe`
|
||||
|
||||
@@ -134,13 +166,13 @@ archives through its `ubi` backend, so there is nothing extra to publish and no
|
||||
registry in the middle:
|
||||
|
||||
```bash
|
||||
mise use -g ubi:Gitlawb/zero
|
||||
mise use -g ubi:Twigpine/zero
|
||||
```
|
||||
|
||||
Pin a version the same way you would any other tool:
|
||||
|
||||
```bash
|
||||
mise use -g ubi:Gitlawb/[email protected]
|
||||
mise use -g ubi:Twigpine/[email protected]
|
||||
```
|
||||
|
||||
Updates come from `mise upgrade`. `zero upgrade` also works, because a
|
||||
@@ -169,7 +201,7 @@ macOS and Windows need no extra helper.
|
||||
## From Source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Gitlawb/zero.git
|
||||
git clone https://github.com/Twigpine/zero.git
|
||||
cd zero
|
||||
go run ./cmd/zero
|
||||
```
|
||||
@@ -213,7 +245,7 @@ seccomp filter on Android:
|
||||
pkg install golang
|
||||
|
||||
# Build Zero for Android
|
||||
git clone https://github.com/Gitlawb/zero.git
|
||||
git clone https://github.com/Twigpine/zero.git
|
||||
cd zero
|
||||
CGO_ENABLED=0 GOOS=android GOARCH=arm64 go build -ldflags="-s -w" -o zero ./cmd/zero
|
||||
|
||||
@@ -302,7 +334,10 @@ See the [update guide](UPDATE.md) for update modes, flags, and platform details.
|
||||
overwriting a binary a package manager owns leaves that manager describing a
|
||||
version that is no longer there:
|
||||
|
||||
- **npm**: runs `npm install -g @gitlawb/zero@latest` for you.
|
||||
- **npm**: runs `npm install -g @twigpine/zero@latest` for you. An install of
|
||||
the old `@gitlawb/zero` package refuses instead and prints the steps in
|
||||
[Moving from @gitlawb/zero](#moving-from-gitlawbzero): that name gets no
|
||||
further releases, so npm would only reinstall the same version.
|
||||
- **Homebrew**: refuses, and tells you to run `brew upgrade zero`. Replacing the
|
||||
keg binary directly would be reverted by the next `brew upgrade` or
|
||||
`brew reinstall`.
|
||||
|
||||
+92
-15
@@ -1,6 +1,6 @@
|
||||
# npm packaging
|
||||
|
||||
How `@gitlawb/zero` is put together on npm, why it is shaped this way, and the
|
||||
How `@twigpine/zero` is put together on npm, why it is shaped this way, and the
|
||||
rules the release pipeline must follow. Read this before touching
|
||||
`package.json`, `bin/zero.js`, `scripts/postinstall.mjs`,
|
||||
`scripts/npm/build-platform-packages.mjs`, or the npm-publish steps of
|
||||
@@ -8,7 +8,7 @@ rules the release pipeline must follow. Read this before touching
|
||||
|
||||
## Goals
|
||||
|
||||
A `npm install -g @gitlawb/zero` must be **silent and self-contained**:
|
||||
A `npm install -g @twigpine/zero` must be **silent and self-contained**:
|
||||
|
||||
- No `EBADENGINE` warnings — ours or from any package in the dependency tree.
|
||||
- No install scripts anywhere in the tree, so npm's `allow-scripts` gating,
|
||||
@@ -25,15 +25,15 @@ The model is the one used by Codex, esbuild, and Biome: a tiny wrapper package
|
||||
plus per-platform payloads carrying the native binaries.
|
||||
|
||||
```
|
||||
@gitlawb/zero <- wrapper: bin/zero.js + optionalDependencies
|
||||
├─ @gitlawb/zero-darwin-arm64 -> npm:@gitlawb/zero@{version}-darwin-arm64
|
||||
├─ @gitlawb/zero-darwin-x64 -> npm:@gitlawb/zero@{version}-darwin-x64
|
||||
├─ @gitlawb/zero-linux-arm64 -> npm:@gitlawb/zero@{version}-linux-arm64
|
||||
├─ @gitlawb/zero-linux-x64 -> npm:@gitlawb/zero@{version}-linux-x64
|
||||
└─ @gitlawb/zero-win32-x64 -> npm:@gitlawb/zero@{version}-win32-x64
|
||||
@twigpine/zero <- wrapper: bin/zero.js + optionalDependencies
|
||||
├─ @twigpine/zero-darwin-arm64 -> npm:@twigpine/zero@{version}-darwin-arm64
|
||||
├─ @twigpine/zero-darwin-x64 -> npm:@twigpine/zero@{version}-darwin-x64
|
||||
├─ @twigpine/zero-linux-arm64 -> npm:@twigpine/zero@{version}-linux-arm64
|
||||
├─ @twigpine/zero-linux-x64 -> npm:@twigpine/zero@{version}-linux-x64
|
||||
└─ @twigpine/zero-win32-x64 -> npm:@twigpine/zero@{version}-win32-x64
|
||||
```
|
||||
|
||||
- The platform "packages" are **versions of the same `@gitlawb/zero` package**,
|
||||
- The platform "packages" are **versions of the same `@twigpine/zero` package**,
|
||||
published at suffixed versions (`0.4.0-linux-x64`) and referenced through
|
||||
`npm:` aliases in `optionalDependencies`. One package name means one npm
|
||||
trusted-publisher configuration — no new publish credentials per platform.
|
||||
@@ -56,7 +56,7 @@ plus per-platform payloads carrying the native binaries.
|
||||
|
||||
### Binary resolution in `bin/zero.js`
|
||||
|
||||
1. Resolve `@gitlawb/zero-<platform>-<arch>` and exec the `zero` binary from
|
||||
1. Resolve `@twigpine/zero-<platform>-<arch>` and exec the `zero` binary from
|
||||
it. This wins over any previously downloaded copy — the platform version is
|
||||
pinned to the wrapper release.
|
||||
2. Fall back to a binary previously downloaded next to the wrapper.
|
||||
@@ -103,10 +103,22 @@ dependencies of the wrapper:
|
||||
platform binary (plus the musl variant on linux, which its launcher detects
|
||||
at runtime) — about 65 MB saved per platform package.
|
||||
|
||||
### One package name
|
||||
|
||||
The npm name lives in `package.json` and nothing else repeats it:
|
||||
`scripts/npm/build-platform-packages.mjs` names the wrapper, the platform
|
||||
versions, the alias names, and the alias targets from it, and `bin/zero.js`
|
||||
reads its own `package.json` to derive the alias it resolves. The Go side
|
||||
needs the name as a constant (`npmPackageName` in
|
||||
`internal/update/installmethod.go`), and a test holds it equal to
|
||||
`package.json`.
|
||||
|
||||
## Publishing rules (release pipeline)
|
||||
|
||||
These are the invariants `release-artifacts.yml` must hold. Breaking the first
|
||||
one is user-visible immediately.
|
||||
one is user-visible immediately. `scripts/npm/publish-packages.sh` implements
|
||||
them, and `internal/npmwrapper/publish_test.go` runs it against a fake
|
||||
registry.
|
||||
|
||||
1. **Platform versions must never become `latest`.** `0.4.0-linux-x64` is a
|
||||
semver *prerelease* of `0.4.0`; publishing it without an explicit
|
||||
@@ -123,15 +135,80 @@ one is user-visible immediately.
|
||||
first would create a window where installs resolve aliases that 404.
|
||||
3. **Exact-version pinning.** The wrapper at `X.Y.Z` references platform
|
||||
versions `X.Y.Z-<platform>-<arch>` exactly — never ranges — so a wrapper
|
||||
and its binaries can never skew.
|
||||
and its binaries can never skew. The publish script refuses to start when
|
||||
the wrapper's aliases do not match the platform payloads beside it.
|
||||
4. All publishes go through the existing npm OIDC trusted-publishing flow and
|
||||
the `npm-publish` environment gate; no tokens. Because the platform
|
||||
payloads are versions of the same package, they reuse the single
|
||||
trusted-publisher configuration.
|
||||
trusted-publisher configuration. npm checks provenance against
|
||||
`package.json` `repository`, so it must name the repository the workflow
|
||||
runs in.
|
||||
5. **Re-runs are safe.** A version already on the registry is skipped rather
|
||||
than republished, so re-running a job that failed between the platform and
|
||||
wrapper publishes finishes the release. Only npm's E404 counts as "not
|
||||
published"; a lookup that fails any other way is retried and then stops the
|
||||
job before anything is published.
|
||||
|
||||
`zero update` keeps working unchanged: it detects an npm install by finding a
|
||||
`package.json` named `@gitlawb/zero` next to the running binary — true inside
|
||||
a platform payload too — and updates via `npm install -g @gitlawb/zero@latest`.
|
||||
`package.json` named `@twigpine/zero` next to the running binary — true inside
|
||||
a platform payload too — and updates via `npm install -g @twigpine/zero@latest`.
|
||||
|
||||
## The move from `@gitlawb/zero`
|
||||
|
||||
Zero was published as `@gitlawb/zero` up to 0.9.0 and as `@twigpine/zero`
|
||||
from 0.10.0. There is no in-place migration: both packages own the `zero`
|
||||
bin, so npm refuses to install one over the other (`EEXIST`), and forcing it
|
||||
lets a later uninstall of the old package delete the shared bin. The user
|
||||
has to uninstall the old package and install the new one, in that order.
|
||||
|
||||
Deprecating the old package alone does not reach existing users. 0.9.0's
|
||||
`zero upgrade` runs `npm install -g @gitlawb/zero@latest`; with nothing newer
|
||||
under that name, npm reinstalls nothing, prints no deprecation warning (it
|
||||
only warns when it installs something), and Zero reports the update as done.
|
||||
So 0.10.0 is published once more under the old name, as a bridge:
|
||||
|
||||
- The publish job assembles the same verified artifacts a second time with
|
||||
`build-platform-packages.mjs --package-name @gitlawb/zero` and publishes
|
||||
them with the same script, after the `@twigpine/zero` publish. A gate runs
|
||||
it only while the old name's `latest` is still 0.9.0 (or is this version,
|
||||
on a re-run), so it cannot happen twice.
|
||||
- The bridge build knows its name from the `package.json` beside it. The Go
|
||||
binary reports it as `InstallMethodNpmLegacy`: `zero upgrade` refuses
|
||||
with the switch steps instead of reinstalling it, and `zero update --check`
|
||||
adds them. `bin/zero.js` prints the same steps after `zero` exits, on a
|
||||
terminal only.
|
||||
- Updating from 0.9.0 now installs a real version, so npm also shows the
|
||||
deprecation message.
|
||||
|
||||
### Runbook
|
||||
|
||||
Before the first release under the new name:
|
||||
|
||||
1. Create the package, since npm only attaches a trusted publisher to a
|
||||
package that exists: publish an empty `@twigpine/[email protected]` by hand with
|
||||
`npm publish --access public --tag bootstrap`, then
|
||||
`npm deprecate @twigpine/[email protected] "Placeholder; install @twigpine/zero@latest"`.
|
||||
2. Trust this workflow for it:
|
||||
`npm trust github @twigpine/zero --repo Twigpine/zero --file release-artifacts.yml --env npm-publish --allow-publish`.
|
||||
3. Point the old package's trusted publisher at this repository too, for the
|
||||
bridge publish: `npm trust list @gitlawb/zero`, revoke the existing entry
|
||||
with `npm trust revoke @gitlawb/zero --id <id>`, then run the same
|
||||
`npm trust github` command for `@gitlawb/zero`.
|
||||
4. Confirm Settings → Environments → `npm-publish` has a required reviewer.
|
||||
The environment exists either way; without a reviewer the job does not
|
||||
wait for approval.
|
||||
|
||||
After the release, once both packages show 0.10.0 as `latest`:
|
||||
|
||||
1. `npm deprecate @gitlawb/zero "Zero moved to @twigpine/zero. Switch with: npm uninstall -g @gitlawb/zero && npm install -g @twigpine/zero"`
|
||||
(no version range, so every version, including the platform payloads,
|
||||
carries it). Never unpublish: existing installs resolve their platform
|
||||
payload through these versions.
|
||||
2. Revoke the old package's trusted publisher (`npm trust revoke`), so
|
||||
nothing can publish under that name again.
|
||||
3. Delete the two `@gitlawb/zero` steps from `release-artifacts.yml`. The Go
|
||||
and wrapper handling of the old name can stay; it only activates in a
|
||||
build installed under that name.
|
||||
|
||||
## Runbook: bumping the vendored helpers
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
Run this checklist when a PR changes npm distribution files such as
|
||||
`package.json`, `bin/zero.js`, `scripts/npm/build-platform-packages.mjs`,
|
||||
Go build or release tooling, or the npm `bin` wrapper.
|
||||
`scripts/npm/publish-packages.sh`, Go build or release tooling, or the npm
|
||||
`bin` wrapper.
|
||||
|
||||
## Required Checks
|
||||
|
||||
@@ -37,24 +38,24 @@ path:
|
||||
|
||||
```bash
|
||||
# platform-package path
|
||||
mkdir -p /tmp/zero-sim/node_modules/@gitlawb
|
||||
cp -R dist/npm/wrapper /tmp/zero-sim/node_modules/@gitlawb/zero
|
||||
mkdir -p /tmp/zero-sim/node_modules/@twigpine
|
||||
cp -R dist/npm/wrapper /tmp/zero-sim/node_modules/@twigpine/zero
|
||||
cp -R dist/npm/platforms/zero-<platform>-<arch> \
|
||||
"/tmp/zero-sim/node_modules/@gitlawb/zero-<platform>-<arch>"
|
||||
node /tmp/zero-sim/node_modules/@gitlawb/zero/bin/zero.js --version
|
||||
"/tmp/zero-sim/node_modules/@twigpine/zero-<platform>-<arch>"
|
||||
node /tmp/zero-sim/node_modules/@twigpine/zero/bin/zero.js --version
|
||||
|
||||
# first-run download fallback (delete the platform package first)
|
||||
rm -rf "/tmp/zero-sim/node_modules/@gitlawb/zero-<platform>-<arch>"
|
||||
node /tmp/zero-sim/node_modules/@gitlawb/zero/bin/zero.js --version
|
||||
rm -rf "/tmp/zero-sim/node_modules/@twigpine/zero-<platform>-<arch>"
|
||||
node /tmp/zero-sim/node_modules/@twigpine/zero/bin/zero.js --version
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
- `package.json` has the expected package name (`@gitlawb/zero`), version,
|
||||
- `package.json` has the expected package name (`@twigpine/zero`), version,
|
||||
`bin.zero` entry, and NO `scripts` entries — the published package must be
|
||||
free of lifecycle scripts (see NPM_PACKAGING.md).
|
||||
- `scripts/npm/build-platform-packages.mjs` emits platform payloads whose
|
||||
`package.json` is `@gitlawb/zero@<version>-<platform>-<arch>` with matching
|
||||
`package.json` is `@twigpine/zero@<version>-<platform>-<arch>` with matching
|
||||
`os`/`cpu`, contains the executable binary and the vendored `helpers/` tree
|
||||
with regular-file (non-symlink) `.bin` shims, and a wrapper payload with no
|
||||
scripts, no dependencies, and the full five-alias `optionalDependencies`
|
||||
|
||||
+8
-4
@@ -7,7 +7,7 @@ downloads, verifies, and installs it.
|
||||
```bash
|
||||
zero update --check
|
||||
zero update --check --json
|
||||
zero update --check --repo Gitlawb/zero
|
||||
zero update --check --repo Twigpine/zero
|
||||
zero update --check --target windows-x64
|
||||
|
||||
zero upgrade
|
||||
@@ -28,7 +28,11 @@ them explicitly; `zero upgrade` is `zero update` with `--apply` implied.
|
||||
|
||||
`--apply` installs the update in place:
|
||||
|
||||
- npm installs delegate to `npm install -g @gitlawb/zero@latest`.
|
||||
- npm installs delegate to `npm install -g @twigpine/zero@latest`. An install
|
||||
of the old `@gitlawb/zero` package refuses, without a release check, and
|
||||
prints the steps to switch to `@twigpine/zero` (see
|
||||
[INSTALL.md](INSTALL.md#moving-from-gitlawbzero)); `--check` prints them
|
||||
too.
|
||||
- Standalone installs download the release archive, verify its checksum,
|
||||
extract it, and atomically replace the running binary plus any installed
|
||||
optional sandbox helpers.
|
||||
@@ -38,7 +42,7 @@ them explicitly; `zero upgrade` is `zero update` with `--apply` implied.
|
||||
- `--target` cannot be combined with `--apply`; it only applies to `--check`,
|
||||
since applying always installs onto the current machine.
|
||||
- `--repo` and `--endpoint` are ignored when applying to an npm-managed
|
||||
install: that path delegates to `npm install -g @gitlawb/zero@latest` and
|
||||
install: that path delegates to `npm install -g @twigpine/zero@latest` and
|
||||
takes its release from the npm registry, not from GitHub. They still apply to
|
||||
`--check` there.
|
||||
- `--json` serializes Zero's final result. For npm-managed installs, npm may
|
||||
@@ -65,7 +69,7 @@ Endpoint resolution order:
|
||||
1. `--endpoint`
|
||||
2. `ZERO_UPDATE_RELEASE_URL`
|
||||
3. `--repo`
|
||||
4. `https://api.github.com/repos/Gitlawb/zero/releases/latest`
|
||||
4. `https://api.github.com/repos/Twigpine/zero/releases/latest`
|
||||
|
||||
Installer scripts download the matching release asset for the local platform and
|
||||
verify its `.sha256` file. If Zero is already installed, run `zero upgrade`
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestUnixInstallerScriptMatchesReleaseContracts(t *testing.T) {
|
||||
}
|
||||
containsAll(t, script, []string{
|
||||
"set -euo pipefail",
|
||||
`ZERO_REPO="${ZERO_REPO:-Gitlawb/zero}"`,
|
||||
`ZERO_REPO="${ZERO_REPO:-Twigpine/zero}"`,
|
||||
`ZERO_INSTALL_DIR="${ZERO_INSTALL_DIR:-$HOME/.local/bin}"`,
|
||||
`archive_name="zero-v${version}-${platform}-${arch}.tar.gz"`,
|
||||
`checksum_name="${archive_name}.sha256"`,
|
||||
@@ -220,7 +220,7 @@ func runUnixInstaller(t *testing.T, fixture unixInstallFixture) (string, string,
|
||||
command.Env = append(os.Environ(),
|
||||
"PATH="+fixture.mockBin+string(os.PathListSeparator)+os.Getenv("PATH"),
|
||||
"ZERO_GITHUB_BASE_URL=https://example.test",
|
||||
"ZERO_REPO=Gitlawb/zero",
|
||||
"ZERO_REPO=Twigpine/zero",
|
||||
)
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
@@ -28,69 +28,9 @@ func TestBuildPlatformPackagesAssemblesPublishPayloads(t *testing.T) {
|
||||
tar := requireCommand(t, "tar")
|
||||
version := packageVersion(t)
|
||||
|
||||
root := t.TempDir()
|
||||
staging := filepath.Join(root, "staging")
|
||||
|
||||
// Payload the platform package must keep.
|
||||
writeFixtureFile(t, filepath.Join(staging, "zero"), "#!/usr/bin/env sh\necho fixture-zero\n", 0o755)
|
||||
writeFixtureFile(t, filepath.Join(staging, "zero-linux-sandbox"), "#!/usr/bin/env sh\n", 0o755)
|
||||
writeFixtureFile(t, filepath.Join(staging, "zero-seccomp"), "#!/usr/bin/env sh\n", 0o755)
|
||||
// Wrapper-owned files the platform package must exclude.
|
||||
writeFixtureFile(t, filepath.Join(staging, "package.json"), `{"name":"@gitlawb/zero"}`, 0o644)
|
||||
writeFixtureFile(t, filepath.Join(staging, "README.md"), "readme\n", 0o644)
|
||||
writeFixtureFile(t, filepath.Join(staging, "VERSION"), version+"\n", 0o644)
|
||||
writeFixtureFile(t, filepath.Join(staging, "bin", "zero.js"), "#!/usr/bin/env node\n", 0o755)
|
||||
// Vendored helpers tree with npm's symlink .bin shims and agent-browser's
|
||||
// multi-platform binary set.
|
||||
agentBrowserDir := filepath.Join(staging, "helpers", "node_modules", "agent-browser")
|
||||
writeFixtureFile(t, filepath.Join(agentBrowserDir, "package.json"), `{"name":"agent-browser"}`, 0o644)
|
||||
writeFixtureFile(t, filepath.Join(agentBrowserDir, "bin", "agent-browser.js"), "#!/usr/bin/env node\nconsole.log('fixture agent-browser')\n", 0o644)
|
||||
for _, name := range []string{
|
||||
"agent-browser-linux-x64",
|
||||
"agent-browser-linux-musl-x64",
|
||||
"agent-browser-linux-arm64",
|
||||
"agent-browser-darwin-arm64",
|
||||
"agent-browser-darwin-x64",
|
||||
"agent-browser-win32-x64.exe",
|
||||
} {
|
||||
writeFixtureFile(t, filepath.Join(agentBrowserDir, "bin", name), "native\n", 0o755)
|
||||
}
|
||||
tuistoryDir := filepath.Join(staging, "helpers", "node_modules", "tuistory")
|
||||
writeFixtureFile(t, filepath.Join(tuistoryDir, "dist", "cli.js"), "#!/usr/bin/env node\n", 0o644)
|
||||
binDir := filepath.Join(staging, "helpers", "node_modules", ".bin")
|
||||
mustMkdirAllFixture(t, binDir)
|
||||
if err := os.Symlink("../agent-browser/bin/agent-browser.js", filepath.Join(binDir, "agent-browser")); err != nil {
|
||||
t.Fatalf("Symlink agent-browser shim: %v", err)
|
||||
}
|
||||
if err := os.Symlink("../tuistory/dist/cli.js", filepath.Join(binDir, "tuistory")); err != nil {
|
||||
t.Fatalf("Symlink tuistory shim: %v", err)
|
||||
}
|
||||
|
||||
// Archive with the payload at the archive root, like zero-release's
|
||||
// createArchive, plus the sha256 sidecar the script verifies against.
|
||||
assetName := fmt.Sprintf("zero-v%s-linux-x64.tar.gz", version)
|
||||
archivePath := filepath.Join(root, assetName)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
if output, err := exec.CommandContext(ctx, tar, "-C", staging, "-czf", archivePath, ".").CombinedOutput(); err != nil {
|
||||
t.Fatalf("tar failed: %v\n%s", err, output)
|
||||
}
|
||||
archiveBytes, err := os.ReadFile(archivePath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile archive: %v", err)
|
||||
}
|
||||
digest := sha256.Sum256(archiveBytes)
|
||||
writeFixtureFile(t, archivePath+".sha256", fmt.Sprintf("%x %s\n", digest, assetName), 0o644)
|
||||
|
||||
root := writePlatformArchiveFixture(t, tar, version)
|
||||
outDir := filepath.Join(root, "out")
|
||||
script := filepath.Join(repoRoot(t), "scripts", "npm", "build-platform-packages.mjs")
|
||||
runCtx, runCancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer runCancel()
|
||||
command := exec.CommandContext(runCtx, node, script, "--artifacts-dir", root, "--out-dir", outDir, "--only", "linux-x64")
|
||||
command.Env = append(os.Environ(), "NODE_OPTIONS=")
|
||||
if output, err := command.CombinedOutput(); err != nil {
|
||||
t.Fatalf("build-platform-packages failed: %v\n%s", err, output)
|
||||
}
|
||||
runBuildPlatformPackages(t, node, root, outDir)
|
||||
|
||||
platformDir := filepath.Join(outDir, "platforms", "zero-linux-x64")
|
||||
var platformPkg struct {
|
||||
@@ -101,8 +41,8 @@ func TestBuildPlatformPackagesAssemblesPublishPayloads(t *testing.T) {
|
||||
Scripts map[string]string
|
||||
}
|
||||
unmarshalJSONFile(t, filepath.Join(platformDir, "package.json"), &platformPkg)
|
||||
if platformPkg.Name != "@gitlawb/zero" {
|
||||
t.Fatalf("platform package name = %q, want @gitlawb/zero (same-name suffixed versions share one trusted publisher)", platformPkg.Name)
|
||||
if platformPkg.Name != "@twigpine/zero" {
|
||||
t.Fatalf("platform package name = %q, want @twigpine/zero (same-name suffixed versions share one trusted publisher)", platformPkg.Name)
|
||||
}
|
||||
if want := version + "-linux-x64"; platformPkg.Version != want {
|
||||
t.Fatalf("platform package version = %q, want %q", platformPkg.Version, want)
|
||||
@@ -198,11 +138,11 @@ func TestBuildPlatformPackagesAssemblesPublishPayloads(t *testing.T) {
|
||||
t.Fatalf("published wrapper has dependencies: %v (helpers are vendored in the platform payloads)", wrapperPkg.Dependencies)
|
||||
}
|
||||
wantAliases := map[string]string{
|
||||
"@gitlawb/zero-darwin-arm64": "npm:@gitlawb/zero@" + version + "-darwin-arm64",
|
||||
"@gitlawb/zero-darwin-x64": "npm:@gitlawb/zero@" + version + "-darwin-x64",
|
||||
"@gitlawb/zero-linux-arm64": "npm:@gitlawb/zero@" + version + "-linux-arm64",
|
||||
"@gitlawb/zero-linux-x64": "npm:@gitlawb/zero@" + version + "-linux-x64",
|
||||
"@gitlawb/zero-win32-x64": "npm:@gitlawb/zero@" + version + "-win32-x64",
|
||||
"@twigpine/zero-darwin-arm64": "npm:@twigpine/zero@" + version + "-darwin-arm64",
|
||||
"@twigpine/zero-darwin-x64": "npm:@twigpine/zero@" + version + "-darwin-x64",
|
||||
"@twigpine/zero-linux-arm64": "npm:@twigpine/zero@" + version + "-linux-arm64",
|
||||
"@twigpine/zero-linux-x64": "npm:@twigpine/zero@" + version + "-linux-x64",
|
||||
"@twigpine/zero-win32-x64": "npm:@twigpine/zero@" + version + "-win32-x64",
|
||||
}
|
||||
if len(wrapperPkg.Optional) != len(wantAliases) {
|
||||
t.Fatalf("wrapper optionalDependencies = %v, want %v", wrapperPkg.Optional, wantAliases)
|
||||
@@ -219,6 +159,160 @@ func TestBuildPlatformPackagesAssemblesPublishPayloads(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// writePlatformArchiveFixture writes a linux-x64 release archive shaped like
|
||||
// the output of `zero-release package`, with its .sha256 sidecar, into a new
|
||||
// directory and returns that directory.
|
||||
func writePlatformArchiveFixture(t *testing.T, tar string, version string) string {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
staging := filepath.Join(root, "staging")
|
||||
|
||||
// Payload the platform package must keep.
|
||||
writeFixtureFile(t, filepath.Join(staging, "zero"), "#!/usr/bin/env sh\necho fixture-zero\n", 0o755)
|
||||
writeFixtureFile(t, filepath.Join(staging, "zero-linux-sandbox"), "#!/usr/bin/env sh\n", 0o755)
|
||||
writeFixtureFile(t, filepath.Join(staging, "zero-seccomp"), "#!/usr/bin/env sh\n", 0o755)
|
||||
// Wrapper-owned files the platform package must exclude.
|
||||
writeFixtureFile(t, filepath.Join(staging, "package.json"), `{"name":"@twigpine/zero"}`, 0o644)
|
||||
writeFixtureFile(t, filepath.Join(staging, "README.md"), "readme\n", 0o644)
|
||||
writeFixtureFile(t, filepath.Join(staging, "VERSION"), version+"\n", 0o644)
|
||||
writeFixtureFile(t, filepath.Join(staging, "bin", "zero.js"), "#!/usr/bin/env node\n", 0o755)
|
||||
// Vendored helpers tree with npm's symlink .bin shims and agent-browser's
|
||||
// multi-platform binary set.
|
||||
agentBrowserDir := filepath.Join(staging, "helpers", "node_modules", "agent-browser")
|
||||
writeFixtureFile(t, filepath.Join(agentBrowserDir, "package.json"), `{"name":"agent-browser"}`, 0o644)
|
||||
writeFixtureFile(t, filepath.Join(agentBrowserDir, "bin", "agent-browser.js"), "#!/usr/bin/env node\nconsole.log('fixture agent-browser')\n", 0o644)
|
||||
for _, name := range []string{
|
||||
"agent-browser-linux-x64",
|
||||
"agent-browser-linux-musl-x64",
|
||||
"agent-browser-linux-arm64",
|
||||
"agent-browser-darwin-arm64",
|
||||
"agent-browser-darwin-x64",
|
||||
"agent-browser-win32-x64.exe",
|
||||
} {
|
||||
writeFixtureFile(t, filepath.Join(agentBrowserDir, "bin", name), "native\n", 0o755)
|
||||
}
|
||||
tuistoryDir := filepath.Join(staging, "helpers", "node_modules", "tuistory")
|
||||
writeFixtureFile(t, filepath.Join(tuistoryDir, "dist", "cli.js"), "#!/usr/bin/env node\n", 0o644)
|
||||
binDir := filepath.Join(staging, "helpers", "node_modules", ".bin")
|
||||
mustMkdirAllFixture(t, binDir)
|
||||
if err := os.Symlink("../agent-browser/bin/agent-browser.js", filepath.Join(binDir, "agent-browser")); err != nil {
|
||||
t.Fatalf("Symlink agent-browser shim: %v", err)
|
||||
}
|
||||
if err := os.Symlink("../tuistory/dist/cli.js", filepath.Join(binDir, "tuistory")); err != nil {
|
||||
t.Fatalf("Symlink tuistory shim: %v", err)
|
||||
}
|
||||
|
||||
// Archive with the payload at the archive root, like zero-release's
|
||||
// createArchive, plus the sha256 sidecar the script verifies against.
|
||||
assetName := fmt.Sprintf("zero-v%s-linux-x64.tar.gz", version)
|
||||
archivePath := filepath.Join(root, assetName)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
if output, err := exec.CommandContext(ctx, tar, "-C", staging, "-czf", archivePath, ".").CombinedOutput(); err != nil {
|
||||
t.Fatalf("tar failed: %v\n%s", err, output)
|
||||
}
|
||||
archiveBytes, err := os.ReadFile(archivePath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile archive: %v", err)
|
||||
}
|
||||
digest := sha256.Sum256(archiveBytes)
|
||||
writeFixtureFile(t, archivePath+".sha256", fmt.Sprintf("%x %s\n", digest, assetName), 0o644)
|
||||
return root
|
||||
}
|
||||
|
||||
func runBuildPlatformPackages(t *testing.T, node string, artifactsDir string, outDir string, extraArgs ...string) {
|
||||
t.Helper()
|
||||
script := filepath.Join(repoRoot(t), "scripts", "npm", "build-platform-packages.mjs")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
args := append([]string{script, "--artifacts-dir", artifactsDir, "--out-dir", outDir, "--only", "linux-x64"}, extraArgs...)
|
||||
command := exec.CommandContext(ctx, node, args...)
|
||||
command.Env = append(os.Environ(), "NODE_OPTIONS=")
|
||||
if output, err := command.CombinedOutput(); err != nil {
|
||||
t.Fatalf("build-platform-packages failed: %v\n%s", err, output)
|
||||
}
|
||||
}
|
||||
|
||||
// The final release under the pre-rename name is the same artifacts assembled
|
||||
// with --package-name. Every place a payload names its package must follow it:
|
||||
// a wrapper that kept the current name in one alias would resolve a package
|
||||
// npm never installed.
|
||||
func TestBuildPlatformPackagesAssemblesUnderAnotherPackageName(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("fixture uses POSIX symlinks and shell scripts")
|
||||
}
|
||||
node := requireNode(t)
|
||||
tar := requireCommand(t, "tar")
|
||||
version := packageVersion(t)
|
||||
root := writePlatformArchiveFixture(t, tar, version)
|
||||
outDir := filepath.Join(root, "out")
|
||||
runBuildPlatformPackages(t, node, root, outDir, "--package-name", "@gitlawb/zero")
|
||||
|
||||
platformManifest := filepath.Join(outDir, "platforms", "zero-linux-x64", "package.json")
|
||||
var platformPkg struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
unmarshalJSONFile(t, platformManifest, &platformPkg)
|
||||
if platformPkg.Name != "@gitlawb/zero" || platformPkg.Version != version+"-linux-x64" {
|
||||
t.Fatalf("platform package = %s@%s, want @gitlawb/zero@%s-linux-x64", platformPkg.Name, platformPkg.Version, version)
|
||||
}
|
||||
|
||||
wrapperManifest := filepath.Join(outDir, "wrapper", "package.json")
|
||||
var wrapperPkg struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Optional map[string]string `json:"optionalDependencies"`
|
||||
Repository struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"repository"`
|
||||
}
|
||||
unmarshalJSONFile(t, wrapperManifest, &wrapperPkg)
|
||||
if wrapperPkg.Name != "@gitlawb/zero" || wrapperPkg.Version != version {
|
||||
t.Fatalf("wrapper = %s@%s, want @gitlawb/zero@%s", wrapperPkg.Name, wrapperPkg.Version, version)
|
||||
}
|
||||
if len(wrapperPkg.Optional) != 5 {
|
||||
t.Fatalf("wrapper optionalDependencies = %v, want the full five-platform matrix", wrapperPkg.Optional)
|
||||
}
|
||||
for alias, spec := range wrapperPkg.Optional {
|
||||
platform := strings.TrimPrefix(alias, "@gitlawb/zero-")
|
||||
if platform == alias || spec != "npm:@gitlawb/zero@"+version+"-"+platform {
|
||||
t.Fatalf("optionalDependencies[%q] = %q, want npm:@gitlawb/zero@%s-<platform> under an @gitlawb/zero-<platform> alias", alias, spec, version)
|
||||
}
|
||||
}
|
||||
// Provenance is checked against the repository the workflow runs in, which
|
||||
// is the same for both names.
|
||||
if !strings.Contains(wrapperPkg.Repository.URL, "github.com/Twigpine/zero") {
|
||||
t.Fatalf("wrapper repository = %q, want the Twigpine/zero repository npm checks provenance against", wrapperPkg.Repository.URL)
|
||||
}
|
||||
for _, manifest := range []string{platformManifest, wrapperManifest} {
|
||||
data, err := os.ReadFile(manifest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(data), "@twigpine/zero") {
|
||||
t.Fatalf("%s still names @twigpine/zero:\n%s", manifest, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPlatformPackagesRejectsAnInvalidPackageName(t *testing.T) {
|
||||
node := requireNode(t)
|
||||
script := filepath.Join(repoRoot(t), "scripts", "npm", "build-platform-packages.mjs")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), nodeWrapperTimeout())
|
||||
defer cancel()
|
||||
command := exec.CommandContext(ctx, node, script, "--artifacts-dir", t.TempDir(), "--package-name", "../zero")
|
||||
command.Env = append(os.Environ(), "NODE_OPTIONS=")
|
||||
output, err := command.CombinedOutput()
|
||||
if err == nil {
|
||||
t.Fatalf("build-platform-packages accepted --package-name ../zero:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(string(output), "is not an npm package name") {
|
||||
t.Fatalf("output does not explain the rejection: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalJSONFile(t *testing.T, path string, target any) {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
|
||||
@@ -24,9 +24,9 @@ const (
|
||||
// by the generic missing-binary path and the doctor text fallback.
|
||||
const (
|
||||
bunRecoveryLead = "You installed with Bun, which does not run dependency lifecycle scripts"
|
||||
bunPmTrustProject = "bun pm trust @gitlawb/zero"
|
||||
bunPmTrustGlobal = "bun pm -g trust @gitlawb/zero"
|
||||
bunRecoveryTrustedDeps = `"trustedDependencies": ["@gitlawb/zero"]`
|
||||
bunPmTrustProject = "bun pm trust @twigpine/zero"
|
||||
bunPmTrustGlobal = "bun pm -g trust @twigpine/zero"
|
||||
bunRecoveryTrustedDeps = `"trustedDependencies": ["@twigpine/zero"]`
|
||||
buildFromSourceLead = "If this platform has no prebuilt binary, build from source:"
|
||||
)
|
||||
|
||||
@@ -80,8 +80,8 @@ func TestPackageBinPointsToNodeWrapper(t *testing.T) {
|
||||
if err := json.Unmarshal(bytes, &pkg); err != nil {
|
||||
t.Fatalf("Unmarshal package.json: %v", err)
|
||||
}
|
||||
if pkg.Name != "@gitlawb/zero" {
|
||||
t.Fatalf("name = %q, want @gitlawb/zero", pkg.Name)
|
||||
if pkg.Name != "@twigpine/zero" {
|
||||
t.Fatalf("name = %q, want @twigpine/zero", pkg.Name)
|
||||
}
|
||||
if pkg.Bin["zero"] != "bin/zero.js" {
|
||||
t.Fatalf("bin.zero = %q, want bin/zero.js", pkg.Bin["zero"])
|
||||
@@ -158,7 +158,7 @@ func TestPostinstallComputesAssetPlan(t *testing.T) {
|
||||
if plan.BinaryName != tc.wantBinary {
|
||||
t.Fatalf("%s/%s: binaryName=%q want %q", tc.platform, tc.arch, plan.BinaryName, tc.wantBinary)
|
||||
}
|
||||
wantURL := "https://github.com/Gitlawb/zero/releases/download/v" + version + "/" + tc.wantAsset
|
||||
wantURL := "https://github.com/Twigpine/zero/releases/download/v" + version + "/" + tc.wantAsset
|
||||
if plan.AssetURL != wantURL {
|
||||
t.Fatalf("%s/%s: assetUrl=%q want %q", tc.platform, tc.arch, plan.AssetURL, wantURL)
|
||||
}
|
||||
@@ -321,11 +321,11 @@ func TestNodeWrapperPrefersPlatformPackageBinary(t *testing.T) {
|
||||
t.Fatalf("WriteFile downloaded fixture: %v", err)
|
||||
}
|
||||
|
||||
platformDir := filepath.Join(root, "node_modules", "@gitlawb", "zero-"+nodePlatformName()+"-"+nodeArchName())
|
||||
platformDir := filepath.Join(root, "node_modules", "@twigpine", "zero-"+nodePlatformName()+"-"+nodeArchName())
|
||||
if err := os.MkdirAll(platformDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll platform package: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(platformDir, "package.json"), []byte(`{"name":"@gitlawb/zero"}`), 0o644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(platformDir, "package.json"), []byte(`{"name":"@twigpine/zero"}`), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile platform package.json: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(platformDir, "zero"), []byte("#!/usr/bin/env sh\nprintf 'platform-zero'; for arg in \"$@\"; do printf ' %s' \"$arg\"; done; printf '\\n'\n"), 0o755); err != nil {
|
||||
@@ -347,6 +347,98 @@ func TestNodeWrapperPrefersPlatformPackageBinary(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// bin/zero.js derives the platform payload alias from its own package.json
|
||||
// name rather than a hardcoded one, so a wrapper assembled under any name
|
||||
// finds the payload published under that same name
|
||||
// (build-platform-packages.mjs names both from one value).
|
||||
func TestNodeWrapperResolvesPlatformPackageUnderItsOwnName(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("mock executable fixture uses a POSIX shell script")
|
||||
}
|
||||
wrapperPath := copyWrapperFixture(t)
|
||||
root := filepath.Dir(filepath.Dir(wrapperPath))
|
||||
writeFixtureFile(t, filepath.Join(root, "package.json"), `{"type":"module","name":"@example/zero"}`, 0o644)
|
||||
// A payload under the default name sits beside it and must lose.
|
||||
writePlatformPackageFixture(t, root, "@twigpine/zero", "default-name-zero")
|
||||
writePlatformPackageFixture(t, root, "@example/zero", "own-name-zero")
|
||||
|
||||
stdout, stderr, exitCode := runWrapperFixture(t, wrapperPath, "--version")
|
||||
if exitCode != wrapperExitSuccess {
|
||||
t.Fatalf("wrapper exit = %d; stdout=%s stderr=%s", exitCode, stdout, stderr)
|
||||
}
|
||||
if got := strings.TrimSpace(stdout); got != "own-name-zero --version" {
|
||||
t.Fatalf("wrapper output = %q, want the payload aliased under its own name (own-name-zero --version)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The bridge release published under the old name runs normally and, once
|
||||
// zero exits, says where the package went and how to switch, in the one order
|
||||
// npm accepts.
|
||||
func TestNodeWrapperLegacyBuildTellsUsersToSwitchAfterExit(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("mock executable fixture uses a POSIX shell script")
|
||||
}
|
||||
wrapperPath := copyWrapperFixture(t)
|
||||
root := filepath.Dir(filepath.Dir(wrapperPath))
|
||||
writeFixtureFile(t, filepath.Join(root, "package.json"), `{"type":"module","name":"@gitlawb/zero"}`, 0o644)
|
||||
writePlatformPackageFixture(t, root, "@gitlawb/zero", "legacy-zero")
|
||||
|
||||
stdout, stderr, exitCode := runWrapperFixtureWithEnv(t, wrapperPath, []string{"ZERO_WRAPPER_SIMULATE_TTY=1"}, "--version")
|
||||
if exitCode != wrapperExitSuccess {
|
||||
t.Fatalf("wrapper exit = %d; stdout=%s stderr=%s", exitCode, stdout, stderr)
|
||||
}
|
||||
if got := strings.TrimSpace(stdout); got != "legacy-zero --version" {
|
||||
t.Fatalf("stdout = %q, want the legacy build to run normally with the notice kept off stdout", got)
|
||||
}
|
||||
uninstall := strings.Index(stderr, "npm uninstall -g @gitlawb/zero")
|
||||
install := strings.Index(stderr, "npm install -g @twigpine/zero")
|
||||
if uninstall < 0 || install < 0 || uninstall > install {
|
||||
t.Fatalf("stderr does not give the switch steps, uninstall first: %q", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeWrapperLegacyNoticeStaysOutOfTheWay(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("mock executable fixture uses a POSIX shell script")
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name, packageName string
|
||||
env []string
|
||||
args []string
|
||||
}{
|
||||
// Scripts capturing stderr must not see a new line in it.
|
||||
{name: "not a terminal", packageName: "@gitlawb/zero", args: []string{"--version"}},
|
||||
// The Go side prints the same note for these; once is enough.
|
||||
{name: "zero upgrade", packageName: "@gitlawb/zero", env: []string{"ZERO_WRAPPER_SIMULATE_TTY=1"}, args: []string{"upgrade"}},
|
||||
{name: "zero update", packageName: "@gitlawb/zero", env: []string{"ZERO_WRAPPER_SIMULATE_TTY=1"}, args: []string{"update", "--check"}},
|
||||
{name: "current package", packageName: "@twigpine/zero", env: []string{"ZERO_WRAPPER_SIMULATE_TTY=1"}, args: []string{"--version"}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
wrapperPath := copyWrapperFixture(t)
|
||||
root := filepath.Dir(filepath.Dir(wrapperPath))
|
||||
writeFixtureFile(t, filepath.Join(root, "package.json"), `{"type":"module","name":"`+tc.packageName+`"}`, 0o644)
|
||||
writePlatformPackageFixture(t, root, tc.packageName, "zero")
|
||||
stdout, stderr, exitCode := runWrapperFixtureWithEnv(t, wrapperPath, tc.env, tc.args...)
|
||||
if exitCode != wrapperExitSuccess {
|
||||
t.Fatalf("wrapper exit = %d; stdout=%s stderr=%s", exitCode, stdout, stderr)
|
||||
}
|
||||
if strings.TrimSpace(stderr) != "" {
|
||||
t.Fatalf("stderr = %q, want nothing", stderr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// writePlatformPackageFixture installs a fake platform payload where npm puts
|
||||
// the wrapper's optionalDependency alias <packageName>-<platform>-<arch>; its
|
||||
// zero binary prints label followed by its arguments.
|
||||
func writePlatformPackageFixture(t *testing.T, root string, packageName string, label string) {
|
||||
t.Helper()
|
||||
dir := filepath.Join(root, "node_modules", filepath.FromSlash(packageName+"-"+nodePlatformName()+"-"+nodeArchName()))
|
||||
writeFixtureFile(t, filepath.Join(dir, "package.json"), `{"name":"`+packageName+`"}`, 0o644)
|
||||
writeFixtureFile(t, filepath.Join(dir, "zero"), "#!/usr/bin/env sh\nprintf '"+label+"'; for arg in \"$@\"; do printf ' %s' \"$arg\"; done; printf '\\n'\n", 0o755)
|
||||
}
|
||||
|
||||
func TestNodeWrapperFallsBackToDownloadedBinary(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("mock executable fixture uses a POSIX shell script")
|
||||
@@ -393,7 +485,7 @@ func TestNodeWrapperRunsFirstRunDownloaderWhenBinaryMissing(t *testing.T) {
|
||||
if err := os.WriteFile(filepath.Join(scriptsDir, "postinstall.mjs"), postinstall, 0o644); err != nil {
|
||||
t.Fatalf("WriteFile postinstall fixture: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "package.json"), []byte(`{"type":"module","name":"@gitlawb/zero","version":"0.0.0"}`), 0o644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(root, "package.json"), []byte(`{"type":"module","name":"@twigpine/zero","version":"0.0.0"}`), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile package fixture: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
package npmwrapper
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// fakeNpm stands in for npm on PATH when scripts/npm/publish-packages.sh runs.
|
||||
// It keeps one package's registry state in FAKE_NPM_STATE and answers the way
|
||||
// npm 11 does: a lookup of a missing package or a missing version fails with
|
||||
// E404, and it refuses a version published twice or any lookup or publish for
|
||||
// a package name other than the one it holds. Every call is appended to
|
||||
// FAKE_NPM_STATE.log.
|
||||
//
|
||||
// FAKE_NPM_CLOBBER_LATEST=1 makes every publish take `latest`, the failure the
|
||||
// script must stop on. FAKE_NPM_VERSION_LOOKUP_ERRORS=<n> fails the first n
|
||||
// version lookups with a network error instead of answering them.
|
||||
const fakeNpm = `#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const stateFile = process.env.FAKE_NPM_STATE;
|
||||
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||
const args = process.argv.slice(2);
|
||||
fs.appendFileSync(stateFile + '.log', JSON.stringify({ args, cwd: process.cwd() }) + '\n');
|
||||
const save = () => fs.writeFileSync(stateFile, JSON.stringify(state));
|
||||
const notFound = (detail) => { process.stderr.write('npm error code E404\nnpm error 404 ' + detail + '\n'); process.exit(1); };
|
||||
if (args[0] === 'view') {
|
||||
const [spec, field] = args.slice(1);
|
||||
const at = spec.lastIndexOf('@');
|
||||
const name = at > 0 ? spec.slice(0, at) : spec;
|
||||
if (field === 'version') {
|
||||
const failures = Number(process.env.FAKE_NPM_VERSION_LOOKUP_ERRORS || 0);
|
||||
state.versionLookups = (state.versionLookups || 0) + 1;
|
||||
save();
|
||||
if (state.versionLookups <= failures) {
|
||||
process.stderr.write('npm error code ECONNRESET\nnpm error network aborted\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
if (name !== state.name || state.versions.length === 0) notFound('Not Found - GET ' + name);
|
||||
if (field === 'version') {
|
||||
const version = spec.slice(at + 1);
|
||||
if (!state.versions.includes(version)) notFound('No match found for version ' + version);
|
||||
process.stdout.write(version + '\n');
|
||||
process.exit(0);
|
||||
}
|
||||
if (field === 'dist-tags.latest') {
|
||||
if (state.distTags.latest) process.stdout.write(state.distTags.latest + '\n');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
if (args[0] === 'publish') {
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
if (pkg.name !== state.name) {
|
||||
process.stderr.write('fake npm: refusing to publish ' + pkg.name + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
if (state.versions.includes(pkg.version)) {
|
||||
process.stderr.write('npm error code EPUBLISHCONFLICT\n');
|
||||
process.exit(1);
|
||||
}
|
||||
const tagIndex = args.indexOf('--tag');
|
||||
state.versions.push(pkg.version);
|
||||
state.distTags[tagIndex === -1 ? 'latest' : args[tagIndex + 1]] = pkg.version;
|
||||
if (process.env.FAKE_NPM_CLOBBER_LATEST === '1') state.distTags.latest = pkg.version;
|
||||
save();
|
||||
process.exit(0);
|
||||
}
|
||||
process.stderr.write('fake npm: unsupported command ' + args.join(' ') + '\n');
|
||||
process.exit(2);
|
||||
`
|
||||
|
||||
const (
|
||||
publishFixtureName = "@example/zero"
|
||||
publishFixtureVersion = "1.2.3"
|
||||
)
|
||||
|
||||
type fakeRegistryState struct {
|
||||
Name string `json:"name"`
|
||||
Versions []string `json:"versions"`
|
||||
DistTags map[string]string `json:"distTags"`
|
||||
// VersionLookups counts `npm view <name>@<version> version` calls.
|
||||
VersionLookups int `json:"versionLookups,omitempty"`
|
||||
}
|
||||
|
||||
type npmCall struct {
|
||||
Args []string `json:"args"`
|
||||
Cwd string `json:"cwd"`
|
||||
}
|
||||
|
||||
type publishRun struct {
|
||||
stdout, stderr string
|
||||
exitCode int
|
||||
state fakeRegistryState
|
||||
publishes []npmCall
|
||||
}
|
||||
|
||||
// writePublishFixture lays out what build-platform-packages.mjs emits: a
|
||||
// wrapper whose optionalDependencies alias the given platform payloads, and
|
||||
// one directory per payload in platforms.
|
||||
func writePublishFixture(t *testing.T, aliasedPlatforms []string, platformDirs []string) string {
|
||||
t.Helper()
|
||||
npmDir := filepath.Join(t.TempDir(), "npm")
|
||||
aliases := map[string]string{}
|
||||
for _, platform := range aliasedPlatforms {
|
||||
aliases[publishFixtureName+"-"+platform] = "npm:" + publishFixtureName + "@" + publishFixtureVersion + "-" + platform
|
||||
}
|
||||
wrapper, err := json.Marshal(map[string]any{
|
||||
"name": publishFixtureName,
|
||||
"version": publishFixtureVersion,
|
||||
"optionalDependencies": aliases,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeFixtureFile(t, filepath.Join(npmDir, "wrapper", "package.json"), string(wrapper), 0o644)
|
||||
for _, platform := range platformDirs {
|
||||
manifest := `{"name":"` + publishFixtureName + `","version":"` + publishFixtureVersion + "-" + platform + `"}`
|
||||
writeFixtureFile(t, filepath.Join(npmDir, "platforms", "zero-"+platform, "package.json"), manifest, 0o644)
|
||||
}
|
||||
return npmDir
|
||||
}
|
||||
|
||||
func runPublishPackages(t *testing.T, npmDir string, registry fakeRegistryState, extraEnv ...string) publishRun {
|
||||
t.Helper()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("publish-packages.sh runs on the Linux release runner; the fake npm is a POSIX executable")
|
||||
}
|
||||
bash := requireCommand(t, "bash")
|
||||
requireNode(t)
|
||||
|
||||
binDir := t.TempDir()
|
||||
writeFixtureFile(t, filepath.Join(binDir, "npm"), fakeNpm, 0o755)
|
||||
if registry.DistTags == nil {
|
||||
registry.DistTags = map[string]string{}
|
||||
}
|
||||
if registry.Versions == nil {
|
||||
registry.Versions = []string{}
|
||||
}
|
||||
stateFile := filepath.Join(t.TempDir(), "registry.json")
|
||||
initial, err := json.Marshal(registry)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeFixtureFile(t, stateFile, string(initial), 0o644)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
command := exec.CommandContext(ctx, bash, filepath.Join(repoRoot(t), "scripts", "npm", "publish-packages.sh"), npmDir)
|
||||
command.Env = append(os.Environ(),
|
||||
"PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"),
|
||||
"FAKE_NPM_STATE="+stateFile,
|
||||
"ZERO_NPM_PUBLISH_RETRY_DELAY=0",
|
||||
"NODE_OPTIONS=",
|
||||
)
|
||||
command.Env = append(command.Env, extraEnv...)
|
||||
var stdout, stderr strings.Builder
|
||||
command.Stdout = &stdout
|
||||
command.Stderr = &stderr
|
||||
run := publishRun{}
|
||||
if err := command.Run(); err != nil {
|
||||
exitErr, ok := err.(*exec.ExitError)
|
||||
if !ok {
|
||||
t.Fatalf("publish-packages.sh: %v; stderr=%s", err, stderr.String())
|
||||
}
|
||||
run.exitCode = exitErr.ExitCode()
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
t.Fatalf("publish-packages.sh timed out; stdout=%s stderr=%s", stdout.String(), stderr.String())
|
||||
}
|
||||
run.stdout, run.stderr = stdout.String(), stderr.String()
|
||||
unmarshalJSONFile(t, stateFile, &run.state)
|
||||
|
||||
if log, err := os.Open(stateFile + ".log"); err == nil {
|
||||
defer log.Close()
|
||||
scanner := bufio.NewScanner(log)
|
||||
for scanner.Scan() {
|
||||
var call npmCall
|
||||
if err := json.Unmarshal(scanner.Bytes(), &call); err != nil {
|
||||
t.Fatalf("fake npm log line %q: %v", scanner.Text(), err)
|
||||
}
|
||||
if len(call.Args) > 0 && call.Args[0] == "publish" {
|
||||
run.publishes = append(run.publishes, call)
|
||||
}
|
||||
}
|
||||
}
|
||||
return run
|
||||
}
|
||||
|
||||
// publishedDirs names each publish by the payload directory it ran in, in order.
|
||||
func (run publishRun) publishedDirs() []string {
|
||||
dirs := []string{}
|
||||
for _, call := range run.publishes {
|
||||
dirs = append(dirs, filepath.Base(call.Cwd))
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
func TestPublishPackagesPublishesPlatformPayloadsBeforeTheWrapper(t *testing.T) {
|
||||
platforms := []string{"darwin-arm64", "linux-x64"}
|
||||
npmDir := writePublishFixture(t, platforms, platforms)
|
||||
run := runPublishPackages(t, npmDir, fakeRegistryState{Name: publishFixtureName})
|
||||
if run.exitCode != 0 {
|
||||
t.Fatalf("exit = %d; stdout=%s stderr=%s", run.exitCode, run.stdout, run.stderr)
|
||||
}
|
||||
|
||||
if got, want := strings.Join(run.publishedDirs(), ","), "zero-darwin-arm64,zero-linux-x64,wrapper"; got != want {
|
||||
t.Fatalf("publish order = %s, want %s (the wrapper pins the platform versions, so it must go last)", got, want)
|
||||
}
|
||||
for index, call := range run.publishes {
|
||||
args := strings.Join(call.Args, " ")
|
||||
if !strings.Contains(args, "--access public") || !strings.Contains(args, "--ignore-scripts") {
|
||||
t.Fatalf("publish %d args = %q, want --access public and --ignore-scripts", index, args)
|
||||
}
|
||||
isWrapper := filepath.Base(call.Cwd) == "wrapper"
|
||||
if hasPlatformTag := strings.Contains(args, "--tag platform"); hasPlatformTag == isWrapper {
|
||||
t.Fatalf("publish %d (%s) args = %q: platform payloads need --tag platform and the wrapper must take latest", index, filepath.Base(call.Cwd), args)
|
||||
}
|
||||
}
|
||||
if latest := run.state.DistTags["latest"]; latest != publishFixtureVersion {
|
||||
t.Fatalf("latest = %q, want %q", latest, publishFixtureVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// "Re-run failed jobs" after a partial publish must finish the release rather
|
||||
// than fail on the versions that already went out.
|
||||
func TestPublishPackagesSkipsVersionsAlreadyOnTheRegistry(t *testing.T) {
|
||||
platforms := []string{"darwin-arm64", "linux-x64"}
|
||||
npmDir := writePublishFixture(t, platforms, platforms)
|
||||
run := runPublishPackages(t, npmDir, fakeRegistryState{
|
||||
Name: publishFixtureName,
|
||||
Versions: []string{"1.2.2", publishFixtureVersion + "-darwin-arm64"},
|
||||
DistTags: map[string]string{"latest": "1.2.2", "platform": publishFixtureVersion + "-darwin-arm64"},
|
||||
})
|
||||
if run.exitCode != 0 {
|
||||
t.Fatalf("exit = %d; stdout=%s stderr=%s", run.exitCode, run.stdout, run.stderr)
|
||||
}
|
||||
if got, want := strings.Join(run.publishedDirs(), ","), "zero-linux-x64,wrapper"; got != want {
|
||||
t.Fatalf("published %s, want %s (darwin-arm64 was already on the registry)", got, want)
|
||||
}
|
||||
if !strings.Contains(run.stdout, publishFixtureName+"@"+publishFixtureVersion+"-darwin-arm64 is already on the registry") {
|
||||
t.Fatalf("stdout does not report the skipped version: %s", run.stdout)
|
||||
}
|
||||
|
||||
// Re-running once everything is out publishes nothing and still succeeds.
|
||||
rerun := runPublishPackages(t, npmDir, run.state)
|
||||
if rerun.exitCode != 0 {
|
||||
t.Fatalf("rerun exit = %d; stdout=%s stderr=%s", rerun.exitCode, rerun.stdout, rerun.stderr)
|
||||
}
|
||||
if len(rerun.publishes) != 0 {
|
||||
t.Fatalf("rerun published %v, want nothing", rerun.publishedDirs())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishPackagesStopsBeforeTheWrapperWhenAPlatformTookLatest(t *testing.T) {
|
||||
platforms := []string{"linux-x64"}
|
||||
npmDir := writePublishFixture(t, platforms, platforms)
|
||||
run := runPublishPackages(t, npmDir, fakeRegistryState{Name: publishFixtureName}, "FAKE_NPM_CLOBBER_LATEST=1")
|
||||
if run.exitCode == 0 {
|
||||
t.Fatalf("exit = 0 after a platform publish took latest; stdout=%s", run.stdout)
|
||||
}
|
||||
if got := strings.Join(run.publishedDirs(), ","); got != "zero-linux-x64" {
|
||||
t.Fatalf("published %s, want only the platform payload (the wrapper must not follow a clobbered latest)", got)
|
||||
}
|
||||
if !strings.Contains(run.stderr, "clobbered") {
|
||||
t.Fatalf("stderr does not explain the failure: %s", run.stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// A wrapper that aliases a platform version with no payload beside it would
|
||||
// install an alias that 404s, so nothing may be published at all.
|
||||
func TestPublishPackagesRefusesAWrapperWhoseAliasesDoNotMatchThePayloads(t *testing.T) {
|
||||
npmDir := writePublishFixture(t, []string{"darwin-arm64", "linux-x64"}, []string{"linux-x64"})
|
||||
run := runPublishPackages(t, npmDir, fakeRegistryState{Name: publishFixtureName})
|
||||
if run.exitCode == 0 {
|
||||
t.Fatalf("exit = 0 with a missing platform payload; stdout=%s", run.stdout)
|
||||
}
|
||||
if len(run.publishes) != 0 {
|
||||
t.Fatalf("published %v before noticing the mismatch, want nothing", run.publishedDirs())
|
||||
}
|
||||
if !strings.Contains(run.stderr, "do not match the platform payloads") {
|
||||
t.Fatalf("stderr does not explain the failure: %s", run.stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// A lookup that fails for a reason other than E404 must not read as "not
|
||||
// published": on the re-run this skip exists for, the version is usually out
|
||||
// already, and publishing it again fails with EPUBLISHCONFLICT.
|
||||
func TestPublishPackagesRetriesAFailedLookupInsteadOfPublishing(t *testing.T) {
|
||||
platforms := []string{"linux-x64"}
|
||||
npmDir := writePublishFixture(t, platforms, platforms)
|
||||
run := runPublishPackages(t, npmDir, fakeRegistryState{
|
||||
Name: publishFixtureName,
|
||||
Versions: []string{publishFixtureVersion + "-linux-x64"},
|
||||
DistTags: map[string]string{"platform": publishFixtureVersion + "-linux-x64"},
|
||||
}, "FAKE_NPM_VERSION_LOOKUP_ERRORS=2")
|
||||
if run.exitCode != 0 {
|
||||
t.Fatalf("exit = %d after two transient lookup failures; stdout=%s stderr=%s", run.exitCode, run.stdout, run.stderr)
|
||||
}
|
||||
if got := strings.Join(run.publishedDirs(), ","); got != "wrapper" {
|
||||
t.Fatalf("published %s, want only the wrapper (the platform version was already out)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// When the registry cannot answer at all, nothing may be published: the
|
||||
// script cannot tell which versions are already out.
|
||||
func TestPublishPackagesFailsWhenTheRegistryCannotBeReached(t *testing.T) {
|
||||
platforms := []string{"linux-x64"}
|
||||
npmDir := writePublishFixture(t, platforms, platforms)
|
||||
run := runPublishPackages(t, npmDir, fakeRegistryState{Name: publishFixtureName}, "FAKE_NPM_VERSION_LOOKUP_ERRORS=1000")
|
||||
if run.exitCode == 0 {
|
||||
t.Fatalf("exit = 0 with the registry unreachable; stdout=%s", run.stdout)
|
||||
}
|
||||
if len(run.publishes) != 0 {
|
||||
t.Fatalf("published %v without being able to look anything up, want nothing", run.publishedDirs())
|
||||
}
|
||||
if !strings.Contains(run.stderr, "could not look up") || !strings.Contains(run.stderr, "ECONNRESET") {
|
||||
t.Fatalf("stderr does not explain the failure or show npm's error: %s", run.stderr)
|
||||
}
|
||||
}
|
||||
@@ -53,11 +53,6 @@ var (
|
||||
// `npm install -g` for npm-managed installs, or by downloading, verifying,
|
||||
// and atomically replacing the binary for standalone installs.
|
||||
func Apply(ctx context.Context, options Options) (ApplyResult, error) {
|
||||
checkResult, err := Check(ctx, options)
|
||||
if err != nil {
|
||||
return ApplyResult{}, err
|
||||
}
|
||||
|
||||
executablePath, err := currentExecutable()
|
||||
if err != nil {
|
||||
return ApplyResult{}, fmt.Errorf("resolve current executable: %w", err)
|
||||
@@ -66,6 +61,18 @@ func Apply(ctx context.Context, options Options) (ApplyResult, error) {
|
||||
executablePath = resolved
|
||||
}
|
||||
method := DetectInstallMethod(executablePath)
|
||||
if method == InstallMethodNpmLegacy {
|
||||
// Nothing newer is ever published under the old name, so npm would
|
||||
// reinstall this same build and the update would report success. Refused
|
||||
// before the release check because the answer does not depend on it, and
|
||||
// it has to reach someone who is offline too.
|
||||
return ApplyResult{}, errors.New(legacyPackageSwitchMessage())
|
||||
}
|
||||
|
||||
checkResult, err := Check(ctx, options)
|
||||
if err != nil {
|
||||
return ApplyResult{}, err
|
||||
}
|
||||
// Only the standalone path rewrites the binary in place, so only it needs the
|
||||
// recovery state. npm and Homebrew hand the work to the package manager.
|
||||
if method == InstallMethodStandalone {
|
||||
|
||||
@@ -433,3 +433,50 @@ func TestVerifyArchiveChecksumRejectsFilenameMismatch(t *testing.T) {
|
||||
t.Fatalf("expected error to name both the referenced file and the expected one, got %q", err)
|
||||
}
|
||||
}
|
||||
|
||||
// An install of the old package name gets no further releases, so npm would
|
||||
// reinstall the same build and Apply would report an update that did not
|
||||
// happen. It must refuse with the switch steps in the one safe order, without
|
||||
// waiting on a release check that cannot change the answer.
|
||||
func TestApplyRefusesALegacyNpmInstallWithTheSwitchSteps(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
binary := filepath.Join(dir, "zero")
|
||||
original := []byte("legacy binary")
|
||||
if err := os.WriteFile(binary, original, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
manifest := `{"name":"@gitlawb/zero","version":"0.10.0-linux-x64","os":["linux"],"cpu":["x64"]}`
|
||||
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(manifest), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
restore := currentExecutable
|
||||
currentExecutable = func() (string, error) { return binary, nil }
|
||||
t.Cleanup(func() { currentExecutable = restore })
|
||||
|
||||
_, err := Apply(context.Background(), Options{
|
||||
CurrentVersion: "0.10.0",
|
||||
Fetch: func(context.Context, string) (Release, error) {
|
||||
t.Error("Apply ran the release check for a legacy install; the refusal must not depend on the network")
|
||||
return Release{}, errors.New("offline")
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("Apply did not refuse a legacy npm install")
|
||||
}
|
||||
message := err.Error()
|
||||
uninstall := strings.Index(message, "npm uninstall -g @gitlawb/zero")
|
||||
install := strings.Index(message, "npm install -g @twigpine/zero")
|
||||
if uninstall < 0 || install < 0 {
|
||||
t.Fatalf("refusal does not give both switch commands: %q", message)
|
||||
}
|
||||
if uninstall > install {
|
||||
t.Fatalf("refusal installs before uninstalling, which npm rejects with EEXIST: %q", message)
|
||||
}
|
||||
after, readErr := os.ReadFile(binary)
|
||||
if readErr != nil {
|
||||
t.Fatal(readErr)
|
||||
}
|
||||
if string(after) != string(original) {
|
||||
t.Error("Apply rewrote the binary it refused to update")
|
||||
}
|
||||
}
|
||||
@@ -9,17 +9,28 @@ import (
|
||||
)
|
||||
|
||||
// npmPackageName is the published package name for the npm distribution of
|
||||
// zero (see package.json). scripts/postinstall.mjs downloads the native
|
||||
// binary into the same directory as package.json and leaves a
|
||||
// ".zero-binary-version" marker file next to it — both are reliable signals
|
||||
// that a given executable came from an npm install.
|
||||
const npmPackageName = "@gitlawb/zero"
|
||||
// zero, and must equal the name in the repository's package.json
|
||||
// (TestNpmPackageNameMatchesPackageJSON holds the two together).
|
||||
// scripts/postinstall.mjs downloads the native binary into the same directory
|
||||
// as package.json and leaves a ".zero-binary-version" marker file next to it —
|
||||
// both are reliable signals that a given executable came from an npm install.
|
||||
const npmPackageName = "@twigpine/zero"
|
||||
|
||||
// legacyNpmPackageName is the name zero was published under on npm up to
|
||||
// 0.9.0. The 0.10.0 release is published once more under it (the legacy step
|
||||
// in .github/workflows/release-artifacts.yml), so that 0.9.0's `zero upgrade`
|
||||
// and anyone still installing the old name land on a build that says where
|
||||
// the package went. Nothing is published under it after that, which is why an
|
||||
// install of it is told to switch instead of offered an update.
|
||||
const legacyNpmPackageName = "@gitlawb/zero"
|
||||
|
||||
// InstallMethod identifies how the running zero binary was installed.
|
||||
type InstallMethod string
|
||||
|
||||
const (
|
||||
InstallMethodNpm InstallMethod = "npm"
|
||||
InstallMethodNpm InstallMethod = "npm"
|
||||
// InstallMethodNpmLegacy is an npm install of legacyNpmPackageName.
|
||||
InstallMethodNpmLegacy InstallMethod = "npm-legacy"
|
||||
InstallMethodHomebrew InstallMethod = "homebrew"
|
||||
InstallMethodStandalone InstallMethod = "standalone"
|
||||
)
|
||||
@@ -53,32 +64,71 @@ func DetectInstallMethod(executablePath string) InstallMethod {
|
||||
return InstallMethodHomebrew
|
||||
}
|
||||
dir := filepath.Dir(executablePath)
|
||||
pkg, hasManifest := readNpmManifest(dir)
|
||||
if _, err := os.Stat(filepath.Join(dir, ".zero-binary-version")); err == nil {
|
||||
// The first-run downloader put this binary next to the wrapper's own
|
||||
// package.json, which names the package it was installed as.
|
||||
if hasManifest && pkg.Name == legacyNpmPackageName {
|
||||
return InstallMethodNpmLegacy
|
||||
}
|
||||
return InstallMethodNpm
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(dir, "package.json"))
|
||||
if err != nil {
|
||||
return InstallMethodStandalone
|
||||
}
|
||||
var pkg struct {
|
||||
Name string `json:"name"`
|
||||
OS []string `json:"os"`
|
||||
CPU []string `json:"cpu"`
|
||||
Bin json.RawMessage `json:"bin"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &pkg); err != nil {
|
||||
if !hasManifest {
|
||||
return InstallMethodStandalone
|
||||
}
|
||||
// The native platform package is inert and constrained to one OS/CPU. The
|
||||
// repository's wrapper package has the same name but carries a bin entry and
|
||||
// broad platform lists, so name alone would misclassify `go build -o zero`
|
||||
// from the repository root as an npm-managed install.
|
||||
if pkg.Name == npmPackageName && len(pkg.OS) == 1 && len(pkg.CPU) == 1 && len(pkg.Bin) == 0 {
|
||||
if len(pkg.OS) != 1 || len(pkg.CPU) != 1 || len(pkg.Bin) != 0 {
|
||||
return InstallMethodStandalone
|
||||
}
|
||||
switch pkg.Name {
|
||||
case npmPackageName:
|
||||
return InstallMethodNpm
|
||||
case legacyNpmPackageName:
|
||||
return InstallMethodNpmLegacy
|
||||
}
|
||||
return InstallMethodStandalone
|
||||
}
|
||||
|
||||
type npmManifest struct {
|
||||
Name string `json:"name"`
|
||||
OS []string `json:"os"`
|
||||
CPU []string `json:"cpu"`
|
||||
Bin json.RawMessage `json:"bin"`
|
||||
}
|
||||
|
||||
// readNpmManifest reads the package.json in dir, reporting false when there is
|
||||
// none or it does not parse.
|
||||
func readNpmManifest(dir string) (npmManifest, bool) {
|
||||
data, err := os.ReadFile(filepath.Join(dir, "package.json"))
|
||||
if err != nil {
|
||||
return npmManifest{}, false
|
||||
}
|
||||
var pkg npmManifest
|
||||
if err := json.Unmarshal(data, &pkg); err != nil {
|
||||
return npmManifest{}, false
|
||||
}
|
||||
return pkg, true
|
||||
}
|
||||
|
||||
// legacyPackageSwitchMessage tells someone running the build published under
|
||||
// legacyNpmPackageName how to move to npmPackageName. bin/zero.js prints the
|
||||
// same note.
|
||||
//
|
||||
// There is no in-place migration. Both packages own the `zero` bin, so npm
|
||||
// refuses to install the new one over the old (EEXIST), and forcing it lets a
|
||||
// later uninstall of the old package delete the shared bin. Uninstalling first
|
||||
// is the only safe order, and it has to be the user's own step: the running
|
||||
// binary is inside the package it would remove.
|
||||
func legacyPackageSwitchMessage() string {
|
||||
return "Zero has moved to " + npmPackageName + " on npm, and " + legacyNpmPackageName + " gets no further releases. To keep getting updates, quit zero and run:\n" +
|
||||
" npm uninstall -g " + legacyNpmPackageName + "\n" +
|
||||
" npm install -g " + npmPackageName + "\n" +
|
||||
"Uninstall first: both packages provide the `zero` command. With bun, pnpm or yarn, use its global remove and add commands instead."
|
||||
}
|
||||
|
||||
// isHomebrewPath reports whether executablePath is inside a Homebrew keg.
|
||||
//
|
||||
// goos is a parameter rather than runtime.GOOS so the decision can be tested on
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The self-updater installs npmPackageName and recognises installs by it, so
|
||||
// it must be the name the release actually publishes. A rename that misses
|
||||
// this constant leaves `zero upgrade` reinstalling the old package.
|
||||
func TestNpmPackageNameMatchesPackageJSON(t *testing.T) {
|
||||
data, err := os.ReadFile(filepath.Join("..", "..", "package.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile package.json: %v", err)
|
||||
}
|
||||
var pkg struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &pkg); err != nil {
|
||||
t.Fatalf("Unmarshal package.json: %v", err)
|
||||
}
|
||||
if pkg.Name != npmPackageName {
|
||||
t.Fatalf("package.json name = %q, npmPackageName = %q; they must match", pkg.Name, npmPackageName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectInstallMethodStandaloneByDefault(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
exePath := filepath.Join(dir, "zero")
|
||||
@@ -37,7 +57,7 @@ func TestDetectInstallMethodNpmViaPackageJSON(t *testing.T) {
|
||||
if err := os.WriteFile(exePath, []byte("binary"), 0o755); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"name":"@gitlawb/zero","version":"0.5.0-linux-x64","os":["linux"],"cpu":["x64"]}`), 0o644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"name":"@twigpine/zero","version":"0.5.0-linux-x64","os":["linux"],"cpu":["x64"]}`), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile package.json: %v", err)
|
||||
}
|
||||
if method := DetectInstallMethod(exePath); method != InstallMethodNpm {
|
||||
@@ -51,7 +71,7 @@ func TestDetectInstallMethodDoesNotTreatRepositoryManifestAsNpmBinary(t *testing
|
||||
if err := os.WriteFile(exePath, []byte("binary"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
manifest := `{"name":"@gitlawb/zero","bin":{"zero":"bin/zero.js"},"os":["linux","darwin","win32","android"],"cpu":["x64","arm64"]}`
|
||||
manifest := `{"name":"@twigpine/zero","bin":{"zero":"bin/zero.js"},"os":["linux","darwin","win32","android"],"cpu":["x64","arm64"]}`
|
||||
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(manifest), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -73,3 +93,57 @@ func TestDetectInstallMethodIgnoresUnrelatedPackageJSON(t *testing.T) {
|
||||
t.Fatalf("DetectInstallMethod = %q, want standalone", method)
|
||||
}
|
||||
}
|
||||
|
||||
// The bridge release published under the old name must recognise itself, so
|
||||
// `zero upgrade` can tell it to switch instead of reinstalling the same build.
|
||||
func TestDetectInstallMethodRecognisesALegacyPlatformPayload(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
exePath := filepath.Join(dir, "zero")
|
||||
if err := os.WriteFile(exePath, []byte("binary"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
manifest := `{"name":"@gitlawb/zero","version":"0.10.0-linux-x64","os":["linux"],"cpu":["x64"]}`
|
||||
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(manifest), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if method := DetectInstallMethod(exePath); method != InstallMethodNpmLegacy {
|
||||
t.Fatalf("DetectInstallMethod = %q, want %q", method, InstallMethodNpmLegacy)
|
||||
}
|
||||
}
|
||||
|
||||
// A binary the first-run downloader fetched sits next to the wrapper's own
|
||||
// package.json, which is the only place the installed name is recorded.
|
||||
func TestDetectInstallMethodRecognisesALegacyDownloadedBinary(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
exePath := filepath.Join(dir, "zero")
|
||||
if err := os.WriteFile(exePath, []byte("binary"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, ".zero-binary-version"), []byte("0.10.0\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
manifest := `{"name":"@gitlawb/zero","bin":{"zero":"bin/zero.js"},"os":["linux","darwin","win32","android"],"cpu":["x64","arm64"]}`
|
||||
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(manifest), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if method := DetectInstallMethod(exePath); method != InstallMethodNpmLegacy {
|
||||
t.Fatalf("DetectInstallMethod = %q, want %q", method, InstallMethodNpmLegacy)
|
||||
}
|
||||
}
|
||||
|
||||
// Without the downloader's marker, a wrapper-shaped manifest under the old
|
||||
// name is a checkout, not an install, exactly as for the current name.
|
||||
func TestDetectInstallMethodDoesNotTreatALegacyWrapperManifestAsAnInstall(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
exePath := filepath.Join(dir, "zero")
|
||||
if err := os.WriteFile(exePath, []byte("binary"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
manifest := `{"name":"@gitlawb/zero","bin":{"zero":"bin/zero.js"},"os":["linux","darwin","win32","android"],"cpu":["x64","arm64"]}`
|
||||
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(manifest), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if method := DetectInstallMethod(exePath); method != InstallMethodStandalone {
|
||||
t.Fatalf("DetectInstallMethod = %q, want standalone", method)
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultRepository = "Gitlawb/zero"
|
||||
DefaultRepository = "Twigpine/zero"
|
||||
DefaultTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
@@ -233,6 +233,10 @@ func Format(result Result) string {
|
||||
"Latest release: " + result.ReleaseURL,
|
||||
}
|
||||
lines = appendAssetLines(lines, result.ReleaseAsset)
|
||||
if result.installMethod == InstallMethodNpmLegacy {
|
||||
// Up to date today, but the next release will not reach this install.
|
||||
lines = append(lines, legacyPackageSwitchMessage())
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
@@ -322,6 +326,11 @@ func upgradeGuidance(asset AssetCheck, sourceFlag string, installMethod InstallM
|
||||
// `brew upgrade zero` would change this one instead.
|
||||
return "This Homebrew-managed installation is updated with `brew upgrade zero`. `zero upgrade` refuses here, because replacing the keg binary would leave Homebrew's records describing a version that is no longer installed."
|
||||
}
|
||||
if installMethod == InstallMethodNpmLegacy {
|
||||
// Placed like the Homebrew answer above, and for the same reason: it is a
|
||||
// property of the install on this machine, and `zero upgrade` refuses it.
|
||||
return legacyPackageSwitchMessage()
|
||||
}
|
||||
if sourceFlag != "" {
|
||||
if installMethod == InstallMethodNpm {
|
||||
return "This npm-managed installation can be updated with `npm install -g " + npmPackageName + "@latest`, which installs the official npm package. The custom `" + sourceFlag + "` source only affects the release check and update gating, not the npm install source."
|
||||
@@ -457,7 +466,7 @@ func releasePlatform(goos string) (string, error) {
|
||||
// self-updating release asset. Point users at `npm update` rather
|
||||
// than a source rebuild, since that's the documented Termux
|
||||
// install/upgrade path and doesn't require a Go toolchain.
|
||||
return "", fmt.Errorf("no published release for %q (release assets: linux, macos, windows). Your build is the current version of record. Upgrade with `npm update -g @gitlawb/zero` to get the latest.", goos) //nolint:staticcheck // Preserve established user-facing error text.
|
||||
return "", fmt.Errorf("no published release for %q (release assets: linux, macos, windows). Your build is the current version of record. Upgrade with `npm update -g %s` to get the latest.", goos, npmPackageName) //nolint:staticcheck // Preserve established user-facing error text.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -587,11 +587,35 @@ func TestFormatNpmCustomSourceExplainsInstallSource(t *testing.T) {
|
||||
SourceFlag: "--repo someone/fork", installMethod: InstallMethodNpm,
|
||||
}
|
||||
output := Format(result)
|
||||
if strings.Contains(output, "zero upgrade --repo") || !strings.Contains(output, "npm install -g @gitlawb/zero@latest") || !strings.Contains(output, "only affects the release check and update gating") {
|
||||
if strings.Contains(output, "zero upgrade --repo") || !strings.Contains(output, "npm install -g @twigpine/zero@latest") || !strings.Contains(output, "only affects the release check and update gating") {
|
||||
t.Fatalf("npm custom-source guidance is misleading: %q", output)
|
||||
}
|
||||
}
|
||||
|
||||
// A legacy install is told to switch whether or not an update is out: up to
|
||||
// date today, the next release will not reach it, and `zero upgrade` refuses.
|
||||
func TestFormatTellsALegacyNpmInstallToSwitch(t *testing.T) {
|
||||
asset := assetCheckForTest(t, "v0.11.0", runtime.GOOS, runtime.GOARCH)
|
||||
for name, result := range map[string]Result{
|
||||
"up to date": {
|
||||
CurrentVersion: "0.10.0", LatestVersion: "0.10.0", ReleaseURL: "https://example.test/release",
|
||||
ReleaseAsset: asset, installMethod: InstallMethodNpmLegacy,
|
||||
},
|
||||
"update available": {
|
||||
CurrentVersion: "0.10.0", LatestVersion: "0.11.0", ReleaseURL: "https://example.test/release",
|
||||
ReleaseAsset: asset, UpdateAvailable: true, installMethod: InstallMethodNpmLegacy,
|
||||
},
|
||||
} {
|
||||
output := Format(result)
|
||||
if !strings.Contains(output, "npm uninstall -g @gitlawb/zero") || !strings.Contains(output, "npm install -g @twigpine/zero") {
|
||||
t.Errorf("%s: output does not give the switch steps: %q", name, output)
|
||||
}
|
||||
if strings.Contains(output, "Run `zero upgrade`") {
|
||||
t.Errorf("%s: output recommends `zero upgrade`, which refuses a legacy install: %q", name, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatCrossTargetCustomSourceKeepsAccurateGuidance(t *testing.T) {
|
||||
goos, goarch := "linux", "amd64"
|
||||
if runtime.GOOS == goos && runtime.GOARCH == goarch {
|
||||
|
||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@gitlawb/zero",
|
||||
"name": "@twigpine/zero",
|
||||
"version": "0.9.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@gitlawb/zero",
|
||||
"name": "@twigpine/zero",
|
||||
"version": "0.9.0",
|
||||
"cpu": [
|
||||
"x64",
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@gitlawb/zero",
|
||||
"name": "@twigpine/zero",
|
||||
"version": "0.9.0",
|
||||
"description": "Zero — a fast, multi-provider terminal coding agent.",
|
||||
"type": "module",
|
||||
@@ -30,11 +30,11 @@
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Gitlawb/zero.git"
|
||||
"url": "git+https://github.com/Twigpine/zero.git"
|
||||
},
|
||||
"homepage": "https://github.com/Gitlawb/zero#readme",
|
||||
"homepage": "https://zero.twigpine.com",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Gitlawb/zero/issues"
|
||||
"url": "https://github.com/Twigpine/zero/issues"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"bootstrap-sha": "02f0c0929bec65f5fc41bdcab3eb518d2b0b329a",
|
||||
"packages": {
|
||||
".": {
|
||||
"package-name": "@gitlawb/zero"
|
||||
"package-name": "@twigpine/zero"
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Version = $env:ZERO_VERSION,
|
||||
[string]$Repository = $(if ($env:ZERO_REPO) { $env:ZERO_REPO } else { "Gitlawb/zero" }),
|
||||
[string]$Repository = $(if ($env:ZERO_REPO) { $env:ZERO_REPO } else { "Twigpine/zero" }),
|
||||
[string]$InstallDir = $env:ZERO_INSTALL_DIR,
|
||||
[string]$GitHubApi = $(if ($env:ZERO_GITHUB_API) { $env:ZERO_GITHUB_API } else { "https://api.github.com" }),
|
||||
[string]$GitHubBaseUrl = $(if ($env:ZERO_GITHUB_BASE_URL) { $env:ZERO_GITHUB_BASE_URL } else { "https://github.com" })
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ZERO_REPO="${ZERO_REPO:-Gitlawb/zero}"
|
||||
ZERO_REPO="${ZERO_REPO:-Twigpine/zero}"
|
||||
ZERO_VERSION="${ZERO_VERSION:-latest}"
|
||||
ZERO_INSTALL_DIR="${ZERO_INSTALL_DIR:-$HOME/.local/bin}"
|
||||
ZERO_GITHUB_API="${ZERO_GITHUB_API:-https://api.github.com}"
|
||||
@@ -16,7 +16,7 @@ Usage:
|
||||
|
||||
Environment:
|
||||
ZERO_VERSION Release version or tag. Defaults to latest.
|
||||
ZERO_REPO GitHub repository. Defaults to Gitlawb/zero.
|
||||
ZERO_REPO GitHub repository. Defaults to Twigpine/zero.
|
||||
ZERO_INSTALL_DIR Directory for the zero binary. Defaults to ~/.local/bin.
|
||||
ZERO_GITHUB_API GitHub API base URL. Defaults to https://api.github.com.
|
||||
ZERO_GITHUB_BASE_URL GitHub web base URL. Defaults to https://github.com.
|
||||
|
||||
@@ -2,21 +2,22 @@
|
||||
// build-platform-packages.mjs: assemble the npm publish payloads for a release
|
||||
// from the per-platform archives built by `zero-release package`.
|
||||
//
|
||||
// Layout written under --out-dir (default dist/npm):
|
||||
// wrapper/ @gitlawb/[email protected] — bin/zero.js, the
|
||||
// Layout written under --out-dir (default dist/npm), where <name> is the
|
||||
// package.json name, or --package-name when given:
|
||||
// wrapper/ <name>@X.Y.Z — bin/zero.js, the
|
||||
// first-run fallback downloader, and a
|
||||
// package.json with NO scripts and NO
|
||||
// dependencies, only optionalDependencies
|
||||
// aliasing the platform versions below.
|
||||
// platforms/zero-<platform>-<arch>/
|
||||
// @gitlawb/zero@X.Y.Z-<platform>-<arch> —
|
||||
// <name>@X.Y.Z-<platform>-<arch> —
|
||||
// the zero binary, sandbox helpers, and the
|
||||
// vendored local-control helpers/ tree,
|
||||
// copied straight out of the release archive.
|
||||
//
|
||||
// The platform "packages" are versions of the SAME @gitlawb/zero package at
|
||||
// suffixed (semver-prerelease) versions, referenced from the wrapper through
|
||||
// npm: aliases. One package name keeps a single npm trusted-publisher config.
|
||||
// The platform "packages" are versions of the SAME package at suffixed
|
||||
// (semver-prerelease) versions, referenced from the wrapper through npm:
|
||||
// aliases. One package name keeps a single npm trusted-publisher config.
|
||||
// See docs/NPM_PACKAGING.md for the publishing rules this feeds.
|
||||
//
|
||||
// Archives are verified against their .sha256 files before extraction. All
|
||||
@@ -24,9 +25,15 @@
|
||||
// CI must always assemble the full matrix, and the wrapper's
|
||||
// optionalDependencies always reference the full matrix regardless).
|
||||
//
|
||||
// --package-name assembles the same artifacts under another name. It exists
|
||||
// for the one final release under the pre-rename name @gitlawb/zero (see
|
||||
// docs/NPM_PACKAGING.md); everything in the payloads that names the package
|
||||
// follows it, so the binary and wrapper know which name they were installed as.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/npm/build-platform-packages.mjs \
|
||||
// --artifacts-dir dist/artifacts [--out-dir dist/npm] [--only linux-x64,...]
|
||||
// --artifacts-dir dist/artifacts [--out-dir dist/npm] [--only linux-x64,...] \
|
||||
// [--package-name @scope/name]
|
||||
|
||||
import {
|
||||
chmodSync,
|
||||
@@ -71,7 +78,7 @@ function fail(message) {
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { artifactsDir: '', outDir: join(repoRoot, 'dist', 'npm'), only: null };
|
||||
const args = { artifactsDir: '', outDir: join(repoRoot, 'dist', 'npm'), only: null, packageName: '' };
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
const next = () => {
|
||||
@@ -89,6 +96,14 @@ function parseArgs(argv) {
|
||||
case '--only':
|
||||
args.only = new Set(next().split(',').map((entry) => entry.trim()).filter(Boolean));
|
||||
break;
|
||||
case '--package-name':
|
||||
args.packageName = next();
|
||||
// A scoped or unscoped npm name; anything else would also become a
|
||||
// path segment in the alias npm installs.
|
||||
if (!/^(@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/.test(args.packageName)) {
|
||||
fail(`--package-name ${JSON.stringify(args.packageName)} is not an npm package name`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
fail(`unknown argument ${arg}`);
|
||||
}
|
||||
@@ -99,6 +114,7 @@ function parseArgs(argv) {
|
||||
|
||||
function readWrapperPackage() {
|
||||
const pkg = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8'));
|
||||
if (!pkg.name) fail('package.json name is empty');
|
||||
if (!pkg.version) fail('package.json version is empty');
|
||||
return pkg;
|
||||
}
|
||||
@@ -107,14 +123,17 @@ function platformVersion(version, entry) {
|
||||
return `${version}-${entry.platform}-${entry.arch}`;
|
||||
}
|
||||
|
||||
function aliasName(entry) {
|
||||
return `@gitlawb/zero-${entry.platform}-${entry.arch}`;
|
||||
// bin/zero.js derives the same alias from its own package name at runtime, so
|
||||
// the wrapper name, the alias names, and the alias targets must all come from
|
||||
// one value.
|
||||
function aliasName(packageName, entry) {
|
||||
return `${packageName}-${entry.platform}-${entry.arch}`;
|
||||
}
|
||||
|
||||
function optionalDependencies(version) {
|
||||
function optionalDependencies(packageName, version) {
|
||||
const aliases = {};
|
||||
for (const entry of MATRIX) {
|
||||
aliases[aliasName(entry)] = `npm:@gitlawb/zero@${platformVersion(version, entry)}`;
|
||||
aliases[aliasName(packageName, entry)] = `npm:${packageName}@${platformVersion(version, entry)}`;
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
@@ -260,7 +279,7 @@ function verifyHelperShims(packageDir, entry, assetName) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildPlatformPackage(entry, version, artifactsDir, outDir) {
|
||||
function buildPlatformPackage(entry, packageName, version, artifactsDir, outDir) {
|
||||
const assetName = `zero-v${version}-${entry.release}.${entry.ext}`;
|
||||
const archivePath = join(artifactsDir, assetName);
|
||||
if (!existsSync(archivePath)) fail(`missing release archive ${archivePath}`);
|
||||
@@ -295,9 +314,9 @@ function buildPlatformPackage(entry, version, artifactsDir, outDir) {
|
||||
|
||||
const wrapperPkg = readWrapperPackage();
|
||||
const manifest = {
|
||||
name: '@gitlawb/zero',
|
||||
name: packageName,
|
||||
version: platformVersion(version, entry),
|
||||
description: `zero native binary for ${entry.platform}-${entry.arch} (installed via @gitlawb/zero@${version})`,
|
||||
description: `zero native binary for ${entry.platform}-${entry.arch} (installed via ${packageName}@${version})`,
|
||||
os: [entry.platform],
|
||||
cpu: [entry.arch],
|
||||
license: wrapperPkg.license,
|
||||
@@ -312,7 +331,7 @@ function buildPlatformPackage(entry, version, artifactsDir, outDir) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildWrapperPackage(version, outDir) {
|
||||
function buildWrapperPackage(packageName, version, outDir) {
|
||||
const wrapperDir = join(outDir, 'wrapper');
|
||||
rmSync(wrapperDir, { recursive: true, force: true });
|
||||
mkdirSync(join(wrapperDir, 'bin'), { recursive: true });
|
||||
@@ -333,14 +352,17 @@ function buildWrapperPackage(version, outDir) {
|
||||
// stages into the platform payloads — see internal/release/release.go).
|
||||
delete pkg.scripts;
|
||||
delete pkg.dependencies;
|
||||
pkg.optionalDependencies = optionalDependencies(version);
|
||||
pkg.name = packageName;
|
||||
pkg.optionalDependencies = optionalDependencies(packageName, version);
|
||||
writeFileSync(join(wrapperDir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
|
||||
return wrapperDir;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const version = readWrapperPackage().version;
|
||||
const wrapperPkg = readWrapperPackage();
|
||||
const packageName = args.packageName || wrapperPkg.name;
|
||||
const { version } = wrapperPkg;
|
||||
|
||||
const selected = MATRIX.filter(
|
||||
(entry) => !args.only || args.only.has(`${entry.platform}-${entry.arch}`),
|
||||
@@ -356,13 +378,14 @@ function main() {
|
||||
|
||||
const built = [];
|
||||
for (const entry of selected) {
|
||||
built.push(buildPlatformPackage(entry, version, args.artifactsDir, args.outDir));
|
||||
built.push(buildPlatformPackage(entry, packageName, version, args.artifactsDir, args.outDir));
|
||||
}
|
||||
const wrapperDir = buildWrapperPackage(version, args.outDir);
|
||||
const wrapperDir = buildWrapperPackage(packageName, version, args.outDir);
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify(
|
||||
{
|
||||
name: packageName,
|
||||
version,
|
||||
wrapperDir,
|
||||
platformDirs: built,
|
||||
|
||||
Executable
+137
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env bash
|
||||
# publish-packages.sh: publish the npm payloads that build-platform-packages.mjs
|
||||
# assembled under <npm-dir> (wrapper/ plus platforms/*/), in the order and
|
||||
# with the dist-tags docs/NPM_PACKAGING.md requires:
|
||||
#
|
||||
# 1. every platform payload, under the `platform` dist-tag, never `latest`;
|
||||
# 2. check that `latest` was not moved onto a platform version;
|
||||
# 3. the wrapper, which takes `latest`, then confirm `latest` is its version.
|
||||
#
|
||||
# The package name comes from the assembled wrapper's package.json, so the
|
||||
# same script publishes whatever name the payloads were built under.
|
||||
#
|
||||
# A version that is already on the registry is skipped, which makes
|
||||
# re-running a release job that failed halfway safe: the platform payloads
|
||||
# that went out are not published twice, and the wrapper still follows them.
|
||||
# Publishes pass --ignore-scripts because the payloads are already assembled
|
||||
# and verified; nothing should run at pack time.
|
||||
#
|
||||
# Usage: scripts/npm/publish-packages.sh <npm-dir>
|
||||
#
|
||||
# Auth comes from the environment (npm OIDC trusted publishing in CI).
|
||||
# ZERO_NPM_PUBLISH_RETRY_DELAY overrides the seconds between dist-tag lookups.
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
echo "::error::$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
npm_dir="${1:-}"
|
||||
[ -n "$npm_dir" ] || fail "usage: publish-packages.sh <npm-dir>"
|
||||
wrapper_dir="${npm_dir%/}/wrapper"
|
||||
[ -f "${wrapper_dir}/package.json" ] || fail "${wrapper_dir}/package.json not found; run build-platform-packages.mjs first"
|
||||
|
||||
manifest_field() {
|
||||
node -e 'const value = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"))[process.argv[2]]; process.stdout.write(value === undefined ? "" : String(value));' "$1/package.json" "$2"
|
||||
}
|
||||
|
||||
name="$(manifest_field "$wrapper_dir" name)"
|
||||
version="$(manifest_field "$wrapper_dir" version)"
|
||||
[ -n "$name" ] && [ -n "$version" ] || fail "${wrapper_dir}/package.json has no name or version"
|
||||
delay="${ZERO_NPM_PUBLISH_RETRY_DELAY:-5}"
|
||||
|
||||
# The wrapper pins every platform version exactly, so a wrapper whose aliases
|
||||
# do not match the payloads beside it would install aliases that 404. Check
|
||||
# before anything is published.
|
||||
shopt -s nullglob
|
||||
platform_dirs=("${npm_dir%/}"/platforms/*/)
|
||||
shopt -u nullglob
|
||||
[ "${#platform_dirs[@]}" -gt 0 ] || fail "no platform payloads under ${npm_dir%/}/platforms"
|
||||
expected_platforms="$(node -e 'const pkg = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); process.stdout.write(Object.values(pkg.optionalDependencies || {}).sort().join("\n"));' "${wrapper_dir}/package.json")"
|
||||
actual_platforms=""
|
||||
for dir in "${platform_dirs[@]}"; do
|
||||
dir="${dir%/}"
|
||||
platform_name="$(manifest_field "$dir" name)"
|
||||
platform_version="$(manifest_field "$dir" version)"
|
||||
[ "$platform_name" = "$name" ] || fail "${dir} is ${platform_name}, not ${name}"
|
||||
case "$platform_version" in
|
||||
"${version}-"*) ;;
|
||||
*) fail "${dir} version ${platform_version} is not a platform version of ${name}@${version}" ;;
|
||||
esac
|
||||
actual_platforms="${actual_platforms}npm:${name}@${platform_version}"$'\n'
|
||||
done
|
||||
actual_platforms="$(printf '%s' "$actual_platforms" | sort)"
|
||||
if [ "$actual_platforms" != "$expected_platforms" ]; then
|
||||
fail "the wrapper's optionalDependencies do not match the platform payloads under ${npm_dir%/}/platforms (wrapper: $(echo "$expected_platforms" | tr '\n' ' '); payloads: $(echo "$actual_platforms" | tr '\n' ' '))"
|
||||
fi
|
||||
|
||||
# npm reports every kind of "not on the registry" (no such package, no such
|
||||
# version) as E404. Any other lookup failure (network, auth, a registry
|
||||
# outage) is retried and then fails the job: reading it as "not published"
|
||||
# would publish a version that may already be out, and npm would reject that
|
||||
# with EPUBLISHCONFLICT on the very re-run this skip exists for.
|
||||
already_published() {
|
||||
local output status errors
|
||||
errors="$(mktemp)"
|
||||
for _ in 1 2 3; do
|
||||
status=0
|
||||
output="$(npm view "${name}@$1" version 2>"$errors")" || status=$?
|
||||
if [ "$status" -eq 0 ]; then
|
||||
rm -f "$errors"
|
||||
[ "$output" = "$1" ]
|
||||
return
|
||||
fi
|
||||
if grep -q 'code E404' "$errors"; then
|
||||
rm -f "$errors"
|
||||
return 1
|
||||
fi
|
||||
sleep "$delay"
|
||||
done
|
||||
cat "$errors" >&2
|
||||
rm -f "$errors"
|
||||
fail "could not look up ${name}@$1 on the registry, so cannot tell whether it is already published"
|
||||
}
|
||||
|
||||
# publish <dir> <version> [npm publish flags...]
|
||||
publish() {
|
||||
local dir="$1" package_version="$2"
|
||||
shift 2
|
||||
if already_published "$package_version"; then
|
||||
echo "${name}@${package_version} is already on the registry; not publishing it again."
|
||||
return 0
|
||||
fi
|
||||
echo "publishing ${name}@${package_version} from ${dir}"
|
||||
(cd "$dir" && npm publish --access public --ignore-scripts "$@")
|
||||
}
|
||||
|
||||
for dir in "${platform_dirs[@]}"; do
|
||||
dir="${dir%/}"
|
||||
publish "$dir" "$(manifest_field "$dir" version)" --tag platform
|
||||
done
|
||||
|
||||
# Platform versions are semver prereleases of the wrapper version, so a
|
||||
# platform publish that took `latest` would make a plain install deliver a
|
||||
# payload with no bin. Stop before the wrapper goes out. A failed lookup is
|
||||
# tolerated (registry lag, or no `latest` yet on a package's first release):
|
||||
# failing here would strand the platform payloads without their wrapper.
|
||||
latest="$(npm view "$name" dist-tags.latest || true)"
|
||||
case "$latest" in
|
||||
"${version}-"*)
|
||||
fail "dist-tag latest points at platform version ${latest}; a platform publish clobbered it. Fix the dist-tag before publishing the wrapper."
|
||||
;;
|
||||
esac
|
||||
echo "dist-tag latest is ${latest:-unset} (unchanged by the platform publishes)."
|
||||
|
||||
publish "$wrapper_dir" "$version"
|
||||
|
||||
# Registry metadata can lag a publish by a few seconds.
|
||||
for _ in 1 2 3 4 5; do
|
||||
latest="$(npm view "$name" dist-tags.latest || true)"
|
||||
if [ "$latest" = "$version" ]; then
|
||||
echo "dist-tag latest is ${version}."
|
||||
exit 0
|
||||
fi
|
||||
sleep "$delay"
|
||||
done
|
||||
fail "dist-tag latest of ${name} is ${latest:-unset}, expected ${version} after the wrapper publish."
|
||||
@@ -16,7 +16,7 @@
|
||||
// ZERO_INSTALL_DRY_RUN=1 print the resolved plan as JSON, no network, exit 0
|
||||
// ZERO_INSTALL_PLATFORM=… override process.platform (linux|darwin|win32|android)
|
||||
// ZERO_INSTALL_ARCH=… override process.arch (x64|arm64)
|
||||
// ZERO_REPO=owner/name override the GitHub repo (default Gitlawb/zero)
|
||||
// ZERO_REPO=owner/name override the GitHub repo (default Twigpine/zero)
|
||||
// ZERO_GITHUB_BASE_URL=… override the download host (default https://github.com)
|
||||
|
||||
import {
|
||||
@@ -41,7 +41,7 @@ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
|
||||
const VERSION = pkg.version;
|
||||
|
||||
const REPO = process.env.ZERO_REPO || 'Gitlawb/zero';
|
||||
const REPO = process.env.ZERO_REPO || 'Twigpine/zero';
|
||||
const BASE = (process.env.ZERO_GITHUB_BASE_URL || 'https://github.com').replace(/\/+$/, '');
|
||||
// The .sha256 is fetched from the same origin as the archive, so TLS authenticity
|
||||
// is the only real integrity control — require https unless explicitly overridden
|
||||
|
||||
Reference in new issue
Block a user