From eb7af72cb66aa4e955eaa40b19cb3c69fa4ff158 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 20 Jun 2026 02:41:16 +0000 Subject: [PATCH] fix(release): harden staged publish validation --- .github/scripts/download-build-artifacts.sh | 137 ++++++++++++++++-- .github/scripts/require-workflow-success.sh | 45 ++++-- .github/workflows/release.yml | 60 ++++---- tools/policy/check-crate-size.sh | 6 +- tools/policy/check-release-policy.py | 89 +++++++++++- .../check_node_direct_release_assets.py | 0 tools/release/release.py | 114 ++++++++++++--- 7 files changed, 376 insertions(+), 75 deletions(-) mode change 100644 => 100755 tools/release/check_node_direct_release_assets.py diff --git a/.github/scripts/download-build-artifacts.sh b/.github/scripts/download-build-artifacts.sh index 691e5b4b..91109d98 100755 --- a/.github/scripts/download-build-artifacts.sh +++ b/.github/scripts/download-build-artifacts.sh @@ -41,10 +41,99 @@ fi artifact_present() { local run_id="$1" local artifact="$2" - gh api "repos/$GH_REPO/actions/runs/$run_id/artifacts" \ - --paginate \ - --jq '.artifacts[].name' | - grep -Fxq "$artifact" + + local artifact_names + artifact_names="$( + gh api "repos/$GH_REPO/actions/runs/$run_id/artifacts?per_page=100" \ + --paginate \ + --jq '.artifacts[].name' + )" || { + echo "failed to list artifacts for $workflow run $run_id" >&2 + exit 1 + } + printf '%s\n' "$artifact_names" | + grep -Fxq -- "$artifact" +} + +merge_checksum_manifest() { + local existing="$1" + local incoming="$2" + python3 - "$existing" "$incoming" <<'PY' +from __future__ import annotations + +import sys +import tempfile +from pathlib import Path + +existing = Path(sys.argv[1]) +incoming = Path(sys.argv[2]) +entries: dict[str, str] = {} + + +def read_manifest(path: Path) -> None: + with path.open("r", encoding="utf-8") as handle: + for line_number, line in enumerate(handle, 1): + stripped = line.strip() + if not stripped: + continue + parts = stripped.split(None, 1) + if len(parts) != 2: + raise SystemExit(f"{path}: invalid checksum line {line_number}: {line.rstrip()}") + digest, raw_name = parts[0], parts[1].strip() + if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest): + raise SystemExit(f"{path}: invalid checksum digest on line {line_number}: {digest}") + name = raw_name.removeprefix("./") + if not name or "/" in name: + raise SystemExit(f"{path}: invalid checksum asset name on line {line_number}: {raw_name}") + previous = entries.get(name) + if previous is not None and previous != digest: + raise SystemExit( + f"{path}: conflicting checksum for {name}: {previous} vs {digest}" + ) + entries[name] = digest + + +read_manifest(existing) +read_manifest(incoming) +with tempfile.NamedTemporaryFile( + "w", + encoding="utf-8", + newline="\n", + dir=str(existing.parent), + delete=False, +) as handle: + temp_path = Path(handle.name) + for name in sorted(entries): + handle.write(f"{entries[name]} ./{name}\n") +temp_path.replace(existing) +PY +} + +merge_downloaded_artifact() { + local artifact="$1" + local source_dir="$2" + + local source + while IFS= read -r source; do + [[ -n "$source" ]] || continue + local relative_path="${source#"$source_dir"/}" + local target="$destination/$relative_path" + mkdir -p "$(dirname "$target")" + if [[ -e "$target" ]]; then + if [[ -f "$target" ]] && cmp -s "$source" "$target"; then + continue + fi + if [[ -f "$target" && -f "$source" && "$(basename "$target")" == *-release-assets.sha256 ]]; then + if ! merge_checksum_manifest "$target" "$source"; then + return 1 + fi + continue + fi + echo "artifact $artifact would overwrite $relative_path with different bytes" >&2 + return 1 + fi + cp -p "$source" "$target" + done < <(find "$source_dir" -type f -print | sort) } required_job_success() { @@ -53,15 +142,27 @@ required_job_success() { return 0 fi + local jobs_file + jobs_file="$(mktemp)" + if ! gh run view "$run_id" --repo "$GH_REPO" --json jobs > "$jobs_file"; then + rm -f "$jobs_file" + return 1 + fi + local conclusion - conclusion="$( - GH_RUN_JSON="$(gh run view "$run_id" --json jobs)" REQUIRED_JOB="$required_job" bun -e ' -const data = JSON.parse(process.env.GH_RUN_JSON ?? "{}"); -const required = process.env.REQUIRED_JOB ?? ""; + if ! conclusion="$( + bun -e ' +const fs = require("node:fs"); +const data = JSON.parse(fs.readFileSync(Bun.argv[1], "utf8")); +const required = Bun.argv[2] ?? ""; const job = (data.jobs ?? []).find((candidate) => candidate?.name === required); console.log(job?.conclusion ?? ""); -' - )" || return 1 +' "$jobs_file" "$required_job" + )"; then + rm -f "$jobs_file" + return 1 + fi + rm -f "$jobs_file" [[ "$conclusion" == "success" ]] } @@ -97,6 +198,7 @@ else done < <( if [[ -n "$required_job" ]]; then gh run list \ + --repo "$GH_REPO" \ --workflow "$workflow" \ --commit "$sha" \ --limit 20 \ @@ -104,6 +206,7 @@ else --jq '.[].databaseId' else gh run list \ + --repo "$GH_REPO" \ --workflow "$workflow" \ --commit "$sha" \ --limit 20 \ @@ -121,7 +224,17 @@ fi mkdir -p "$destination" for artifact in "${artifacts[@]}"; do echo "Downloading $workflow artifact $artifact from run $run_id" - gh run download "$run_id" \ + artifact_dir="$(mktemp -d)" + if ! gh run download "$run_id" \ + --repo "$GH_REPO" \ --name "$artifact" \ - --dir "$destination" + --dir "$artifact_dir"; then + rm -rf "$artifact_dir" + exit 1 + fi + if ! merge_downloaded_artifact "$artifact" "$artifact_dir"; then + rm -rf "$artifact_dir" + exit 1 + fi + rm -rf "$artifact_dir" done diff --git a/.github/scripts/require-workflow-success.sh b/.github/scripts/require-workflow-success.sh index 1b886772..a21232ba 100644 --- a/.github/scripts/require-workflow-success.sh +++ b/.github/scripts/require-workflow-success.sh @@ -46,35 +46,55 @@ emit_run_id() { } required_artifacts_present() { - run_id="$1" + local run_id="$1" if [[ "${#required_artifacts[@]}" -eq 0 ]]; then return 0 fi - artifacts="$(gh api "repos/$GH_REPO/actions/runs/$run_id/artifacts" \ - --paginate \ - --jq '.artifacts[].name')" || return 1 + local artifacts + artifacts="$( + gh api "repos/$GH_REPO/actions/runs/$run_id/artifacts?per_page=100" \ + --paginate \ + --jq '.artifacts[].name' + )" || { + echo "failed to list artifacts for $workflow run $run_id" >&2 + return 1 + } + local expected for expected in "${required_artifacts[@]}"; do - if ! printf '%s\n' "$artifacts" | grep -Fxq "$expected"; then + if ! printf '%s\n' "$artifacts" | grep -Fxq -- "$expected"; then return 1 fi done } required_job_success() { - run_id="$1" + local run_id="$1" if [[ -z "$required_job" ]]; then return 0 fi - conclusion="$( - GH_RUN_JSON="$(gh run view "$run_id" --json jobs)" REQUIRED_JOB="$required_job" bun -e ' -const data = JSON.parse(process.env.GH_RUN_JSON ?? "{}"); -const required = process.env.REQUIRED_JOB ?? ""; + local jobs_file + jobs_file="$(mktemp)" + if ! gh run view "$run_id" --repo "$GH_REPO" --json jobs > "$jobs_file"; then + rm -f "$jobs_file" + return 1 + fi + + local conclusion + if ! conclusion="$( + bun -e ' +const fs = require("node:fs"); +const data = JSON.parse(fs.readFileSync(Bun.argv[1], "utf8")); +const required = Bun.argv[2] ?? ""; const job = (data.jobs ?? []).find((candidate) => candidate?.name === required); console.log(job?.conclusion ?? ""); -' - )" || return 1 +' "$jobs_file" "$required_job" + )"; then + rm -f "$jobs_file" + return 1 + fi + rm -f "$jobs_file" [[ "$conclusion" == "success" ]] } @@ -90,6 +110,7 @@ fi deadline=$((SECONDS + timeout)) while true; do runs="$(gh run list \ + --repo "$GH_REPO" \ --workflow "$workflow" \ --commit "$sha" \ --limit 10 \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99f112b3..93a73e3c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -280,34 +280,6 @@ jobs: NODE pnpm --version - - name: Create release-please target branch - if: ${{ inputs.operation == 'publish' && steps.release_head.outputs.uses_temporary_target_branch == 'true' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh auth setup-git - git push origin "${RELEASE_HEAD_SHA}:refs/heads/${{ steps.release_head.outputs.target_branch }}" --force - - - name: Create release-please GitHub releases - id: release_please - if: ${{ inputs.operation == 'publish' }} - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 - with: - token: ${{ secrets.GITHUB_TOKEN }} - target-branch: ${{ steps.release_head.outputs.target_branch }} - config-file: release-please-config.json - manifest-file: .release-please-manifest.json - skip-github-pull-request: true - - - name: Remove release-please target branch - if: ${{ always() && inputs.operation == 'publish' && steps.release_head.outputs.uses_temporary_target_branch == 'true' }} - continue-on-error: true - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh auth setup-git - git push origin ":refs/heads/${{ steps.release_head.outputs.target_branch }}" - - name: Plan product releases id: release_plan run: | @@ -477,6 +449,10 @@ jobs: with: bun-version: ${{ env.BUN_VERSION }} + - name: Install TypeScript release tooling + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_oliphaunt_js == 'true' }} + run: pnpm install --frozen-lockfile + - name: Download native helper release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && (steps.release_plan.outputs.product_oliphaunt_broker == 'true' || steps.release_plan.outputs.product_oliphaunt_node_direct == 'true') }} env: @@ -537,6 +513,34 @@ jobs: PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} run: tools/release/release.py publish-dry-run --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" + - name: Create release-please target branch + if: ${{ inputs.operation == 'publish' && steps.release_plan.outputs.has_release_changes == 'true' && steps.release_head.outputs.uses_temporary_target_branch == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh auth setup-git + git push origin "${RELEASE_HEAD_SHA}:refs/heads/${{ steps.release_head.outputs.target_branch }}" --force + + - name: Create release-please GitHub releases + id: release_please + if: ${{ inputs.operation == 'publish' && steps.release_plan.outputs.has_release_changes == 'true' }} + uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 + with: + token: ${{ secrets.GITHUB_TOKEN }} + target-branch: ${{ steps.release_head.outputs.target_branch }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + skip-github-pull-request: true + + - name: Remove release-please target branch + if: ${{ always() && inputs.operation == 'publish' && steps.release_head.outputs.uses_temporary_target_branch == 'true' }} + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh auth setup-git + git push origin ":refs/heads/${{ steps.release_head.outputs.target_branch }}" + - name: Publish liboliphaunt GitHub release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_native == 'true' }} env: diff --git a/tools/policy/check-crate-size.sh b/tools/policy/check-crate-size.sh index b0e1b051..82180ad6 100755 --- a/tools/policy/check-crate-size.sh +++ b/tools/policy/check-crate-size.sh @@ -22,7 +22,11 @@ for crate_file in $crate_files; do continue fi - message="crate size warning: $crate_file is ${size_mib}MiB > ${limit_mib}MiB" + label="warning" + if [ "$mode" = "--enforce" ]; then + label="error" + fi + message="crate size $label: $crate_file is ${size_mib}MiB > ${limit_mib}MiB" echo "$message" >&2 failed=1 done diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index 57be891c..02480bf9 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -43,6 +43,27 @@ def read_text(path: str) -> str: return (ROOT / path).read_text(encoding="utf-8") +def assert_direct_release_python_tools_are_executable(release_script: str) -> None: + direct_invocations = sorted( + set( + match.group(1) + for match in re.finditer( + r'\[\s*"(tools/release/[^"]+\.py)"', + release_script, + flags=re.MULTILINE, + ) + ) + ) + for tool in direct_invocations: + path = ROOT / tool + if not path.is_file(): + fail(f"directly invoked release tool does not exist: {tool}") + if path.stat().st_mode & 0o111 == 0: + fail( + f"directly invoked release tool must be executable or called through python3: {tool}" + ) + + def read_toml(path: pathlib.Path) -> dict: with path.open("rb") as handle: return tomllib.load(handle) @@ -664,7 +685,6 @@ def check_release_workflow_policy() -> None: publish_block, [ "Resolve release commit", - "Create release-please GitHub releases", "Plan product releases", "Require release-commit CI build gate", "Download WASIX runtime build artifacts", @@ -672,11 +692,16 @@ def check_release_workflow_policy() -> None: "Download exact-extension package artifacts", "Download SDK package artifacts", "Download liboliphaunt release assets", + "Install TypeScript release tooling", "Download native helper release assets", "Download Node direct optional npm packages", "Validate selected release product dry-runs", + "Create release-please target branch", + "Create release-please GitHub releases", + "Remove release-please target branch", + "Publish liboliphaunt GitHub release assets", ], - "Release dry-run must validate release-commit builder outputs before product dry-runs", + "Release publish must validate release-commit builder outputs before creating release tags", ) for snippet in ( @@ -697,6 +722,7 @@ def check_release_workflow_policy() -> None: "download_sdk_artifact oliphaunt-js oliphaunt-js-sdk-package-artifacts", "download_sdk_artifact oliphaunt-wasix-rust oliphaunt-wasix-rust-package-artifacts", "--artifact oliphaunt-node-direct-npm-package-macos-arm64", + "pnpm install --frozen-lockfile", "target/oliphaunt-broker/release-assets", "target/oliphaunt-node-direct/release-assets", "tools/release/release.py publish-dry-run --products-json", @@ -729,14 +755,71 @@ def check_release_workflow_policy() -> None: "selected_run_id", 'required_job_success "$run_id"', 'artifact_present "$run_id" "$artifact"', + 'actions/runs/$run_id/artifacts?per_page=100', + 'gh run view "$run_id" --repo "$GH_REPO" --json jobs > "$jobs_file"', + "Bun.argv", + "merge_downloaded_artifact", + "merge_checksum_manifest", + "*-release-assets.sha256", + "would overwrite", ): if snippet not in build_artifact_script: fail(f"shared CI artifact downloader must support and verify pinned run ids: missing {snippet!r}") + if "GH_RUN_JSON=" in build_artifact_script: + fail("shared CI artifact downloader must not pass full workflow job JSON through the environment") require_workflow_script = read_text(".github/scripts/require-workflow-success.sh") - for snippet in ("--run-id", "GITHUB_OUTPUT", "run_id=", 'emit_run_id "$run_id"'): + for snippet in ( + "--run-id", + "GITHUB_OUTPUT", + "run_id=", + 'emit_run_id "$run_id"', + 'actions/runs/$run_id/artifacts?per_page=100', + 'gh run view "$run_id" --repo "$GH_REPO" --json jobs > "$jobs_file"', + "Bun.argv", + ): if snippet not in require_workflow_script: fail(f"CI build gate must emit and validate selected run ids: missing {snippet!r}") + if "GH_RUN_JSON=" in require_workflow_script: + fail("CI build gate must not pass full workflow job JSON through the environment") + + release_script = read_text("tools/release/release.py") + assert_direct_release_python_tools_are_executable(release_script) + if 'xtask(["assets", "check", "--strict-generated"])' in release_script: + fail( + "release-staged WASIX validation must not require transient generated source checkouts; " + "CI runtime artifact download already verifies generated asset hashes and extension surface" + ) + if 'xtask(["assets", "check"])' not in release_script: + fail("release-staged WASIX validation must still run the shared asset integrity checks") + if '"assets", "aot-targets"' in release_script: + fail("release-staged WASIX validation must check AOT artifacts by raw CI target triples, not release target ids") + if "for target in wasm_aot_target_triples():" not in release_script: + fail("release-staged WASIX validation must reuse the CI matrix raw AOT target triples") + for snippet in ( + "WASIX_ASSETS_CRATE_PAYLOAD_DIR", + "WASIX_AOT_CRATES_DIR", + "materialized_wasix_runtime_crate_payloads", + "clean_wasix_runtime_crate_payloads", + "target/oliphaunt-wasix/assets", + "target/oliphaunt-wasix/aot", + "wasix_runtime_internal_packages", + 'package_check.extend(["--package", package])', + "tools/release/check_wasm_crate_payloads.py", + "cargo_package_args(True)", + "cargo_publish_args(True)", + "finally:", + ): + if snippet not in release_script: + fail(f"release-staged WASIX crates must materialize and clean generated payloads: missing {snippet!r}") + for snippet in ( + '["pnpm", "exec", "jsr", "publish", "--dry-run"]', + 'command.append("--allow-dirty")', + 'run(command, cwd=jsr_source)', + '"--product",\n "oliphaunt-node-direct",\n "--require-published"', + ): + if snippet not in release_script: + fail(f"release dry-runs and package publishes must cover registry-native checks: missing {snippet!r}") release_head_script = read_text(".github/scripts/resolve-release-head.sh") for snippet in ( diff --git a/tools/release/check_node_direct_release_assets.py b/tools/release/check_node_direct_release_assets.py old mode 100644 new mode 100755 diff --git a/tools/release/release.py b/tools/release/release.py index e70daaa8..28e48224 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -12,8 +12,9 @@ import sys import tarfile import time +from contextlib import contextmanager from pathlib import Path -from typing import NoReturn +from typing import Iterator, NoReturn import artifact_targets import check_cratesio_publication @@ -30,6 +31,10 @@ "@oliphaunt/node-direct-linux-arm64-gnu": ROOT / "src/runtimes/node-direct/packages/linux-arm64-gnu", "@oliphaunt/node-direct-win32-x64-msvc": ROOT / "src/runtimes/node-direct/packages/win32-x64-msvc", } +WASIX_ASSETS_CRATE_PAYLOAD_DIR = ( + ROOT / "src/runtimes/liboliphaunt/wasix/crates/assets/payload" +) +WASIX_AOT_CRATES_DIR = ROOT / "src/runtimes/liboliphaunt/wasix/crates/aot" def fail(message: str) -> NoReturn: @@ -338,6 +343,13 @@ def wasm_aot_target_triples() -> list[str]: return targets +def wasix_runtime_internal_packages() -> list[str]: + packages = output( + ["cargo", "run", "--quiet", "-p", "xtask", "--", "assets", "internal-packages"] + ) + return [line.strip() for line in packages.splitlines() if line.strip()] + + def require_release_portable_assets() -> None: manifest = ROOT / "target" / "oliphaunt-wasix" / "assets" / "manifest.json" if not manifest.is_file(): @@ -351,6 +363,35 @@ def require_release_aot_artifacts() -> None: fail(f"missing release AOT artifacts for {target}") +def copy_clean_tree(source: Path, destination: Path) -> None: + if not source.is_dir(): + fail(f"release payload source directory does not exist: {source.relative_to(ROOT)}") + if destination.exists(): + shutil.rmtree(destination) + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(source, destination) + + +def clean_wasix_runtime_crate_payloads() -> None: + shutil.rmtree(WASIX_ASSETS_CRATE_PAYLOAD_DIR, ignore_errors=True) + for target in wasm_aot_target_triples(): + shutil.rmtree(WASIX_AOT_CRATES_DIR / target / "artifacts", ignore_errors=True) + + +@contextmanager +def materialized_wasix_runtime_crate_payloads() -> Iterator[None]: + copy_clean_tree(ROOT / "target/oliphaunt-wasix/assets", WASIX_ASSETS_CRATE_PAYLOAD_DIR) + for target in wasm_aot_target_triples(): + copy_clean_tree( + ROOT / "target/oliphaunt-wasix/aot" / target, + WASIX_AOT_CRATES_DIR / target / "artifacts", + ) + try: + yield + finally: + clean_wasix_runtime_crate_payloads() + + def passthrough_value(args: list[str], name: str) -> str | None: index = 0 while index < len(args): @@ -516,22 +557,35 @@ def cargo_publish_package(package: str, version: str, *, allow_dirty: bool = Fal wait_for_cratesio_package(package, version) -def validate_wasix_runtime_inputs(allow_dirty: bool) -> None: +def validate_wasix_runtime_inputs() -> None: require_release_portable_assets() require_release_aot_artifacts() - xtask(["assets", "check", "--strict-generated"]) - targets = output(["cargo", "run", "--quiet", "-p", "xtask", "--", "assets", "aot-targets"]) - for target in [line.strip() for line in targets.splitlines() if line.strip()]: + xtask(["assets", "check"]) + for target in wasm_aot_target_triples(): xtask(["assets", "check-aot", "--target-triple", target]) - run(["tools/policy/check-crate-package.sh", *cargo_package_args(allow_dirty)]) - run(["tools/release/check_wasm_crate_payloads.py", *cargo_package_args(allow_dirty)]) def run_wasix_runtime_staged_dry_run(allow_dirty: bool) -> None: - validate_wasix_runtime_inputs(allow_dirty) - packages = output(["cargo", "run", "--quiet", "-p", "xtask", "--", "assets", "internal-packages"]) - for package in [line.strip() for line in packages.splitlines() if line.strip()]: - run(["cargo", "publish", "-p", package, "--dry-run", "--locked", *cargo_publish_args(allow_dirty)]) + validate_wasix_runtime_inputs() + with materialized_wasix_runtime_crate_payloads(): + packages = wasix_runtime_internal_packages() + package_check = ["tools/policy/check-crate-package.sh", *cargo_package_args(True)] + for package in packages: + package_check.extend(["--package", package]) + run(package_check) + run(["tools/release/check_wasm_crate_payloads.py", *cargo_package_args(True)]) + for package in packages: + run( + [ + "cargo", + "publish", + "-p", + package, + "--dry-run", + "--locked", + *cargo_publish_args(True), + ] + ) def run_wasix_runtime_release_dry_run(allow_dirty: bool) -> None: @@ -550,11 +604,17 @@ def run_wasm_release_dry_run(allow_dirty: bool) -> None: def publish_wasix_runtime_staged_crates() -> None: - validate_wasix_runtime_inputs(allow_dirty=True) - version = current_product_version("liboliphaunt-wasix") - packages = output(["cargo", "run", "--quiet", "-p", "xtask", "--", "assets", "internal-packages"]) - for package in [line.strip() for line in packages.splitlines() if line.strip()]: - cargo_publish_package(package, version, allow_dirty=True) + validate_wasix_runtime_inputs() + with materialized_wasix_runtime_crate_payloads(): + packages = wasix_runtime_internal_packages() + package_check = ["tools/policy/check-crate-package.sh", *cargo_package_args(True)] + for package in packages: + package_check.extend(["--package", package]) + run(package_check) + run(["tools/release/check_wasm_crate_payloads.py", *cargo_package_args(True)]) + version = current_product_version("liboliphaunt-wasix") + for package in packages: + cargo_publish_package(package, version, allow_dirty=True) def publish_wasm_staged_crates() -> None: @@ -988,10 +1048,14 @@ def run_react_native_sdk_dry_run() -> None: require_staged_sdk_artifact("oliphaunt-react-native", "npm package", (".tgz",)) -def run_typescript_sdk_dry_run() -> None: +def run_typescript_sdk_dry_run(allow_dirty: bool) -> None: validate_staged_sdk_package("oliphaunt-js") require_staged_sdk_artifact("oliphaunt-js", "npm package", (".tgz",)) - staged_jsr_source_dir("oliphaunt-js") + jsr_source = staged_jsr_source_dir("oliphaunt-js") + command = ["pnpm", "exec", "jsr", "publish", "--dry-run"] + if allow_dirty: + command.append("--allow-dirty") + run(command, cwd=jsr_source) def run_node_direct_dry_run() -> None: @@ -1019,7 +1083,7 @@ def run_product_publish_dry_runs(products: list[str], *, allow_dirty: bool, head elif product == "oliphaunt-react-native": run_react_native_sdk_dry_run() elif product == "oliphaunt-js": - run_typescript_sdk_dry_run() + run_typescript_sdk_dry_run(allow_dirty) elif product == "oliphaunt-wasix-rust": if published_rerun("oliphaunt-wasix-rust", head_ref): print("oliphaunt-wasix is already published at this commit; skipping WASM publish dry-run.") @@ -1350,6 +1414,18 @@ def publish_node_direct_npm_optional_packages(head_ref: str) -> None: print(f"{package_name} {version} is already published on npm; skipping npm publish.") continue run(["npm", "publish", str(tarball), "--access", "public", "--provenance"]) + run( + [ + "tools/release/check_registry_publication.py", + "--product", + "oliphaunt-node-direct", + "--require-published", + "--retries", + "12", + "--retry-delay", + "10", + ] + ) def publish_typescript_npm_jsr(head_ref: str) -> None: