mirror of
https://github.com/github/codeql-action.git
synced 2026-05-17 08:40:16 +00:00
Merge pull request #3908 from github/henrymercer/token-stdin
Update scripts to read tokens more securely
This commit is contained in:
@@ -16,12 +16,23 @@ No user facing changes.
|
||||
"""
|
||||
|
||||
# NB: This exact commit message is used to find commits for reverting during backports.
|
||||
# Changing it requires a transition period where both old and new versions are supported.
|
||||
# Changing it requires a transition period where both old and new versions are supported.
|
||||
BACKPORT_COMMIT_MESSAGE = 'Update version and changelog for v'
|
||||
|
||||
# Name of the remote
|
||||
ORIGIN = 'origin'
|
||||
|
||||
# Environment variables to check for a GitHub API token.
|
||||
TOKEN_ENVIRONMENT_VARIABLES = ('GH_TOKEN', 'GITHUB_TOKEN')
|
||||
|
||||
# Gets a GitHub API token from one of the supported environment variables.
|
||||
def get_github_token():
|
||||
for variable_name in TOKEN_ENVIRONMENT_VARIABLES:
|
||||
token = os.environ.get(variable_name, '').strip()
|
||||
if token:
|
||||
return token
|
||||
raise Exception('Missing GitHub token. Set GITHUB_TOKEN or GH_TOKEN.')
|
||||
|
||||
# Runs git with the given args and returns the stdout.
|
||||
# Raises an error if git does not exit successfully (unless passed
|
||||
# allow_non_zero_exit_code=True).
|
||||
@@ -270,12 +281,6 @@ def update_changelog(version):
|
||||
def main():
|
||||
parser = argparse.ArgumentParser('update-release-branch.py')
|
||||
|
||||
parser.add_argument(
|
||||
'--github-token',
|
||||
type=str,
|
||||
required=True,
|
||||
help='GitHub token, typically from GitHub Actions.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--repository-nwo',
|
||||
type=str,
|
||||
@@ -313,7 +318,7 @@ def main():
|
||||
target_branch = args.target_branch
|
||||
is_primary_release = args.is_primary_release
|
||||
|
||||
repo = Github(args.github_token).get_repo(args.repository_nwo)
|
||||
repo = Github(get_github_token()).get_repo(args.repository_nwo)
|
||||
|
||||
# the target branch will be of the form releases/vN, where N is the major version number
|
||||
target_branch_major_version = target_branch.strip('releases/v')
|
||||
|
||||
@@ -64,11 +64,12 @@ jobs:
|
||||
|
||||
- name: Update current release branch
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo SOURCE_BRANCH=${REF_NAME}
|
||||
echo TARGET_BRANCH=releases/${MAJOR_VERSION}
|
||||
python .github/update-release-branch.py \
|
||||
--github-token ${{ secrets.GITHUB_TOKEN }} \
|
||||
--repository-nwo ${{ github.repository }} \
|
||||
--source-branch '${{ env.REF_NAME }}' \
|
||||
--target-branch 'releases/${{ env.MAJOR_VERSION }}' \
|
||||
@@ -107,11 +108,12 @@ jobs:
|
||||
- uses: ./.github/actions/release-initialise
|
||||
|
||||
- name: Update older release branch
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo SOURCE_BRANCH=${SOURCE_BRANCH}
|
||||
echo TARGET_BRANCH=${TARGET_BRANCH}
|
||||
python .github/update-release-branch.py \
|
||||
--github-token ${{ secrets.GITHUB_TOKEN }} \
|
||||
--repository-nwo ${{ github.repository }} \
|
||||
--source-branch ${SOURCE_BRANCH} \
|
||||
--target-branch ${TARGET_BRANCH} \
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ Once the mergeback and backport pull request have been merged, the release is co
|
||||
|
||||
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:
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- If you run the script without any other arguments, it will retrieve the set of workflows that ran for the latest commit on `main`.
|
||||
- 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.
|
||||
|
||||
@@ -7,7 +7,13 @@ Tests for the sync-checks.ts script
|
||||
import * as assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { CheckInfo, Exclusions, Options, removeExcluded } from "./sync-checks";
|
||||
import {
|
||||
CheckInfo,
|
||||
Exclusions,
|
||||
Options,
|
||||
removeExcluded,
|
||||
resolveToken,
|
||||
} from "./sync-checks";
|
||||
|
||||
const defaultOptions: Options = {
|
||||
apply: false,
|
||||
@@ -58,3 +64,46 @@ describe("removeExcluded", async () => {
|
||||
assert.deepEqual(retained, expectedExactMatches);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveToken", async () => {
|
||||
await it("reads the token from standard input", async () => {
|
||||
const token = await resolveToken(
|
||||
{ tokenStdin: true },
|
||||
{ env: {}, readStdin: async () => " stdin-token\n" },
|
||||
);
|
||||
assert.equal(token, "stdin-token");
|
||||
});
|
||||
|
||||
await it("reads the token from the GH_TOKEN environment variable", async () => {
|
||||
const token = await resolveToken(
|
||||
{},
|
||||
{ env: { GH_TOKEN: "env-token" }, readStdin: async () => "" },
|
||||
);
|
||||
assert.equal(token, "env-token");
|
||||
});
|
||||
|
||||
await it("reads the token from the GITHUB_TOKEN environment variable", async () => {
|
||||
const token = await resolveToken(
|
||||
{},
|
||||
{ env: { GITHUB_TOKEN: "env-token" }, readStdin: async () => "" },
|
||||
);
|
||||
assert.equal(token, "env-token");
|
||||
});
|
||||
|
||||
await it("rejects an empty standard input token", async () => {
|
||||
await assert.rejects(
|
||||
resolveToken(
|
||||
{ tokenStdin: true },
|
||||
{ env: {}, readStdin: async () => "\n" },
|
||||
),
|
||||
/No token received on standard input/,
|
||||
);
|
||||
});
|
||||
|
||||
await it("rejects missing token sources", async () => {
|
||||
await assert.rejects(
|
||||
resolveToken({}, { env: {}, readStdin: async () => "" }),
|
||||
/Missing authentication token/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
|
||||
/** Represents the command-line options. */
|
||||
export interface Options {
|
||||
/** The token to use to authenticate to the GitHub API. */
|
||||
token?: string;
|
||||
/** Whether to read the GitHub API token from standard input. */
|
||||
tokenStdin?: boolean;
|
||||
/** The git ref to use the checks for. */
|
||||
ref?: string;
|
||||
/** Whether to actually apply the changes or not. */
|
||||
@@ -31,6 +31,65 @@ const codeqlActionRepo = {
|
||||
repo: "codeql-action",
|
||||
};
|
||||
|
||||
/** Environment variables to check for a GitHub API token. */
|
||||
const TOKEN_ENVIRONMENT_VARIABLES = ["GH_TOKEN", "GITHUB_TOKEN"];
|
||||
|
||||
/** Represents the sources from which we can retrieve the GitHub API token. */
|
||||
interface TokenSource {
|
||||
/** Environment variables to inspect. */
|
||||
env: NodeJS.ProcessEnv;
|
||||
/** Reads a token from standard input. */
|
||||
readStdin: () => Promise<string>;
|
||||
}
|
||||
|
||||
/** Reads the GitHub API token from standard input. */
|
||||
async function readTokenFromStdin(): Promise<string> {
|
||||
let token = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
for await (const chunk of process.stdin) {
|
||||
token += chunk;
|
||||
}
|
||||
return token.trim();
|
||||
}
|
||||
|
||||
/** Gets a GitHub API token from one of the supported environment variables. */
|
||||
function getTokenFromEnvironment(env: NodeJS.ProcessEnv): string | undefined {
|
||||
for (const variableName of TOKEN_ENVIRONMENT_VARIABLES) {
|
||||
const token = env[variableName]?.trim();
|
||||
if (token) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Gets the token to use to authenticate to the GitHub API. */
|
||||
export async function resolveToken(
|
||||
options: Pick<Options, "tokenStdin">,
|
||||
tokenSource: TokenSource = {
|
||||
env: process.env,
|
||||
readStdin: readTokenFromStdin,
|
||||
},
|
||||
): Promise<string> {
|
||||
if (options.tokenStdin) {
|
||||
const token = (await tokenSource.readStdin()).trim();
|
||||
if (token.length === 0) {
|
||||
throw new Error("No token received on standard input.");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
const environmentToken = getTokenFromEnvironment(tokenSource.env);
|
||||
if (environmentToken !== undefined) {
|
||||
return environmentToken;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Missing authentication token. Set GH_TOKEN/GITHUB_TOKEN or pipe a token " +
|
||||
"to --token-stdin.",
|
||||
);
|
||||
}
|
||||
|
||||
/** Represents a configuration of which checks should not be set up as required checks. */
|
||||
export interface Exclusions {
|
||||
/** A list of strings that, if contained in a check name, are excluded. */
|
||||
@@ -205,9 +264,10 @@ async function updateBranch(
|
||||
async function main(): Promise<void> {
|
||||
const { values: options } = parseArgs({
|
||||
options: {
|
||||
// The token to use to authenticate to the API.
|
||||
token: {
|
||||
type: "string",
|
||||
// Read the token to use to authenticate to the API from standard input.
|
||||
"token-stdin": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
},
|
||||
// The git ref for which to retrieve the check runs.
|
||||
ref: {
|
||||
@@ -228,16 +288,16 @@ async function main(): Promise<void> {
|
||||
strict: true,
|
||||
});
|
||||
|
||||
if (options.token === undefined) {
|
||||
throw new Error("Missing --token");
|
||||
}
|
||||
const token = await resolveToken({
|
||||
tokenStdin: options["token-stdin"],
|
||||
});
|
||||
|
||||
console.info(
|
||||
`Oldest supported major version is: ${OLDEST_SUPPORTED_MAJOR_VERSION}`,
|
||||
);
|
||||
|
||||
// Initialise the API client.
|
||||
const client = getApiClient(options.token);
|
||||
const client = getApiClient(token);
|
||||
|
||||
// Find the check runs for the specified `ref` that we will later set as the required checks
|
||||
// for the main and release branches.
|
||||
|
||||
Reference in New Issue
Block a user