Skip to content
Draft
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
33 changes: 31 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version-file: package.json
cache: 'npm'
cache: "npm"

- run: npm ci
- run: npm test
Expand All @@ -39,11 +39,13 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version-file: package.json
cache: 'npm'
cache: "npm"
- run: npm ci
- run: npm run build
- uses: ./ # Uses the action in the root directory
id: test
env:
https_proxy: https://example.com
with:
app-id: ${{ vars.TEST_APP_ID }}
private-key: ${{ secrets.TEST_APP_PRIVATE_KEY }}
Expand All @@ -54,3 +56,30 @@ jobs:
with:
route: GET /installation/repositories
- run: echo '${{ steps.get-repository.outputs.data }}'

end-to-end-proxy:
name: End-to-End test using proxy
runs-on: ubuntu-latest
# do not run from forks, as forks don’t have access to repository secrets
if: github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version-file: package.json
cache: "npm"
- run: npm ci
- run: npm run build
- uses: ./ # Uses the action in the root directory
id: test
env:
https_proxy: https://example.com
with:
app-id: ${{ vars.TEST_APP_ID }}
private-key: ${{ secrets.TEST_APP_PRIVATE_KEY }}

# fail CI if prior step succeeds and vice versa
- if: ${{ success() }}
run: exit 1
- if: ${{ failure() }}
run: echo "action failed as expected"
93 changes: 64 additions & 29 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,67 @@ if (!process.env.GITHUB_REPOSITORY_OWNER) {
throw new Error("GITHUB_REPOSITORY_OWNER missing, must be set to '<owner>'");
}

const appId = core.getInput("app-id");
const privateKey = core.getInput("private-key");
const owner = core.getInput("owner");
const repositories = core
.getInput("repositories")
.split(/[\n,]+/)
.map((s) => s.trim())
.filter((x) => x !== "");

const skipTokenRevoke = core.getBooleanInput("skip-token-revoke");

const permissions = getPermissionsFromInputs(process.env);

// Export promise for testing
export default main(
appId,
privateKey,
owner,
repositories,
permissions,
core,
createAppAuth,
request,
skipTokenRevoke,
).catch((error) => {
/* c8 ignore next 3 */
console.error(error);
core.setFailed(error.message);
});
async function run() {
// spawn a child process if proxy is set
const httpProxyEnvVars = [
"https_proxy",
"HTTPS_PROXY",
"http_proxy",
"HTTP_PROXY",
];
const nodeHasProxySupportEnabled = process.env.NODE_USE_ENV_PROXY === "1";
const shouldUseProxy = httpProxyEnvVars.some((v) => process.env[v]);

// There is no other way to enable proxy support in Node.js as of 2025-09-19
// https://github.com/nodejs/node/blob/4612c793cb9007a91cb3fd82afe518440473826e/lib/internal/process/pre_execution.js#L168-L187
if (!nodeHasProxySupportEnabled && shouldUseProxy) {
return new Promise(async (resolve, reject) => {
const { spawn } = await import("node:child_process");
// spawn itself with NODE_USE_ENV_PROXY=1
const child = spawn(process.execPath, process.argv.slice(1), {
env: { ...process.env, NODE_USE_ENV_PROXY: "1" },
stdio: "inherit",
});
child.on("exit", (code) => {
process.exitCode = code;
if (code !== 0) {
reject(new Error(`Child process exited with code ${code}`));
} else {
resolve();
}
});
});
}

const appId = core.getInput("app-id");
const privateKey = core.getInput("private-key");
const owner = core.getInput("owner");
const repositories = core
.getInput("repositories")
.split(/[\n,]+/)
.map((s) => s.trim())
.filter((x) => x !== "");

const skipTokenRevoke = core.getBooleanInput("skip-token-revoke");

const permissions = getPermissionsFromInputs(process.env);

// Export promise for testing
return main(
appId,
privateKey,
owner,
repositories,
permissions,
core,
createAppAuth,
request,
skipTokenRevoke,
).catch((error) => {
/* c8 ignore next 3 */
console.error(error);
core.setFailed(error.message);
});
}

