Skip to content

Commit 2a02de1

Browse files
committed
Read token from stdin in sync-checks.ts
Also allow specifying the token using an environment variable.
1 parent 67f4038 commit 2a02de1

3 files changed

Lines changed: 120 additions & 11 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ Once the mergeback and backport pull request have been merged, the release is co
7171

7272
Since the `codeql-action` runs most of its testing through individual Actions workflows, there are over two hundred required jobs that need to pass in order for a PR to turn green. It would be too tedious to maintain that list manually. You can regenerate the set of required checks automatically by running the [sync-checks.ts](pr-checks/sync-checks.ts) script:
7373

74-
- At a minimum, you must provide an argument for the `--token` input. For example, `--token "$(gh auth token)"` to use the same token that `gh` uses. If no token is provided or the token has insufficient permissions, the script will fail.
74+
- At a minimum, you must provide a token with permissions to update branch protection rules. For example, `gh auth token | pr-checks/sync-checks.ts --token-stdin` uses the same token that `gh` uses. You can also set the `GH_TOKEN` or `GITHUB_TOKEN` environment variable. If no token is provided or the token has insufficient permissions, the script will fail.
7575
- By default, the script performs a dry run and outputs information about the changes it would make to the branch protection rules. To actually apply the changes, specify the `--apply` flag.
7676
- If you run the script without any other arguments, it will retrieve the set of workflows that ran for the latest commit on `main`.
7777
- You can specify a different git ref with the `--ref` input. You will likely want to use this if you have a PR that removes or adds PR checks. For example, `--ref "some/branch/name"` to use the HEAD of the `some/branch/name` branch.

pr-checks/sync-checks.test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ Tests for the sync-checks.ts script
77
import * as assert from "node:assert/strict";
88
import { describe, it } from "node:test";
99

10-
import { CheckInfo, Exclusions, Options, removeExcluded } from "./sync-checks";
10+
import {
11+
CheckInfo,
12+
Exclusions,
13+
Options,
14+
removeExcluded,
15+
resolveToken,
16+
} from "./sync-checks";
1117

1218
const defaultOptions: Options = {
1319
apply: false,
@@ -58,3 +64,46 @@ describe("removeExcluded", async () => {
5864
assert.deepEqual(retained, expectedExactMatches);
5965
});
6066
});
67+
68+
describe("resolveToken", async () => {
69+
await it("reads the token from standard input", async () => {
70+
const token = await resolveToken(
71+
{ tokenStdin: true },
72+
{ env: {}, readStdin: async () => " stdin-token\n" },
73+
);
74+
assert.equal(token, "stdin-token");
75+
});
76+
77+
await it("reads the token from the GH_TOKEN environment variable", async () => {
78+
const token = await resolveToken(
79+
{},
80+
{ env: { GH_TOKEN: "env-token" }, readStdin: async () => "" },
81+
);
82+
assert.equal(token, "env-token");
83+
});
84+
85+
await it("reads the token from the GITHUB_TOKEN environment variable", async () => {
86+
const token = await resolveToken(
87+
{},
88+
{ env: { GITHUB_TOKEN: "env-token" }, readStdin: async () => "" },
89+
);
90+
assert.equal(token, "env-token");
91+
});
92+
93+
await it("rejects an empty standard input token", async () => {
94+
await assert.rejects(
95+
resolveToken(
96+
{ tokenStdin: true },
97+
{ env: {}, readStdin: async () => "\n" },
98+
),
99+
/No token received on standard input/,
100+
);
101+
});
102+
103+
await it("rejects missing token sources", async () => {
104+
await assert.rejects(
105+
resolveToken({}, { env: {}, readStdin: async () => "" }),
106+
/Missing authentication token/,
107+
);
108+
});
109+
});

