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
20 changes: 0 additions & 20 deletions .github/scripts/release/package-node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const bindingsDirectory = path.resolve("bindings/js");
const nativePackagesDirectory = path.join(bindingsDirectory, "npm");
const outputDirectory = path.resolve("package-artifacts/npm");
const packagePath = path.join(bindingsDirectory, "package.json");
const readmePath = path.join(bindingsDirectory, "README.md");

const nativePackages = fs
.readdirSync(nativePackagesDirectory, { withFileTypes: true })
Expand Down Expand Up @@ -38,7 +37,6 @@ for (const nativePackage of nativePackages) {
}

const originalPackage = fs.readFileSync(packagePath, "utf8");
const originalReadme = fs.readFileSync(readmePath, "utf8");
const rootPackage = JSON.parse(originalPackage);
rootPackage.optionalDependencies = {};

Expand All @@ -61,24 +59,6 @@ try {
"--pack-destination",
outputDirectory,
]);

rootPackage.name = "shell-use";
fs.writeFileSync(
packagePath,
`${JSON.stringify(rootPackage, null, 2)}\n`,
);
fs.writeFileSync(
readmePath,
originalReadme.replaceAll("@microsoft/shell-use", "shell-use"),
);
runNpm([
"pack",
bindingsDirectory,
"--ignore-scripts",
"--pack-destination",
outputDirectory,
]);
} finally {
fs.writeFileSync(packagePath, originalPackage);
fs.writeFileSync(readmePath, originalReadme);
}
22 changes: 14 additions & 8 deletions .github/scripts/release/publish-npm.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ const packages = listPackageTarballs("npm-packages").map((tarball) => ({
manifest: readPackageManifest(tarball),
}));

if (packages.length < 10) {
throw new Error("Expected eight native packages and two root packages");
}

