-
Notifications
You must be signed in to change notification settings - Fork 26
fix: forward termination signals to the CLI binary #462
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| // Copyright © 2026 Ory Corp | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| "use strict" | ||
|
|
||
| const { test } = require("node:test") | ||
| const assert = require("node:assert/strict") | ||
| const { spawn } = require("node:child_process") | ||
| const fs = require("node:fs") | ||
| const os = require("node:os") | ||
| const path = require("node:path") | ||
|
|
||
| const platformPackage = "@ory/cli-" + process.platform + "-" + process.arch | ||
|
|
||
| // The binary stand-in is a Node script: it exits with a requested status, | ||
| // echoes its arguments, waits for a signal and records which one arrived, or | ||
| // terminates itself with SIGTERM. In wait mode it gives up after ten seconds so | ||
| // that a launcher which orphans it cannot hold the test's pipes open forever. | ||
| const fakeBinary = `#!/usr/bin/env node | ||
| "use strict" | ||
| const fs = require("node:fs") | ||
| const [mode, arg] = process.argv.slice(2) | ||
| switch (mode) { | ||
| case "echo": | ||
| console.log(process.argv.slice(3).join(" ")) | ||
| break | ||
| case "exit": | ||
| process.exit(Number(arg)) | ||
| break | ||
| case "wait": | ||
| for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) { | ||
| process.on(signal, () => { | ||
| fs.writeFileSync(arg, signal) | ||
| process.exit(0) | ||
| }) | ||
| } | ||
| fs.writeFileSync(arg, "ready") | ||
| setTimeout(() => process.exit(2), 10000) | ||
| break | ||
| case "self-kill": | ||
| process.kill(process.pid, "SIGTERM") | ||
| break | ||
| } | ||
| ` | ||
|
|
||
| // Lays out @ory/cli and the platform package the way npm installs them, so the | ||
| // launcher resolves the stand-in through the same require.resolve call it uses | ||
| // for the real binary. | ||
| function install(t, withBinary) { | ||
| const root = fs.mkdtempSync(path.join(os.tmpdir(), "ory-cli-launcher-")) | ||
| t.after(() => fs.rmSync(root, { recursive: true, force: true })) | ||
|
|
||
| const launcherDir = path.join(root, "node_modules", "@ory", "cli", "npm") | ||
| fs.mkdirSync(launcherDir, { recursive: true }) | ||
| fs.copyFileSync( | ||
| path.join(__dirname, "run.js"), | ||
| path.join(launcherDir, "run.js"), | ||
| ) | ||
|
|
||
| if (withBinary) { | ||
| const binDir = path.join(root, "node_modules", platformPackage, "bin") | ||
| fs.mkdirSync(binDir, { recursive: true }) | ||
| fs.writeFileSync(path.join(binDir, "ory"), fakeBinary, { mode: 0o755 }) | ||
| } | ||
|
|
||
| return { root, launcher: path.join(launcherDir, "run.js") } | ||
| } | ||
|
|
||
| function run(launcher, args) { | ||
| const child = spawn(process.execPath, [launcher, ...args], { | ||
| stdio: ["ignore", "pipe", "pipe"], | ||
| env: { | ||
| ...process.env, | ||
| // The stand-in's shebang needs the node running this test on the PATH. | ||
| PATH: path.dirname(process.execPath) + path.delimiter + process.env.PATH, | ||
| }, | ||
| }) | ||
| let stdout = "" | ||
| let stderr = "" | ||
| child.stdout.on("data", (chunk) => (stdout += chunk)) | ||
| child.stderr.on("data", (chunk) => (stderr += chunk)) | ||
| const closed = new Promise((resolve) => { | ||
| child.on("close", (code, signal) => resolve({ code, signal })) | ||
| }) | ||
| return { | ||
| child, | ||
| done: () => closed.then((result) => ({ ...result, stdout, stderr })), | ||
| } | ||
| } | ||
|
|
||
| async function waitForFile(file, content) { | ||
| for (let i = 0; i < 200; i++) { | ||
| if (fs.existsSync(file) && fs.readFileSync(file, "utf8") === content) { | ||
| return | ||
| } | ||
| await new Promise((resolve) => setTimeout(resolve, 50)) | ||
| } | ||
| throw new Error("timed out waiting for " + file + " to contain " + content) | ||
| } | ||
|
|
||
| const opts = { skip: process.platform === "win32" && "no shebang support" } | ||
|
|
||
| test( | ||
| "passes arguments through and exits with the binary's status", | ||
| opts, | ||
| async (t) => { | ||
| const { launcher } = install(t, true) | ||
|
|
||
| const echoed = await run(launcher, [ | ||
| "echo", | ||
| "tunnel", | ||
| "--port", | ||
| "4000", | ||
| ]).done() | ||
| assert.equal(echoed.stdout, "tunnel --port 4000\n") | ||
| assert.equal(echoed.code, 0) | ||
|
|
||
| const failed = await run(launcher, ["exit", "3"]).done() | ||
| assert.equal(failed.code, 3) | ||
| }, | ||
| ) | ||
|
|
||
| test("forwards termination signals to the binary", opts, async (t) => { | ||
| for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) { | ||
| const { root, launcher } = install(t, true) | ||
| const marker = path.join(root, "signal") | ||
| const proc = run(launcher, ["wait", marker]) | ||
| await waitForFile(marker, "ready") | ||
|
|
||
| proc.child.kill(signal) | ||
|
|
||
| const result = await proc.done() | ||
| assert.equal(fs.readFileSync(marker, "utf8"), signal) | ||
| assert.equal(result.code, 0) | ||
| assert.equal(result.signal, null) | ||
| } | ||
| }) | ||
|
|
||
| test( | ||
| "terminates with the signal that terminated the binary", | ||
| opts, | ||
| async (t) => { | ||
| const { launcher } = install(t, true) | ||
|
|
||
| const result = await run(launcher, ["self-kill"]).done() | ||
| assert.equal(result.signal, "SIGTERM") | ||
| assert.equal(result.code, null) | ||
| }, | ||
| ) | ||
|
|
||
| test("fails when the platform package is missing", opts, async (t) => { | ||
| const { launcher } = install(t, false) | ||
|
|
||
| const result = await run(launcher, ["version"]).done() | ||
| assert.equal(result.code, 1) | ||
| assert.match(result.stderr, new RegExp(platformPackage + " is missing")) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: ory/cli
Length of output: 13934
🏁 Script executed:
Repository: ory/cli
Length of output: 594
🌐 Web query:
GitHub Actions GITHUB_TOKEN permissions unspecified workflow and job-level permissions default contents read documentation💡 Result:
The GITHUB_TOKEN permissions in GitHub Actions are determined by a hierarchical configuration process that prioritizes the principle of least privilege [1][2]. Default Permissions The initial permissions for the GITHUB_TOKEN are determined by the default settings established at the enterprise, organization, or repository level [3][4]. Admins can configure these defaults in the repository or organization settings under Actions > General [5][6]. There are typically two modes: 1. Permissive: Provides broad read and write access to most scopes [2][6]. 2. Restricted: Provides read-only access to repository contents and packages [5][2]. Workflow and Job-Level Overrides You can explicitly define or modify these permissions within your workflow file using the permissions key [7][2]. This configuration can be applied at two levels: 1. Workflow level: Specify the permissions key at the top level of the YAML file to apply them to all jobs in that workflow [3][8]. 2. Job level: Specify the permissions key within a specific job (jobs.<job_id>.permissions) to override or refine permissions for that job only [9][3]. Behavior of the Permissions Key When you use the permissions key, any permission not explicitly included in your list is automatically set to none [2]. For example, if you define only contents: read, all other scopes (such as packages or issues) will be set to none, even if they were enabled by the repository-level default [9][2]. Special Case: Pull Requests Regardless of your configured settings, workflows triggered by pull requests from forked repositories are subject to additional security restrictions. In these cases, the GITHUB_TOKEN is automatically downgraded to read-only mode to prevent unauthorized access or modification, unless the repository explicitly permits sending write tokens to workflows from pull requests [3][2][8]. Summary of Precedence The effective permissions for a job are calculated in the following order: 1. Repository/Organization/Enterprise default settings [3][4]. 2. Workflow-level permissions (if specified) [3][4]. 3. Job-level permissions (if specified) [3][4]. 4. Automatic restrictions applied to pull requests from forks [3][8].
Citations:
🏁 Script executed:
Repository: ory/cli
Length of output: 4936
Set explicit permissions for
test-launcher.ory/ci/checkoutonly requires repository contents access. Addcontents: readto prevent broader default token permissions.🧰 Tools
🪛 zizmor (1.29.0)
[warning] 14-22: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block
(excessive-permissions)
[error] 19-19: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default
(cache-poisoning)
🤖 Prompt for AI Agents
Source: Linters/SAST tools