From 98fca1af4913fb26b8640507e152c7244a5a2cea Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Thu, 22 Oct 2026 18:22:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(release):=20=E5=87=86=E5=A4=87=20UI=20?= =?UTF-8?q?=E5=8C=85=E5=85=AC=E5=BC=80=20tarball=20=E5=88=86=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 校验已发布原包并以追加方式镜像到既有 GitHub Release - 默认只读并要求显式发布,覆盖重复执行和上传失败边界 - 接入主分支发布流程并记录消费者迁移前提;本轮不执行发布 --- .changeset/public-install-guidance.md | 5 ++ .github/workflows/release-and-publish.yml | 12 ++- CONTRIBUTING.md | 40 ++++++++- README.md | 4 + package.json | 4 +- packages/web/README.md | 2 +- scripts/publish-public-package.ts | 105 ++++++++++++++++++++++ scripts/test-public-package.ts | 27 ++++++ 8 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 .changeset/public-install-guidance.md create mode 100644 scripts/publish-public-package.ts create mode 100644 scripts/test-public-package.ts diff --git a/.changeset/public-install-guidance.md b/.changeset/public-install-guidance.md new file mode 100644 index 0000000..f848630 --- /dev/null +++ b/.changeset/public-install-guidance.md @@ -0,0 +1,5 @@ +--- +"@inkcre/ui-web": patch +--- + +修正安装指南中的 GitHub Packages 认证链接。 diff --git a/.github/workflows/release-and-publish.yml b/.github/workflows/release-and-publish.yml index d96f913..480835a 100644 --- a/.github/workflows/release-and-publish.yml +++ b/.github/workflows/release-and-publish.yml @@ -35,9 +35,15 @@ jobs: uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0 with: version: pnpm changeset:version - publish: pnpm run release - commit: "chore: version packages" - title: "chore: version packages" + publish: pnpm changeset:publish + commit: "chore(release): 更新包版本" + title: "chore(release): 更新包版本" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Publish anonymous package download + if: steps.changesets.outputs.hasChangesets == 'false' + run: pnpm release:public "$(node -p "require('./packages/web/package.json').version")" --publish + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7fe77d7..b2bc1f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -102,6 +102,43 @@ registry artifact. The consumer's [`docs/40-deployment/development-runtime.md`](https://github.com/InKCre/impl-ts/blob/main/docs/40-deployment/development-runtime.md#joint-development-lanes) owns the full startup, remotes, cleanup, troubleshooting, and release-fidelity contract. +## Public package delivery + +`@inkcre/ui-web` has no confidentiality requirement. GitHub Packages remains the publishing +registry; anonymous consumers use the original package tarball attached to its versioned GitHub +Release. Changing GitHub Package visibility alone does not remove npm authentication. + +The release workflow publishes with Changesets, then runs `pnpm release:public --publish`. +The command reads the already-published registry tarball, checks its integrity and package identity, +and attaches those exact bytes to the existing `@inkcre/ui-web@` Release. It does not build, +version, or publish an npm package. An identical attachment is a successful no-op; a conflicting +attachment fails without overwriting it. Anonymous download and byte comparison complete the check. + +Maintainers can prepare or recover a single version independently of npm publication: + +```bash +pnpm release:public 2.3.0 # Read-only checks; does not upload +pnpm release:public 2.3.0 --publish # Explicit public upload or verification +``` + +This requires GitHub CLI authentication for release access and npm authentication for package reads; +upload additionally requires repository contents write access. No token is embedded in the download +URL. Run `pnpm check:public-package` for the append-only/retry regression check. + +Consumers retain semver dependencies where runtime sharing reads those ranges. After the asset is +published and anonymously verified, a pnpm workspace can route the exact version using a root +`pnpm-workspace.yaml` override: + +```yaml +overrides: + '@inkcre/ui-web@2.3.0': https://github.com/InKCre/ui/releases/download/%40inkcre%2Fui-web%402.3.0/inkcre-ui-web-2.3.0.tgz +``` + +Regenerate and review the lockfile, then verify a fresh `pnpm install --frozen-lockfile` with an empty +store and no GitHub token, plus consumer builds. Never activate an override before its attachment +exists. The initial 2.3.0 attachment and `impl-ts` consumer cutover are pending release approval; +the current consumer lockfile still requires GitHub Packages access. + ## GitHub Packages authentication The committed `.npmrc` routes only the `@inkcre` scope to GitHub Packages. It never @@ -109,7 +146,8 @@ stores or expands a credential. Local credentials belong to the trusted user configuration. For interactive one-time setup, create a classic GitHub personal access token with -`read:packages`, then let npm write it to your user-level configuration: +`read:packages` and confirm read access to the package, then let npm write it to your user-level +configuration. A token scope does not grant the package ACL: ```bash npm login --scope=@inkcre --auth-type=legacy --registry=https://npm.pkg.github.com diff --git a/README.md b/README.md index de860ee..d11ca4d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ The InKCre design system and the `@inkcre/ui-web` web renderer — design tokens ### Use the package +The current GitHub Packages path requires package read access and npm authentication. A public +Release-tarball path is being prepared; see the [delivery and cutover contract](CONTRIBUTING.md#public-package-delivery) +before selecting an installation path. + ```bash pnpm add @inkcre/ui-web ``` diff --git a/package.json b/package.json index 8d18e06..a3dd641 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "check:generated": "tsx scripts/check-generated.ts", "check:stories": "tsx scripts/check-story-coverage.ts", "check:package": "tsx scripts/check-package-contract.ts", + "check:public-package": "tsx --test scripts/test-public-package.ts", + "release:public": "tsx scripts/publish-public-package.ts", "check:token-workflow": "tsx scripts/prepare-token-update.ts fixture", "tokens:update": "tsx scripts/prepare-token-update.ts apply", "type-check": "pnpm type-check:root && pnpm --filter @inkcre/ui-web type-check", @@ -23,7 +25,7 @@ "format": "oxfmt .oxfmtrc.json package.json pnpm-workspace.yaml tsconfig.json scripts .github/workflows/*.yml README.md DESIGN.md docs/design && pnpm --filter @inkcre/ui-web format", "format:check": "oxfmt --check .oxfmtrc.json package.json pnpm-workspace.yaml tsconfig.json scripts .github/workflows/*.yml README.md DESIGN.md docs/design && pnpm --filter @inkcre/ui-web format:check", "lint": "oxlint --deny-warnings scripts && pnpm --filter @inkcre/ui-web lint", - "check": "pnpm format:check && pnpm lint && pnpm check:generated && pnpm --filter @inkcre/ui-web skill:validate && pnpm check:stories && pnpm check:token-workflow && pnpm type-check:root && pnpm build && pnpm check:package && pnpm story", + "check": "pnpm format:check && pnpm lint && pnpm check:generated && pnpm --filter @inkcre/ui-web skill:validate && pnpm check:stories && pnpm check:token-workflow && pnpm check:public-package && pnpm type-check:root && pnpm build && pnpm check:package && pnpm story", "changeset": "changeset", "changeset:add": "changeset add", "changeset:version": "changeset version && pnpm build-package-metadata", diff --git a/packages/web/README.md b/packages/web/README.md index b64864c..853c9e7 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -4,7 +4,7 @@ InKCre 的 Vue 3 Web UI 库,提供组件、设计 Token、样式工具和随 ## 安装 -包发布到 GitHub Packages 的 `@inkcre` scope,需要消费者具有读取权限。先按[仓库认证说明](https://github.com/InKCre/ui#github-packages-authentication)配置注册表和凭据,再在消费者项目中安装: +包发布到 GitHub Packages 的 `@inkcre` scope,需要消费者具有读取权限。先按[仓库认证说明](https://github.com/InKCre/ui/blob/main/CONTRIBUTING.md#github-packages-authentication)配置注册表和凭据,再在消费者项目中安装: ```bash pnpm add @inkcre/ui-web vue@^3.5.25 diff --git a/scripts/publish-public-package.ts b/scripts/publish-public-package.ts new file mode 100644 index 0000000..bc50f57 --- /dev/null +++ b/scripts/publish-public-package.ts @@ -0,0 +1,105 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +const repository = "InKCre/ui"; +const packageName = "@inkcre/ui-web"; + +export function ensureAsset( + original: Buffer, + existing: Buffer | undefined, + upload: () => void, +): void { + if (existing !== undefined) { + assert.ok( + existing.equals(original), + "Existing release asset differs; refusing to overwrite it", + ); + return; + } + upload(); +} + +async function download(url: string): Promise { + // This intentionally sends no GitHub credential: consumers must be able to do the same. + const response = await fetch(url, { signal: AbortSignal.timeout(60_000) }); + assert.ok(response.ok, `Anonymous download failed: ${response.status} ${url}`); + return Buffer.from(await response.arrayBuffer()); +} + +async function main(): Promise { + const [version, mode] = process.argv.slice(2); + assert.ok( + version && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version), + "Usage: pnpm release:public [--publish] (default: read-only preparation)", + ); + assert.ok(mode === undefined || mode === "--publish", "Only --publish is supported"); + assert.ok(process.argv.length <= 4, "Unexpected arguments"); + const tag = `${packageName}@${version}`; + const filename = `inkcre-ui-web-${version}.tgz`; + const url = `https://github.com/${repository}/releases/download/${encodeURIComponent(tag)}/${filename}`; + const release = JSON.parse( + execFileSync("gh", ["release", "view", tag, "--repo", repository, "--json", "isDraft,assets"], { + encoding: "utf8", + }), + ) as { isDraft: boolean; assets: { name: string; url: string }[] }; + assert.ok(!release.isDraft, "The corresponding published GitHub Release must already exist"); + + const directory = mkdtempSync(join(tmpdir(), "inkcre-public-package-")); + try { + const registry = "--registry=https://npm.pkg.github.com"; + const integrity = JSON.parse( + execFileSync("npm", ["view", tag, "dist.integrity", "--json", registry], { + encoding: "utf8", + }), + ) as string; + execFileSync( + "npm", + ["pack", tag, "--ignore-scripts", "--pack-destination", directory, "--json", registry], + { stdio: ["ignore", "pipe", "inherit"] }, + ); + const tarball = join(directory, filename); + const original = readFileSync(tarball); + assert.equal(`sha512-${createHash("sha512").update(original).digest("base64")}`, integrity); + const manifest = JSON.parse( + execFileSync("tar", ["-xOf", tarball, "package/package.json"], { encoding: "utf8" }), + ) as { name: string; version: string }; + assert.equal(manifest.name, packageName); + assert.equal(manifest.version, version); + + const asset = release.assets.find((item) => item.name === filename); + const existing = asset ? await download(url) : undefined; + ensureAsset(original, existing, () => { + if (mode !== "--publish") return; + // Never clobber: concurrent or conflicting publication must stop for inspection. + execFileSync("gh", ["release", "upload", tag, tarball, "--repo", repository], { + stdio: "inherit", + }); + }); + if (mode === "--publish") { + assert.ok( + (await download(url)).equals(original), + "Public asset does not match registry bytes", + ); + } + console.log( + JSON.stringify({ + version, + integrity, + url, + published: mode === "--publish", + existing: !!asset, + }), + ); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + +if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) { + await main(); +} diff --git a/scripts/test-public-package.ts b/scripts/test-public-package.ts new file mode 100644 index 0000000..5540928 --- /dev/null +++ b/scripts/test-public-package.ts @@ -0,0 +1,27 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { ensureAsset } from "./publish-public-package.js"; + +test("release assets are append-only, repeatable, and preserve upload failures", () => { + const original = Buffer.from("published registry bytes"); + let uploads = 0; + const upload = () => { + uploads += 1; + }; + ensureAsset(original, undefined, upload); + assert.equal(uploads, 1); + ensureAsset(original, Buffer.from(original), upload); + assert.equal(uploads, 1); + assert.throws( + () => ensureAsset(original, Buffer.from("different build"), upload), + /refusing to overwrite/, + ); + assert.equal(uploads, 1); + assert.throws( + () => + ensureAsset(original, undefined, () => { + throw new Error("upload failed"); + }), + /upload failed/, + ); +});