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
12 changes: 12 additions & 0 deletions .github/workflows/release-candidate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,10 @@ jobs:
LICENSE README.md client.js client.d.ts index.js index.d.ts package.json; do
cp "${client_dir}/${source}" "${root_stage}/${source}"
done
python3 release/scripts/client_registry.py bind-optional-deps \
--package-json "${root_stage}/package.json" \
--version "${CLIENT_VERSION}" \
--client "${client}"
(cd "${root_stage}" && npm pack \
--ignore-scripts --pack-destination "${smoke}")
root_package="${smoke}/registrystack-${client}-client-${CLIENT_VERSION}.tgz"
Expand Down Expand Up @@ -1066,6 +1070,14 @@ jobs:
node_client="${GITHUB_WORKSPACE}/crates/registry-${client}-client-node"
(cd "${node_client}" && npm ci --ignore-scripts)
test -z "$(find "${node_client}" -maxdepth 1 -name '*.node' -print -quit)"
# The tree binds no platform versions, which is what keeps the
# `npm ci` above satisfiable once this release publishes. Bind
# them here, after the install and before the pack, so the
# published root package still resolves its native binary.
python3 release/scripts/client_registry.py bind-optional-deps \
--package-json "${node_client}/package.json" \
--version "${version}" \
--client "${client}"
Comment thread
jeremi marked this conversation as resolved.
(cd "${node_client}" && npm pack \
--ignore-scripts --pack-destination "${RUNNER_TEMP}")
root_package="registrystack-${client}-client-${version}.tgz"
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/release-rehearsal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ jobs:
LICENSE README.md client.js client.d.ts index.js index.d.ts package.json; do
cp "${client_dir}/${source}" "${root_stage}/${source}"
done
python3 release/scripts/client_registry.py bind-optional-deps \
--package-json "${root_stage}/package.json" \
--version "${CLIENT_VERSION}" \
--client "${client}"
(cd "${root_stage}" && npm pack \
--ignore-scripts --pack-destination "${smoke}")
root_package="${smoke}/registrystack-${client}-client-${CLIENT_VERSION}.tgz"
Expand Down
59 changes: 0 additions & 59 deletions crates/registry-discovery-client-node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions crates/registry-discovery-client-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,5 @@
"@napi-rs/cli": "3.8.2",
"@types/node": "22.20.1",
"typescript": "6.0.3"
},
"optionalDependencies": {
"@registrystack/discovery-client-darwin-arm64": "0.23.0",
"@registrystack/discovery-client-linux-arm64-gnu": "0.23.0",
"@registrystack/discovery-client-linux-x64-gnu": "0.23.0"
}
}
59 changes: 0 additions & 59 deletions crates/registry-evidence-client-node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions crates/registry-evidence-client-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,5 @@
},
"devDependencies": {
"@napi-rs/cli": "3.8.2"
},
"optionalDependencies": {
"@registrystack/evidence-client-darwin-arm64": "0.23.0",
"@registrystack/evidence-client-linux-arm64-gnu": "0.23.0",
"@registrystack/evidence-client-linux-x64-gnu": "0.23.0"
}
}
59 changes: 0 additions & 59 deletions crates/registry-relay-client-node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions crates/registry-relay-client-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,5 @@
"@napi-rs/cli": "3.8.2",
"@types/node": "22.20.1",
"typescript": "6.0.3"
},
"optionalDependencies": {
"@registrystack/relay-client-darwin-arm64": "0.23.0",
"@registrystack/relay-client-linux-arm64-gnu": "0.23.0",
"@registrystack/relay-client-linux-x64-gnu": "0.23.0"
}
}
9 changes: 9 additions & 0 deletions release/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ checks pass. The merge commit is the intended candidate source. The exact
protected-main revision accepted by `request-candidate` becomes the candidate
source and future tag target. There is no finalization or closeout PR.

The Node client manifests and their lockfiles deliberately bind no platform
package versions. Those versions name the release being prepared, which is
unpublished for as long as the PR is open, so a tree that carries them records
placeholder lock entries and leaves `npm ci` unsatisfiable on protected `main`
from the moment the release publishes. The candidate binds them into the root
manifest it packs, and `client_registry.py validate-dist` proves the published
root package carries the exact set. The planner rejects a prepared tree that
binds them.

Before opening the release PR, push the prepared branch and run the read-only
Ubuntu rehearsal from that branch:

Expand Down
54 changes: 50 additions & 4 deletions release/scripts/client_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,48 @@ def npm_platforms(client: str) -> tuple[tuple[str, str], ...]:
)


def expected_optional_dependencies(client: str, version: str) -> dict[str, str]:
Comment thread
jeremi marked this conversation as resolved.
definition = client_definition(client)
return {
f"{definition.npm_root_package}-{platform}": version
for platform, _binary in npm_platforms(client)
}


def bind_optional_dependencies(package_json: Path, version: str, client: str) -> None:
"""Bind a root manifest to its exact platform packages before it is packed.

The checked-in manifest cannot carry these versions. At preparation time
they name a release that is not published yet, so npm resolves them to
placeholder lock entries and `npm ci` stops being satisfiable on the
default branch from the moment that release publishes. The published root
package must still bind them exactly, so the binding happens here, against
the manifest that is about to be packed, and validate_npm_packages proves
it landed by reading the packed tarball back.
"""
definition = client_definition(client)
try:
metadata = json.loads(package_json.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ClientRegistryError(
f"cannot read root manifest {package_json}: {exc}"
) from exc
if not isinstance(metadata, dict):
raise ClientRegistryError(f"root manifest {package_json} is malformed")
if (
metadata.get("name") != definition.npm_root_package
or metadata.get("version") != version
):
raise ClientRegistryError(
f"root manifest {package_json} must identify "
f"{definition.npm_root_package} at version {version}"
)
metadata["optionalDependencies"] = expected_optional_dependencies(client, version)
package_json.write_text(
json.dumps(metadata, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
)


def npm_tarballs(directory: Path, version: str, client: str) -> list[Path]:
definition = client_definition(client)
return [
Expand Down Expand Up @@ -169,10 +211,7 @@ def npm_package_metadata(path: Path) -> tuple[dict[str, Any], set[str]]:

def validate_npm_packages(directory: Path, version: str, client: str) -> list[Path]:
definition = client_definition(client)
expected_optional = {
f"{definition.npm_root_package}-{platform}": version
for platform, _binary in npm_platforms(client)
}
expected_optional = expected_optional_dependencies(client, version)
paths = npm_tarballs(directory, version, client)
for path in paths:
metadata, names = npm_package_metadata(path)
Expand Down Expand Up @@ -375,6 +414,10 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
validate.add_argument("--directory", type=Path, required=True)
validate.add_argument("--version", required=True)
validate.add_argument("--client", choices=sorted(CLIENTS), required=True)
bind = subparsers.add_parser("bind-optional-deps")
bind.add_argument("--package-json", type=Path, required=True)
bind.add_argument("--version", required=True)
bind.add_argument("--client", choices=sorted(CLIENTS), required=True)
npm = subparsers.add_parser("npm-state")
npm.add_argument("--tarball", type=Path, required=True)
pypi = subparsers.add_parser("pypi-state")
Expand All @@ -390,6 +433,9 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "validate-dist":
validate_distribution(args.directory, args.version, args.client)
print("validated")
elif args.command == "bind-optional-deps":
bind_optional_dependencies(args.package_json, args.version, args.client)
print("bound")
elif args.command == "npm-state":
print(npm_registry_state(args.tarball, npm_metadata(args.tarball)))
elif args.command == "pypi-state":
Expand Down
Loading