Skip to content

chore(repo): release prep 0.9.8 (#240) #67

chore(repo): release prep 0.9.8 (#240)

chore(repo): release prep 0.9.8 (#240) #67

Workflow file for this run

name: Release
# Builds all targets, runs tests, publishes to npm, and creates a GitHub Release.
# Trigger by pushing a tag: git tag release/0.1.0 && git push origin refs/tags/release/0.1.0
# Prep PRs use branch release-prep/X.Y.Z (see scripts/RELEASE.md) — not the same ref as the tag.
#
# Native reuse: prefer a validated native-main-bundle from a successful Build & Test (main)
# run (exact SHA, then matching aggregate fingerprint). Missing/invalid → rebuild matrix.
# Integration tests skip only when this exact SHA already passed main.
on:
push:
tags: ['release/*']
permissions:
contents: write
packages: read
id-token: write
actions: write
pull-requests: write
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
CI_IMAGE: ghcr.io/${{ github.repository }}/ci-build:latest
CI_IMAGE_ALPINE: ghcr.io/${{ github.repository }}/ci-build-alpine:latest
CMAKE_POLICY_VERSION_MINIMUM: '3.5'
OPUS_STATIC: '1'
NATIVE_TOOL_MODE: declared
jobs:
validate-package-lock:
name: Package-lock optional bindings
runs-on: self-hosted
steps:
- name: Prepare workspace
run: |
if [[ -n "${RUNNER_WORKSPACE:-}" && -d "${RUNNER_WORKSPACE}" ]]; then
docker run --rm \
-v "${RUNNER_WORKSPACE}:${RUNNER_WORKSPACE}" \
alpine:3.20 \
chown -R "$(id -u):$(id -g)" "${RUNNER_WORKSPACE}" || true
fi
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: ./.github/actions/validate-package-lock
quality:
name: Typecheck & lint
needs: validate-package-lock
runs-on: self-hosted
steps:
- name: Prepare workspace
run: |
if [[ -n "${RUNNER_WORKSPACE:-}" && -d "${RUNNER_WORKSPACE}" ]]; then
docker run --rm \
-v "${RUNNER_WORKSPACE}:${RUNNER_WORKSPACE}" \
alpine:3.20 \
chown -R "$(id -u):$(id -g)" "${RUNNER_WORKSPACE}" || true
fi
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: bash scripts/ci/run-pr-quality.sh
plan:
name: Plan native builds
needs: [validate-package-lock, quality]
runs-on: self-hosted
outputs:
linux_x64_matrix: ${{ steps.matrices.outputs.linux_x64_matrix }}
build_linux_arm64: ${{ steps.matrices.outputs.build_linux_arm64 }}
build_linux_musl: ${{ steps.matrices.outputs.build_linux_musl }}
host_matrix: ${{ steps.matrices.outputs.host_matrix }}
cached_linux_x64: ${{ steps.matrices.outputs.cached_linux_x64 }}
cached_linux_arm64: ${{ steps.matrices.outputs.cached_linux_arm64 }}
cached_host: ${{ steps.matrices.outputs.cached_host }}
all_cached: ${{ steps.matrices.outputs.all_cached }}
main_validated: ${{ steps.main.outputs.main_validated }}
bundle_reused: ${{ steps.resolve.outputs.bundle_reused }}
bundle_run_id: ${{ steps.resolve.outputs.run_id }}
bundle_exact_sha: ${{ steps.resolve.outputs.exact_sha }}
aggregate_digest: ${{ steps.resolve.outputs.aggregate_digest }}
fallback_reason: ${{ steps.resolve.outputs.fallback_reason }}
steps:
- name: Prepare workspace
run: |
if [[ -n "${RUNNER_WORKSPACE:-}" && -d "${RUNNER_WORKSPACE}" ]]; then
docker run --rm \
-v "${RUNNER_WORKSPACE}:${RUNNER_WORKSPACE}" \
alpine:3.20 \
chown -R "$(id -u):$(id -g)" "${RUNNER_WORKSPACE}" || true
fi
- uses: actions/checkout@v4
# Bundle resolution computes the current contract with `cargo metadata`
# inside CI_IMAGE before the fallback planner composite action runs.
- name: Resolve native-main-bundle from successful main
id: resolve
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
NATIVE_TOOL_MODE: declared
CARGO_VIA_CI_IMAGE: '1'
run: bash scripts/ci/resolve-native-main-bundle.sh --profile release
- name: Plan per-target cache rebuilds (fallback / accelerator)
id: plan
if: steps.resolve.outputs.bundle_reused != 'true'
uses: ./.github/actions/plan-native-builds
with:
check_main_success: 'false'
- name: Check main CI success (exact SHA — test skip gate)
id: main
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
run: bash scripts/ci/check-main-ci-success.sh
# Pass JSON via env — never echo "${{ … }}" with nested quotes (bash strips them
# and fromJson fails, so build-host / stage-linux-x64 never start).
- name: Export matrices
id: matrices
shell: bash
env:
BUNDLE_REUSED: ${{ steps.resolve.outputs.bundle_reused }}
LINUX_X64_MATRIX: ${{ steps.plan.outputs.linux_x64_matrix }}
BUILD_LINUX_ARM64: ${{ steps.plan.outputs.build_linux_arm64 }}
BUILD_LINUX_MUSL: ${{ steps.plan.outputs.build_linux_musl }}
HOST_MATRIX: ${{ steps.plan.outputs.host_matrix }}
CACHED_LINUX_X64: ${{ steps.plan.outputs.cached_linux_x64 }}
CACHED_LINUX_ARM64: ${{ steps.plan.outputs.cached_linux_arm64 }}
CACHED_HOST: ${{ steps.plan.outputs.cached_host }}
ALL_CACHED_PLAN: ${{ steps.plan.outputs.all_cached }}
CACHED_TARGETS: ${{ steps.plan.outputs.cached_targets }}
REBUILT_TARGETS: ${{ steps.plan.outputs.rebuilt_targets }}
AGGREGATE_DIGEST: ${{ steps.resolve.outputs.aggregate_digest }}
PREFERENCE: ${{ steps.resolve.outputs.preference }}
PRODUCER_SHA: ${{ steps.resolve.outputs.head_sha }}
PRODUCER_RUN_ID: ${{ steps.resolve.outputs.run_id }}
FALLBACK_REASON: ${{ steps.resolve.outputs.fallback_reason }}
MAIN_VALIDATED: ${{ steps.main.outputs.main_validated }}
run: |
set -euo pipefail
write_json_out() {
local key="$1"
local value="$2"
echo "${key}<<EOF" >> "$GITHUB_OUTPUT"
printf '%s\n' "${value}" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
}
if [[ "${BUNDLE_REUSED}" == "true" ]]; then
echo "linux_x64_matrix=[]" >> "$GITHUB_OUTPUT"
echo "build_linux_arm64=false" >> "$GITHUB_OUTPUT"
echo "build_linux_musl=false" >> "$GITHUB_OUTPUT"
echo "host_matrix=[]" >> "$GITHUB_OUTPUT"
echo "cached_linux_x64=[]" >> "$GITHUB_OUTPUT"
echo "cached_linux_arm64=false" >> "$GITHUB_OUTPUT"
echo "cached_host=[]" >> "$GITHUB_OUTPUT"
echo "all_cached=true" >> "$GITHUB_OUTPUT"
else
write_json_out linux_x64_matrix "${LINUX_X64_MATRIX:-[]}"
echo "build_linux_arm64=${BUILD_LINUX_ARM64:-false}" >> "$GITHUB_OUTPUT"
echo "build_linux_musl=${BUILD_LINUX_MUSL:-false}" >> "$GITHUB_OUTPUT"
write_json_out host_matrix "${HOST_MATRIX:-[]}"
write_json_out cached_linux_x64 "${CACHED_LINUX_X64:-[]}"
echo "cached_linux_arm64=${CACHED_LINUX_ARM64:-false}" >> "$GITHUB_OUTPUT"
write_json_out cached_host "${CACHED_HOST:-[]}"
echo "all_cached=${ALL_CACHED_PLAN:-false}" >> "$GITHUB_OUTPUT"
fi
if [[ "${BUNDLE_REUSED}" == "true" ]]; then
all_cached_sum=true
cached_sum='["*bundle*"]'
rebuilt_sum='[]'
else
all_cached_sum="${ALL_CACHED_PLAN:-false}"
cached_sum="${CACHED_TARGETS:-[]}"
rebuilt_sum="${REBUILT_TARGETS:-[]}"
fi
SUMMARY_TITLE="Release native plan" \
AGGREGATE_DIGEST="${AGGREGATE_DIGEST}" \
ALL_CACHED="$all_cached_sum" \
CACHED_TARGETS_JSON="$cached_sum" \
REBUILT_TARGETS_JSON="$rebuilt_sum" \
BUNDLE_REUSED="${BUNDLE_REUSED}" \
PREFERENCE="${PREFERENCE}" \
PRODUCER_SHA="${PRODUCER_SHA}" \
PRODUCER_RUN_ID="${PRODUCER_RUN_ID}" \
FALLBACK_REASON="${FALLBACK_REASON}" \
MAIN_VALIDATED="${MAIN_VALIDATED}" \
bash scripts/ci/write-native-ci-summary.sh
reuse-bundle:
name: Stage reused native-main-bundle
needs: [quality, plan]
if: |
!cancelled() &&
needs.quality.result == 'success' &&
needs.plan.result == 'success' &&
needs.plan.outputs.bundle_reused == 'true'
runs-on: self-hosted
steps:
- name: Prepare workspace
run: |
if [[ -n "${RUNNER_WORKSPACE:-}" && -d "${RUNNER_WORKSPACE}" ]]; then
docker run --rm \
-v "${RUNNER_WORKSPACE}:${RUNNER_WORKSPACE}" \
alpine:3.20 \
chown -R "$(id -u):$(id -g)" "${RUNNER_WORKSPACE}" || true
fi
- uses: actions/checkout@v4
- uses: ./.github/actions/ci-restore-workspace-ownership
# Validation recomputes the current six-target native contract via CI_IMAGE cargo.
- name: Download native-main-bundle from producer run
uses: actions/download-artifact@v4
with:
name: native-main-bundle
path: native-main-bundle
github-token: ${{ github.token }}
run-id: ${{ needs.plan.outputs.bundle_run_id }}
- name: Validate bundle against current contract
shell: bash
env:
NATIVE_TOOL_MODE: declared
CARGO_VIA_CI_IMAGE: '1'
run: |
set -euo pipefail
bash scripts/ci/native-artifact-bundle.sh validate \
--bundle native-main-bundle \
--profile release \
--expect-aggregate "${{ needs.plan.outputs.aggregate_digest }}"
- name: Stage bindings-* for current run
shell: bash
run: |
set -euo pipefail
mkdir -p ci-native-out
python3 scripts/ci/native_build_contract.py stage-bundle \
--bundle native-main-bundle \
--output ci-native-out
- name: Upload staged binding artifacts
uses: actions/upload-artifact@v4
with:
# upload-artifact@v4 does not support multiple names in one step;
# upload each bindings-* directory via a small loop job alternative:
# use a single archive artifact then expand in publish — but publish
# expects bindings-* names. Use matrix via bash + multiple uploads.
name: reused-native-staging
path: ci-native-out/
if-no-files-found: error
# Re-upload each target under the canonical bindings-<triple> name for publish/napi.
- name: Upload per-target bindings artifacts
shell: bash
run: |
set -euo pipefail
# actions/upload-artifact cannot loop natively; use gh api? Prefer
# @actions/upload-artifact via a node script is heavy. Use the
# undocumented pattern: one artifact per dir with a composite approach.
echo "Per-target uploads follow"
- uses: actions/upload-artifact@v4
with:
name: bindings-x86_64-unknown-linux-gnu
path: ci-native-out/bindings-x86_64-unknown-linux-gnu/
if-no-files-found: error
- uses: actions/upload-artifact@v4
with:
name: bindings-x86_64-unknown-linux-musl
path: ci-native-out/bindings-x86_64-unknown-linux-musl/
if-no-files-found: error
- uses: actions/upload-artifact@v4
with:
name: bindings-aarch64-unknown-linux-gnu
path: ci-native-out/bindings-aarch64-unknown-linux-gnu/
if-no-files-found: error
- uses: actions/upload-artifact@v4
with:
name: bindings-x86_64-apple-darwin
path: ci-native-out/bindings-x86_64-apple-darwin/
if-no-files-found: error
- uses: actions/upload-artifact@v4
with:
name: bindings-aarch64-apple-darwin
path: ci-native-out/bindings-aarch64-apple-darwin/
if-no-files-found: error
- uses: actions/upload-artifact@v4
with:
name: bindings-x86_64-pc-windows-msvc
path: ci-native-out/bindings-x86_64-pc-windows-msvc/
if-no-files-found: error
build-linux:
needs: [quality, plan]
if: |
!cancelled() &&
needs.quality.result == 'success' &&
needs.plan.result == 'success' &&
needs.plan.outputs.bundle_reused != 'true' &&
needs.plan.outputs.all_cached != 'true' &&
(
needs.plan.outputs.linux_x64_matrix != '[]' ||
needs.plan.outputs.build_linux_arm64 == 'true' ||
needs.plan.outputs.build_linux_musl == 'true'
)
uses: ./.github/workflows/reusable-build-linux.yml
with:
cache_prefix: v3-release-otel
linux_x64_matrix: ${{ needs.plan.outputs.linux_x64_matrix }}
build_linux_arm64: ${{ needs.plan.outputs.build_linux_arm64 == 'true' }}
build_linux_musl: ${{ needs.plan.outputs.build_linux_musl == 'true' }}
secrets: inherit
build-host:
needs: [quality, plan]
if: |
!cancelled() &&
needs.quality.result == 'success' &&
needs.plan.result == 'success' &&
needs.plan.outputs.bundle_reused != 'true' &&
needs.plan.outputs.all_cached != 'true' &&
needs.plan.outputs.host_matrix != '[]'
uses: ./.github/workflows/reusable-build-host.yml
with:
cache_prefix: v3-release-otel
host_matrix: ${{ needs.plan.outputs.host_matrix }}
secrets: inherit
stage-cached:
needs: [quality, plan]
if: |
!cancelled() &&
needs.quality.result == 'success' &&
needs.plan.result == 'success' &&
needs.plan.outputs.bundle_reused != 'true' &&
(
needs.plan.outputs.cached_linux_x64 != '[]' ||
needs.plan.outputs.cached_linux_arm64 == 'true' ||
needs.plan.outputs.cached_host != '[]'
)
uses: ./.github/workflows/reusable-stage-cached-bindings.yml
with:
cached_linux_x64: ${{ needs.plan.outputs.cached_linux_x64 }}
cached_linux_arm64: ${{ needs.plan.outputs.cached_linux_arm64 == 'true' }}
cached_host: ${{ needs.plan.outputs.cached_host }}
secrets: inherit
test:
needs: [quality, plan, build-linux, build-host, stage-cached, reuse-bundle]
if: |
!cancelled() &&
needs.quality.result == 'success' &&
needs.plan.result == 'success' &&
needs.plan.outputs.main_validated != 'true' &&
(needs.build-linux.result == 'success' || needs.build-linux.result == 'skipped') &&
(needs.build-host.result == 'success' || needs.build-host.result == 'skipped') &&
(needs.stage-cached.result == 'success' || needs.stage-cached.result == 'skipped') &&
(needs.reuse-bundle.result == 'success' || needs.reuse-bundle.result == 'skipped')
uses: ./.github/workflows/reusable-test.yml
with:
test_script: scripts/ci/run-pr-integration.sh
quality_result: ${{ needs.quality.result }}
binding_artifact: bindings-x86_64-unknown-linux-gnu
binding_profile: release
require_binding_artifact: 'true'
needs_native_binding: 'true'
needs_ts_dist: 'true'
secrets: inherit
publish:
name: Publish release
needs: [quality, plan, build-linux, build-host, stage-cached, reuse-bundle, test]
if: |
!cancelled() &&
needs.quality.result == 'success' &&
needs.plan.result == 'success' &&
(needs.test.result == 'success' || needs.test.result == 'skipped') &&
(needs.build-linux.result == 'success' || needs.build-linux.result == 'skipped') &&
(needs.build-host.result == 'success' || needs.build-host.result == 'skipped') &&
(needs.stage-cached.result == 'success' || needs.stage-cached.result == 'skipped') &&
(needs.reuse-bundle.result == 'success' || needs.reuse-bundle.result == 'skipped')
runs-on: self-hosted
container:
image: ghcr.io/${{ github.repository }}/ci-build:latest
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Mark workspace safe for git
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Resolve release version from tag
id: version
shell: bash
run: |
VERSION="${GITHUB_REF_NAME#release/}"
if [ -z "$VERSION" ]; then
echo "Could not parse version from tag: $GITHUB_REF_NAME" >&2
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Publishing release version: $VERSION"
- run: npm ci --ignore-scripts
- name: Download all build artifacts
uses: actions/download-artifact@v4
with:
path: ci-artifacts
pattern: bindings-*
merge-multiple: false
- name: Validate staged binding manifests
shell: bash
env:
NATIVE_TOOL_MODE: declared
run: |
set -euo pipefail
missing=0
while IFS= read -r target; do
dir="ci-artifacts/bindings-${target}"
if [[ ! -d "$dir" ]]; then
echo "Missing current-run artifact dir: $dir" >&2
missing=1
continue
fi
shopt -s nullglob
nodes=("$dir"/*.node)
if [[ ${#nodes[@]} -eq 0 ]]; then
echo "Missing .node in $dir" >&2
missing=1
continue
fi
if [[ -f "$dir/manifest.json" ]]; then
mkdir -p packages/bindings/native-manifests
cp "$dir"/*.node packages/bindings/
cp "$dir/manifest.json" "packages/bindings/native-manifests/${target}.json"
bash scripts/ci/native-artifact-manifest.sh validate \
--manifest "packages/bindings/native-manifests/${target}.json" || missing=1
else
echo "::warning::No manifest.json in $dir — proceeding with .node only"
fi
done < <(bash scripts/ci/list-release-targets.sh)
if [[ "$missing" -ne 0 ]]; then
echo "Publish blocked: incomplete/invalid current-run native artifacts" >&2
exit 1
fi
- name: Stage binding artifacts
shell: bash
working-directory: packages/bindings
run: |
# napi artifacts crawls recursively and matches *.node basenames.
# Copy only .node files — leave provenance manifest.json out of the tree.
mkdir -p artifacts
shopt -s nullglob
for dir in ../../ci-artifacts/bindings-*; do
dest="artifacts/$(basename "$dir")"
mkdir -p "$dest"
cp "$dir"/*.node "$dest/"
done
npx napi artifacts --dir artifacts
- name: Set workspace package versions
shell: bash
run: |
VERSION="${{ steps.version.outputs.version }}"
cd packages/bindings
npm version "$VERSION" --no-git-tag-version --allow-same-version
npx napi version
cd ../..
bash scripts/ci/set-release-deps.sh "$VERSION"
- name: Build TypeScript packages
run: bash scripts/ci/build-ts-workspace.sh
- name: Verify binding packages are staged
shell: bash
working-directory: packages/bindings
run: |
set -euo pipefail
for dir in npm/*/; do
shopt -s nullglob
nodes=("$dir"*.node)
if [[ ${#nodes[@]} -eq 0 ]]; then
echo "Missing .node in $dir (napi artifacts should have copied it)" >&2
exit 1
fi
echo " ok: $(basename "$dir") (${nodes[0]##*/})"
done
- name: Configure npm registry auth
shell: bash
run: |
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > "$HOME/.npmrc"
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish npm packages
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
BINDINGS="packages/bindings"
PUBLISH=(bash scripts/ci/publish-npm-if-needed.sh)
echo "==> Platform binding packages"
for dir in "${BINDINGS}"/npm/*/; do
pkg="$(node -p "require('./${dir}package.json').name")"
shopt -s nullglob
nodes=("$dir"*.node)
if [[ ${#nodes[@]} -eq 0 ]]; then
echo "Missing .node in $dir" >&2
exit 1
fi
"${PUBLISH[@]}" "$dir" "$pkg" "$VERSION"
done
echo "==> @node-webrtc-rust/bindings"
"${PUBLISH[@]}" "$BINDINGS/" "@node-webrtc-rust/bindings" "$VERSION" --ignore-scripts
echo "==> @node-webrtc-rust/signaling"
"${PUBLISH[@]}" "packages/signaling/" "@node-webrtc-rust/signaling" "$VERSION" --ignore-scripts --omit=dev
echo "==> @node-webrtc-rust/sdk"
"${PUBLISH[@]}" "packages/sdk/" "@node-webrtc-rust/sdk" "$VERSION" --ignore-scripts
echo "==> @node-webrtc-rust/helpers"
"${PUBLISH[@]}" "packages/helpers/" "@node-webrtc-rust/helpers" "$VERSION" --ignore-scripts
echo "==> Published @node-webrtc-rust/*@${VERSION} to npm"
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Prepare GitHub release notes from CHANGELOG
id: release_notes
shell: bash
run: |
VERSION="${{ steps.version.outputs.version }}"
BODY_FILE="${RUNNER_TEMP}/release-notes.md"
bash scripts/changelog-release-body.sh "$VERSION" > "$BODY_FILE"
echo "body_file=$BODY_FILE" >> "$GITHUB_OUTPUT"
- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: Release ${{ steps.version.outputs.version }}
body_path: ${{ steps.release_notes.outputs.body_file }}
generate_release_notes: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
sync-main-package-lock:
name: Sync main package-lock (PR)
needs: publish
if: ${{ !cancelled() && needs.publish.result == 'success' }}
runs-on: self-hosted
permissions:
contents: write
pull-requests: write
steps:
- name: Prepare workspace
run: |
if [[ -n "${RUNNER_WORKSPACE:-}" && -d "${RUNNER_WORKSPACE}" ]]; then
docker run --rm \
-v "${RUNNER_WORKSPACE}:${RUNNER_WORKSPACE}" \
alpine:3.20 \
chown -R "$(id -u):$(id -g)" "${RUNNER_WORKSPACE}" || true
fi
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Resolve release version from tag
id: version
shell: bash
run: |
VERSION="${GITHUB_REF_NAME#release/}"
if [ -z "$VERSION" ]; then
echo "Could not parse version from tag: $GITHUB_REF_NAME" >&2
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Syncing main after release $VERSION"
- name: Sync workspace versions and package-lock from npm
run: bash scripts/ci/post-release-sync-main-package-lock.sh "${{ steps.version.outputs.version }}"
- name: Open PR to main
id: open_pr
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.REPO_SYNC_PAT != '' && secrets.REPO_SYNC_PAT || github.token }}
commit-message: 'chore(ci): sync package-lock after release ${{ steps.version.outputs.version }}'
title: 'chore(ci): sync package-lock after release ${{ steps.version.outputs.version }}'
body: |
Automated follow-up to tag `${{ github.ref_name }}` after npm publish.
- Workspace `@node-webrtc-rust/*` versions aligned to `${{ steps.version.outputs.version }}`
- `package-lock.json` optional platform binding entries refreshed from the npm registry
**Merge promptly** so `main` stays green for `npm ci` (see `validate-package-lock` CI job).
Re-run locally if needed:
```bash
bash scripts/ci/post-release-sync-main-package-lock.sh ${{ steps.version.outputs.version }}
```
branch: chore/post-release-package-lock-${{ steps.version.outputs.version }}
base: main
delete-branch: true
labels: dependencies,automation
- name: Manual PR fallback (when GITHUB_TOKEN cannot open PRs)
if: failure() && steps.open_pr.outcome == 'failure'
shell: bash
run: |
BRANCH="chore/post-release-package-lock-${{ steps.version.outputs.version }}"
echo "::warning::Automated PR creation failed (org may block GITHUB_TOKEN from opening PRs)."
echo "Fix: repo Settings → Actions → allow Actions to create PRs, or add secret REPO_SYNC_PAT (classic PAT, repo scope)."
echo "Open manually: https://github.com/${{ github.repository }}/compare/main...${BRANCH}?expand=1"