export default run();
18 changes: 18 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"devDependencies": {
"@octokit/openapi": "^19.1.0",
"@sinonjs/fake-timers": "^14.0.0",
"@types/node": "^24.5.2",
"ava": "^6.4.1",
"c8": "^10.1.3",
"dotenv": "^17.2.1",
Expand Down
10 changes: 9 additions & 1 deletion tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@ for (const file of testFiles) {
GITHUB_OUTPUT: undefined,
GITHUB_STATE: undefined,
};
const { stderr, stdout } = await execa("node", [`tests/${file}`], { env });
const { stderr, stdout } = await execa(
"node",
[
"--experimental-test-module-mocks",
"--disable-warning=ExperimentalWarning",
`tests/${file}`,
],
{ env },
);
t.snapshot(stderr, "stderr");
t.snapshot(stdout, "stdout");
});
Expand Down
88 changes: 88 additions & 0 deletions tests/main-spawns-sub-process-for-proxy.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import test from "node:test";
import assert from "node:assert";

test("spawns a child process if proxy is set and NODE_USE_ENV_PROXY is not set", async (t) => {
let spawnCalled = false;

// https://nodejs.org/api/test.html#class-mocktracker
t.mock.module("node:child_process", {
namedExports: {
spawn() {
spawnCalled = true;
return {
on(event, callback) {
callback(0);
},
};
},
},
});

process.env.GITHUB_REPOSITORY = "foo/bar";
process.env.GITHUB_REPOSITORY_OWNER = "foo";
process.env.https_proxy = "http://example.com";

const { default: runPromise } = await import("../main.js?" + Math.random());
await runPromise;

assert(spawnCalled, "spawn was called");
assert.equal(process.exitCode, 0, "process exit code is 0");
});

test("child process throws error", async (t) => {
let spawnCalled = false;

// https://nodejs.org/api/test.html#class-mocktracker
t.mock.module("node:child_process", {
namedExports: {
spawn() {
spawnCalled = true;
return {
on(event, callback) {
callback(1);
},
};
},
},
});

process.env.GITHUB_REPOSITORY = "foo/bar";
process.env.GITHUB_REPOSITORY_OWNER = "foo";
process.env.https_proxy = "http://example.com";

const { default: runPromise } = await import("../main.js?" + Math.random());
await runPromise.catch((error) => {
assert.equal(
error.message,
"Child process exited with code 1",
"error message is correct",
);
});

assert(spawnCalled, "spawn was called");
assert.equal(process.exitCode, 1, "process exit code is 0");
process.exitCode = 0; // reset for other tests
});

test("does not spawn a child process if proxy is set and NODE_USE_ENV_PROXY is set", async (t) => {
let mainCalled = false;

t.mock.module("../lib/main.js", {
namedExports: {
async main() {
mainCalled = true;
},
},
});

process.env.GITHUB_REPOSITORY = "foo/bar";
process.env.GITHUB_REPOSITORY_OWNER = "foo";
process.env.https_proxy = "http://example.com";
process.env.NODE_USE_ENV_PROXY = "1";
process.env["INPUT_SKIP-TOKEN-REVOKE"] = "false";

const { default: runPromise } = await import("../main.js?" + Math.random());
await runPromise;

assert(mainCalled, "main was called");
});
20 changes: 20 additions & 0 deletions tests/snapshots/index.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,26 @@ Generated by [AVA](https://avajs.dev).
POST /app/installations/123456/access_tokens␊
{"repositories":["failed-repo"]}`

## main-spawns-sub-process-for-proxy.test.js

> stderr

''

> stdout

`✔ spawns a child process if proxy is set and NODE_USE_ENV_PROXY is not set (87.402375ms)␊
✔ child process throws error (4.003417ms)␊
✔ does not spawn a child process if proxy is set and NODE_USE_ENV_PROXY is set (3.248625ms)␊
ℹ tests 3␊
ℹ suites 0␊
ℹ pass 3␊
ℹ fail 0␊
ℹ cancelled 0␊
ℹ skipped 0␊
ℹ todo 0␊
ℹ duration_ms 97.942333`

## main-token-get-owner-set-fail-response.test.js

> stderr
Expand Down
Binary file modified tests/snapshots/index.js.snap
Binary file not shown.