Skip to content
Merged
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
21 changes: 13 additions & 8 deletions .github/update-release-branch.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,23 @@
"""

# 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).
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Comment thread
mbg marked this conversation as resolved.
# 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')
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/update-release-branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}' \
Expand Down Expand Up @@ -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} \
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
51 changes: 50 additions & 1 deletion pr-checks/sync-checks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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/,
);
});
});
78 changes: 69 additions & 9 deletions pr-checks/sync-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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();
Comment on lines +46 to +52
}

/** 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. */
Expand Down Expand Up @@ -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: {
Expand All @@ -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.
Expand Down
Loading