function publishIfMissing({ tarball, manifest }) {
const packageVersion = `${manifest.name}@${manifest.version}`;
const result = spawnNpm(["view", packageVersion, "version"], {
Expand Down Expand Up @@ -47,11 +43,21 @@ function publishIfMissing({ tarball, manifest }) {
const nativePackages = packages.filter(({ manifest }) =>
manifest.name.startsWith("@microsoft/shell-use-"),
);
const rootPackages = packages.filter(
({ manifest }) =>
manifest.name === "@microsoft/shell-use" || manifest.name === "shell-use",
const rootPackage = packages.filter(
({ manifest }) => manifest.name === "@microsoft/shell-use",
);

for (const packageArtifact of [...nativePackages, ...rootPackages]) {
if (
nativePackages.length !== 8 ||
rootPackage.length !== 1
) {
throw new Error(
`Expected eight native packages and @microsoft/shell-use; found ${packages
.map(({ manifest }) => manifest.name)
.join(", ")}`,
);
}

for (const packageArtifact of [...nativePackages, ...rootPackage]) {
publishIfMissing(packageArtifact);
}
32 changes: 26 additions & 6 deletions .github/scripts/release/smoke-node.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";

Expand All @@ -19,30 +20,49 @@ function findPackage(name) {
if (matches.length !== 1) {
throw new Error(`Expected one ${name} package, found ${matches.length}`);
}
return matches[0].tarball;
return matches[0];
}

const rootPackage = findPackage("@microsoft/shell-use");
const platformPackage = findPackage("@microsoft/shell-use-linux-x64-gnu");
const smokeDirectory = path.resolve("smoke");

fs.mkdirSync(smokeDirectory);
for (const { manifest } of [rootPackage, platformPackage]) {
if (manifest.bin !== undefined) {
throw new Error(`${manifest.name} unexpectedly declares a CLI executable`);
}
}

fs.mkdirSync(smokeDirectory, { recursive: true });
runNpm(["init", "-y"], {
cwd: smokeDirectory,
stdio: ["ignore", "ignore", "inherit"],
});
runNpm(["install", "--ignore-scripts", platformPackage], {
runNpm(["install", "--ignore-scripts", platformPackage.tarball], {
cwd: smokeDirectory,
});
runNpm(
["install", "--ignore-scripts", "--omit=optional", rootPackage],
[
"install",
"--ignore-scripts",
"--omit=optional",
rootPackage.tarball,
],
{ cwd: smokeDirectory },
);

process.env.SHELL_USE_BIN = path.join(smokeDirectory, "missing-shell-use");
if (process.platform !== "win32") {
process.env.PATH = "/usr/bin:/bin";
}
const cliProbe = spawnSync("shell-use", ["--version"], { stdio: "ignore" });
if (!cliProbe.error || cliProbe.error.code !== "ENOENT") {
throw new Error("shell-use CLI unexpectedly available in smoke PATH");
}

const requireFromSmoke = createRequire(path.join(smokeDirectory, "package.json"));
const packageEntry = requireFromSmoke.resolve("@microsoft/shell-use");
const packageEntry = requireFromSmoke.resolve(rootPackage.manifest.name);
const { ShellUse } = await import(pathToFileURL(packageEntry).href);

const session = ShellUse.ephemeral("release-smoke");
try {
await session.open();
Expand Down
29 changes: 26 additions & 3 deletions .github/scripts/release/smoke-python.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import os
import shutil
import subprocess
import sys
import venv
Expand All @@ -22,8 +23,11 @@ def main():
return

wheels = sorted(Path("dist").glob("*.whl"))
if not wheels:
raise RuntimeError("No Python wheels found in dist")
if len(wheels) != 1:
raise RuntimeError(f"Expected one Python wheel in dist, found {len(wheels)}")
wheel = wheels[0]
if "abi3" not in wheel.name:
raise RuntimeError(f"Expected an abi3 wheel, found {wheel.name}")

smoke_directory = Path("smoke")
venv.create(smoke_directory, with_pip=True)
Expand All @@ -38,13 +42,32 @@ def main():
"pip",
"install",
"--disable-pip-version-check",
*wheels,
wheel,
],
check=True,
)

runtime_env = os.environ.copy()
runtime_env["SHELL_USE_BIN"] = str(
(smoke_directory / "missing-shell-use").resolve()
)
if os.name == "nt":
runtime_path = [str(python.parent)]
system_root = runtime_env.get("SystemRoot")
if system_root:
runtime_path.extend(
[str(Path(system_root) / "System32"), str(Path(system_root))]
)
else:
runtime_path = [str(python.parent), "/usr/bin", "/bin"]
runtime_env["PATH"] = os.pathsep.join(runtime_path)
if shutil.which("shell-use", path=runtime_env["PATH"]) is not None:
raise RuntimeError("shell-use CLI unexpectedly available in smoke PATH")

subprocess.run(
[python, Path(__file__).resolve(), "--run-smoke"],
check=True,
env=runtime_env,
)


Expand Down
68 changes: 58 additions & 10 deletions .github/scripts/release/verify-versions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,42 @@ if (!releaseTag) {
}

const expected = releaseTag.replace(/^v/, "");
const jsPackage = JSON.parse(
fs.readFileSync("bindings/js/package.json", "utf8"),
);

function read(file) {
return fs.readFileSync(file, "utf8");
}

function matchedVersion(file, pattern) {
const version = read(file).match(pattern)?.[1];
if (!version) {
throw new Error(`Could not read a version from ${file}`);
}
return version;
}

const jsPackage = JSON.parse(read("bindings/js/package.json"));
const jsPackageLock = JSON.parse(read("bindings/js/package-lock.json"));
const versions = {
"Cargo.toml": fs.readFileSync("Cargo.toml", "utf8").match(/^\s*version\s*=\s*"([^"]+)"/m)?.[1],
"Cargo.toml [workspace.package]": matchedVersion(
"Cargo.toml",
/^\[workspace\.package\]\s*$[\s\S]*?^\s*version\s*=\s*"([^"]+)"/m,
),
"bindings/js/package.json": jsPackage.version,
"bindings/js/src/version.ts": fs.readFileSync("bindings/js/src/version.ts", "utf8").match(/VERSION\s*=\s*"([^"]+)"/)?.[1],
"bindings/python/pyproject.toml": fs.readFileSync("bindings/python/pyproject.toml", "utf8").match(/^\s*version\s*=\s*"([^"]+)"/m)?.[1],
"bindings/python/src/shell_use/_config.py": fs.readFileSync("bindings/python/src/shell_use/_config.py", "utf8").match(/VERSION\s*=\s*"([^"]+)"/)?.[1],
"bindings/js/package-lock.json": jsPackageLock.version,
"bindings/js/package-lock.json packages['']":
jsPackageLock.packages?.[""]?.version,
"bindings/js/src/version.ts": matchedVersion(
"bindings/js/src/version.ts",
/VERSION\s*=\s*"([^"]+)"/,
),
"bindings/python/pyproject.toml": matchedVersion(
"bindings/python/pyproject.toml",
/^\s*version\s*=\s*"([^"]+)"/m,
),
"bindings/python/src/shell_use/_config.py": matchedVersion(
"bindings/python/src/shell_use/_config.py",
/VERSION\s*=\s*"([^"]+)"/,
),
};

