Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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"
84 changes: 55 additions & 29 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,58 @@ 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]);

if (!nodeHasProxySupportEnabled && shouldUseProxy) {
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.exit(code));
Copy link

Copilot AI Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exit code could be null when the child process is terminated by a signal. This should handle null values: child.on('exit', (code) => process.exit(code || 1));

Suggested change
child.on("exit", (code) => process.exit(code));
child.on("exit", (code) => process.exit(code ?? 1));

Copilot uses AI. Check for mistakes.
// TODO: return promise that resolves once sub process exits (for testing)
return;
Copy link

Copilot AI Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO indicates incomplete implementation that affects testability. Consider implementing the promise-based approach to properly support testing scenarios.

Suggested change
child.on("exit", (code) => process.exit(code));
// TODO: return promise that resolves once sub process exits (for testing)
return;
// Return a promise that resolves once sub process exits (for testing)
return new Promise((resolve, reject) => {
child.on("exit", (code) => {
process.exit(code);
resolve(code);
});
child.on("error", (err) => {
reject(err);
});
});

Copilot uses AI. Check for mistakes.
}

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();
21 changes: 21 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,21 @@
import test from "node:test";

test("spawns a child process if proxy is set and NODE_USE_ENV_PROXY is not set", async (t) => {
// https://nodejs.org/api/test.html#class-mocktracker
// TODO: why u not work
Copy link

Copilot AI Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment contains informal language. Consider updating to: // TODO: Mock not working as expected

Suggested change
// TODO: why u not work
// TODO: Mock not working as expected

Copilot uses AI. Check for mistakes.
t.mock.module("node:child_process", {
namedExports: {
spawn() {
throw new Error("----- nope!!! -----");
},
},
});

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

await import("../main.js");

await new Promise((resolve) => setTimeout(resolve, 1000));
});
Loading