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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 51 additions & 7 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,52 @@ name: Publish npm package
on:
release:
types: [published]
workflow_dispatch:
inputs:
release_tag:
description: Existing published GitHub Release tag to retry (for example v0.5.0)
required: true
type: string

permissions:
contents: read
id-token: write

concurrency:
group: npm-publish-${{ github.event.release.tag_name }}
group: npm-publish-${{ inputs.release_tag || github.event.release.tag_name }}
cancel-in-progress: false

jobs:
publish-npm:
name: Publish to npm with OIDC
runs-on: ubuntu-latest
timeout-minutes: 30
env:
RELEASE_TAG: ${{ inputs.release_tag || github.event.release.tag_name }}
WORKFLOW_SHA: ${{ github.workflow_sha }}
steps:
- name: Validate release request
env:
GH_TOKEN: ${{ github.token }}
run: |
if [[ "$GITHUB_EVENT_NAME" == workflow_dispatch && "$GITHUB_REF" != refs/heads/main ]]; then
echo 'Manual publication must use the main workflow.' >&2
exit 1
fi
if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo 'A version tag is required.' >&2
exit 1
fi
gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG" > "$RUNNER_TEMP/release.json"
if ! jq -e --arg tag "$RELEASE_TAG" '.draft == false and .published_at != null and .tag_name == $tag' "$RUNNER_TEMP/release.json" > /dev/null; then
echo 'Only an existing published GitHub Release can be published to npm.' >&2
exit 1
fi

- name: Checkout the release tag
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: ${{ github.event.release.tag_name }}
ref: refs/tags/${{ env.RELEASE_TAG }}
fetch-depth: 0
persist-credentials: false

Expand All @@ -42,9 +69,12 @@ jobs:

- name: Verify release identity
id: release
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
tag_commit="$(git rev-parse "refs/tags/$RELEASE_TAG^{commit}")"
if [[ "$(git rev-parse HEAD)" != "$tag_commit" ]]; then
echo 'Checked out source does not match the exact release tag.' >&2
exit 1
fi
package_version="$(node -p "require('./package.json').version")"
if [[ "$RELEASE_TAG" != "v$package_version" ]]; then
echo "Release tag $RELEASE_TAG does not match package version $package_version" >&2
Expand All @@ -55,11 +85,18 @@ jobs:
echo "Release tag $RELEASE_TAG is not reachable from main" >&2
exit 1
fi
if ! git merge-base --is-ancestor "$WORKFLOW_SHA" origin/main; then
echo 'Release tooling must come from a commit reachable from main.' >&2
exit 1
fi
echo "Release source: $(git rev-parse HEAD); workflow tooling: $WORKFLOW_SHA"

published_version="$(npm view "react-native-bs-diff-patch@$package_version" version 2>/dev/null || true)"
if [[ "$published_version" == "$package_version" ]]; then
if npm view "react-native-bs-diff-patch@$package_version" version --json --registry=https://registry.npmjs.org/ > "$RUNNER_TEMP/registry-version.json"; then
echo "react-native-bs-diff-patch@$package_version is already published" >&2
exit 1
elif ! jq -e '.error.code == "E404"' "$RUNNER_TEMP/registry-version.json" > /dev/null; then
echo 'Cannot confirm that the release version is absent from npm; refusing to publish.' >&2
exit 1
fi

if [[ "$package_version" == *-* ]]; then
Expand All @@ -77,6 +114,9 @@ jobs:

- name: Run release quality gates
run: |
# A retry can fix this unpackaged consumer harness without moving the
# release tag. Restore it before publication so only tag source ships.
git show "$WORKFLOW_SHA:scripts/test-sdk-consumers.mjs" > scripts/test-sdk-consumers.mjs
yarn prepare
yarn typecheck
yarn lint
Expand All @@ -91,6 +131,8 @@ jobs:
yarn test:action
yarn test:package
yarn test:sdk
git show HEAD:scripts/test-sdk-consumers.mjs > scripts/test-sdk-consumers.mjs
git diff --exit-code HEAD
npm pack --dry-run --ignore-scripts
env:
CHROME_PATH: /usr/bin/google-chrome
Expand Down Expand Up @@ -118,4 +160,6 @@ jobs:
env:
PACKAGE_SPEC: react-native-bs-diff-patch@${{ steps.release.outputs.package_version }}
CHROME_PATH: /usr/bin/google-chrome
run: yarn test:sdk
run: |
git show "$WORKFLOW_SHA:scripts/test-sdk-consumers.mjs" > scripts/test-sdk-consumers.mjs
yarn test:sdk
28 changes: 28 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,34 @@ repository and the `npm-publish.yml` workflow. No npm-side configuration is
required for a release. The release tag must exactly match
`v<package.json version>`.