pr-checks/sync-checks.ts

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import {
1515

1616
/** Represents the command-line options. */
1717
export interface Options {
18-
/** The token to use to authenticate to the GitHub API. */
19-
token?: string;
18+
/** Whether to read the GitHub API token from standard input. */
19+
tokenStdin?: boolean;
2020
/** The git ref to use the checks for. */
2121
ref?: string;
2222
/** Whether to actually apply the changes or not. */
@@ -31,6 +31,65 @@ const codeqlActionRepo = {
3131
repo: "codeql-action",
3232
};
3333

34+
/** Environment variables to check for a GitHub API token. */
35+
const TOKEN_ENVIRONMENT_VARIABLES = ["GH_TOKEN", "GITHUB_TOKEN"];
36+
37+
/** Represents the sources from which we can retrieve the GitHub API token. */
38+
interface TokenSource {
39+
/** Environment variables to inspect. */
40+
env: NodeJS.ProcessEnv;
41+
/** Reads a token from standard input. */
42+
readStdin: () => Promise<string>;
43+
}
44+
45+
/** Reads the GitHub API token from standard input. */
46+
async function readTokenFromStdin(): Promise<string> {
47+
let token = "";
48+
process.stdin.setEncoding("utf8");
49+
for await (const chunk of process.stdin) {
50+
token += chunk;
51+
}
52+
return token.trim();
53+
}
54+
55+
/** Gets a GitHub API token from one of the supported environment variables. */
56+
function getTokenFromEnvironment(env: NodeJS.ProcessEnv): string | undefined {
57+
for (const variableName of TOKEN_ENVIRONMENT_VARIABLES) {
58+
const token = env[variableName]?.trim();
59+
if (token) {
60+
return token;
61+
}
62+
}
63+
return undefined;
64+
}
65+
66+
/** Gets the token to use to authenticate to the GitHub API. */
67+
export async function resolveToken(
68+
options: Pick<Options, "tokenStdin">,
69+
tokenSource: TokenSource = {
70+
env: process.env,
71+
readStdin: readTokenFromStdin,
72+
},
73+
): Promise<string> {
74+
if (options.tokenStdin) {
75+
const token = (await tokenSource.readStdin()).trim();
76+
if (token.length === 0) {
77+
throw new Error("No token received on standard input.");
78+
}
79+
return token;
80+
}
81+
82+
const environmentToken = getTokenFromEnvironment(tokenSource.env);
83+
if (environmentToken !== undefined) {
84+
return environmentToken;
85+
}
86+
87+
throw new Error(
88+
"Missing authentication token. Set GH_TOKEN/GITHUB_TOKEN or pipe a token " +
89+
"to --token-stdin.",
90+
);
91+
}
92+
3493
/** Represents a configuration of which checks should not be set up as required checks. */
3594
export interface Exclusions {
3695
/** A list of strings that, if contained in a check name, are excluded. */
@@ -205,9 +264,10 @@ async function updateBranch(
205264
async function main(): Promise<void> {
206265
const { values: options } = parseArgs({
207266
options: {
208-
// The token to use to authenticate to the API.
209-
token: {
210-
type: "string",
267+
// Read the token to use to authenticate to the API from standard input.
268+
"token-stdin": {
269+
type: "boolean",
270+
default: false,
211271
},
212272
// The git ref for which to retrieve the check runs.
213273
ref: {
@@ -228,16 +288,16 @@ async function main(): Promise<void> {
228288
strict: true,
229289
});
230290

231-
if (options.token === undefined) {
232-
throw new Error("Missing --token");
233-
}
291+
const token = await resolveToken({
292+
tokenStdin: options["token-stdin"],
293+
});
234294

235295
console.info(
236296
`Oldest supported major version is: ${OLDEST_SUPPORTED_MAJOR_VERSION}`,
237297
);
238298

239299
// Initialise the API client.
240-
const client = getApiClient(options.token);
300+
const client = getApiClient(token);
241301

242302
// Find the check runs for the specified `ref` that we will later set as the required checks
243303
// for the main and release branches.

0 commit comments

Comments
 (0)