for (const [file, version] of Object.entries(versions)) {
Expand All @@ -23,9 +50,30 @@ for (const [file, version] of Object.entries(versions)) {
}
}

const nativeLoader = fs.readFileSync("bindings/js/native/index.js", "utf8");
if (!nativeLoader.includes(`'${expected}'`)) {
const workspaceVersionManifests = [
"crates/shell-use/Cargo.toml",
"crates/shell-use-cli/Cargo.toml",
"bindings/js/Cargo.toml",
"bindings/python/native/Cargo.toml",
];
for (const file of workspaceVersionManifests) {
if (!/^\s*version\.workspace\s*=\s*true\s*$/m.test(read(file))) {
throw new Error(`${file} must inherit workspace.package.version`);
}
}

const nativeLoader = read("bindings/js/native/index.js");
const loaderVersions = new Set(
[...nativeLoader.matchAll(/bindingPackageVersion !== '([^']+)'/g)].map(
([, version]) => version,
),
);
if (loaderVersions.size !== 1 || !loaderVersions.has(expected)) {
throw new Error(
`bindings/js/native/index.js was not regenerated for ${expected}`,
`bindings/js/native/index.js has package versions ${[...loaderVersions].join(", ") || "none"}; expected ${expected}`,
);
}

console.log(
`Verified ${expected} across release metadata; Rust packages inherit the workspace version.`,
);
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ jobs:
- name: Test Python binding
run: python -m unittest discover -s bindings/python/tests -v

- name: Verify generated Python stubs
run: python bindings/python/scripts/generate_stubs.py --check

- name: Install Node dependencies
run: npm ci --prefix bindings/js

Expand All @@ -71,7 +74,7 @@ jobs:
shell: bash
run: |
git diff --exit-code -- bindings/js/native/index.js bindings/js/native/index.d.ts
! grep -q "JsonValue" bindings/js/native/index.d.ts
! grep -Eq 'JsonValue|Promise<unknown>|\brequest\(|\bBuffer\b' bindings/js/native/index.d.ts

- uses: oven-sh/setup-bun@v2
if: runner.os == 'Linux'
Expand Down
26 changes: 22 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,17 @@ jobs:
artifacts/*

smoke-node:
name: Smoke Node (${{ matrix.name }})
needs: package-node
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: Node 20
version: "20"
- name: Node current
version: "node"
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -401,14 +410,23 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: "24"
node-version: ${{ matrix.version }}

- name: Install packed root and platform packages
- name: Smoke packed root and platform packages without the CLI
run: node .github/scripts/release/smoke-node.mjs

smoke-python:
name: Smoke Python (${{ matrix.name }})
needs: build-python
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: Python 3.8
version: "3.8"
- name: Python current
version: "3.14"
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -421,7 +439,7 @@ jobs:

- uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: ${{ matrix.version }}

- name: Install built wheel
- name: Smoke the built abi3 wheel without the CLI
run: python .github/scripts/release/smoke-python.py
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
/target
*.gif
*.pyc
*.trace
__pycache__/
*.node
.shell-use/
bindings/js/target
bindings/js/native/target
bindings/python/target
bindings/python/native/target
bindings/python/native/target
bindings/python/stub-gen/target
Loading
Loading