### Recovering a failed npm publication

For an existing tag and published GitHub Release (the example below uses
`v0.5.0`), recovery reuses that release. Classify a failed publication by
checking the registry first:

```sh
npm view react-native-bs-diff-patch@0.5.0 version \
dist.attestations.provenance.predicateType --registry=https://registry.npmjs.org/
```

If `0.5.0` is already present, do not run `npm publish` again; complete only
the provenance and registry consumer checks. If the registry confirms an
explicit E404, fix the publishing tool or fixture, merge that fix into `main`,
and retry the existing release from `main`:

```sh
gh workflow run npm-publish.yml --ref main -f release_tag=v0.5.0
gh run list --workflow npm-publish.yml --limit 5
gh run watch <run-id>
```

See [Development and verification](./docs/development.md#recovering-a-failed-npm-publication)
for the exact tag checkout, commit, fixture, tree-cleanliness, OIDC, provenance,
and registry-smoke invariants. Do not move or delete the existing tag, run
release-it again for the same version, or change the npm Trusted Publisher
configuration.

### Scripts

The `package.json` file contains various scripts for common tasks:
Expand Down
47 changes: 47 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,50 @@ The npm package's Trusted Publisher is already configured with these values:

No npm-side change is required for a normal release, and the workflow does not
use a long-lived npm token.

## Recovering a failed npm publication

For an existing tag and published GitHub Release (the examples below use
`v0.5.0`), recovery reuses that release. Check the registry before retrying a
failed `npm-publish.yml` run:

```sh
npm view react-native-bs-diff-patch@0.5.0 version \
dist.attestations.provenance.predicateType --registry=https://registry.npmjs.org/
```

If `0.5.0` is already published, do not publish it again. Finish only the
provenance and registry smoke checks, such as
`PACKAGE_SPEC=react-native-bs-diff-patch@0.5.0 yarn test:sdk` and the applicable
`yarn test:registry:vite` or `yarn test:registry:expo` check. Proceed with a
retry only when the official registry query confirms an explicit E404; network
errors, 403 responses, timeouts, and any other ambiguous result must stop the
recovery. After fixing the release tool or fixture, merge that fix into `main`
and retry the existing GitHub Release from `main`:

```sh
gh workflow run npm-publish.yml --ref main -f release_tag=v0.5.0
gh run list --workflow npm-publish.yml --limit 5
gh run watch <run-id>
```

After the retry completes, inspect the run, provenance metadata, and registry
smoke output before announcing availability.

The manual workflow accepts only an existing published GitHub Release. It
checks out the exact `refs/tags/<release_tag>` commit and verifies that `HEAD`
matches that tag, then checks the tag/package version, tag and workflow commit
reachability from `main`, and that the npm version is not already present. It
temporarily takes the `test-sdk-consumers` harness from the workflow commit for
the quality gates, restores the tag script before packing, and asserts that the
tracked tree is clean. The full quality gates, OIDC Trusted Publishing,
provenance verification, and published-package smoke test remain enabled.

The npm 12 cross-version fixture resolves an exact package version through the
official `https://registry.npmjs.org/` registry and compares its expected
SHA-512 SRI. Keep that exact-registry resolution; do not restore a download URL
fixture or weaken its integrity assertion.

Do not move or delete the existing tag, run release-it again for the same
version, or change the npm Trusted Publisher settings. A retry repairs the
publication path for the existing release.
38 changes: 38 additions & 0 deletions docs/zh-CN/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,41 @@ npm 包的 Trusted Publisher 已按以下值配置完成:
- Environment:留空。

正常发布无需再修改 npm 侧配置,工作流也不使用长期 npm token。

## npm 发布失败后的恢复

对于已有 tag 和已发布的 GitHub Release(以下示例使用 `v0.5.0`),恢复流程会复用已有
release。重试失败的 `npm-publish.yml` 前,先检查 registry:

```sh
npm view react-native-bs-diff-patch@0.5.0 version \
dist.attestations.provenance.predicateType --registry=https://registry.npmjs.org/
```

如果 `0.5.0` 已经存在,不要再次运行 `npm publish`,只补做 provenance 和 registry
消费者检查,例如 `PACKAGE_SPEC=react-native-bs-diff-patch@0.5.0 yarn test:sdk`,以及适用的
`yarn test:registry:vite` 或 `yarn test:registry:expo`。只有官方 registry 查询明确返回 E404
时才可继续重试;网络错误、403、超时或其他不确定结果都必须停止恢复流程。修复发布工具或
fixture 后,将修复合并到 `main`,再从 `main` 重试已有 GitHub Release:

```sh
gh workflow run npm-publish.yml --ref main -f release_tag=v0.5.0
gh run list --workflow npm-publish.yml --limit 5
gh run watch <run-id>
```

重试完成后,先检查 workflow run、provenance 元数据和 registry smoke 输出,再宣布版本可用。

手动 workflow 只接受已经发布的 GitHub Release。它会从精确的
`refs/tags/<release_tag>` checkout 并确认 `HEAD` 就是该 tag 的 commit,然后检查 release
tag 与 `package.json` 版本一致、tag 和 workflow commit 可从 `main` 到达,以及 npm 中尚不
存在该版本。质量门禁期间可以临时使用 workflow commit 中的 `test-sdk-consumers` harness,
打包前恢复 tag 中的脚本,并断言 tracked tree 干净。完整门禁、npm OIDC Trusted Publishing、
provenance 校验和发布包 smoke test 均保留在 workflow 中。

npm 12 的跨版本 fixture 必须通过官方 registry(`https://registry.npmjs.org/`)解析精确的
包版本,并比对预期的 SHA-512 SRI。不要恢复下载 URL fixture,也不要为了绕过 npm 默认的
URL 策略而削弱完整性断言。

不要移动或删除已有 tag,不要对同一版本再次运行 release-it,也不要修改 npm Trusted Publisher
配置。重试只修复已有 GitHub Release 的发布路径,不会创建第二个 release。
30 changes: 20 additions & 10 deletions scripts/test-sdk-consumers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ const chromeCandidates = [
].filter(Boolean);
const browserCsp =
"default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'";
const registry040TarballUrl =
'https://registry.npmjs.org/react-native-bs-diff-patch/-/react-native-bs-diff-patch-0.4.0.tgz';
const registry040PackageSpec = 'react-native-bs-diff-patch@0.4.0';
const officialNpmRegistry = 'https://registry.npmjs.org/';
const registry040Integrity =
'sha512-pQXEVIn9yx8zqYtJjnw2xws3g3EB8E2Qz8N5WTwyUtUSpW/fhBFUtE1ACqbVvW6LE+7ndy7NjlrTJIERD0Y0lQ==';

Expand Down Expand Up @@ -153,15 +153,25 @@ function readPackedManifest(tarballPath) {
}

async function prepareRegistry040Tarball() {
const registryPackageDirectory = path.join(
temporaryDirectory,
'registry-package-resolution'
);
await mkdir(registryPackageDirectory);
const metadata = parseTrailingJson(
run('npm', [
'pack',
'--ignore-scripts',
'--json',
'--pack-destination',
temporaryDirectory,
registry040TarballUrl,
])
run(
'npm',
[
'pack',
'--ignore-scripts',
'--json',
'--pack-destination',
temporaryDirectory,
`--registry=${officialNpmRegistry}`,
registry040PackageSpec,
],
{ cwd: registryPackageDirectory }
)
);
const tarballPath = path.join(
temporaryDirectory,
Expand Down