Skip to content

Commit a66a872

Browse files
alnrclaude
andcommitted
fix: publish npm binaries as per-platform optionalDependencies
The @ory/cli npm package used binwrap to download the CLI binary at install time. binwrap is unmaintained and depends on the deprecated request package, whose transitive dependencies trigger unfixable critical npm audit findings in every consuming project. The npm package now follows the esbuild pattern: the release pipeline publishes one package per platform (e.g. @ory/cli-linux-x64) containing just the prebuilt binary, and @ory/cli itself ships a dependency-free launcher plus exact-version optionalDependencies on those packages. npm's os/cpu fields ensure only the binary matching the consumer's platform is downloaded. Install scripts and all runtime npm dependencies are gone, and npm audit reports zero vulnerabilities. Prereleases are now published under the "next" dist-tag instead of "latest", and Windows arm64 binaries are now published to npm. Closes #410 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NaL51TwfZAAEEWU2v5CNWT
1 parent 5d23615 commit a66a872

7 files changed

Lines changed: 359 additions & 643 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,14 @@ jobs:
8989
- release
9090
steps:
9191
- uses: ory/ci/checkout@master
92-
- uses: actions/setup-node@v2
92+
- uses: actions/setup-node@v4
9393
with:
94-
node-version: "16"
94+
node-version: "22"
9595
- env:
9696
NPM_TOKEN: ${{ secrets.NPM_TOKEN_AENEASR }}
9797
run: |
98-
npm install
99-
npm version --no-git-tag-version ${{ github.ref_name }}
100-
npm run test:binwrap
10198
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
102-
npm publish --access public
99+
node npm/publish.js ${{ github.ref_name }}
103100
104101
newsletter-draft:
105102
name: Draft newsletter

.npmignore

Lines changed: 0 additions & 3 deletions
This file was deleted.

npm/index.js

Lines changed: 0 additions & 21 deletions
This file was deleted.

npm/publish.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/env node
2+
// Copyright © 2023 Ory Corp
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
// Publishes the Ory CLI to npm for a tagged release. Not part of the npm
6+
// package itself — run by the npm-publish job in .github/workflows/ci.yaml.
7+
//
8+
// For every supported platform this downloads the release archive from GitHub,
9+
// extracts the ory binary into a minimal per-platform package
10+
// (e.g. @ory/cli-linux-x64) and publishes it. It then publishes @ory/cli
11+
// itself, which contains only the npm/run.js launcher plus exact-version
12+
// optionalDependencies on the platform packages, so that npm downloads just
13+
// the binary matching the consumer's platform.
14+
//
15+
// Usage: node npm/publish.js <version> [--dry-run]
16+
// version the release tag, with or without the leading "v"
17+
// --dry-run build and pack everything, but do not upload to npm
18+
19+
"use strict"
20+
21+
const { execFileSync } = require("child_process")
22+
const fs = require("fs")
23+
const path = require("path")
24+
25+
const platforms = [
26+
{ os: "darwin", cpu: "arm64", assetSuffix: "macOS_arm64.tar.gz" },
27+
{ os: "darwin", cpu: "x64", assetSuffix: "macOS_64bit.tar.gz" },
28+
{ os: "linux", cpu: "arm64", assetSuffix: "linux_arm64.tar.gz" },
29+
{ os: "linux", cpu: "x64", assetSuffix: "linux_64bit.tar.gz" },
30+
{ os: "win32", cpu: "arm64", assetSuffix: "windows_arm64.zip" },
31+
{ os: "win32", cpu: "x64", assetSuffix: "windows_64bit.zip" },
32+
]
33+
34+
const rootDir = path.join(__dirname, "..")
35+
const distDir = path.join(rootDir, "dist", "npm")
36+
37+
function run(cmd, args, opts) {
38+
console.log("+ " + cmd + " " + args.join(" "))
39+
return execFileSync(cmd, args, Object.assign({ stdio: "inherit" }, opts))
40+
}
41+
42+
function buildPlatformPackage(platform, version) {
43+
const pkgName = "@ory/cli-" + platform.os + "-" + platform.cpu
44+
const asset = "ory_" + version + "-" + platform.assetSuffix
45+
const url =
46+
"https://github.com/ory/cli/releases/download/v" + version + "/" + asset
47+
const pkgDir = path.join(distDir, platform.os + "-" + platform.cpu)
48+
const binDir = path.join(pkgDir, "bin")
49+
const binName = platform.os === "win32" ? "ory.exe" : "ory"
50+
const archive = path.join(distDir, asset)
51+
52+
fs.mkdirSync(binDir, { recursive: true })
53+
run("curl", ["-fsSL", "--retry", "3", "-o", archive, url])
54+
if (asset.endsWith(".zip")) {
55+
run("unzip", ["-oq", archive, binName, "-d", binDir])
56+
} else {
57+
run("tar", ["-xzf", archive, "-C", binDir, binName])
58+
}
59+
fs.chmodSync(path.join(binDir, binName), 0o755)
60+
fs.rmSync(archive)
61+
fs.copyFileSync(path.join(rootDir, "LICENSE"), path.join(pkgDir, "LICENSE"))
62+
fs.writeFileSync(
63+
path.join(pkgDir, "package.json"),
64+
JSON.stringify(
65+
{
66+
name: pkgName,
67+
version: version,
68+
description:
69+
"The Ory CLI binary for " + platform.os + " " + platform.cpu + ".",
70+
repository: { type: "git", url: "git+https://github.com/ory/cli.git" },
71+
homepage: "https://ory.com/cli",
72+
license: "Apache-2.0",
73+
os: [platform.os],
74+
cpu: [platform.cpu],
75+
files: ["bin"],
76+
preferUnplugged: true,
77+
},
78+
null,
79+
2,
80+
) + "\n",
81+
)
82+
return { name: pkgName, dir: pkgDir, bin: path.join(binDir, binName) }
83+
}
84+
85+
function smokeTest(pkg, platform, version) {
86+
if (platform.os !== process.platform || platform.cpu !== process.arch) {
87+
return
88+
}
89+
const out = execFileSync(pkg.bin, ["version"], { encoding: "utf8" })
90+
console.log(out)
91+
if (!out.includes(version)) {
92+
throw new Error(
93+
"smoke test failed: `ory version` does not mention " + version,
94+
)
95+
}
96+
}
97+
98+
function publish(dir, distTag, dryRun) {
99+
const args = ["publish", "--access", "public", "--tag", distTag]
100+
if (dryRun) {
101+
args.push("--dry-run")
102+
}
103+
run("npm", args, { cwd: dir })
104+
}
105+
106+
function main() {
107+
const args = process.argv.slice(2)
108+
const dryRun = args.includes("--dry-run")
109+
const version = (args.find((a) => !a.startsWith("--")) || "").replace(
110+
/^v/,
111+
"",
112+
)
113+
if (!/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version)) {
114+
console.error("Usage: node npm/publish.js <version> [--dry-run]")
115+
process.exit(1)
116+
}
117+
118+
// Prereleases must not become the version a plain `npm install @ory/cli`
119+
// resolves to, so publish them under the "next" dist-tag instead of "latest".
120+
const distTag = version.includes("-") ? "next" : "latest"
121+
122+
fs.rmSync(distDir, { recursive: true, force: true })
123+
const built = platforms.map((platform) => {
124+
const pkg = buildPlatformPackage(platform, version)
125+
smokeTest(pkg, platform, version)
126+
return pkg
127+
})
128+
129+
// Publish the platform packages before @ory/cli itself so a failure cannot
130+
// leave @ory/cli pointing at binary packages that do not exist.
131+
for (const pkg of built) {
132+
publish(pkg.dir, distTag, dryRun)
133+
}
134+
135+
const rootPkgPath = path.join(rootDir, "package.json")
136+
const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf8"))
137+
rootPkg.version = version
138+
rootPkg.optionalDependencies = {}
139+
for (const pkg of built) {
140+
rootPkg.optionalDependencies[pkg.name] = version
141+
}
142+
fs.writeFileSync(rootPkgPath, JSON.stringify(rootPkg, null, 2) + "\n")
143+
publish(rootDir, distTag, dryRun)
144+
}
145+
146+
main()

npm/run.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env node
2+
// Copyright © 2023 Ory Corp
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
// Launcher for the Ory CLI. The actual binary is shipped in per-platform
6+
// packages (see npm/publish.js) that @ory/cli declares as optionalDependencies,
7+
// so that npm only downloads the one matching the current platform.
8+
9+
"use strict"
10+
11+
var spawnSync = require("child_process").spawnSync
12+
13+
var packages = {
14+
"darwin arm64": "@ory/cli-darwin-arm64",
15+
"darwin x64": "@ory/cli-darwin-x64",
16+
"linux arm64": "@ory/cli-linux-arm64",
17+
"linux x64": "@ory/cli-linux-x64",
18+
"win32 arm64": "@ory/cli-win32-arm64",
19+
"win32 x64": "@ory/cli-win32-x64",
20+
}
21+
22+
function binaryPath() {
23+
var platformKey = process.platform + " " + process.arch
24+
var pkg = packages[platformKey]
25+
if (!pkg) {
26+
console.error(
27+
"@ory/cli does not ship a prebuilt Ory CLI binary for " +
28+
platformKey +
29+
".\nSee https://www.ory.com/docs/guides/cli/installation for other installation options.",
30+
)
31+
process.exit(1)
32+
}
33+
var bin = process.platform === "win32" ? "bin/ory.exe" : "bin/ory"
34+
try {
35+
return require.resolve(pkg + "/" + bin)
36+
} catch (err) {
37+
console.error(
38+
"The Ory CLI binary package " +
39+
pkg +
40+
" is missing. It is an optional dependency of @ory/cli, so make sure\n" +
41+
"optional dependencies are not disabled (e.g. via --omit=optional or\n" +
42+
"--no-optional) and reinstall.",
43+
)
44+
process.exit(1)
45+
}
46+
}
47+
48+
var result = spawnSync(binaryPath(), process.argv.slice(2), {
49+
stdio: "inherit",
50+
})
51+
if (result.error) {
52+
throw result.error
53+
}
54+
if (result.signal) {
55+
process.kill(process.pid, result.signal)
56+
}
57+
process.exit(typeof result.status === "number" ? result.status : 1)

0 commit comments

Comments
 (0)