mirror of
https://github.com/github/codeql-action
synced 2026-10-27 10:01:50 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08d09a53f0 | ||
|
|
74406f0856 | ||
|
|
8dfbacbba5 | ||
|
|
ab2931b320 | ||
|
|
c87e7fae54 | ||
|
|
5a1ba3bdf6 | ||
|
|
e0647621c2 | ||
|
|
e0faed8391 | ||
|
|
73aad0eaa9 | ||
|
|
385bcdc5af | ||
|
|
de0229cea1 | ||
|
|
115e8cbbe9 | ||
|
|
dbdf0b0c7d | ||
|
|
830c23121c | ||
|
|
69fd9e97ba | ||
|
|
b85568788a | ||
|
|
dd35309c87 | ||
|
|
da21ad6a71 | ||
|
|
cf463419fd | ||
|
|
7db34ae6f4 | ||
|
|
8125f87336 | ||
|
|
b7351df727 | ||
|
|
27e669d3d6 | ||
|
|
1138fefa2b | ||
|
|
7c4a258544 | ||
|
|
3fa858af16 | ||
|
|
4b646a0e5d | ||
|
|
44c5914b66 | ||
|
|
0d531cfd92 | ||
|
|
0297913805 | ||
|
|
1226301537 | ||
|
|
7188fc3636 | ||
|
|
c8b5f69be6 | ||
|
|
9e7c070092 | ||
|
|
4292bd7215 | ||
|
|
7b19180f8d | ||
|
|
3492b7e9ab | ||
|
|
3654baa924 | ||
|
|
2d682ac05f | ||
|
|
23f6a50753 | ||
|
|
1ee3c75d19 | ||
|
|
e053684dc5 | ||
|
|
6803c5671d | ||
|
|
8507f884db | ||
|
|
c31b06d582 | ||
|
|
cc5c777dcd | ||
|
|
c224750821 | ||
|
|
db76a66b98 | ||
|
|
c263ed7d83 | ||
|
|
60e79ccb20 | ||
|
|
b82a08934e | ||
|
|
247ce88c8f | ||
|
|
decb87044a | ||
|
|
abce6483ea | ||
|
|
23339952d6 | ||
|
|
57ca205769 | ||
|
|
001d4ae610 | ||
|
|
6d3bde3729 | ||
|
|
9832a9df15 | ||
|
|
c73d965e34 | ||
|
|
9c0c11a49e | ||
|
|
423e3416b1 | ||
|
|
41c7af7cf0 | ||
|
|
e58424170f | ||
|
|
1a0eca3555 | ||
|
|
d705aa30b5 | ||
|
|
405f67d326 | ||
|
|
f3d47397e8 | ||
|
|
8aa21b44a3 | ||
|
|
358d197a13 | ||
|
|
8bc3738008 | ||
|
|
a1c676d2f6 | ||
|
|
47e9c29998 | ||
|
|
b946565527 | ||
|
|
205b37b035 | ||
|
|
6d70593fb7 | ||
|
|
80599cc5d9 | ||
|
|
ae48798f3b | ||
|
|
a464bf19e9 | ||
|
|
9b314f4394 | ||
|
|
d694648fd8 | ||
|
|
29d5e4cb18 | ||
|
|
583bf3e8c5 | ||
|
|
f9a9f4862b | ||
|
|
d4b3323463 | ||
|
|
fd0ae66c1e | ||
|
|
e2472fc5f9 | ||
|
|
4b861b89fc | ||
|
|
4f688dedd0 | ||
|
|
28a0813a12 | ||
|
|
3d2713f64e | ||
|
|
2c45c8158b | ||
|
|
5c030f4a48 | ||
|
|
78d71fb252 | ||
|
|
5e212030b8 | ||
|
|
639fc5d7ea | ||
|
|
c8ed70e459 | ||
|
|
212aa33f48 | ||
|
|
460cc0c970 | ||
|
|
557921759b | ||
|
|
c3da0a9ad3 | ||
|
|
4ca9f5301b | ||
|
|
440cebc19d | ||
|
|
1b146d1b6a | ||
|
|
d0b11cae68 | ||
|
|
a10d7a7891 | ||
|
|
14952376dc | ||
|
|
85052938f8 | ||
|
|
4c2bf01170 | ||
|
|
2f9048cfbd | ||
|
|
2dbfdcaa83 | ||
|
|
1542951d09 | ||
|
|
4fb6147969 | ||
|
|
f133996fb5 | ||
|
|
50b3687dd7 | ||
|
|
b6d92e33f7 | ||
|
|
b0eaa56a8f | ||
|
|
e387ec1de1 | ||
|
|
a29dee455c | ||
|
|
5172487de5 | ||
|
|
8763bac625 | ||
|
|
dab0ab7cb7 | ||
|
|
0c2cea31ba | ||
|
|
75773288da | ||
|
|
3f1e4da48d | ||
|
|
89343ee1aa | ||
|
|
ba5fbf159a | ||
|
|
9a1225df61 | ||
|
|
aec5441507 | ||
|
|
f8ed33e067 | ||
|
|
ec0b8ae45f | ||
|
|
36ef889329 | ||
|
|
50d76e2fa3 | ||
|
|
6a62e99aa7 | ||
|
|
fb5048ee88 | ||
|
|
f181d70282 | ||
|
|
2773684c29 | ||
|
|
66c15f441a | ||
|
|
8096e4f957 | ||
|
|
85830a2768 | ||
|
|
f630f8fd04 | ||
|
|
26cbf9f504 | ||
|
|
e26215c5d8 | ||
|
|
0b10dee3ee | ||
|
|
512bf6f1a3 | ||
|
|
8d8f054dfa | ||
|
|
e4752e74e1 | ||
|
|
cee0056fc0 | ||
|
|
7c961fa357 | ||
|
|
847e7af699 | ||
|
|
29c712e0b7 | ||
|
|
9573f05654 | ||
|
|
98dbd66eae | ||
|
|
1f91ec8f3c | ||
|
|
dc2d916874 | ||
|
|
6973946465 | ||
|
|
6829d6ce38 | ||
|
|
d2472aa525 | ||
|
|
ff973a1072 | ||
|
|
ed1bc3d57d | ||
|
|
ac331c28db | ||
|
|
9a9ab4cc7d | ||
|
|
d8866c2578 | ||
|
|
355aa4a77f | ||
|
|
81d0f34a44 | ||
|
|
d0a69d77dc | ||
|
|
a1e3f70a6f | ||
|
|
6b3ba17c47 | ||
|
|
da75ca58fa | ||
|
|
ab6fd520bd | ||
|
|
c77051d879 | ||
|
|
1a57d7ae11 | ||
|
|
6a782906dc | ||
|
|
a4319e3365 | ||
|
|
a8ac1e029c | ||
|
|
b571644f21 | ||
|
|
31842a7e06 | ||
|
|
c23977f57b | ||
|
|
0c7580ae89 | ||
|
|
37ea21c1fc | ||
|
|
4b164dbc17 | ||
|
|
97b52d19da | ||
|
|
3f2a29899f | ||
|
|
80dcb7ccdf | ||
|
|
a440e35eda | ||
|
|
be67a7be93 | ||
|
|
732911a2fa | ||
|
|
1d0e1b20e5 | ||
|
|
4c868b3e7e | ||
|
|
8d24507592 | ||
|
|
c7d9083db4 | ||
|
|
2831d9a0e9 | ||
|
|
258af3df60 | ||
|
|
d8168ab655 | ||
|
|
ac36b7d725 | ||
|
|
9fdfd3e4c8 | ||
|
|
1c42f21061 | ||
|
|
06898d7b8b | ||
|
|
7ccd988125 | ||
|
|
ef79560bc8 | ||
|
|
4581e395f3 | ||
|
|
76d37c509a | ||
|
|
02c5e83432 | ||
|
|
0e974e6288 | ||
|
|
721b977126 | ||
|
|
af1b30ee40 | ||
|
|
530c909a55 | ||
|
|
0507814306 | ||
|
|
157d34d3cf | ||
|
|
b8c8426f7c | ||
|
|
329fc9cde0 | ||
|
|
f27c81bb1c | ||
|
|
99df26d4f1 | ||
|
|
31c27074fd | ||
|
|
72df2181aa | ||
|
|
c7c896d71b | ||
|
|
3f34ff0ea3 | ||
|
|
43bec09f1d | ||
|
|
f58f0d11eb | ||
|
|
8be707bd2c | ||
|
|
7dc37cbb5b | ||
|
|
8e22350a7e | ||
|
|
69c9e8c7d9 | ||
|
|
73f4741039 | ||
|
|
5094019011 | ||
|
|
006d029c8d | ||
|
|
8476401b09 | ||
|
|
4509fb3c4b | ||
|
|
44d6b3b016 | ||
|
|
d309c0e332 | ||
|
|
e34d7fe5bf | ||
|
|
5fa9c0c3e2 | ||
|
|
eba9d55e1f | ||
|
|
9a06fa6b38 | ||
|
|
764f470b74 | ||
|
|
d149f93d92 | ||
|
|
3707b44eaa | ||
|
|
42c0c6aedd | ||
|
|
31577749b3 | ||
|
|
18d20b7c42 | ||
|
|
d1db924e0b | ||
|
|
0188f395e2 | ||
|
|
6860cc1f50 | ||
|
|
53a488ce2e | ||
|
|
132634ddfd | ||
|
|
a52a9662e9 | ||
|
|
cd604f831b | ||
|
|
c5128610cf | ||
|
|
ae4c8d057d | ||
|
|
73b0850a89 | ||
|
|
5e3e6b81b9 | ||
|
|
3cf0a529d8 | ||
|
|
de65da38e5 | ||
|
|
b16b719b76 | ||
|
|
37ae669b89 | ||
|
|
b987514f02 | ||
|
|
a04a87c9c9 | ||
|
|
c1e7c7a0de | ||
|
|
50bd53bfbc | ||
|
|
289efcad49 | ||
|
|
17a28bc4c5 | ||
|
|
cbf67da2fa | ||
|
|
4c18ab020b | ||
|
|
d5b8046386 | ||
|
|
a8958a9883 | ||
|
|
1ba744977b | ||
|
|
e0f825ed66 | ||
|
|
fbcae9afa6 | ||
|
|
8f47ee793f | ||
|
|
67ec1e3914 | ||
|
|
1578ba0b4f | ||
|
|
ef29bf2e43 | ||
|
|
ceac4af134 | ||
|
|
772e8e9310 | ||
|
|
fcd9f6cbac | ||
|
|
a9ceac150d | ||
|
|
6136b93088 | ||
|
|
6ab762cada | ||
|
|
a3a1cdd43a | ||
|
|
9c16c3b47b | ||
|
|
b009eac559 | ||
|
|
26e9e9e932 | ||
|
|
9b35ab94e1 | ||
|
|
e1905475d1 | ||
|
|
2399df7427 | ||
|
|
7d1c6e90aa | ||
|
|
6cdc6063c0 | ||
|
|
ffb3423848 | ||
|
|
708e106533 | ||
|
|
b6be81d5b9 | ||
|
|
7b2af8940d | ||
|
|
f86aa521ce | ||
|
|
e446d55e5a | ||
|
|
d8d1d6d754 | ||
|
|
f77cf557cd | ||
|
|
812b882dee | ||
|
|
3fe7ef97d3 | ||
|
|
8d69da902e | ||
|
|
00e5a58139 | ||
|
|
8102fa6675 | ||
|
|
81ad479e13 | ||
|
|
12821cff0c | ||
|
|
e537ff20a4 | ||
|
|
598d00854a | ||
|
|
85c8a8cebe | ||
|
|
c7a94c979d | ||
|
|
82e5ca6c55 | ||
|
|
9ef8be7176 | ||
|
|
07b6b1eac5 | ||
|
|
9c4ee01a28 | ||
|
|
652296eb9e | ||
|
|
c05a5b71ad | ||
|
|
1023397ae4 | ||
|
|
68ec727966 | ||
|
|
cb4720a101 |
No files matched your search
@@ -22,7 +22,6 @@ runs:
|
||||
MAJOR_VERSION: ${{ inputs.major_version }}
|
||||
LATEST_TAG: ${{ inputs.latest_tag }}
|
||||
run: |
|
||||
npm ci
|
||||
npx tsx ./pr-checks/release-branches.ts \
|
||||
--major-version "$MAJOR_VERSION" \
|
||||
--latest-tag "$LATEST_TAG"
|
||||
|
||||
@@ -21,6 +21,10 @@ runs:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install JavaScript dependencies
|
||||
shell: bash
|
||||
run: npm ci
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
|
||||
@@ -1,474 +0,0 @@
|
||||
import argparse
|
||||
import datetime
|
||||
import fileinput
|
||||
import re
|
||||
from github import Github
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
EMPTY_CHANGELOG = """# CodeQL Action Changelog
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
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.
|
||||
BACKPORT_COMMIT_MESSAGE = 'Update version and changelog for v'
|
||||
|
||||
# Commit message used for rebuild commits, both those produced by this script and those produced
|
||||
# by the `Rebuild Action` workflow (`.github/workflows/rebuild.yml`).
|
||||
REBUILD_COMMIT_MESSAGE = 'Rebuild'
|
||||
|
||||
# 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).
|
||||
def run_git(*args, allow_non_zero_exit_code=False):
|
||||
cmd = ['git', *args]
|
||||
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if not allow_non_zero_exit_code and p.returncode != 0:
|
||||
raise Exception(f'Call to {" ".join(cmd)} exited with code {p.returncode} stderr: {p.stderr.decode("ascii")}.')
|
||||
return p.stdout.decode('ascii')
|
||||
|
||||
# Runs the given command, streaming output to the console.
|
||||
# Raises an error if the command does not exit successfully.
|
||||
def run_command(*args):
|
||||
cmd = list(args)
|
||||
print(f'Running `{" ".join(cmd)}`.')
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
# Rebuilds the action and commits any changes.
|
||||
def rebuild_action():
|
||||
# For backports, the only source-level change vs the source branch is the new version number,
|
||||
# so we just need to refresh the version embedded in `lib/`.
|
||||
run_command('npm', 'ci')
|
||||
run_command('npm', 'run', 'build')
|
||||
|
||||
run_git('add', '--all')
|
||||
# `git diff --cached --quiet` exits 0 if there are no staged changes, 1 if there are.
|
||||
if subprocess.run(['git', 'diff', '--cached', '--quiet']).returncode == 0:
|
||||
print('Rebuild produced no changes; skipping Rebuild commit.')
|
||||
else:
|
||||
run_git('commit', '-m', REBUILD_COMMIT_MESSAGE)
|
||||
print('Created Rebuild commit.')
|
||||
|
||||
# Returns true if the given branch exists on the origin remote
|
||||
def branch_exists_on_remote(branch_name):
|
||||
return run_git('ls-remote', '--heads', ORIGIN, branch_name).strip() != ''
|
||||
|
||||
# Opens a PR from the given branch to the target branch
|
||||
def open_pr(
|
||||
repo, all_commits, source_branch_short_sha, new_branch_name, source_branch, target_branch,
|
||||
conductor, is_primary_release, conflicted_files):
|
||||
# Sort the commits into the pull requests that introduced them,
|
||||
# and any commits that don't have a pull request
|
||||
pull_requests = []
|
||||
commits_without_pull_requests = []
|
||||
for commit in all_commits:
|
||||
pr = get_pr_for_commit(commit)
|
||||
|
||||
if pr is None:
|
||||
commits_without_pull_requests.append(commit)
|
||||
elif not any(p for p in pull_requests if p.number == pr.number):
|
||||
pull_requests.append(pr)
|
||||
|
||||
print(f'Found {len(pull_requests)} pull requests.')
|
||||
print(f'Found {len(commits_without_pull_requests)} commits not in a pull request.')
|
||||
|
||||
# Sort PRs and commits by age
|
||||
pull_requests = sorted(pull_requests, key=lambda pr: pr.number)
|
||||
commits_without_pull_requests = sorted(commits_without_pull_requests, key=lambda c: c.commit.author.date)
|
||||
|
||||
# Start constructing the body text
|
||||
body = []
|
||||
body.append(f'Merging {source_branch_short_sha} into `{target_branch}`.')
|
||||
|
||||
body.append('')
|
||||
body.append(f'Conductor for this PR is @{conductor}.')
|
||||
|
||||
# List all PRs merged
|
||||
if len(pull_requests) > 0:
|
||||
body.append('')
|
||||
body.append('Contains the following pull requests:')
|
||||
for pr in pull_requests:
|
||||
# Use PR author if they are GitHub staff, otherwise use the merger
|
||||
display_user = get_pr_author_if_staff(pr) or get_merger_of_pr(repo, pr)
|
||||
body.append(f'- #{pr.number} (@{display_user})')
|
||||
|
||||
# List all commits not part of a PR
|
||||
if len(commits_without_pull_requests) > 0:
|
||||
body.append('')
|
||||
body.append('Contains the following commits not from a pull request:')
|
||||
for commit in commits_without_pull_requests:
|
||||
author_description = f' (@{commit.author.login})' if commit.author is not None else ''
|
||||
body.append(f'- {commit.sha} - {get_truncated_commit_message(commit)}{author_description}')
|
||||
|
||||
body.append('')
|
||||
body.append('Please do the following:')
|
||||
if len(conflicted_files) > 0:
|
||||
body.append(' - [ ] Ensure `package.json` file contains the correct version.')
|
||||
body.append(' - [ ] Add a commit to this branch to resolve the merge conflicts ' +
|
||||
'in the following files:')
|
||||
body.extend([f' - `{file}`' for file in conflicted_files])
|
||||
body.append(' - [ ] Rebuild the Action locally (`npm run build`) and push any changes to the ' +
|
||||
f'built output in `lib` as a separate commit named exactly `{REBUILD_COMMIT_MESSAGE}`.')
|
||||
body.append(' - [ ] Ensure another maintainer has reviewed the additional commits you added to this ' +
|
||||
'branch to resolve the merge conflicts.')
|
||||
body.append(' - [ ] Ensure the CHANGELOG displays the correct version and date.')
|
||||
body.append(' - [ ] Ensure the CHANGELOG includes all relevant, user-facing changes since the last release.')
|
||||
body.append(f' - [ ] Check that there are not any unexpected commits being merged into the `{target_branch}` branch.')
|
||||
body.append(' - [ ] Ensure the docs team is aware of any documentation changes that need to be released.')
|
||||
|
||||
body.append(' - [ ] Approve running the full set of PR checks if you have not pushed any changes.')
|
||||
body.append(' - [ ] Approve and merge this PR. Make sure `Create a merge commit` is selected rather than `Squash and merge` or `Rebase and merge`.')
|
||||
|
||||
if is_primary_release:
|
||||
body.append(' - [ ] Merge the mergeback PR that will automatically be created once this PR is merged.')
|
||||
body.append(' - [ ] Merge all backport PRs to older release branches, that will automatically be created once this PR is merged.')
|
||||
|
||||
title = f'Merge {source_branch} into {target_branch}'
|
||||
|
||||
# Create the pull request
|
||||
pr = repo.create_pull(title=title, body='\n'.join(body), head=new_branch_name, base=target_branch)
|
||||
print(f'Created PR #{str(pr.number)}')
|
||||
|
||||
# Assign the conductor
|
||||
pr.add_to_assignees(conductor)
|
||||
print(f'Assigned PR to {conductor}')
|
||||
|
||||
# Gets a list of the SHAs of all commits that have happened on the source branch
|
||||
# since the last release to the target branch.
|
||||
# This will not include any commits that exist on the target branch
|
||||
# that aren't on the source branch.
|
||||
def get_commit_difference(repo, source_branch, target_branch):
|
||||
# Passing split nothing means that the empty string splits to nothing: compare `''.split() == []`
|
||||
# to `''.split('\n') == ['']`.
|
||||
commits = run_git('log', '--pretty=format:%H', f'{ORIGIN}/{target_branch}..{ORIGIN}/{source_branch}').strip().split()
|
||||
|
||||
# Convert to full-fledged commit objects
|
||||
commits = [repo.get_commit(c) for c in commits]
|
||||
|
||||
# Filter out merge commits for PRs
|
||||
return list(filter(lambda c: not is_pr_merge_commit(c), commits))
|
||||
|
||||
# Is the given commit the automatic merge commit from when merging a PR
|
||||
def is_pr_merge_commit(commit):
|
||||
return commit.committer is not None and commit.committer.login == 'web-flow' and len(commit.parents) > 1
|
||||
|
||||
# Gets a copy of the commit message that should display nicely
|
||||
def get_truncated_commit_message(commit):
|
||||
message = commit.commit.message.split('\n')[0]
|
||||
if len(message) > 60:
|
||||
return f'{message[:57]}...'
|
||||
else:
|
||||
return message
|
||||
|
||||
# Converts a commit into the PR that introduced it to the source branch.
|
||||
# Returns the PR object, or None if no PR could be found.
|
||||
def get_pr_for_commit(commit):
|
||||
prs = commit.get_pulls()
|
||||
|
||||
if prs.totalCount > 0:
|
||||
# In the case that there are multiple PRs, return the earliest one
|
||||
prs = list(prs)
|
||||
sorted_prs = sorted(prs, key=lambda pr: int(pr.number))
|
||||
return sorted_prs[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
# Get the person who merged the pull request.
|
||||
# For most cases this will be the same as the author, but for PRs opened
|
||||
# by external contributors getting the merger will get us the GitHub
|
||||
# employee who reviewed and merged the PR.
|
||||
def get_merger_of_pr(repo, pr):
|
||||
return repo.get_commit(pr.merge_commit_sha).author.login
|
||||
|
||||
# Get the PR author if they are GitHub staff, otherwise None.
|
||||
def get_pr_author_if_staff(pr):
|
||||
if pr.user is None:
|
||||
return None
|
||||
if getattr(pr.user, 'site_admin', False):
|
||||
return pr.user.login
|
||||
return None
|
||||
|
||||
def get_current_version():
|
||||
with open('package.json', 'r') as f:
|
||||
return json.load(f)['version']
|
||||
|
||||
# `npm version` doesn't always work because of merge conflicts, so we
|
||||
# replace the version in package.json textually.
|
||||
def replace_version_package_json(prev_version, new_version):
|
||||
prev_line_is_codeql = False
|
||||
for line in fileinput.input('package.json', inplace = True, encoding='utf-8'):
|
||||
if prev_line_is_codeql and f'\"version\": \"{prev_version}\"' in line:
|
||||
print(line.replace(prev_version, new_version), end='')
|
||||
else:
|
||||
prev_line_is_codeql = False
|
||||
print(line, end='')
|
||||
if '\"name\": \"codeql\",' in line:
|
||||
prev_line_is_codeql = True
|
||||
|
||||
def get_today_string():
|
||||
today = datetime.datetime.today()
|
||||
return '{:%d %b %Y}'.format(today)
|
||||
|
||||
def process_changelog_for_backports(source_branch_major_version, target_branch_major_version):
|
||||
|
||||
# changelog entries can use the following format to indicate
|
||||
# that they only apply to newer versions
|
||||
some_versions_only_regex = re.compile(r'\[v(\d+)\+ only\]')
|
||||
|
||||
output = ''
|
||||
|
||||
with open('CHANGELOG.md', 'r') as f:
|
||||
|
||||
# until we find the first section, just duplicate all lines
|
||||
found_first_section = False
|
||||
while not found_first_section:
|
||||
line = f.readline()
|
||||
if not line:
|
||||
raise Exception('Could not find any change sections in CHANGELOG.md') # EOF
|
||||
|
||||
if line.startswith('## '):
|
||||
line = line.replace(f'## {source_branch_major_version}', f'## {target_branch_major_version}')
|
||||
found_first_section = True
|
||||
|
||||
output += line
|
||||
|
||||
# found_content tracks whether we hit two headings in a row
|
||||
found_content = False
|
||||
output += '\n'
|
||||
while True:
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break # EOF
|
||||
line = line.rstrip('\n')
|
||||
|
||||
# filter out changenote entries that apply only to newer versions
|
||||
match = some_versions_only_regex.search(line)
|
||||
if match:
|
||||
if int(target_branch_major_version) < int(match.group(1)):
|
||||
continue
|
||||
|
||||
if line.startswith('## '):
|
||||
line = line.replace(f'## {source_branch_major_version}', f'## {target_branch_major_version}')
|
||||
if found_content == False:
|
||||
# we have found two headings in a row, so we need to add the placeholder message.
|
||||
output += 'No user facing changes.\n'
|
||||
found_content = False
|
||||
output += f'\n{line}\n\n'
|
||||
else:
|
||||
if line.strip() != '':
|
||||
found_content = True
|
||||
# we use the original line here, rather than the stripped version
|
||||
# so that we preserve indentation
|
||||
output += line + '\n'
|
||||
|
||||
with open('CHANGELOG.md', 'w') as f:
|
||||
f.write(output)
|
||||
|
||||
def update_changelog(version):
|
||||
if (os.path.exists('CHANGELOG.md')):
|
||||
content = ''
|
||||
with open('CHANGELOG.md', 'r') as f:
|
||||
content = f.read()
|
||||
else:
|
||||
content = EMPTY_CHANGELOG
|
||||
|
||||
newContent = content.replace('[UNRELEASED]', f'{version} - {get_today_string()}', 1)
|
||||
|
||||
with open('CHANGELOG.md', 'w') as f:
|
||||
f.write(newContent)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser('update-release-branch.py')
|
||||
|
||||
parser.add_argument(
|
||||
'--repository-nwo',
|
||||
type=str,
|
||||
required=True,
|
||||
help='The nwo of the repository, for example github/codeql-action.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--source-branch',
|
||||
type=str,
|
||||
required=True,
|
||||
help='Source branch for release branch update.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--target-branch',
|
||||
type=str,
|
||||
required=True,
|
||||
help='Target branch for release branch update.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--is-primary-release',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Whether this update is the primary release for the current major version.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--conductor',
|
||||
type=str,
|
||||
required=True,
|
||||
help='The GitHub handle of the person who is conducting the release process.'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
source_branch = args.source_branch
|
||||
target_branch = args.target_branch
|
||||
is_primary_release = args.is_primary_release
|
||||
|
||||
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')
|
||||
|
||||
# split version into major, minor, patch
|
||||
_, v_minor, v_patch = get_current_version().split('.')
|
||||
|
||||
version = f"{target_branch_major_version}.{v_minor}.{v_patch}"
|
||||
|
||||
# Print what we intend to go
|
||||
print(f'Considering difference between {source_branch} and {target_branch}...')
|
||||
source_branch_short_sha = run_git('rev-parse', '--short', f'{ORIGIN}/{source_branch}').strip()
|
||||
print(f'Current head of {source_branch} is {source_branch_short_sha}.')
|
||||
|
||||
# See if there are any commits to merge in
|
||||
commits = get_commit_difference(repo=repo, source_branch=source_branch, target_branch=target_branch)
|
||||
if len(commits) == 0:
|
||||
print(f'No commits to merge from {source_branch} to {target_branch}.')
|
||||
return
|
||||
|
||||
# define distinct prefix in order to support specific pr checks on backports
|
||||
branch_prefix = 'update' if is_primary_release else 'backport'
|
||||
|
||||
# The branch name is based off of the name of branch being merged into
|
||||
# and the SHA of the branch being merged from. Thus if the branch already
|
||||
# exists we can assume we don't need to recreate it.
|
||||
new_branch_name = f'{branch_prefix}-v{version}-{source_branch_short_sha}'
|
||||
print(f'Branch name is {new_branch_name}.')
|
||||
|
||||
# Check if the branch already exists. If so we can abort as this script
|
||||
# has already run on this combination of branches.
|
||||
if branch_exists_on_remote(new_branch_name):
|
||||
print(f'Branch {new_branch_name} already exists. Nothing to do.')
|
||||
return
|
||||
|
||||
# Create the new branch and push it to the remote
|
||||
print(f'Creating branch {new_branch_name}.')
|
||||
|
||||
# The process of creating the v{Older} release can run into merge conflicts. We commit the unresolved
|
||||
# conflicts so a maintainer can easily resolve them (vs erroring and requiring maintainers to
|
||||
# reconstruct the release manually)
|
||||
conflicted_files = []
|
||||
|
||||
if not is_primary_release:
|
||||
|
||||
# the source branch will be of the form releases/vN, where N is the major version number
|
||||
source_branch_major_version = source_branch.strip('releases/v')
|
||||
|
||||
# If we're performing a backport, start from the target branch
|
||||
print(f'Creating {new_branch_name} from the {ORIGIN}/{target_branch} branch')
|
||||
run_git('checkout', '-b', new_branch_name, f'{ORIGIN}/{target_branch}')
|
||||
|
||||
# Revert the commit that we made as part of the last release that updated the version number and
|
||||
# changelog to refer to {older}.x.x variants. This avoids merge conflicts in the changelog and
|
||||
# package.json files when we merge in the v{latest} branch.
|
||||
# This commit will not exist the first time we release the v{N-1} branch from the v{N} branch, so we
|
||||
# use `git log --grep` to conditionally revert the commit.
|
||||
print('Reverting the version number and changelog updates from the last release to avoid conflicts')
|
||||
vOlder_update_commits = run_git('log', '--grep', f'^{BACKPORT_COMMIT_MESSAGE}', '--format=%H').split()
|
||||
|
||||
if len(vOlder_update_commits) > 0:
|
||||
print(f' Reverting {vOlder_update_commits[0]}')
|
||||
# Only revert the newest commit as older ones will already have been reverted in previous
|
||||
# releases.
|
||||
run_git('revert', vOlder_update_commits[0], '--no-edit')
|
||||
|
||||
# Also revert the "Rebuild" commit, whether created by this script or by the
|
||||
# `Rebuild Action` workflow.
|
||||
rebuild_commit = run_git('log', '--grep', f'^{REBUILD_COMMIT_MESSAGE}$', '--format=%H').split()[0]
|
||||
print(f' Reverting {rebuild_commit}')
|
||||
run_git('revert', rebuild_commit, '--no-edit')
|
||||
|
||||
else:
|
||||
print(' Nothing to revert.')
|
||||
|
||||
print(f'Merging {ORIGIN}/{source_branch} into the release prep branch')
|
||||
# Commit any conflicts (see the comment for `conflicted_files`)
|
||||
run_git('merge', f'{ORIGIN}/{source_branch}', allow_non_zero_exit_code=True)
|
||||
conflicted_files = run_git('diff', '--name-only', '--diff-filter', 'U').splitlines()
|
||||
if len(conflicted_files) > 0:
|
||||
run_git('add', '.')
|
||||
run_git('commit', '--no-edit')
|
||||
|
||||
# Migrate the package version number from a vLatest version number to a vOlder version number.
|
||||
# `package-lock.json` is updated as part of the subsequent rebuild step (see `rebuild_action`).
|
||||
print(f'Setting version number to {version} in package.json')
|
||||
replace_version_package_json(get_current_version(), version)
|
||||
run_git('add', 'package.json')
|
||||
|
||||
# Migrate the changelog notes from vLatest version numbers to vOlder version numbers
|
||||
print(f'Migrating changelog notes from v{source_branch_major_version} to v{target_branch_major_version}')
|
||||
process_changelog_for_backports(source_branch_major_version, target_branch_major_version)
|
||||
|
||||
# Amend the commit generated by `npm version` to update the CHANGELOG
|
||||
run_git('add', 'CHANGELOG.md')
|
||||
run_git('commit', '-m', f'{BACKPORT_COMMIT_MESSAGE}{version}')
|
||||
else:
|
||||
# If we're performing a standard release, there won't be any new commits on the target branch,
|
||||
# as these will have already been merged back into the source branch. Therefore we can just
|
||||
# start from the source branch.
|
||||
run_git('checkout', '-b', new_branch_name, f'{ORIGIN}/{source_branch}')
|
||||
|
||||
print('Updating changelog')
|
||||
update_changelog(version)
|
||||
|
||||
# Create a commit that updates the CHANGELOG
|
||||
run_git('add', 'CHANGELOG.md')
|
||||
run_git('commit', '-m', f'Update changelog for v{version}')
|
||||
|
||||
if not is_primary_release:
|
||||
if len(conflicted_files) == 0:
|
||||
print('Rebuilding the Action.')
|
||||
rebuild_action()
|
||||
else:
|
||||
print(f'Skipping automatic rebuild because the merge produced conflicts in {conflicted_files}.')
|
||||
|
||||
run_git('push', ORIGIN, new_branch_name)
|
||||
|
||||
# Open a PR to update the branch
|
||||
open_pr(
|
||||
repo,
|
||||
commits,
|
||||
source_branch_short_sha,
|
||||
new_branch_name,
|
||||
source_branch=source_branch,
|
||||
target_branch=target_branch,
|
||||
conductor=args.conductor,
|
||||
is_primary_release=is_primary_release,
|
||||
conflicted_files=conflicted_files
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+1
-1
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
Generated
+1
-1
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Prepare test
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
with:
|
||||
java-version: ${{ inputs.java-version || '17' }}
|
||||
distribution: temurin
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
with:
|
||||
java-version: ${{ inputs.java-version || '17' }}
|
||||
distribution: temurin
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
Generated
+1
-1
@@ -47,7 +47,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
Generated
+1
-1
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
go-version: ${{ inputs.go-version || '>=1.21.0' }}
|
||||
cache: false
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
go-version: ${{ inputs.go-version || '>=1.21.0' }}
|
||||
cache: false
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
go-version: ${{ inputs.go-version || '>=1.21.0' }}
|
||||
cache: false
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
go-version: ${{ inputs.go-version || '>=1.21.0' }}
|
||||
cache: false
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
|
||||
Generated
+1
-1
@@ -69,7 +69,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ jobs:
|
||||
use-all-platform-bundle: 'false'
|
||||
setup-kotlin: 'true'
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@89f90524b88a01fe6e0b732220432cc6142926af # v1.313.0
|
||||
uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
|
||||
with:
|
||||
ruby-version: 2.6
|
||||
- name: Install Code Scanning integration
|
||||
|
||||
Generated
+1
-1
@@ -77,7 +77,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
Generated
+12
-5
@@ -57,15 +57,11 @@ jobs:
|
||||
version: ${{ matrix.version }}
|
||||
use-all-platform-bundle: 'false'
|
||||
setup-kotlin: 'true'
|
||||
- uses: ./../action/init
|
||||
with:
|
||||
languages: csharp
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
|
||||
- name: Setup proxy for registries
|
||||
id: proxy
|
||||
uses: ./../action/start-proxy
|
||||
with:
|
||||
language: java
|
||||
registry_secrets: |
|
||||
[
|
||||
{
|
||||
@@ -94,5 +90,16 @@ jobs:
|
||||
|| !contains(steps.proxy.outputs.proxy_urls, 'https://repo.maven.apache.org/maven2/')
|
||||
|| !contains(steps.proxy.outputs.proxy_urls, 'https://repo1.maven.org/maven2')
|
||||
run: exit 1
|
||||
|
||||
- uses: ./../action/init
|
||||
env:
|
||||
CODEQL_PROXY_HOST: ${{ steps.proxy.outputs.proxy_host }}
|
||||
CODEQL_PROXY_PORT: ${{ steps.proxy.outputs.proxy_port }}
|
||||
CODEQL_PROXY_CA_CERTIFICATE: ${{ steps.proxy.outputs.proxy_ca_certificate }}
|
||||
with:
|
||||
languages: java
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
config-file: codeql-action@main:tests/multi-language-repo/.github/codeql/custom-queries.yml
|
||||
env:
|
||||
CODEQL_ACTION_PROXY_API_REQUESTS: 'true'
|
||||
CODEQL_ACTION_TEST_MODE: true
|
||||
+1
-1
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
Generated
+1
-1
@@ -74,7 +74,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
with:
|
||||
go-version: ^1.13.1
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
- name: Assert best-effort artifact scan completed
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
with:
|
||||
go-version: ^1.13.1
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
- name: Assert best-effort artifact scan completed
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0 # ensure we have all tags and can push commits
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
ref: ${{ env.HEAD_REF }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
version: ${{ matrix.version }}
|
||||
use-all-platform-bundle: true
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
- id: init
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
run: |
|
||||
echo SOURCE_BRANCH=${REF_NAME}
|
||||
echo TARGET_BRANCH=releases/${MAJOR_VERSION}
|
||||
python .github/update-release-branch.py \
|
||||
npx tsx ./pr-checks/update-release-branch.ts \
|
||||
--repository-nwo ${{ github.repository }} \
|
||||
--source-branch '${{ env.REF_NAME }}' \
|
||||
--target-branch 'releases/${{ env.MAJOR_VERSION }}' \
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
run: |
|
||||
echo SOURCE_BRANCH=${SOURCE_BRANCH}
|
||||
echo TARGET_BRANCH=${TARGET_BRANCH}
|
||||
python .github/update-release-branch.py \
|
||||
npx tsx ./pr-checks/update-release-branch.ts \
|
||||
--repository-nwo ${{ github.repository }} \
|
||||
--source-branch ${SOURCE_BRANCH} \
|
||||
--target-branch ${TARGET_BRANCH} \
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
See the [releases page](https://github.com/github/codeql-action/releases) for the relevant changes to the CodeQL CLI and language packs.
|
||||
|
||||
## 3.37.2 - 21 Jul 2026
|
||||
|
||||
- The new address format for the `config-file` input that was introduced in CodeQL Action 4.37.0 is now enabled by default. In addition to the format described there, the `remote=` prefix can now be used to explicitly indicate that the input refers to a remote file. All previous input formats continue to be accepted as well. [#4023](https://github.com/github/codeql-action/pull/4023)
|
||||
- The CodeQL Action can now make use of [configured private registries](https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries) in Default Setup to retrieve CodeQL configuration files from remote repositories that require authentication. This will allow customers to store their CodeQL configuration in a single repository that can then be referenced by Default Setup workflows in other repositories. We expect to roll this and other, related changes out to everyone in July. [#4007](https://github.com/github/codeql-action/pull/4007)
|
||||
|
||||
## 3.37.1 - 16 Jul 2026
|
||||
|
||||
- _Upcoming breaking change_: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. [#3956](https://github.com/github/codeql-action/pull/3956)
|
||||
- Update default CodeQL bundle version to [2.26.1](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.1). [#4019](https://github.com/github/codeql-action/pull/4019)
|
||||
|
||||
## 3.37.0 - 08 Jul 2026
|
||||
|
||||
- Update default CodeQL bundle version to [2.26.0](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.0). [#3995](https://github.com/github/codeql-action/pull/3995)
|
||||
- In addition to the existing input format, the `config-file` input for the `codeql-action/init` step will soon support a new `[owner/]repo[@ref][:path]` format. All components except the repository name are optional. If omitted, `owner` defaults to the same owner as the repository the analysis is running for, `ref` to `main`, and `path` to `.github/codeql-action.yaml`. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. [#3973](https://github.com/github/codeql-action/pull/3973)
|
||||
|
||||
## 3.36.3 - 01 Jul 2026
|
||||
|
||||
No user facing changes.
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.25.6",
|
||||
"cliVersion": "2.25.6",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.5",
|
||||
"priorCliVersion": "2.25.5"
|
||||
"bundleVersion": "codeql-bundle-v2.26.1",
|
||||
"cliVersion": "2.26.1",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.0",
|
||||
"priorCliVersion": "2.26.0"
|
||||
}
|
||||
Generated
+2315
-1713
File diff suppressed because it is too large.
Load diff
Generated
+121
-187
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "4.36.3",
|
||||
"version": "3.37.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "codeql",
|
||||
"version": "4.36.3",
|
||||
"version": "3.37.2",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"pr-checks"
|
||||
@@ -22,18 +22,22 @@
|
||||
"@actions/http-client": "^3.0.0",
|
||||
"@actions/io": "^2.0.0",
|
||||
"@actions/tool-cache": "^3.0.1",
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/plugin-paginate-rest": "^14.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"@octokit/plugin-retry": "^8.1.0",
|
||||
"archiver": "^8.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.0.0",
|
||||
"js-yaml": "^5.2.1",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
"semver": "^7.8.4",
|
||||
"uuid": "^14.0.0"
|
||||
"semver": "^7.8.5",
|
||||
"undici": "^6.24.0",
|
||||
"uuid": "^14.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ava/typescript": "6.0.0",
|
||||
@@ -47,21 +51,21 @@
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"@types/sarif": "^2.1.7",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/sinon": "^21.0.1",
|
||||
"@types/sinon": "^22.0.0",
|
||||
"ava": "^6.4.1",
|
||||
"esbuild": "^0.28.1",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-github": "^6.0.0",
|
||||
"eslint-plugin-import-x": "^4.16.2",
|
||||
"eslint-plugin-github": "^6.1.0",
|
||||
"eslint-plugin-import-x": "^4.17.1",
|
||||
"eslint-plugin-jsdoc": "^62.9.0",
|
||||
"eslint-plugin-no-async-foreach": "^0.1.1",
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.6.0",
|
||||
"nock": "^14.0.15",
|
||||
"globals": "^17.7.0",
|
||||
"nock": "^14.0.16",
|
||||
"sinon": "^22.0.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.61.1"
|
||||
"typescript-eslint": "^8.63.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -2274,13 +2278,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@package-json/types": {
|
||||
"version": "0.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@package-json/types/-/types-0.0.12.tgz",
|
||||
"integrity": "sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pkgr/core": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
|
||||
@@ -2579,9 +2576,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/sinon": {
|
||||
"version": "21.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-21.0.1.tgz",
|
||||
"integrity": "sha512-5yoJSqLbjH8T9V2bksgRayuhpZy+723/z6wBOR+Soe4ZlXC0eW8Na71TeaZPUWDQvM7LYKa9UGFc6LRqxiR5fQ==",
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-22.0.0.tgz",
|
||||
"integrity": "sha512-TDbVpbccc2HfiqHR09Argj3mHV1KMW7sCCKj52fsl8lbRLkEn7fB1966EWhOKWUBcqfBueZuPoA7/OK1CKiy3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2594,17 +2591,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz",
|
||||
"integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz",
|
||||
"integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.61.1",
|
||||
"@typescript-eslint/type-utils": "8.61.1",
|
||||
"@typescript-eslint/utils": "8.61.1",
|
||||
"@typescript-eslint/visitor-keys": "8.61.1",
|
||||
"@typescript-eslint/scope-manager": "8.63.0",
|
||||
"@typescript-eslint/type-utils": "8.63.0",
|
||||
"@typescript-eslint/utils": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -2617,7 +2614,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.61.1",
|
||||
"@typescript-eslint/parser": "^8.63.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
@@ -2633,16 +2630,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz",
|
||||
"integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz",
|
||||
"integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.61.1",
|
||||
"@typescript-eslint/types": "8.61.1",
|
||||
"@typescript-eslint/typescript-estree": "8.61.1",
|
||||
"@typescript-eslint/visitor-keys": "8.61.1",
|
||||
"@typescript-eslint/scope-manager": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2676,14 +2673,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz",
|
||||
"integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
|
||||
"integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.61.1",
|
||||
"@typescript-eslint/types": "^8.61.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.63.0",
|
||||
"@typescript-eslint/types": "^8.63.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2716,14 +2713,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz",
|
||||
"integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
|
||||
"integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.61.1",
|
||||
"@typescript-eslint/visitor-keys": "8.61.1"
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2734,9 +2731,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz",
|
||||
"integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
|
||||
"integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2751,15 +2748,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz",
|
||||
"integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz",
|
||||
"integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.61.1",
|
||||
"@typescript-eslint/typescript-estree": "8.61.1",
|
||||
"@typescript-eslint/utils": "8.61.1",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0",
|
||||
"@typescript-eslint/utils": "8.63.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -2794,9 +2791,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz",
|
||||
"integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
|
||||
"integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2808,16 +2805,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz",
|
||||
"integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
|
||||
"integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.61.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.61.1",
|
||||
"@typescript-eslint/types": "8.61.1",
|
||||
"@typescript-eslint/visitor-keys": "8.61.1",
|
||||
"@typescript-eslint/project-service": "8.63.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -2846,9 +2843,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2893,16 +2890,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz",
|
||||
"integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz",
|
||||
"integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.61.1",
|
||||
"@typescript-eslint/types": "8.61.1",
|
||||
"@typescript-eslint/typescript-estree": "8.61.1"
|
||||
"@typescript-eslint/scope-manager": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2917,13 +2914,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz",
|
||||
"integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
|
||||
"integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.61.1",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4991,13 +4988,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-6.0.0.tgz",
|
||||
"integrity": "sha512-J8MvUoiR/TU/Y9NnEmg1AnbvMUj9R6IO260z47zymMLLvso7B4c80IKjd8diqmqtSmeXXlbIus4i0SvK84flag==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-6.1.0.tgz",
|
||||
"integrity": "sha512-+mA0K1/I1JSE9AOiJ4ifMDGu7NplRZX0e3Uy0SjbwyXb2rsDfo5yfT0UCUO+TiOJ/99R1YxFRltizP/PZuB4PQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint/compat": "^1.2.3",
|
||||
"@eslint/compat": "^2.0.0",
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@github/browserslist-config": "^1.0.0",
|
||||
@@ -5014,79 +5011,18 @@
|
||||
"eslint-plugin-no-only-tests": "^3.0.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-rule-documentation": ">=1.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"globals": "^17.7.0",
|
||||
"jsx-ast-utils": "^3.3.2",
|
||||
"prettier": "^3.0.0",
|
||||
"svg-element-attributes": "^1.3.1",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.14.0"
|
||||
},
|
||||
"bin": {
|
||||
"eslint-ignore-errors": "bin/eslint-ignore-errors.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8 || ^9"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/@eslint/compat": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.1.tgz",
|
||||
"integrity": "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.40 || 9"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/@eslint/core": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/globals": {
|
||||
"version": "16.5.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
|
||||
"integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
"eslint": "^8 || ^9 || ^10"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-i18n-text": {
|
||||
@@ -5132,13 +5068,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import-x": {
|
||||
"version": "4.16.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.16.2.tgz",
|
||||
"integrity": "sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==",
|
||||
"version": "4.17.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.17.1.tgz",
|
||||
"integrity": "sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@package-json/types": "^0.0.12",
|
||||
"@typescript-eslint/types": "^8.56.0",
|
||||
"comment-parser": "^1.4.1",
|
||||
"debug": "^4.4.1",
|
||||
@@ -6143,9 +6078,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "17.6.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz",
|
||||
"integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==",
|
||||
"version": "17.7.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz",
|
||||
"integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -6986,9 +6921,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.0.0.tgz",
|
||||
"integrity": "sha512-GSvaPUbk1U+FMZ7rJzF+F8e5YVtu7KnD40et/5rBXXRBv2jCO9L3qCewvIDDdudC0QycTFlf6EAA+h3kxBsuUw==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz",
|
||||
"integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -7451,9 +7386,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nock": {
|
||||
"version": "14.0.15",
|
||||
"resolved": "https://registry.npmjs.org/nock/-/nock-14.0.15.tgz",
|
||||
"integrity": "sha512-S0a47C9pLvcYx/Ugf0H30BVBEcUgMMBDk9VJIDlJ8XGrfH2QDUD4Tgdp45qDIiHttokBG+IbsOtsvIjGR/j3bg==",
|
||||
"version": "14.0.16",
|
||||
"resolved": "https://registry.npmjs.org/nock/-/nock-14.0.16.tgz",
|
||||
"integrity": "sha512-8r4KEc6nT1D/fdLD/R1BO1CPaVEL8o40u/guFRJlXabN7vr3RmMqyjsY5Krt0nMwhsOAwXQ/mtN5vy5Jh3aErg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -8367,9 +8302,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
|
||||
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -8904,9 +8839,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/supertap/node_modules/js-yaml": {
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz",
|
||||
"integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -8983,9 +8918,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.16",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
|
||||
"integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==",
|
||||
"version": "7.5.20",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.20.tgz",
|
||||
"integrity": "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
@@ -9173,9 +9108,9 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
|
||||
"integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
|
||||
"version": "4.23.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz",
|
||||
"integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9325,16 +9260,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz",
|
||||
"integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz",
|
||||
"integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.61.1",
|
||||
"@typescript-eslint/parser": "8.61.1",
|
||||
"@typescript-eslint/typescript-estree": "8.61.1",
|
||||
"@typescript-eslint/utils": "8.61.1"
|
||||
"@typescript-eslint/eslint-plugin": "8.63.0",
|
||||
"@typescript-eslint/parser": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0",
|
||||
"@typescript-eslint/utils": "8.63.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -9368,10 +9303,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.24.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
|
||||
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
|
||||
"license": "MIT",
|
||||
"version": "6.27.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
|
||||
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
@@ -9498,9 +9432,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
|
||||
"integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz",
|
||||
"integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
@@ -9818,12 +9752,12 @@
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/plugin-paginate-rest": ">=9.2.2",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"semver": "^7.8.0",
|
||||
"semver": "^7.8.5",
|
||||
"yaml": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.43",
|
||||
"tsx": "^4.22.4"
|
||||
"tsx": "^4.23.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "3.36.3",
|
||||
"version": "3.37.2",
|
||||
"private": true,
|
||||
"description": "CodeQL action",
|
||||
"scripts": {
|
||||
@@ -30,18 +30,22 @@
|
||||
"@actions/http-client": "^3.0.0",
|
||||
"@actions/io": "^2.0.0",
|
||||
"@actions/tool-cache": "^3.0.1",
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/plugin-paginate-rest": "^14.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"@octokit/plugin-retry": "^8.1.0",
|
||||
"archiver": "^8.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.0.0",
|
||||
"js-yaml": "^5.2.1",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
"semver": "^7.8.4",
|
||||
"uuid": "^14.0.0"
|
||||
"semver": "^7.8.5",
|
||||
"uuid": "^14.0.1",
|
||||
"undici": "^6.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ava/typescript": "6.0.0",
|
||||
@@ -55,21 +59,21 @@
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"@types/sarif": "^2.1.7",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/sinon": "^21.0.1",
|
||||
"@types/sinon": "^22.0.0",
|
||||
"ava": "^6.4.1",
|
||||
"esbuild": "^0.28.1",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-github": "^6.0.0",
|
||||
"eslint-plugin-import-x": "^4.16.2",
|
||||
"eslint-plugin-github": "^6.1.0",
|
||||
"eslint-plugin-import-x": "^4.17.1",
|
||||
"eslint-plugin-jsdoc": "^62.9.0",
|
||||
"eslint-plugin-no-async-foreach": "^0.1.1",
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.6.0",
|
||||
"nock": "^14.0.15",
|
||||
"globals": "^17.7.0",
|
||||
"nock": "^14.0.16",
|
||||
"sinon": "^22.0.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.61.1"
|
||||
"typescript-eslint": "^8.63.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@actions/tool-cache": {
|
||||
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Tests for `changelog.ts`.
|
||||
*/
|
||||
|
||||
import * as assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import {
|
||||
EMPTY_CHANGELOG,
|
||||
getReleaseDateString,
|
||||
processChangelogForBackports,
|
||||
setVersionAndDate,
|
||||
} from "./changelog";
|
||||
|
||||
const testDate = new Date(2026, 7, 14);
|
||||
|
||||
describe("getReleaseDateString", async () => {
|
||||
await it("formats dates as expected", async () => {
|
||||
assert.equal(getReleaseDateString(testDate), "14 Aug 2026");
|
||||
});
|
||||
});
|
||||
|
||||
const emptyChangelogExpected = `# CodeQL Action Changelog
|
||||
|
||||
## 9.99.9 - 14 Aug 2026
|
||||
|
||||
No user facing changes.
|
||||
|
||||
`;
|
||||
|
||||
describe("setVersionAndDate", async () => {
|
||||
await it("replaces the placeholder", async () => {
|
||||
const result = setVersionAndDate("9.99.9", EMPTY_CHANGELOG, testDate);
|
||||
assert.equal(result, emptyChangelogExpected);
|
||||
});
|
||||
});
|
||||
|
||||
const testChangelog = `# CodeQL Action Changelog
|
||||
|
||||
## 4.12.3 - 14 Aug 2026
|
||||
|
||||
No user facing changes.
|
||||
`;
|
||||
|
||||
const testChangelogResult: string = `# CodeQL Action Changelog
|
||||
|
||||
## 3.12.3 - 14 Aug 2026
|
||||
|
||||
No user facing changes.
|
||||
`;
|
||||
|
||||
describe("processChangelogForBackports", async () => {
|
||||
await it("replaces major versions", async () => {
|
||||
const result = processChangelogForBackports("4", "3", testChangelog);
|
||||
|
||||
assert.deepEqual(result.split("\n"), testChangelogResult.split("\n"));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
import * as fs from "node:fs";
|
||||
|
||||
import { CHANGELOG_FILE, DryRunOption } from "./config";
|
||||
|
||||
/** Placeholder changelog content for a new release. */
|
||||
export const EMPTY_CHANGELOG = `# CodeQL Action Changelog
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
No user facing changes.
|
||||
|
||||
`;
|
||||
|
||||
/** Returns `date` formatted as `DD Mon YYYY`. */
|
||||
export function getReleaseDateString(today: Date = new Date()): string {
|
||||
return today.toLocaleDateString("en-GB", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
export interface OpenChangelogOptions {
|
||||
initChangelog?: boolean;
|
||||
}
|
||||
|
||||
export function withChangelog(
|
||||
transformer: (contents: string) => string,
|
||||
options: DryRunOption & OpenChangelogOptions,
|
||||
): void {
|
||||
let content: string;
|
||||
|
||||
if (options.initChangelog && !fs.existsSync(CHANGELOG_FILE)) {
|
||||
content = EMPTY_CHANGELOG;
|
||||
} else {
|
||||
content = fs.readFileSync(CHANGELOG_FILE, "utf8");
|
||||
}
|
||||
|
||||
if (!options.dryRun) {
|
||||
fs.writeFileSync(CHANGELOG_FILE, transformer(content), "utf8");
|
||||
} else {
|
||||
console.info(`[DRY RUN] Would have written updated changelog.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the `[UNRELEASED]` marker in `CHANGELOG.md` with the given version
|
||||
* and today's date.
|
||||
*/
|
||||
export function setVersionAndDate(
|
||||
version: string,
|
||||
content: string,
|
||||
date: Date = new Date(),
|
||||
): string {
|
||||
const versionAndDate = `${version} - ${getReleaseDateString(date)}`;
|
||||
return content.replace("[UNRELEASED]", versionAndDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes changelog entries for a backport, converting version references
|
||||
* from the source major version to the target major version and filtering
|
||||
* entries that only apply to newer versions.
|
||||
*/
|
||||
export function processChangelogForBackports(
|
||||
sourceBranchMajorVersion: string,
|
||||
targetBranchMajorVersion: string,
|
||||
content: string,
|
||||
): string {
|
||||
const lines = content.split("\n");
|
||||
|
||||
// Changelog entries can use the following format to indicate
|
||||
// that they only apply to newer versions
|
||||
const someVersionsOnlyRegex = /\[v(\d+)\+ only\]/;
|
||||
|
||||
let output = "";
|
||||
let i = 0;
|
||||
|
||||
// Copy lines until we find the first section heading.
|
||||
let foundFirstSection = false;
|
||||
while (!foundFirstSection && i < lines.length) {
|
||||
let line = lines[i];
|
||||
if (line.startsWith("## ")) {
|
||||
line = line.replace(
|
||||
`## ${sourceBranchMajorVersion}`,
|
||||
`## ${targetBranchMajorVersion}`,
|
||||
);
|
||||
foundFirstSection = true;
|
||||
}
|
||||
output += `${line}\n`;
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!foundFirstSection) {
|
||||
throw new Error("Could not find any change sections in CHANGELOG.md");
|
||||
}
|
||||
|
||||
// Process remaining lines.
|
||||
// `foundContent` tracks whether we hit two headings in a row
|
||||
let foundContent = false;
|
||||
output += "\n";
|
||||
|
||||
while (i < lines.length) {
|
||||
let line = lines[i];
|
||||
i++;
|
||||
|
||||
// Filter out changelog entries that only apply to newer versions.
|
||||
const match = someVersionsOnlyRegex.exec(line);
|
||||
if (match) {
|
||||
if (
|
||||
Number.parseInt(targetBranchMajorVersion) < Number.parseInt(match[1])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (line.startsWith("## ")) {
|
||||
line = line.replace(
|
||||
`## ${sourceBranchMajorVersion}`,
|
||||
`## ${targetBranchMajorVersion}`,
|
||||
);
|
||||
if (!foundContent) {
|
||||
output += "No user facing changes.\n";
|
||||
}
|
||||
foundContent = false;
|
||||
output += `\n${line}\n\n`;
|
||||
} else {
|
||||
if (line.trim() !== "") {
|
||||
foundContent = true;
|
||||
output += `${line}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ versions:
|
||||
- default
|
||||
steps:
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@89f90524b88a01fe6e0b732220432cc6142926af # v1.313.0
|
||||
uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
|
||||
with:
|
||||
ruby-version: 2.6
|
||||
- name: Install Code Scanning integration
|
||||
|
||||
@@ -6,16 +6,14 @@ operatingSystems:
|
||||
- windows
|
||||
versions:
|
||||
- linked
|
||||
env:
|
||||
CODEQL_ACTION_PROXY_API_REQUESTS: "true"
|
||||
steps:
|
||||
- uses: ./../action/init
|
||||
with:
|
||||
languages: csharp
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
|
||||
- name: Setup proxy for registries
|
||||
id: proxy
|
||||
uses: ./../action/start-proxy
|
||||
with:
|
||||
language: java
|
||||
registry_secrets: |
|
||||
[
|
||||
{
|
||||
@@ -44,3 +42,13 @@ steps:
|
||||
|| !contains(steps.proxy.outputs.proxy_urls, 'https://repo.maven.apache.org/maven2/')
|
||||
|| !contains(steps.proxy.outputs.proxy_urls, 'https://repo1.maven.org/maven2')
|
||||
run: exit 1
|
||||
|
||||
- uses: ./../action/init
|
||||
env:
|
||||
CODEQL_PROXY_HOST: ${{ steps.proxy.outputs.proxy_host }}
|
||||
CODEQL_PROXY_PORT: ${{ steps.proxy.outputs.proxy_port }}
|
||||
CODEQL_PROXY_CA_CERTIFICATE: ${{ steps.proxy.outputs.proxy_ca_certificate }}
|
||||
with:
|
||||
languages: java
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
config-file: codeql-action@main:tests/multi-language-repo/.github/codeql/custom-queries.yml
|
||||
@@ -12,6 +12,12 @@ export const REPO_ROOT = path.join(PR_CHECKS_DIR, "..");
|
||||
/** The path of the file configuring which checks shouldn't be required. */
|
||||
export const PR_CHECK_EXCLUDED_FILE = path.join(PR_CHECKS_DIR, "excluded.yml");
|
||||
|
||||
/** The path of the main `package.json`. */
|
||||
export const PACKAGE_JSON = path.join(REPO_ROOT, "package.json");
|
||||
|
||||
/** The path of the changelog. */
|
||||
export const CHANGELOG_FILE = path.join(REPO_ROOT, "CHANGELOG.md");
|
||||
|
||||
/** The path to the esbuild metadata file. */
|
||||
export const BUNDLE_METADATA_FILE = path.join(REPO_ROOT, "meta.json");
|
||||
|
||||
@@ -30,3 +36,9 @@ export const API_COMPATIBILITY_FILE = path.join(
|
||||
SOURCE_ROOT,
|
||||
"api-compatibility.json",
|
||||
);
|
||||
|
||||
/** A common interface for operations that support dry runs. */
|
||||
export interface DryRunOption {
|
||||
/** A value indicating whether to perform operations with side effects. */
|
||||
dryRun?: boolean;
|
||||
}
|
||||
@@ -7,11 +7,11 @@
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/plugin-paginate-rest": ">=9.2.2",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"semver": "^7.8.0",
|
||||
"semver": "^7.8.5",
|
||||
"yaml": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.43",
|
||||
"tsx": "^4.22.4"
|
||||
"tsx": "^4.23.0"
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -211,8 +211,8 @@ const languageSetups: LanguageSetups = {
|
||||
name: "Install Node.js",
|
||||
uses: pinnedUses(
|
||||
"actions/setup-node",
|
||||
"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e",
|
||||
"v6.4.0",
|
||||
"820762786026740c76f36085b0efc47a31fe5020",
|
||||
"v7.0.0",
|
||||
),
|
||||
with: {
|
||||
"node-version": defaultLanguageVersions.javascript,
|
||||
@@ -253,8 +253,8 @@ const languageSetups: LanguageSetups = {
|
||||
name: "Install Java",
|
||||
uses: pinnedUses(
|
||||
"actions/setup-java",
|
||||
"ad2b38190b15e4d6bdf0c97fb4fca8412226d287",
|
||||
"v5.3.0",
|
||||
"0f481fcb613427c0f801b606911222b5b6f3083a",
|
||||
"v5.5.0",
|
||||
),
|
||||
with: {
|
||||
"java-version": `\${{ inputs.java-version || '${defaultLanguageVersions.java}' }}`,
|
||||
@@ -288,8 +288,8 @@ const languageSetups: LanguageSetups = {
|
||||
name: "Install .NET",
|
||||
uses: pinnedUses(
|
||||
"actions/setup-dotnet",
|
||||
"9a946fdbd5fb07b82b2f5a4466058b876ab72bb2",
|
||||
"v5.3.0",
|
||||
"26b0ec14cb23fa6904739307f278c14f94c95bf1",
|
||||
"v5.4.0",
|
||||
),
|
||||
with: {
|
||||
"dotnet-version": `\${{ inputs.dotnet-version || '${defaultLanguageVersions.csharp}' }}`,
|
||||
|
||||
Executable
+840
@@ -0,0 +1,840 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Creates a release preparation branch and opens a PR to merge changes from a
|
||||
* source branch into a target release branch.
|
||||
*
|
||||
* For primary releases this merges `main` into the latest `releases/vN` branch.
|
||||
* For backports this merges a newer release branch into an older one, handling
|
||||
* version number and changelog migration automatically.
|
||||
*
|
||||
* Usage:
|
||||
* update-release-branch.ts \
|
||||
* --repository-nwo github/codeql-action \
|
||||
* --source-branch main \
|
||||
* --target-branch releases/v4 \
|
||||
* --conductor username \
|
||||
* [--is-primary-release] \
|
||||
* [--dry-run]
|
||||
*/
|
||||
|
||||
import { execFileSync, type ExecFileSyncOptions } from "node:child_process";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import { type ApiClient, getApiClient } from "./api-client";
|
||||
import * as changelog from "./changelog";
|
||||
import { DryRunOption, REPO_ROOT } from "./config";
|
||||
import {
|
||||
getCurrentVersion,
|
||||
replaceVersionInPackageJson,
|
||||
withPackageJson,
|
||||
} from "./versions";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const BACKPORT_COMMIT_MESSAGE = "Update version and changelog for v";
|
||||
|
||||
/**
|
||||
* Commit message used for rebuild commits, both those produced by this script and those produced
|
||||
* by the `Rebuild Action` workflow (`.github/workflows/rebuild.yml`).
|
||||
*/
|
||||
export const REBUILD_COMMIT_MESSAGE = "Rebuild";
|
||||
|
||||
/** The name of the git remote. */
|
||||
const ORIGIN = "origin";
|
||||
|
||||
/** Environment variables checked (in order) for a GitHub API token. */
|
||||
const TOKEN_ENVIRONMENT_VARIABLES = ["GH_TOKEN", "GITHUB_TOKEN"] as const;
|
||||
|
||||
/** The expected prefix for release branch names. */
|
||||
const RELEASE_BRANCH_PREFIX = "releases/v";
|
||||
|
||||
/**
|
||||
* Gets a GitHub API token from one of the supported environment variables.
|
||||
* @throws If none of the supported environment variables is set.
|
||||
*/
|
||||
export function getGitHubToken(): string {
|
||||
for (const name of TOKEN_ENVIRONMENT_VARIABLES) {
|
||||
const token = process.env[name]?.trim();
|
||||
if (token) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
throw new Error("Missing GitHub token. Set GITHUB_TOKEN or GH_TOKEN.");
|
||||
}
|
||||
|
||||
/** Options for {@link runCommand}. */
|
||||
export interface RunCommandOptions extends DryRunOption {
|
||||
/** Options for `execFileSync`. */
|
||||
execOptions?: ExecFileSyncOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a command, streaming output to the console by default.
|
||||
*
|
||||
* @param command The name of the command to run.
|
||||
* @param args The arguments for the command.
|
||||
* @throws When the process exits with a non-zero exit code.
|
||||
* @param options How to run the command.
|
||||
*/
|
||||
export function runCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
options?: RunCommandOptions,
|
||||
) {
|
||||
if (!options?.dryRun) {
|
||||
console.log(`Running \`${command} ${args.join(" ")}\`.`);
|
||||
return execFileSync(command, args, {
|
||||
stdio: "inherit",
|
||||
cwd: REPO_ROOT,
|
||||
...options?.execOptions,
|
||||
});
|
||||
} else {
|
||||
console.info(
|
||||
`[DRY RUN] Would have executed '${command} ${args.join(" ")}'`,
|
||||
);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/** Options for {@link runGit}. */
|
||||
export interface RunGitOptions extends DryRunOption {
|
||||
/** When true, non-zero exit codes will not throw. */
|
||||
allowNonZeroExitCode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `git` with the given `args` and returns the stdout.
|
||||
*
|
||||
* @param args - Arguments to pass to `git`.
|
||||
* @param options - Optional settings.
|
||||
* @throws If `git` does not exit successfully, unless
|
||||
* `options.allowNonZeroExitCode` is `true`.
|
||||
* @returns The trimmed stdout output.
|
||||
*/
|
||||
export function runGit(args: string[], options?: RunGitOptions): string {
|
||||
const execOptions: ExecFileSyncOptions = {
|
||||
encoding: "utf8",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
};
|
||||
|
||||
try {
|
||||
const result = runCommand("git", args, {
|
||||
dryRun: options?.dryRun,
|
||||
execOptions,
|
||||
}) as string;
|
||||
return result.trimEnd();
|
||||
} catch (error: unknown) {
|
||||
if (options?.allowNonZeroExitCode) {
|
||||
// execFileSync throws an object with `stdout` when the process exits
|
||||
// with a non-zero code.
|
||||
const execError = error as { stdout?: Buffer | string };
|
||||
if (typeof execError.stdout === "string") {
|
||||
return execError.stdout.trimEnd();
|
||||
}
|
||||
if (Buffer.isBuffer(execError.stdout)) {
|
||||
return execError.stdout.toString("utf8").trimEnd();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if the given branch exists on the origin remote. */
|
||||
export function branchExistsOnRemote(branchName: string): boolean {
|
||||
const result = runGit(["ls-remote", "--heads", ORIGIN, branchName]);
|
||||
return result !== "";
|
||||
}
|
||||
|
||||
/** Represents commits returned by the GitHub API (relevant fields only). */
|
||||
export interface GitHubCommit {
|
||||
sha: string;
|
||||
commit: { message: string; author: { date?: string } | null };
|
||||
author: { login: string } | null;
|
||||
committer: { login: string } | null;
|
||||
parents: Array<{ sha: string }>;
|
||||
}
|
||||
|
||||
/** Returns true if the commit is an automatic PR merge commit made by GitHub. */
|
||||
export function isPrMergeCommit(commit: GitHubCommit): boolean {
|
||||
return commit.committer?.login === "web-flow" && commit.parents.length > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of commits on the source branch that are not on the target branch,
|
||||
* excluding automatic PR merge commits. This will not include any commits that
|
||||
* exist on the target branch that aren't on the source branch.
|
||||
*
|
||||
* Uses `git log` to find the SHAs, then fetches each commit from the GitHub API
|
||||
* to obtain full metadata (author, parents, associated PRs, etc.).
|
||||
*
|
||||
* @param client - An authenticated GitHub API client.
|
||||
* @param owner - The repository owner.
|
||||
* @param repo - The repository name.
|
||||
* @param sourceBranch - The source branch name (without `origin/` prefix).
|
||||
* @param targetBranch - The target branch name (without `origin/` prefix).
|
||||
* @returns The list of non-merge commits unique to the source branch.
|
||||
*/
|
||||
export async function getCommitDifference(
|
||||
client: ApiClient,
|
||||
owner: string,
|
||||
repo: string,
|
||||
sourceBranch: string,
|
||||
targetBranch: string,
|
||||
): Promise<GitHubCommit[]> {
|
||||
const logOutput = runGit([
|
||||
"log",
|
||||
"--pretty=format:%H",
|
||||
`${ORIGIN}/${targetBranch}..${ORIGIN}/${sourceBranch}`,
|
||||
]);
|
||||
|
||||
// An empty log output means no commits to merge.
|
||||
if (logOutput === "") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const shas = logOutput.split("\n");
|
||||
|
||||
// Fetch full commit objects from the API.
|
||||
console.info(
|
||||
`Fetching information about ${shas.length} commits from the API...`,
|
||||
);
|
||||
|
||||
const commits: GitHubCommit[] = [];
|
||||
for (const sha of shas) {
|
||||
const { data } = await client.rest.repos.getCommit({
|
||||
owner,
|
||||
repo,
|
||||
ref: sha,
|
||||
});
|
||||
commits.push(data as GitHubCommit);
|
||||
}
|
||||
|
||||
// Filter out automatic PR merge commits.
|
||||
return commits.filter((c) => !isPrMergeCommit(c));
|
||||
}
|
||||
|
||||
/** Truncates a commit message for display. */
|
||||
export function getTruncatedCommitMessage(message: string): string {
|
||||
const firstLine = message.split("\n")[0];
|
||||
if (firstLine.length > 60) {
|
||||
return `${firstLine.slice(0, 57)}...`;
|
||||
}
|
||||
return firstLine;
|
||||
}
|
||||
|
||||
/** Represents pull requests associated with a commit (relevant fields only). */
|
||||
export interface AssociatedPullRequest {
|
||||
number: number;
|
||||
user: { login: string; site_admin: boolean } | null;
|
||||
merge_commit_sha: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the pull request that introduced a commit to the source branch.
|
||||
* Returns the earliest PR by number if multiple are associated.
|
||||
*/
|
||||
export async function getPrForCommit(
|
||||
client: ApiClient,
|
||||
owner: string,
|
||||
repo: string,
|
||||
commit: GitHubCommit,
|
||||
): Promise<AssociatedPullRequest | undefined> {
|
||||
const prs = await client.paginate(
|
||||
client.rest.repos.listPullRequestsAssociatedWithCommit,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
commit_sha: commit.sha,
|
||||
},
|
||||
);
|
||||
|
||||
if (prs.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Return the earliest PR by number.
|
||||
const sorted = [...prs].sort((a, b) => a.number - b.number);
|
||||
return sorted[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the login of the person who merged a pull request.
|
||||
* Falls back to the commit author of the merge commit.
|
||||
* For most cases this will be the same as the author, but for PRs opened
|
||||
* by external contributors getting the merger will get us the GitHub
|
||||
* employee who reviewed and merged the PR.
|
||||
*/
|
||||
export async function getMergerOfPr(
|
||||
client: ApiClient,
|
||||
owner: string,
|
||||
repo: string,
|
||||
pr: AssociatedPullRequest,
|
||||
): Promise<string> {
|
||||
if (!pr.merge_commit_sha) {
|
||||
return "unknown";
|
||||
}
|
||||
const { data: commit } = await client.rest.repos.getCommit({
|
||||
owner,
|
||||
repo,
|
||||
ref: pr.merge_commit_sha,
|
||||
});
|
||||
return commit.author?.login ?? "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PR author's login if they are GitHub staff (site_admin),
|
||||
* otherwise undefined.
|
||||
*/
|
||||
export function getPrAuthorIfStaff(
|
||||
pr: AssociatedPullRequest,
|
||||
): string | undefined {
|
||||
if (pr.user?.site_admin) {
|
||||
return pr.user.login;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Parameters for {@link openPr}. */
|
||||
interface OpenPrParams {
|
||||
client: ApiClient;
|
||||
owner: string;
|
||||
repo: string;
|
||||
commits: GitHubCommit[];
|
||||
sourceBranchShortSha: string;
|
||||
newBranchName: string;
|
||||
sourceBranch: string;
|
||||
targetBranch: string;
|
||||
conductor: string;
|
||||
isPrimaryRelease: boolean;
|
||||
conflictedFiles: string[];
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a pull request from the new branch to the target branch and assigns
|
||||
* the conductor.
|
||||
*/
|
||||
export async function openPr(params: OpenPrParams): Promise<void> {
|
||||
const {
|
||||
client,
|
||||
owner,
|
||||
repo,
|
||||
commits,
|
||||
sourceBranchShortSha,
|
||||
newBranchName,
|
||||
sourceBranch,
|
||||
targetBranch,
|
||||
conductor,
|
||||
isPrimaryRelease,
|
||||
conflictedFiles,
|
||||
dryRun,
|
||||
} = params;
|
||||
|
||||
// Sort the commits into those with and without associated PRs.
|
||||
const pullRequests: AssociatedPullRequest[] = [];
|
||||
const commitsWithoutPrs: GitHubCommit[] = [];
|
||||
|
||||
console.info(`Finding PRs for ${commits.length} commits...`);
|
||||
|
||||
for (const commit of commits) {
|
||||
const pr = await getPrForCommit(client, owner, repo, commit);
|
||||
if (!pr) {
|
||||
commitsWithoutPrs.push(commit);
|
||||
} else if (!pullRequests.some((p) => p.number === pr.number)) {
|
||||
pullRequests.push(pr);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found ${pullRequests.length} pull requests.`);
|
||||
console.log(
|
||||
`Found ${commitsWithoutPrs.length} commits not in a pull request.`,
|
||||
);
|
||||
|
||||
// Sort PRs by number (ascending) and commits by date.
|
||||
pullRequests.sort((a, b) => a.number - b.number);
|
||||
commitsWithoutPrs.sort((a, b) => {
|
||||
const dateA = a.commit.author?.date ?? "";
|
||||
const dateB = b.commit.author?.date ?? "";
|
||||
return dateA.localeCompare(dateB);
|
||||
});
|
||||
|
||||
// Build the PR body.
|
||||
const body: string[] = [];
|
||||
body.push(`Merging ${sourceBranchShortSha} into \`${targetBranch}\`.`);
|
||||
body.push("");
|
||||
body.push(`Conductor for this PR is @${conductor}.`);
|
||||
|
||||
if (pullRequests.length > 0) {
|
||||
body.push("");
|
||||
body.push("Contains the following pull requests:");
|
||||
for (const pr of pullRequests) {
|
||||
const displayUser =
|
||||
getPrAuthorIfStaff(pr) ??
|
||||
(await getMergerOfPr(client, owner, repo, pr));
|
||||
body.push(`- #${pr.number} (@${displayUser})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (commitsWithoutPrs.length > 0) {
|
||||
body.push("");
|
||||
body.push("Contains the following commits not from a pull request:");
|
||||
for (const commit of commitsWithoutPrs) {
|
||||
const authorDesc = commit.author ? ` (@${commit.author.login})` : "";
|
||||
body.push(
|
||||
`- ${commit.sha} - ${getTruncatedCommitMessage(commit.commit.message)}${authorDesc}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
body.push("");
|
||||
body.push("Please do the following:");
|
||||
if (conflictedFiles.length > 0) {
|
||||
body.push(
|
||||
" - [ ] Ensure `package.json` file contains the correct version.",
|
||||
);
|
||||
body.push(
|
||||
" - [ ] Add a commit to this branch to resolve the merge conflicts in the following files:",
|
||||
);
|
||||
for (const file of conflictedFiles) {
|
||||
body.push(` - \`${file}\``);
|
||||
}
|
||||
body.push(
|
||||
` - [ ] Rebuild the Action locally (\`npm run build\`) and push any changes to the built output in \`lib\` as a separate commit named exactly \`${REBUILD_COMMIT_MESSAGE}\`.`,
|
||||
);
|
||||
body.push(
|
||||
" - [ ] Ensure another maintainer has reviewed the additional commits you added to this branch to resolve the merge conflicts.",
|
||||
);
|
||||
}
|
||||
body.push(
|
||||
" - [ ] Ensure the CHANGELOG displays the correct version and date.",
|
||||
);
|
||||
body.push(
|
||||
" - [ ] Ensure the CHANGELOG includes all relevant, user-facing changes since the last release.",
|
||||
);
|
||||
body.push(
|
||||
` - [ ] Check that there are not any unexpected commits being merged into the \`${targetBranch}\` branch.`,
|
||||
);
|
||||
body.push(
|
||||
" - [ ] Ensure the docs team is aware of any documentation changes that need to be released.",
|
||||
);
|
||||
body.push(
|
||||
" - [ ] Approve running the full set of PR checks if you have not pushed any changes.",
|
||||
);
|
||||
body.push(
|
||||
" - [ ] Approve and merge this PR. Make sure `Create a merge commit` is selected rather than `Squash and merge` or `Rebase and merge`.",
|
||||
);
|
||||
|
||||
if (isPrimaryRelease) {
|
||||
body.push(
|
||||
" - [ ] Merge the mergeback PR that will automatically be created once this PR is merged.",
|
||||
);
|
||||
body.push(
|
||||
" - [ ] Merge all backport PRs to older release branches, that will automatically be created once this PR is merged.",
|
||||
);
|
||||
}
|
||||
|
||||
const title = `Merge ${sourceBranch} into ${targetBranch}`;
|
||||
|
||||
if (dryRun) {
|
||||
console.info(`[DRY RUN] Would create PR: "${title}" with body:`);
|
||||
|
||||
for (const line of body) {
|
||||
console.info(`[DRY RUN] > ${line}`);
|
||||
}
|
||||
|
||||
console.info(`[DRY RUN] and assign it to @${conductor}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the pull request.
|
||||
const { data: pr } = await client.rest.pulls.create({
|
||||
owner,
|
||||
repo,
|
||||
title,
|
||||
body: body.join("\n"),
|
||||
head: newBranchName,
|
||||
base: targetBranch,
|
||||
});
|
||||
console.log(`Created PR #${pr.number}`);
|
||||
|
||||
// Assign the conductor.
|
||||
await client.rest.issues.addAssignees({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
assignees: [conductor],
|
||||
});
|
||||
console.log(`Assigned PR to ${conductor}`);
|
||||
}
|
||||
|
||||
interface MainOptions {
|
||||
dryRun: boolean;
|
||||
repositoryNwo: string;
|
||||
sourceBranch: string;
|
||||
targetBranch: string;
|
||||
isPrimaryRelease: boolean;
|
||||
conductor: string;
|
||||
}
|
||||
|
||||
function parseCliOptions(): MainOptions {
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
"dry-run": { type: "boolean", default: false },
|
||||
"repository-nwo": { type: "string" },
|
||||
"source-branch": { type: "string" },
|
||||
"target-branch": { type: "string" },
|
||||
"is-primary-release": { type: "boolean", default: false },
|
||||
conductor: { type: "string" },
|
||||
},
|
||||
strict: true,
|
||||
});
|
||||
|
||||
if (!values["repository-nwo"]) {
|
||||
throw new Error("--repository-nwo is required");
|
||||
}
|
||||
if (!values["source-branch"]) {
|
||||
throw new Error("--source-branch is required");
|
||||
}
|
||||
if (!values["target-branch"]) {
|
||||
throw new Error("--target-branch is required");
|
||||
}
|
||||
if (!values["conductor"]) {
|
||||
throw new Error("--conductor is required");
|
||||
}
|
||||
|
||||
return {
|
||||
dryRun: values["dry-run"],
|
||||
repositoryNwo: values["repository-nwo"],
|
||||
sourceBranch: values["source-branch"],
|
||||
targetBranch: values["target-branch"],
|
||||
isPrimaryRelease: values["is-primary-release"] ?? false,
|
||||
conductor: values["conductor"],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds the action (npm ci + npm run build) and commits any changes.
|
||||
*/
|
||||
export function rebuildAction(options: MainOptions): void {
|
||||
// For backports, the only source-level change vs the source branch is the new version number,
|
||||
// so we just need to refresh the version embedded in `lib/`.
|
||||
runCommand("npm", ["ci"]);
|
||||
runCommand("npm", ["run", "build"]);
|
||||
|
||||
runGit(["add", "--all"], { dryRun: options.dryRun });
|
||||
|
||||
// `git diff --cached --quiet` exits 0 if there are no staged changes.
|
||||
try {
|
||||
execFileSync("git", ["diff", "--cached", "--quiet"]);
|
||||
console.log("Rebuild produced no changes; skipping Rebuild commit.");
|
||||
} catch {
|
||||
runGit(["commit", "-m", REBUILD_COMMIT_MESSAGE], {
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
console.log("Created Rebuild commit.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the new update/backport branch.
|
||||
*
|
||||
* @param options The options we are running with.
|
||||
* @param newBranchName The name of the new branch to create.
|
||||
* @param targetBranchMajorVersion The target branch's major version.
|
||||
* @param version The target version.
|
||||
*/
|
||||
export async function prepareNewBranch(
|
||||
options: MainOptions,
|
||||
newBranchName: string,
|
||||
targetBranchMajorVersion: string,
|
||||
version: string,
|
||||
): Promise<string[]> {
|
||||
// The process of creating the v{Older} release can run into merge conflicts. We commit the unresolved
|
||||
// conflicts so a maintainer can easily resolve them (vs erroring and requiring maintainers to
|
||||
// reconstruct the release manually)
|
||||
let conflictedFiles: string[] = [];
|
||||
|
||||
if (!options.isPrimaryRelease) {
|
||||
// For backports, the source branch is also a release branch.
|
||||
const sourceBranchMajorVersion = options.sourceBranch.replace(
|
||||
RELEASE_BRANCH_PREFIX,
|
||||
"",
|
||||
);
|
||||
|
||||
// Start from the target branch.
|
||||
console.log(
|
||||
`Creating ${newBranchName} from the ${ORIGIN}/${options.targetBranch} branch`,
|
||||
);
|
||||
|
||||
runGit(
|
||||
["checkout", "-b", newBranchName, `${ORIGIN}/${options.targetBranch}`],
|
||||
{ dryRun: options.dryRun },
|
||||
);
|
||||
|
||||
// Revert the commit that we made as part of the last release that updated the version number and
|
||||
// changelog to refer to {older}.x.x variants. This avoids merge conflicts in the changelog and
|
||||
// package.json files when we merge in the v{latest} branch.
|
||||
// This commit will not exist the first time we release the v{N-1} branch from the v{N} branch, so we
|
||||
// use `git log --grep` to conditionally revert the commit.
|
||||
console.log(
|
||||
"Reverting the version number and changelog updates from the last release to avoid conflicts",
|
||||
);
|
||||
const vOlderUpdateCommits = runGit([
|
||||
"log",
|
||||
"--grep",
|
||||
`^${BACKPORT_COMMIT_MESSAGE}`,
|
||||
"--format=%H",
|
||||
])
|
||||
.split("\n")
|
||||
.filter((s) => s !== "");
|
||||
|
||||
if (vOlderUpdateCommits.length > 0) {
|
||||
// Only revert the newest commit as older ones will already have been
|
||||
// reverted in previous releases.
|
||||
console.log(` Reverting ${vOlderUpdateCommits[0]}`);
|
||||
runGit(["revert", vOlderUpdateCommits[0], "--no-edit"], {
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
|
||||
// Also revert the "Rebuild" commit, whether created by this script or
|
||||
// by the `Rebuild Action` workflow.
|
||||
const rebuildCommits = runGit([
|
||||
"log",
|
||||
"--grep",
|
||||
`^${REBUILD_COMMIT_MESSAGE}$`,
|
||||
"--format=%H",
|
||||
])
|
||||
.split("\n")
|
||||
.filter((s) => s !== "");
|
||||
const rebuildCommit = rebuildCommits[0];
|
||||
console.log(` Reverting ${rebuildCommit}`);
|
||||
runGit(["revert", rebuildCommit, "--no-edit"], {
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
} else {
|
||||
console.log(" Nothing to revert.");
|
||||
}
|
||||
|
||||
// Merge the source branch into the release prep branch.
|
||||
console.log(
|
||||
`Merging ${ORIGIN}/${options.sourceBranch} into the release prep branch`,
|
||||
);
|
||||
runGit(["merge", `${ORIGIN}/${options.sourceBranch}`], {
|
||||
allowNonZeroExitCode: true,
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
conflictedFiles = runGit(["diff", "--name-only", "--diff-filter", "U"])
|
||||
.split("\n")
|
||||
.filter((s) => s !== "");
|
||||
if (conflictedFiles.length > 0) {
|
||||
runGit(["add", "."], {
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
runGit(["commit", "--no-edit"], {
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
}
|
||||
|
||||
// Migrate the package version number.
|
||||
console.log(`Setting version number to '${version}' in package.json`);
|
||||
withPackageJson((content) => {
|
||||
const currentPkgVersion = getCurrentVersion(content);
|
||||
if (currentPkgVersion) {
|
||||
return {
|
||||
content: replaceVersionInPackageJson(
|
||||
currentPkgVersion,
|
||||
version,
|
||||
content,
|
||||
),
|
||||
value: currentPkgVersion,
|
||||
};
|
||||
}
|
||||
return { value: currentPkgVersion };
|
||||
}, options);
|
||||
runGit(["add", "package.json"], {
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
|
||||
// Migrate the changelog notes from the source major version to the target.
|
||||
console.log(
|
||||
`Migrating changelog notes from v${sourceBranchMajorVersion} to v${targetBranchMajorVersion}`,
|
||||
);
|
||||
changelog.withChangelog(
|
||||
(contents) =>
|
||||
changelog.processChangelogForBackports(
|
||||
sourceBranchMajorVersion,
|
||||
targetBranchMajorVersion,
|
||||
contents,
|
||||
),
|
||||
options,
|
||||
);
|
||||
|
||||
runGit(["add", "CHANGELOG.md"], {
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
runGit(["commit", "-m", `${BACKPORT_COMMIT_MESSAGE}${version}`], {
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
} else {
|
||||
// For a standard (primary) release, there won't be new commits on the
|
||||
// target branch that aren't already on the source branch, so we can just
|
||||
// start from the source branch.
|
||||
runGit(
|
||||
["checkout", "-b", newBranchName, `${ORIGIN}/${options.sourceBranch}`],
|
||||
{
|
||||
dryRun: options.dryRun,
|
||||
},
|
||||
);
|
||||
|
||||
console.log("Updating changelog");
|
||||
changelog.withChangelog(
|
||||
(contents) => changelog.setVersionAndDate(version, contents),
|
||||
{ ...options, initChangelog: true },
|
||||
);
|
||||
|
||||
runGit(["add", "CHANGELOG.md"], {
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
runGit(["commit", "-m", `Update changelog for v${version}`], {
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
}
|
||||
|
||||
// For backports, rebuild the action unless there were merge conflicts.
|
||||
if (!options.isPrimaryRelease) {
|
||||
if (conflictedFiles.length === 0) {
|
||||
console.log("Rebuilding the Action.");
|
||||
rebuildAction(options);
|
||||
} else {
|
||||
console.log(
|
||||
`Skipping automatic rebuild because the merge produced conflicts in: ${conflictedFiles.join(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return conflictedFiles;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const options = parseCliOptions();
|
||||
const token = getGitHubToken();
|
||||
const client = getApiClient(token);
|
||||
|
||||
if (!options.targetBranch.startsWith(RELEASE_BRANCH_PREFIX)) {
|
||||
throw new Error(
|
||||
`Expected target branch to start with '${RELEASE_BRANCH_PREFIX}', but got '${options.targetBranch}'.`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
!options.isPrimaryRelease &&
|
||||
!options.sourceBranch.startsWith(RELEASE_BRANCH_PREFIX)
|
||||
) {
|
||||
throw new Error(
|
||||
`Expected source branch to start with '${RELEASE_BRANCH_PREFIX}' for backports, but got '${options.sourceBranch}'.`,
|
||||
);
|
||||
}
|
||||
if (!options.repositoryNwo.includes("/")) {
|
||||
throw new Error(
|
||||
`Expected repository name with owner in 'owner/repo' format, but got '${options.repositoryNwo}'`,
|
||||
);
|
||||
}
|
||||
|
||||
const targetBranchMajorVersion = options.targetBranch.replace(
|
||||
RELEASE_BRANCH_PREFIX,
|
||||
"",
|
||||
);
|
||||
|
||||
const currentVersion = withPackageJson((content) => {
|
||||
return { value: getCurrentVersion(content) };
|
||||
}, options);
|
||||
|
||||
if (!currentVersion) {
|
||||
throw new Error("Failed to read current version from package.json");
|
||||
}
|
||||
|
||||
const [, vMinor, vPatch] = currentVersion.split(".");
|
||||
const version = `${targetBranchMajorVersion}.${vMinor}.${vPatch}`;
|
||||
|
||||
console.log(
|
||||
`Considering difference between ${options.sourceBranch} and ${options.targetBranch}...`,
|
||||
);
|
||||
|
||||
const sourceBranchShortSha = runGit([
|
||||
"rev-parse",
|
||||
"--short",
|
||||
`${ORIGIN}/${options.sourceBranch}`,
|
||||
]);
|
||||
console.log(
|
||||
`Current head of ${options.sourceBranch} is ${sourceBranchShortSha}.`,
|
||||
);
|
||||
|
||||
const [owner, repo] = options.repositoryNwo.split("/");
|
||||
const commits = await getCommitDifference(
|
||||
client,
|
||||
owner,
|
||||
repo,
|
||||
options.sourceBranch,
|
||||
options.targetBranch,
|
||||
);
|
||||
|
||||
if (commits.length === 0) {
|
||||
console.log(
|
||||
`No commits to merge from ${options.sourceBranch} to ${options.targetBranch}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a distinct branch prefix to support specific PR checks on backports.
|
||||
const branchPrefix = options.isPrimaryRelease ? "update" : "backport";
|
||||
|
||||
// The branch name is based on the target version and the SHA of the source
|
||||
// branch head. If the branch already exists we can assume this script has
|
||||
// already run for this combination.
|
||||
const newBranchName = `${branchPrefix}-v${version}-${sourceBranchShortSha}`;
|
||||
console.log(`Branch name is '${newBranchName}'.`);
|
||||
|
||||
// Check if the branch already exists. If so we can abort as this script
|
||||
// has already run on this combination of branches.
|
||||
if (branchExistsOnRemote(newBranchName)) {
|
||||
console.log(`Branch '${newBranchName}' already exists. Nothing to do.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare the update/backport branch.
|
||||
const conflictedFiles = await prepareNewBranch(
|
||||
options,
|
||||
newBranchName,
|
||||
targetBranchMajorVersion,
|
||||
version,
|
||||
);
|
||||
|
||||
// Push the new branch to the remote.
|
||||
console.log(`Creating branch ${newBranchName}.`);
|
||||
runGit(["push", ORIGIN, newBranchName], { dryRun: options.dryRun });
|
||||
|
||||
// Open a PR to merge the new branch into the target branch.
|
||||
await openPr({
|
||||
client,
|
||||
owner,
|
||||
repo,
|
||||
commits,
|
||||
sourceBranchShortSha,
|
||||
newBranchName,
|
||||
sourceBranch: options.sourceBranch,
|
||||
targetBranch: options.targetBranch,
|
||||
conductor: options.conductor,
|
||||
isPrimaryRelease: options.isPrimaryRelease,
|
||||
conflictedFiles,
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
}
|
||||
|
||||
// Only call `main` if this script was run directly.
|
||||
if (require.main === module) {
|
||||
void main();
|
||||
}
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Tests for `versions.ts`.
|
||||
*/
|
||||
|
||||
import * as assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { getCurrentVersion, replaceVersionInPackageJson } from "./versions";
|
||||
|
||||
describe("getCurrentVersion", async () => {
|
||||
await it("reads versions", async () => {
|
||||
const result = getCurrentVersion(`{ "version": "1.23.4" }`);
|
||||
assert.deepEqual(result, "1.23.4");
|
||||
});
|
||||
});
|
||||
|
||||
const packageJsonContents = `{
|
||||
"name": "codeql",
|
||||
"version": "1.23.4"
|
||||
}
|
||||
`;
|
||||
|
||||
const packageJsonContentsExpected = `{
|
||||
"name": "codeql",
|
||||
"version": "2.23.4"
|
||||
}
|
||||
`;
|
||||
|
||||
describe("replaceVersionInPackageJson", async () => {
|
||||
await it("replaces versions", async () => {
|
||||
const result = replaceVersionInPackageJson(
|
||||
"1.23.4",
|
||||
"2.23.4",
|
||||
packageJsonContents,
|
||||
);
|
||||
assert.deepEqual(
|
||||
result.split("\n"),
|
||||
packageJsonContentsExpected.split("\n"),
|
||||
);
|
||||
assert.deepEqual(JSON.parse(result), { name: "codeql", version: "2.23.4" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import * as fs from "node:fs";
|
||||
|
||||
import { DryRunOption, PACKAGE_JSON } from "./config";
|
||||
|
||||
export function withPackageJson<T>(
|
||||
transformer: (content: string) => { value: T; content?: string },
|
||||
options: DryRunOption,
|
||||
): T {
|
||||
const content = fs.readFileSync(PACKAGE_JSON, "utf8");
|
||||
const result = transformer(content);
|
||||
|
||||
if (result.content !== undefined) {
|
||||
if (!options.dryRun) {
|
||||
fs.writeFileSync(PACKAGE_JSON, result.content, "utf8");
|
||||
} else {
|
||||
console.info(`[DRY RUN] Would have written an updated package.json`);
|
||||
}
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
/** Reads the current version from `package.json`. */
|
||||
export function getCurrentVersion(content: string): string | undefined {
|
||||
const pkg: { version: string } = JSON.parse(content);
|
||||
return pkg.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the version in `package.json` textually. Only updates the version
|
||||
* field that immediately follows the `"name": "codeql"` line.
|
||||
* `npm version` doesn't always work because of merge conflicts, so we
|
||||
* replace the version in package.json textually.
|
||||
*/
|
||||
export function replaceVersionInPackageJson(
|
||||
prevVersion: string,
|
||||
newVersion: string,
|
||||
content: string,
|
||||
): string {
|
||||
const lines = content.split("\n");
|
||||
let prevLineIsCodeql = false;
|
||||
const output: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (prevLineIsCodeql && line.includes(`"version": "${prevVersion}"`)) {
|
||||
output.push(line.replace(prevVersion, newVersion));
|
||||
} else {
|
||||
output.push(line);
|
||||
}
|
||||
prevLineIsCodeql = line.includes('"name": "codeql",');
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { ActionsEnv, getActionsEnv } from "./actions-util";
|
||||
import type { ApiClient } from "./api-client";
|
||||
import { Env, ReadOnlyEnv } from "./environment";
|
||||
import type { FeatureEnablement } from "./feature-flags";
|
||||
import { getActionsLogger, Logger } from "./logging";
|
||||
import {
|
||||
ActionName,
|
||||
getDisplayActionName,
|
||||
sendUnhandledErrorStatusReport,
|
||||
} from "./status-report";
|
||||
import { getEnv, getErrorMessage } from "./util";
|
||||
|
||||
/** Base state that is available to an Action on startup. */
|
||||
export interface BaseState {
|
||||
/** The name of the Action. */
|
||||
name: ActionName;
|
||||
/** When the Action was started. */
|
||||
startedAt: Date;
|
||||
}
|
||||
|
||||
/** Describes different state features that an Action may have. */
|
||||
export interface FeatureState {
|
||||
Base: BaseState;
|
||||
Logger: {
|
||||
/** The logger that is in use. */
|
||||
logger: Logger;
|
||||
};
|
||||
Env: {
|
||||
/** Information about environment variables. */
|
||||
env: Env;
|
||||
};
|
||||
ReadOnlyEnv: {
|
||||
env: ReadOnlyEnv;
|
||||
};
|
||||
Actions: {
|
||||
/** Access to Actions-related functionality. */
|
||||
actions: ActionsEnv;
|
||||
};
|
||||
Api: {
|
||||
/** A GitHub API client. */
|
||||
apiClient: ApiClient;
|
||||
};
|
||||
FeatureFlags: {
|
||||
/** Information about enabled feature flags. */
|
||||
features: FeatureEnablement;
|
||||
};
|
||||
}
|
||||
|
||||
/** Identifies a type of state an Action may have. */
|
||||
export type StateFeature = keyof FeatureState;
|
||||
|
||||
/** Constructs the intersection of all state types identifies by `Fs`. */
|
||||
export type FieldsOf<Fs extends readonly StateFeature[]> = Fs extends []
|
||||
? Record<never, never>
|
||||
: Fs extends [
|
||||
infer Head extends StateFeature,
|
||||
...infer Tail extends readonly StateFeature[],
|
||||
]
|
||||
? FeatureState[Head] & FieldsOf<Tail>
|
||||
: never;
|
||||
|
||||
/** Describes the state of an Action that has access to the state corresponding to `Fs`. */
|
||||
export type ActionState<Fs extends readonly StateFeature[]> = FieldsOf<Fs>;
|
||||
|
||||
/** The type of an Action's main entry point. This is a function that is provided
|
||||
* with a basic `ActionState` object with features that are always available.
|
||||
* Each Action can then augment the `state` further if additional features are required.
|
||||
*/
|
||||
export type ActionMain = (
|
||||
state: ActionState<["Base", "Logger", "Env", "Actions"]>,
|
||||
) => Promise<void>;
|
||||
|
||||
/** A specification for a CodeQL Action step. */
|
||||
export interface Action {
|
||||
/** The name of the Action. */
|
||||
name: ActionName;
|
||||
/** The entry point for the Action. */
|
||||
run: ActionMain;
|
||||
}
|
||||
|
||||
/** A generic entry point that sets up the basic environment for the `action` and runs it. */
|
||||
export async function runInActions(action: Action) {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
const env = getEnv();
|
||||
const actionsEnv = getActionsEnv();
|
||||
|
||||
try {
|
||||
await action.run({
|
||||
name: action.name,
|
||||
startedAt,
|
||||
logger,
|
||||
env,
|
||||
actions: actionsEnv,
|
||||
});
|
||||
} catch (error) {
|
||||
core.setFailed(
|
||||
`${getDisplayActionName(action.name)} action failed: ${getErrorMessage(error)}`,
|
||||
);
|
||||
await sendUnhandledErrorStatusReport(action.name, startedAt, error, logger);
|
||||
}
|
||||
}
|
||||
+52
-39
@@ -7,12 +7,13 @@ import * as github from "@actions/github";
|
||||
import * as io from "@actions/io";
|
||||
|
||||
import type { Config } from "./config-utils";
|
||||
import { Env, EnvVar, ActionsEnvVars } from "./environment";
|
||||
import { Logger } from "./logging";
|
||||
import {
|
||||
doesDirectoryExist,
|
||||
getCodeQLDatabasePath,
|
||||
getRequiredEnvParam,
|
||||
ConfigurationError,
|
||||
getEnv,
|
||||
} from "./util";
|
||||
|
||||
/**
|
||||
@@ -61,17 +62,21 @@ export const getOptionalInput = function (name: string): string | undefined {
|
||||
return value.length > 0 ? value : undefined;
|
||||
};
|
||||
|
||||
export function getTemporaryDirectory(): string {
|
||||
const value = process.env["CODEQL_ACTION_TEMP"];
|
||||
return value !== undefined && value !== ""
|
||||
? value
|
||||
: getRequiredEnvParam("RUNNER_TEMP");
|
||||
/**
|
||||
* Gets the temporary directory used by the CodeQL Action. This will either be the temporary
|
||||
* directory that has been set in `CODEQL_ACTION_TEMP` by e.g. a previous step, or the
|
||||
* value of `RUNNER_TEMP` otherwise.
|
||||
*/
|
||||
export function getTemporaryDirectory(env: Env = getEnv()): string {
|
||||
return (
|
||||
env.getOptional(EnvVar.TEMP) ?? env.getRequired(ActionsEnvVars.RUNNER_TEMP)
|
||||
);
|
||||
}
|
||||
|
||||
const PR_DIFF_RANGE_JSON_FILENAME = "pr-diff-range.json";
|
||||
|
||||
export function getDiffRangesJsonFilePath(): string {
|
||||
return path.join(getTemporaryDirectory(), PR_DIFF_RANGE_JSON_FILENAME);
|
||||
export function getDiffRangesJsonFilePath(env: Env = getEnv()): string {
|
||||
return path.join(getTemporaryDirectory(env), PR_DIFF_RANGE_JSON_FILENAME);
|
||||
}
|
||||
|
||||
export function getActionVersion(): string {
|
||||
@@ -83,16 +88,16 @@ export function getActionVersion(): string {
|
||||
*
|
||||
* This will be "dynamic" for default setup workflow runs.
|
||||
*/
|
||||
export function getWorkflowEventName() {
|
||||
return getRequiredEnvParam("GITHUB_EVENT_NAME");
|
||||
export function getWorkflowEventName(env: Env = getEnv()) {
|
||||
return env.getRequired(ActionsEnvVars.GITHUB_EVENT_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current workflow is executing a local copy of the Action, e.g. we're running
|
||||
* a workflow on the codeql-action repo itself.
|
||||
*/
|
||||
export function isRunningLocalAction(): boolean {
|
||||
const relativeScriptPath = getRelativeScriptPath();
|
||||
export function isRunningLocalAction(env: Env = getEnv()): boolean {
|
||||
const relativeScriptPath = getRelativeScriptPath(env);
|
||||
return (
|
||||
relativeScriptPath.startsWith("..") || path.isAbsolute(relativeScriptPath)
|
||||
);
|
||||
@@ -103,15 +108,15 @@ export function isRunningLocalAction(): boolean {
|
||||
*
|
||||
* This can be used to get the Action's name or tell if we're running a local Action.
|
||||
*/
|
||||
function getRelativeScriptPath(): string {
|
||||
const runnerTemp = getRequiredEnvParam("RUNNER_TEMP");
|
||||
function getRelativeScriptPath(env: Env): string {
|
||||
const runnerTemp = env.getRequired(ActionsEnvVars.RUNNER_TEMP);
|
||||
const actionsDirectory = path.join(path.dirname(runnerTemp), "_actions");
|
||||
return path.relative(actionsDirectory, __filename);
|
||||
}
|
||||
|
||||
/** Returns the contents of `GITHUB_EVENT_PATH` as a JSON object. */
|
||||
export function getWorkflowEvent(): any {
|
||||
const eventJsonFile = getRequiredEnvParam("GITHUB_EVENT_PATH");
|
||||
export function getWorkflowEvent(env: Env = getEnv()): any {
|
||||
const eventJsonFile = env.getRequired(ActionsEnvVars.GITHUB_EVENT_PATH);
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(eventJsonFile, "utf-8"));
|
||||
} catch (e) {
|
||||
@@ -180,17 +185,17 @@ export function getUploadValue(input: string | undefined): UploadKind {
|
||||
/**
|
||||
* Get the workflow run ID.
|
||||
*/
|
||||
export function getWorkflowRunID(): number {
|
||||
const workflowRunIdString = getRequiredEnvParam("GITHUB_RUN_ID");
|
||||
export function getWorkflowRunID(env: Env = getEnv()): number {
|
||||
const workflowRunIdString = env.getRequired(ActionsEnvVars.GITHUB_RUN_ID);
|
||||
const workflowRunID = parseInt(workflowRunIdString, 10);
|
||||
if (Number.isNaN(workflowRunID)) {
|
||||
throw new Error(
|
||||
`GITHUB_RUN_ID must define a non NaN workflow run ID. Current value is ${workflowRunIdString}`,
|
||||
`${ActionsEnvVars.GITHUB_RUN_ID} must define a non NaN workflow run ID. Current value is ${workflowRunIdString}`,
|
||||
);
|
||||
}
|
||||
if (workflowRunID < 0) {
|
||||
throw new Error(
|
||||
`GITHUB_RUN_ID must be a non-negative integer. Current value is ${workflowRunIdString}`,
|
||||
`${ActionsEnvVars.GITHUB_RUN_ID} must be a non-negative integer. Current value is ${workflowRunIdString}`,
|
||||
);
|
||||
}
|
||||
return workflowRunID;
|
||||
@@ -199,17 +204,19 @@ export function getWorkflowRunID(): number {
|
||||
/**
|
||||
* Get the workflow run attempt number.
|
||||
*/
|
||||
export function getWorkflowRunAttempt(): number {
|
||||
const workflowRunAttemptString = getRequiredEnvParam("GITHUB_RUN_ATTEMPT");
|
||||
export function getWorkflowRunAttempt(env: Env = getEnv()): number {
|
||||
const workflowRunAttemptString = env.getRequired(
|
||||
ActionsEnvVars.GITHUB_RUN_ATTEMPT,
|
||||
);
|
||||
const workflowRunAttempt = parseInt(workflowRunAttemptString, 10);
|
||||
if (Number.isNaN(workflowRunAttempt)) {
|
||||
throw new Error(
|
||||
`GITHUB_RUN_ATTEMPT must define a non NaN workflow run attempt. Current value is ${workflowRunAttemptString}`,
|
||||
`${ActionsEnvVars.GITHUB_RUN_ATTEMPT} must define a non NaN workflow run attempt. Current value is ${workflowRunAttemptString}`,
|
||||
);
|
||||
}
|
||||
if (workflowRunAttempt <= 0) {
|
||||
throw new Error(
|
||||
`GITHUB_RUN_ATTEMPT must be a positive integer. Current value is ${workflowRunAttemptString}`,
|
||||
`${ActionsEnvVars.GITHUB_RUN_ATTEMPT} must be a positive integer. Current value is ${workflowRunAttemptString}`,
|
||||
);
|
||||
}
|
||||
return workflowRunAttempt;
|
||||
@@ -266,18 +273,18 @@ export const getFileType = async (filePath: string): Promise<string> => {
|
||||
}
|
||||
};
|
||||
|
||||
export function isSelfHostedRunner() {
|
||||
return process.env.RUNNER_ENVIRONMENT === "self-hosted";
|
||||
export function isSelfHostedRunner(env: Env = getEnv()) {
|
||||
return env.getOptional(ActionsEnvVars.RUNNER_ENVIRONMENT) === "self-hosted";
|
||||
}
|
||||
|
||||
/** Determines whether the workflow trigger is `dynamic`. */
|
||||
export function isDynamicWorkflow(): boolean {
|
||||
return getWorkflowEventName() === "dynamic";
|
||||
export function isDynamicWorkflow(env: Env = getEnv()): boolean {
|
||||
return getWorkflowEventName(env) === "dynamic";
|
||||
}
|
||||
|
||||
/** Determines whether we are running in default setup. */
|
||||
export function isDefaultSetup(): boolean {
|
||||
return isDynamicWorkflow();
|
||||
export function isDefaultSetup(env: Env = getEnv()): boolean {
|
||||
return isDynamicWorkflow(env);
|
||||
}
|
||||
|
||||
export function prettyPrintInvocation(cmd: string, args: string[]): string {
|
||||
@@ -375,9 +382,10 @@ const persistedInputsKey = "persisted_inputs";
|
||||
* This would be simplified if actions/runner#3514 is addressed.
|
||||
* https://github.com/actions/runner/issues/3514
|
||||
*/
|
||||
export const persistInputs = function () {
|
||||
const inputEnvironmentVariables = Object.entries(process.env).filter(
|
||||
([name]) => name.startsWith("INPUT_"),
|
||||
export const persistInputs = function (env: Env = getEnv()) {
|
||||
const entries = env.entries();
|
||||
const inputEnvironmentVariables = entries.filter(([name]) =>
|
||||
name.startsWith("INPUT_"),
|
||||
);
|
||||
core.saveState(persistedInputsKey, JSON.stringify(inputEnvironmentVariables));
|
||||
};
|
||||
@@ -405,7 +413,9 @@ export interface PullRequestBranches {
|
||||
* @returns the base and head branches of the pull request, or undefined if
|
||||
* we are not analyzing a pull request.
|
||||
*/
|
||||
export function getPullRequestBranches(): PullRequestBranches | undefined {
|
||||
export function getPullRequestBranches(
|
||||
env: Env = getEnv(),
|
||||
): PullRequestBranches | undefined {
|
||||
const pullRequest = github.context.payload.pull_request;
|
||||
if (pullRequest) {
|
||||
return {
|
||||
@@ -419,8 +429,10 @@ export function getPullRequestBranches(): PullRequestBranches | undefined {
|
||||
|
||||
// PR analysis under Default Setup does not have the pull_request context,
|
||||
// but it should set CODE_SCANNING_REF and CODE_SCANNING_BASE_BRANCH.
|
||||
const codeScanningRef = process.env.CODE_SCANNING_REF;
|
||||
const codeScanningBaseBranch = process.env.CODE_SCANNING_BASE_BRANCH;
|
||||
const codeScanningRef = env.getOptional(EnvVar.CODE_SCANNING_REF);
|
||||
const codeScanningBaseBranch = env.getOptional(
|
||||
EnvVar.CODE_SCANNING_BASE_BRANCH,
|
||||
);
|
||||
if (codeScanningRef && codeScanningBaseBranch) {
|
||||
return {
|
||||
base: codeScanningBaseBranch,
|
||||
@@ -435,8 +447,8 @@ export function getPullRequestBranches(): PullRequestBranches | undefined {
|
||||
/**
|
||||
* Returns whether we are analyzing a pull request.
|
||||
*/
|
||||
export function isAnalyzingPullRequest(): boolean {
|
||||
return getPullRequestBranches() !== undefined;
|
||||
export function isAnalyzingPullRequest(env: Env = getEnv()): boolean {
|
||||
return getPullRequestBranches(env) !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -460,13 +472,14 @@ const qualityCategoryMapping: Record<string, string> = {
|
||||
export function fixCodeQualityCategory(
|
||||
logger: Logger,
|
||||
category?: string,
|
||||
env: Env = getEnv(),
|
||||
): string | undefined {
|
||||
// The `category` should always be set by Default Setup. We perform this check
|
||||
// to avoid potential issues if Code Quality supports Advanced Setup in the future
|
||||
// and before this workaround is removed.
|
||||
if (
|
||||
category !== undefined &&
|
||||
isDefaultSetup() &&
|
||||
isDefaultSetup(env) &&
|
||||
category.startsWith("/language:")
|
||||
) {
|
||||
const language = category.substring("/language:".length);
|
||||
|
||||
+9
-16
@@ -4,6 +4,7 @@ import { performance } from "perf_hooks";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import * as analyses from "./analyses";
|
||||
import {
|
||||
@@ -40,7 +41,6 @@ import {
|
||||
createStatusReportBase,
|
||||
DatabaseCreationTimings,
|
||||
getActionsStatus,
|
||||
sendUnhandledErrorStatusReport,
|
||||
StatusReportBase,
|
||||
} from "./status-report";
|
||||
import {
|
||||
@@ -212,7 +212,7 @@ async function runAutobuildIfLegacyGoWorkflow(config: Config, logger: Logger) {
|
||||
await runAutobuild(config, BuiltInLanguage.go, logger);
|
||||
}
|
||||
|
||||
async function run(startedAt: Date) {
|
||||
async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
@@ -228,7 +228,6 @@ async function run(startedAt: Date) {
|
||||
let didUploadTrapCaches = false;
|
||||
let dependencyCacheResults: DependencyCacheUploadStatusReport | undefined;
|
||||
let databaseUploadResults: DatabaseUploadResult[] = [];
|
||||
const logger = getActionsLogger();
|
||||
|
||||
try {
|
||||
util.initializeEnvironment(actionsUtil.getActionVersion());
|
||||
@@ -523,19 +522,13 @@ async function run(startedAt: Date) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Defines the `analyze` Action. */
|
||||
const analyze: Action = {
|
||||
name: ActionName.Analyze,
|
||||
run,
|
||||
};
|
||||
|
||||
export async function runWrapper() {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
try {
|
||||
await run(startedAt);
|
||||
} catch (error) {
|
||||
core.setFailed(`analyze action failed: ${util.getErrorMessage(error)}`);
|
||||
await sendUnhandledErrorStatusReport(
|
||||
ActionName.Analyze,
|
||||
startedAt,
|
||||
error,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
await runInActions(analyze);
|
||||
await util.checkForTimeout();
|
||||
}
|
||||
+1
-1
@@ -285,7 +285,7 @@ extensions:
|
||||
// characters are escaped, and that the path is always rendered as a
|
||||
// quoted string on a single line.
|
||||
return (
|
||||
` - [${yaml.dump(filename, { quoteStyle: "single" }).trim()}, ` +
|
||||
` - [${yaml.dump(filename, { forceQuotes: true, quoteStyle: "single" }).trim()}, ` +
|
||||
`${range.startLine}, ${range.endLine}]\n`
|
||||
);
|
||||
})
|
||||
|
||||
+54
-9
@@ -6,7 +6,8 @@ import * as sinon from "sinon";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import { DO_NOT_RETRY_STATUSES } from "./api-client";
|
||||
import { setupTests } from "./testing-utils";
|
||||
import { ActionsEnvVars, RegistryProxyVars } from "./environment";
|
||||
import { callee, getTestEnv, setupTests } from "./testing-utils";
|
||||
import * as util from "./util";
|
||||
|
||||
setupTests(test);
|
||||
@@ -20,23 +21,23 @@ test.serial("getApiClient", async (t) => {
|
||||
const githubStub: sinon.SinonStub = sinon.stub();
|
||||
pluginStub.returns(githubStub);
|
||||
|
||||
const env = getTestEnv();
|
||||
env.set(ActionsEnvVars.GITHUB_SERVER_URL, "http://github.localhost");
|
||||
env.set(ActionsEnvVars.GITHUB_API_URL, "http://api.github.localhost");
|
||||
|
||||
sinon.stub(actionsUtil, "getRequiredInput").withArgs("token").returns("xyz");
|
||||
const requiredEnvParamStub = sinon.stub(util, "getRequiredEnvParam");
|
||||
requiredEnvParamStub
|
||||
.withArgs("GITHUB_SERVER_URL")
|
||||
.returns("http://github.localhost");
|
||||
requiredEnvParamStub
|
||||
.withArgs("GITHUB_API_URL")
|
||||
.returns("http://api.github.localhost");
|
||||
|
||||
api.getApiClient();
|
||||
const apiClient = api.getApiClient(env);
|
||||
t.truthy(apiClient);
|
||||
|
||||
t.true(githubStub.calledOnce);
|
||||
t.assert(
|
||||
githubStub.calledOnceWithExactly({
|
||||
auth: "token xyz",
|
||||
baseUrl: "http://api.github.localhost",
|
||||
log: sinon.match.any,
|
||||
userAgent: `CodeQL-Action/${actionsUtil.getActionVersion()}`,
|
||||
request: sinon.match.any,
|
||||
retry: {
|
||||
doNotRetry: DO_NOT_RETRY_STATUSES,
|
||||
},
|
||||
@@ -206,3 +207,47 @@ test.serial(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test("getRegistryProxy - returns undefined if the proxy is not configured", async (t) => {
|
||||
const target = callee(api.getRegistryProxy).withArgs();
|
||||
|
||||
// Empty environment.
|
||||
await target.passes(t.is, undefined);
|
||||
// Only the host.
|
||||
await target
|
||||
.withEnv(getTestEnv({ [RegistryProxyVars.PROXY_HOST]: "localhost" }))
|
||||
.passes(t.is, undefined);
|
||||
// Only the port.
|
||||
await target
|
||||
.withEnv(getTestEnv({ [RegistryProxyVars.PROXY_PORT]: "1234" }))
|
||||
.passes(t.is, undefined);
|
||||
});
|
||||
|
||||
test("getRegistryProxy - returns value when both vars are set", async (t) => {
|
||||
await callee(api.getRegistryProxy)
|
||||
.withArgs()
|
||||
.withEnv(
|
||||
getTestEnv({
|
||||
[RegistryProxyVars.PROXY_HOST]: "localhost",
|
||||
[RegistryProxyVars.PROXY_PORT]: "1234",
|
||||
}),
|
||||
)
|
||||
.passes(t.truthy);
|
||||
});
|
||||
|
||||
test("getRegistryProxyConfig - gets the configuration from the env vars", async (t) => {
|
||||
const host = "localhost";
|
||||
const port = "1234";
|
||||
const ca = "cert";
|
||||
|
||||
await callee(api.getRegistryProxyConfig)
|
||||
.withArgs()
|
||||
.withEnv(
|
||||
getTestEnv({
|
||||
[RegistryProxyVars.PROXY_HOST]: host,
|
||||
[RegistryProxyVars.PROXY_PORT]: port,
|
||||
[RegistryProxyVars.PROXY_CA_CERTIFICATE]: ca,
|
||||
}),
|
||||
)
|
||||
.passes(t.like, { host, port, ca });
|
||||
});
|
||||
+99
-9
@@ -1,9 +1,26 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as githubUtils from "@actions/github/lib/utils";
|
||||
import { type Octokit } from "@octokit/core";
|
||||
import { type PaginateInterface } from "@octokit/plugin-paginate-rest";
|
||||
import { type Api } from "@octokit/plugin-rest-endpoint-methods";
|
||||
import * as retry from "@octokit/plugin-retry";
|
||||
import { RequestRequestOptions } from "@octokit/types";
|
||||
import {
|
||||
ProxyAgent,
|
||||
RequestInfo,
|
||||
RequestInit,
|
||||
fetch as undiciFetch,
|
||||
} from "undici";
|
||||
|
||||
import type { ActionState } from "./action-common";
|
||||
import { getActionVersion, getRequiredInput } from "./actions-util";
|
||||
import { EnvVar } from "./environment";
|
||||
import {
|
||||
ActionsEnvVars,
|
||||
EnvVar,
|
||||
ReadOnlyEnv,
|
||||
RegistryProxyVars,
|
||||
getEnv,
|
||||
} from "./environment";
|
||||
import { Logger } from "./logging";
|
||||
import { getRepositoryNwo, RepositoryNwo } from "./repository";
|
||||
import {
|
||||
@@ -43,13 +60,84 @@ export interface GitHubApiExternalRepoDetails {
|
||||
apiURL: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configuration for the private registry authentication proxy,
|
||||
* if it is available in the environment.
|
||||
*
|
||||
* @param action The required Action state.
|
||||
* @returns The hostname, port, and CA retrieved from the corresponding environment variables.
|
||||
*/
|
||||
export function getRegistryProxyConfig(action: ActionState<["ReadOnlyEnv"]>) {
|
||||
return {
|
||||
host: action.env.getOptional(RegistryProxyVars.PROXY_HOST),
|
||||
port: action.env.getOptional(RegistryProxyVars.PROXY_PORT),
|
||||
ca: action.env.getOptional(RegistryProxyVars.PROXY_CA_CERTIFICATE),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configuration for the private registry authentication proxy,
|
||||
* and uses it to initialise a corresponding `ProxyAgent`.
|
||||
*
|
||||
* @param action The required Action state.
|
||||
* @returns A `ProxyAgent` corresponding to the private registry proxy,
|
||||
* or `undefined` if we couldn't retrieve the host and port.
|
||||
*/
|
||||
export function getRegistryProxy(
|
||||
action: ActionState<["Logger", "ReadOnlyEnv"]>,
|
||||
): ProxyAgent | undefined {
|
||||
const { host, port, ca } = getRegistryProxyConfig(action);
|
||||
|
||||
if (host && port) {
|
||||
const uri = `http://${host}:${port}`;
|
||||
action.logger.debug(
|
||||
`Using private registry proxy at '${uri}' for API client.`,
|
||||
);
|
||||
return new ProxyAgent({
|
||||
uri,
|
||||
keepAliveTimeout: 10,
|
||||
keepAliveMaxTimeout: 10,
|
||||
requestTls: ca ? { ca } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a `RequestRequestOptions` with a custom `fetch` implementation
|
||||
* that uses `dispatcher` as a proxy for requests.
|
||||
*
|
||||
* @param dispatcher The proxy to use.
|
||||
*/
|
||||
export function makeProxyRequestOptions(
|
||||
dispatcher: ProxyAgent,
|
||||
): RequestRequestOptions {
|
||||
return {
|
||||
fetch: (req: RequestInfo, init?: RequestInit) => {
|
||||
return undiciFetch(req, { ...init, dispatcher });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** The type of GitHub API client we use. */
|
||||
export type ApiClient = Octokit & Api & { paginate: PaginateInterface };
|
||||
|
||||
/** Options for `createApiClientWithDetails`. */
|
||||
interface CreateApiClientOptions {
|
||||
allowExternal?: boolean;
|
||||
proxy?: ProxyAgent;
|
||||
}
|
||||
|
||||
function createApiClientWithDetails(
|
||||
apiDetails: GitHubApiCombinedDetails,
|
||||
{ allowExternal = false } = {},
|
||||
) {
|
||||
{ allowExternal = false, proxy = undefined }: CreateApiClientOptions = {},
|
||||
): ApiClient {
|
||||
const auth =
|
||||
(allowExternal && apiDetails.externalRepoAuth) || apiDetails.auth;
|
||||
const retryingOctokit = githubUtils.GitHub.plugin(retry.retry);
|
||||
const requestOptions =
|
||||
proxy === undefined ? undefined : makeProxyRequestOptions(proxy);
|
||||
return new retryingOctokit(
|
||||
githubUtils.getOctokitOptions(auth, {
|
||||
baseUrl: apiDetails.apiURL,
|
||||
@@ -60,6 +148,7 @@ function createApiClientWithDetails(
|
||||
warn: core.warning,
|
||||
error: core.error,
|
||||
},
|
||||
request: requestOptions,
|
||||
retry: {
|
||||
doNotRetry: DO_NOT_RETRY_STATUSES,
|
||||
},
|
||||
@@ -67,22 +156,23 @@ function createApiClientWithDetails(
|
||||
);
|
||||
}
|
||||
|
||||
export function getApiDetails(): GitHubApiDetails {
|
||||
export function getApiDetails(env: ReadOnlyEnv = getEnv()): GitHubApiDetails {
|
||||
return {
|
||||
auth: getRequiredInput("token"),
|
||||
url: getRequiredEnvParam("GITHUB_SERVER_URL"),
|
||||
apiURL: getRequiredEnvParam("GITHUB_API_URL"),
|
||||
url: env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL),
|
||||
apiURL: env.getRequired(ActionsEnvVars.GITHUB_API_URL),
|
||||
};
|
||||
}
|
||||
|
||||
export function getApiClient() {
|
||||
return createApiClientWithDetails(getApiDetails());
|
||||
export function getApiClient(env: ReadOnlyEnv = getEnv()) {
|
||||
return createApiClientWithDetails(getApiDetails(env));
|
||||
}
|
||||
|
||||
export function getApiClientWithExternalAuth(
|
||||
apiDetails: GitHubApiCombinedDetails,
|
||||
proxy?: ProxyAgent,
|
||||
) {
|
||||
return createApiClientWithDetails(apiDetails, { allowExternal: true });
|
||||
return createApiClientWithDetails(apiDetails, { allowExternal: true, proxy });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"maximumVersion": "3.22", "minimumVersion": "3.16"}
|
||||
{"maximumVersion": "3.22", "minimumVersion": "3.17"}
|
||||
+10
-18
@@ -1,5 +1,6 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import {
|
||||
getActionVersion,
|
||||
getOptionalInput,
|
||||
@@ -11,13 +12,12 @@ import { getCodeQL } from "./codeql";
|
||||
import { Config, getConfig } from "./config-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import { Language } from "./languages";
|
||||
import { Logger, getActionsLogger } from "./logging";
|
||||
import { Logger } from "./logging";
|
||||
import {
|
||||
StatusReportBase,
|
||||
getActionsStatus,
|
||||
createStatusReportBase,
|
||||
sendStatusReport,
|
||||
sendUnhandledErrorStatusReport,
|
||||
ActionName,
|
||||
} from "./status-report";
|
||||
import { endTracingForCluster } from "./tracer-config";
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
checkDiskUsage,
|
||||
checkGitHubVersionInRange,
|
||||
ConfigurationError,
|
||||
getErrorMessage,
|
||||
initializeEnvironment,
|
||||
wrapError,
|
||||
} from "./util";
|
||||
@@ -69,11 +68,10 @@ async function sendCompletedStatusReport(
|
||||
}
|
||||
}
|
||||
|
||||
async function run(startedAt: Date) {
|
||||
async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
const logger = getActionsLogger();
|
||||
let config: Config | undefined;
|
||||
let currentLanguage: Language | undefined;
|
||||
let languages: Language[] | undefined;
|
||||
@@ -142,18 +140,12 @@ async function run(startedAt: Date) {
|
||||
await sendCompletedStatusReport(config, logger, startedAt, languages ?? []);
|
||||
}
|
||||
|
||||
/** Defines the `autobuild` Action. */
|
||||
const autobuild: Action = {
|
||||
name: ActionName.Autobuild,
|
||||
run,
|
||||
};
|
||||
|
||||
export async function runWrapper() {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
try {
|
||||
await run(startedAt);
|
||||
} catch (error) {
|
||||
core.setFailed(`autobuild action failed. ${getErrorMessage(error)}`);
|
||||
await sendUnhandledErrorStatusReport(
|
||||
ActionName.Autobuild,
|
||||
startedAt,
|
||||
error,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
await runInActions(autobuild);
|
||||
}
|
||||
+2
-2
@@ -5,7 +5,7 @@ import { getGitHubVersion } from "./api-client";
|
||||
import { CodeQL, getCodeQL } from "./codeql";
|
||||
import * as configUtils from "./config-utils";
|
||||
import { DocUrl } from "./doc-url";
|
||||
import { EnvVar } from "./environment";
|
||||
import { ActionsEnvVars, EnvVar } from "./environment";
|
||||
import { Feature, featureConfig, initFeatures } from "./feature-flags";
|
||||
import { BuiltInLanguage, Language } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
@@ -126,7 +126,7 @@ export async function setupCppAutobuild(codeql: CodeQL, logger: Logger) {
|
||||
if (await features.getValue(Feature.CppDependencyInstallation, codeql)) {
|
||||
// disable autoinstall on self-hosted runners unless explicitly requested
|
||||
if (
|
||||
process.env["RUNNER_ENVIRONMENT"] === "self-hosted" &&
|
||||
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] === "self-hosted" &&
|
||||
process.env[envVar] !== "true"
|
||||
) {
|
||||
logger.info(
|
||||
|
||||
+46
-51
@@ -272,17 +272,17 @@ const CODEQL_MINIMUM_VERSION = "2.19.4";
|
||||
/**
|
||||
* This version will shortly become the oldest version of CodeQL that the Action will run with.
|
||||
*/
|
||||
const CODEQL_NEXT_MINIMUM_VERSION = "2.19.4";
|
||||
const CODEQL_NEXT_MINIMUM_VERSION = "2.20.7";
|
||||
|
||||
/**
|
||||
* This is the version of GHES that was most recently deprecated.
|
||||
*/
|
||||
const GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.15";
|
||||
const GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.16";
|
||||
|
||||
/**
|
||||
* This is the deprecation date for the version of GHES that was most recently deprecated.
|
||||
*/
|
||||
const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-04-09";
|
||||
const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-07-01";
|
||||
|
||||
/** The CLI verbosity level to use for extraction in debug mode. */
|
||||
const EXTRACTION_DEBUG_MODE_VERBOSITY = "progress++";
|
||||
@@ -514,16 +514,13 @@ async function getCodeQLForCmd(
|
||||
async getVersion() {
|
||||
let result = util.getCachedCodeQlVersion(cmd);
|
||||
if (result === undefined) {
|
||||
const output = await runCli(cmd, ["version", "--format=json"], {
|
||||
noStreamStdout: true,
|
||||
});
|
||||
try {
|
||||
result = JSON.parse(output) as VersionInfo;
|
||||
} catch {
|
||||
throw Error(
|
||||
`Invalid JSON output from \`version --format=json\`: ${output}`,
|
||||
);
|
||||
}
|
||||
result = await runCliJson<VersionInfo>(
|
||||
cmd,
|
||||
["version", "--format=json"],
|
||||
{
|
||||
noStreamStdout: true,
|
||||
},
|
||||
);
|
||||
util.cacheCodeQlVersion(cmd, result);
|
||||
}
|
||||
return result;
|
||||
@@ -731,26 +728,20 @@ async function getCodeQLForCmd(
|
||||
filterToLanguagesWithQueries: boolean;
|
||||
} = { filterToLanguagesWithQueries: false },
|
||||
) {
|
||||
const codeqlArgs = [
|
||||
return runCliJson<ResolveLanguagesOutput>(cmd, [
|
||||
"resolve",
|
||||
"languages",
|
||||
"--format=betterjson",
|
||||
"--extractor-options-verbosity=4",
|
||||
"--extractor-include-aliases",
|
||||
// TODO: Unconditionally include `--filter-to-languages-with-queries`
|
||||
// once CODEQL_MINIMUM_VERSION is at least v2.23.0
|
||||
// — the first version to support this flag.
|
||||
...(filterToLanguagesWithQueries
|
||||
? ["--filter-to-languages-with-queries"]
|
||||
: []),
|
||||
...getExtraOptionsFromEnv(["resolve", "languages"]),
|
||||
];
|
||||
const output = await runCli(cmd, codeqlArgs);
|
||||
|
||||
try {
|
||||
return JSON.parse(output) as ResolveLanguagesOutput;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Unexpected output from codeql resolve languages with --format=betterjson: ${e}`,
|
||||
);
|
||||
}
|
||||
]);
|
||||
},
|
||||
async resolveBuildEnvironment(
|
||||
workingDir: string | undefined,
|
||||
@@ -766,15 +757,7 @@ async function getCodeQLForCmd(
|
||||
if (workingDir !== undefined) {
|
||||
codeqlArgs.push("--working-dir", workingDir);
|
||||
}
|
||||
const output = await runCli(cmd, codeqlArgs);
|
||||
|
||||
try {
|
||||
return JSON.parse(output) as ResolveBuildEnvironmentOutput;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Unexpected output from codeql resolve build-environment: ${e} in\n${output}`,
|
||||
);
|
||||
}
|
||||
return await runCliJson<ResolveBuildEnvironmentOutput>(cmd, codeqlArgs);
|
||||
},
|
||||
async databaseRunQueries(
|
||||
databasePath: string,
|
||||
@@ -976,15 +959,9 @@ async function getCodeQLForCmd(
|
||||
...getExtraOptionsFromEnv(["resolve", "queries"]),
|
||||
...queries,
|
||||
];
|
||||
const output = await runCli(cmd, codeqlArgs, { noStreamStdout: true });
|
||||
|
||||
try {
|
||||
return JSON.parse(output) as string[];
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Unexpected output from codeql resolve queries --format=startingpacks: ${e}`,
|
||||
);
|
||||
}
|
||||
return await runCliJson<string[]>(cmd, codeqlArgs, {
|
||||
noStreamStdout: true,
|
||||
});
|
||||
},
|
||||
async resolveDatabase(
|
||||
databasePath: string,
|
||||
@@ -996,15 +973,9 @@ async function getCodeQLForCmd(
|
||||
"--format=json",
|
||||
...getExtraOptionsFromEnv(["resolve", "database"]),
|
||||
];
|
||||
const output = await runCli(cmd, codeqlArgs, { noStreamStdout: true });
|
||||
|
||||
try {
|
||||
return JSON.parse(output) as ResolveDatabaseOutput;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Unexpected output from codeql resolve database --format=json: ${e}`,
|
||||
);
|
||||
}
|
||||
return await runCliJson<ResolveDatabaseOutput>(cmd, codeqlArgs, {
|
||||
noStreamStdout: true,
|
||||
});
|
||||
},
|
||||
async mergeResults(
|
||||
sarifFiles: string[],
|
||||
@@ -1160,6 +1131,30 @@ async function runCli(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the command executor {@link runCli} and tries to parse the output as JSON.
|
||||
* @param cmd The command to run.
|
||||
* @param args The arguments to pass to the command.
|
||||
* @param opts The options for running the command.
|
||||
* @param opts.stdin Optional string to pass to the command's standard input.
|
||||
* @param opts.noStreamStdout Optional boolean to indicate whether to stream the command's standard output.
|
||||
* @returns The parsed JSON output from the command.
|
||||
*/
|
||||
async function runCliJson<T>(
|
||||
cmd: string,
|
||||
args: string[] = [],
|
||||
opts: { stdin?: string; noStreamStdout?: boolean } = {},
|
||||
): Promise<T> {
|
||||
const output = await runCli(cmd, args, opts);
|
||||
try {
|
||||
return JSON.parse(output) as T;
|
||||
} catch (e) {
|
||||
throw Error(
|
||||
`Unexpected output from codeql ${args.join(" ")}: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the code scanning configuration that is to be used by the CLI.
|
||||
*
|
||||
|
||||
+495
-62
@@ -6,12 +6,14 @@ import test, { ExecutionContext } from "ava";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as sinon from "sinon";
|
||||
|
||||
import { ActionState } from "./action-common";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { AnalysisKind, supportedAnalysisKinds } from "./analyses";
|
||||
import * as api from "./api-client";
|
||||
import { CachingKind } from "./caching-utils";
|
||||
import { createStubCodeQL } from "./codeql";
|
||||
import { UserConfig } from "./config/db-config";
|
||||
import * as file from "./config/file";
|
||||
import * as configUtils from "./config-utils";
|
||||
import * as errorMessages from "./error-messages";
|
||||
import { Feature } from "./feature-flags";
|
||||
@@ -36,6 +38,10 @@ import {
|
||||
mockCodeQLVersion,
|
||||
createTestConfig,
|
||||
makeMacro,
|
||||
initAllState,
|
||||
callee,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
AssertableTarget,
|
||||
} from "./testing-utils";
|
||||
import {
|
||||
GitHubVariant,
|
||||
@@ -160,8 +166,9 @@ test.serial("load empty config", async (t) => {
|
||||
},
|
||||
});
|
||||
|
||||
const state = initAllState({ logger });
|
||||
const config = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput: languages,
|
||||
repository: { owner: "github", repo: "example" },
|
||||
@@ -202,8 +209,9 @@ test.serial("load code quality config", async (t) => {
|
||||
},
|
||||
});
|
||||
|
||||
const state = initAllState({ logger });
|
||||
const config = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
analysisKinds: [AnalysisKind.CodeQuality],
|
||||
languagesInput: languages,
|
||||
@@ -280,9 +288,10 @@ test.serial(
|
||||
repositoryProperties,
|
||||
});
|
||||
|
||||
const state = initAllState({ logger });
|
||||
await t.notThrowsAsync(async () => {
|
||||
const config = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
analysisKinds: [AnalysisKind.CodeQuality],
|
||||
languagesInput: languages,
|
||||
@@ -321,8 +330,9 @@ test.serial("loading a saved config produces the same config", async (t) => {
|
||||
// Sanity check that getConfig returns undefined before we have called initConfig
|
||||
t.deepEqual(await configUtils.getConfig(tempDir, logger), undefined);
|
||||
|
||||
const state = initAllState({ logger });
|
||||
const config1 = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput: "javascript,python",
|
||||
tempDir,
|
||||
@@ -373,8 +383,9 @@ test.serial("loading config with version mismatch throws", async (t) => {
|
||||
.stub(actionsUtil, "getActionVersion")
|
||||
.returns("does-not-exist");
|
||||
|
||||
const state = initAllState({ logger });
|
||||
const config = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput: "javascript,python",
|
||||
tempDir,
|
||||
@@ -402,8 +413,9 @@ test.serial("loading config with version mismatch throws", async (t) => {
|
||||
test.serial("load input outside of workspace", async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
try {
|
||||
const state = initAllState();
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
configFile: "../input",
|
||||
tempDir,
|
||||
@@ -424,34 +436,6 @@ test.serial("load input outside of workspace", async (t) => {
|
||||
});
|
||||
});
|
||||
|
||||
test.serial("load non-local input with invalid repo syntax", async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
// no filename given, just a repo
|
||||
const configFile = "octo-org/codeql-config@main";
|
||||
|
||||
try {
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
createTestInitConfigInputs({
|
||||
configFile,
|
||||
tempDir,
|
||||
workspacePath: tempDir,
|
||||
}),
|
||||
);
|
||||
throw new Error("initConfig did not throw error");
|
||||
} catch (err) {
|
||||
t.deepEqual(
|
||||
err,
|
||||
new ConfigurationError(
|
||||
errorMessages.getConfigFileRepoFormatInvalidMessage(
|
||||
"octo-org/codeql-config@main",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.serial("load non-existent input", async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
const languagesInput = "javascript";
|
||||
@@ -459,8 +443,9 @@ test.serial("load non-existent input", async (t) => {
|
||||
t.false(fs.existsSync(path.join(tempDir, configFile)));
|
||||
|
||||
try {
|
||||
const state = initAllState();
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
configFile,
|
||||
@@ -482,6 +467,24 @@ test.serial("load non-existent input", async (t) => {
|
||||
});
|
||||
});
|
||||
|
||||
/** A non-empty, but fairly minimal configuration file. */
|
||||
const simpleConfigFileContents = `
|
||||
name: my config
|
||||
queries:
|
||||
- uses: ./foo_file`;
|
||||
|
||||
/** A less minimal configuration file. */
|
||||
const otherConfigFileContents = `
|
||||
name: my config
|
||||
disable-default-queries: true
|
||||
queries:
|
||||
- uses: ./foo
|
||||
paths-ignore:
|
||||
- a
|
||||
- b
|
||||
paths:
|
||||
- c/d`;
|
||||
|
||||
test.serial("load non-empty input", async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
setupActionsVars(tempDir, tempDir);
|
||||
@@ -496,18 +499,6 @@ test.serial("load non-empty input", async (t) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Just create a generic config object with non-default values for all fields
|
||||
const inputFileContents = `
|
||||
name: my config
|
||||
disable-default-queries: true
|
||||
queries:
|
||||
- uses: ./foo
|
||||
paths-ignore:
|
||||
- a
|
||||
- b
|
||||
paths:
|
||||
- c/d`;
|
||||
|
||||
fs.mkdirSync(path.join(tempDir, "foo"));
|
||||
|
||||
const userConfig: UserConfig = {
|
||||
@@ -534,10 +525,11 @@ test.serial("load non-empty input", async (t) => {
|
||||
});
|
||||
|
||||
const languagesInput = "javascript";
|
||||
const configFilePath = createConfigFile(inputFileContents, tempDir);
|
||||
const configFilePath = createConfigFile(otherConfigFileContents, tempDir);
|
||||
|
||||
const state = initAllState();
|
||||
const actualConfig = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
buildModeInput: "none",
|
||||
@@ -559,14 +551,12 @@ test.serial(
|
||||
"Using config input and file together, config input should be used.",
|
||||
async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
process.env["RUNNER_TEMP"] = tempDir;
|
||||
process.env["GITHUB_WORKSPACE"] = tempDir;
|
||||
setupActionsVars(tempDir, tempDir);
|
||||
|
||||
const inputFileContents = `
|
||||
name: my config
|
||||
queries:
|
||||
- uses: ./foo_file`;
|
||||
const configFilePath = createConfigFile(inputFileContents, tempDir);
|
||||
const configFilePath = createConfigFile(
|
||||
simpleConfigFileContents,
|
||||
tempDir,
|
||||
);
|
||||
|
||||
const configInput = `
|
||||
name: my config
|
||||
@@ -595,8 +585,9 @@ test.serial(
|
||||
// Only JS, python packs will be ignored
|
||||
const languagesInput = "javascript";
|
||||
|
||||
const state = initAllState({ env: util.getEnv() });
|
||||
const config = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
configFile: configFilePath,
|
||||
@@ -647,8 +638,9 @@ test.serial("API client used when reading remote config", async (t) => {
|
||||
const configFile = "octo-org/codeql-config/config.yaml@main";
|
||||
const languagesInput = "javascript";
|
||||
|
||||
const state = initAllState();
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
configFile,
|
||||
@@ -669,9 +661,10 @@ test.serial(
|
||||
mockGetContents(dummyResponse);
|
||||
|
||||
const repoReference = "octo-org/codeql-config/config.yaml@main";
|
||||
const state = initAllState();
|
||||
try {
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
configFile: repoReference,
|
||||
tempDir,
|
||||
@@ -699,9 +692,10 @@ test.serial("Invalid format of remote config handled correctly", async (t) => {
|
||||
mockGetContents(dummyResponse);
|
||||
|
||||
const repoReference = "octo-org/codeql-config/config.yaml@main";
|
||||
const state = initAllState();
|
||||
try {
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
configFile: repoReference,
|
||||
tempDir,
|
||||
@@ -729,9 +723,10 @@ test.serial("No detected languages", async (t) => {
|
||||
},
|
||||
});
|
||||
|
||||
const state = initAllState();
|
||||
try {
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
tempDir,
|
||||
codeql,
|
||||
@@ -752,9 +747,10 @@ test.serial("Unknown languages", async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
const languagesInput = "rubbish,english";
|
||||
|
||||
const state = initAllState();
|
||||
try {
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
tempDir,
|
||||
@@ -2272,3 +2268,440 @@ test("applyIncrementalAnalysisSettings: adds exclusions for diff-informed-only r
|
||||
{ exclude: { tags: "exclude-from-incremental" } },
|
||||
]);
|
||||
});
|
||||
|
||||
test("determineUserConfig - empty config when neither input is specified", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const target = callee(configUtils.determineUserConfig)
|
||||
.withDefaultActionsEnv()
|
||||
.withFeatures([])
|
||||
.withArgs(
|
||||
tmpDir,
|
||||
createTestInitConfigInputs({
|
||||
configInput: undefined,
|
||||
configFile: undefined,
|
||||
workspacePath: tmpDir,
|
||||
}),
|
||||
);
|
||||
|
||||
// The returned configuration should be empty.
|
||||
await target
|
||||
// The fact that no configuration was provided should have been logged,
|
||||
.logs(t, "No configuration file was provided")
|
||||
// But not the messages for the two input sources
|
||||
// or the warning about both inputs.
|
||||
.notLogs(
|
||||
t,
|
||||
"Using config from action input:",
|
||||
"Using configuration file:",
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
.passes(t.deepEqual, {});
|
||||
});
|
||||
});
|
||||
|
||||
test("determineUserConfig - loads config file", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const configFilePath = createConfigFile(simpleConfigFileContents, tmpDir);
|
||||
|
||||
const inputs = createTestInitConfigInputs({
|
||||
configInput: undefined,
|
||||
configFile: configFilePath,
|
||||
workspacePath: tmpDir,
|
||||
});
|
||||
const target = callee(configUtils.determineUserConfig)
|
||||
.withDefaultActionsEnv()
|
||||
.withArgs(tmpDir, inputs);
|
||||
|
||||
await target
|
||||
// The path of the input config file should have been logged,
|
||||
.logs(t, `Using configuration file: ${configFilePath}`)
|
||||
.notLogs(
|
||||
t,
|
||||
// The other two origin messages and the warning about both inputs should
|
||||
// not have been logged.
|
||||
"No configuration file was provided",
|
||||
"Using config from action input:",
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
// The loaded configuration should match `simpleConfigFileContents`.
|
||||
.passes(t.deepEqual, {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo_file" }],
|
||||
});
|
||||
|
||||
// The `configFile` input should not have changed.
|
||||
t.is(inputs.configFile, configFilePath);
|
||||
});
|
||||
});
|
||||
|
||||
test("determineUserConfig - loads config input", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
|
||||
const inputs = createTestInitConfigInputs({
|
||||
configInput: simpleConfigFileContents,
|
||||
configFile: undefined,
|
||||
workspacePath: tmpDir,
|
||||
});
|
||||
const target = callee(configUtils.determineUserConfig)
|
||||
.withDefaultActionsEnv()
|
||||
.withArgs(tmpDir, inputs);
|
||||
|
||||
await target
|
||||
// The input source and path of the generated config file should have been logged.
|
||||
.logs(
|
||||
t,
|
||||
"Using config from action input:",
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
)
|
||||
// The message about no configuration input and
|
||||
// the warning about both inputs should not have been logged.
|
||||
.notLogs(
|
||||
t,
|
||||
"No configuration file was provided",
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
// The loaded configuration should match `simpleConfigFileContents`.
|
||||
.passes(t.deepEqual, {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo_file" }],
|
||||
});
|
||||
|
||||
// The `configFile` input should have been mutated to the generated path.
|
||||
t.is(inputs.configFile, expectedConfigPath);
|
||||
});
|
||||
});
|
||||
|
||||
test("determineUserConfig - ignores config file input when both specified", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const configFilePath = createConfigFile(otherConfigFileContents, tmpDir);
|
||||
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
|
||||
const inputs = createTestInitConfigInputs({
|
||||
configInput: simpleConfigFileContents,
|
||||
configFile: configFilePath,
|
||||
workspacePath: tmpDir,
|
||||
});
|
||||
const target = callee(configUtils.determineUserConfig)
|
||||
.withDefaultActionsEnv()
|
||||
.withArgs(tmpDir, inputs);
|
||||
|
||||
await target
|
||||
// The path of the generated config file and
|
||||
// the warning about both inputs should have been logged.
|
||||
.logs(
|
||||
t,
|
||||
`Using config from action input: ${expectedConfigPath}`,
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
.notLogs(t, "No configuration file was provided")
|
||||
// The loaded configuration should match `simpleConfigFileContents`.
|
||||
.passes(t.deepEqual, {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo_file" }],
|
||||
});
|
||||
|
||||
// The `configFile` input should have been mutated to the generated path.
|
||||
t.is(inputs.configFile, expectedConfigPath);
|
||||
});
|
||||
});
|
||||
|
||||
/** A `config` input that we might get from Default Setup. */
|
||||
const defaultSetupConfigInput = `
|
||||
threat-models: [local, remote]
|
||||
default-setup:
|
||||
org:
|
||||
model-packs: [foo, bar]`;
|
||||
|
||||
test("determineUserConfig - merges configs if FF is enabled in Default Setup", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const configFilePath = createConfigFile(simpleConfigFileContents, tmpDir);
|
||||
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
|
||||
const inputs = createTestInitConfigInputs({
|
||||
configInput: defaultSetupConfigInput,
|
||||
configFile: configFilePath,
|
||||
workspacePath: tmpDir,
|
||||
});
|
||||
const target = callee(configUtils.determineUserConfig)
|
||||
.withDefaultActionsEnv({ GITHUB_EVENT_NAME: "dynamic" })
|
||||
.withFeatures([Feature.AllowMergeConfigFiles])
|
||||
.withArgs(tmpDir, inputs);
|
||||
|
||||
// The loaded configuration should match the result of merging
|
||||
// `defaultSetupConfigInput` and `simpleConfigFileContents`.
|
||||
const expectedConfig = {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo_file" }],
|
||||
"threat-models": ["local", "remote"],
|
||||
"default-setup": {
|
||||
org: {
|
||||
"model-packs": ["foo", "bar"],
|
||||
},
|
||||
},
|
||||
} satisfies UserConfig;
|
||||
|
||||
await target
|
||||
.logs(
|
||||
t,
|
||||
`Using merged configurations from 'config' input with configuration from '${configFilePath}': ${expectedConfigPath}`,
|
||||
)
|
||||
.notLogs(
|
||||
t,
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
"No configuration file was provided",
|
||||
`Using config from action input: ${expectedConfigPath}`,
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
.passes(t.deepEqual, expectedConfig);
|
||||
|
||||
// The `configFile` input should have been mutated to the generated path.
|
||||
t.is(inputs.configFile, expectedConfigPath);
|
||||
|
||||
// Since `result` is the result of merging the configurations in-memory,
|
||||
// also check whether loading the configuration from disk that was written
|
||||
// by `determineUserConfig` matches our expectations.
|
||||
const loadedFromDisk = configUtils.getLocalConfig(
|
||||
getRunnerLogger(true),
|
||||
expectedConfigPath,
|
||||
false,
|
||||
);
|
||||
t.deepEqual(loadedFromDisk, expectedConfig);
|
||||
});
|
||||
});
|
||||
|
||||
test("determineUserConfig - ignores config file input in Default Setup if FF is off", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const configFilePath = createConfigFile(otherConfigFileContents, tmpDir);
|
||||
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
|
||||
const target = callee(configUtils.determineUserConfig)
|
||||
.withDefaultActionsEnv({ GITHUB_EVENT_NAME: "dynamic" })
|
||||
.withArgs(
|
||||
tmpDir,
|
||||
createTestInitConfigInputs({
|
||||
configInput: simpleConfigFileContents,
|
||||
configFile: configFilePath,
|
||||
workspacePath: tmpDir,
|
||||
}),
|
||||
);
|
||||
|
||||
await target
|
||||
.logs(
|
||||
t,
|
||||
`Using config from action input: ${expectedConfigPath}`,
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
.notLogs(t, "No configuration file was provided")
|
||||
.passes(t.deepEqual, {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo_file" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("determineUserConfig - ignores config file input outside Default Setup if FF is on", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const configFilePath = createConfigFile(otherConfigFileContents, tmpDir);
|
||||
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
|
||||
const target = callee(configUtils.determineUserConfig)
|
||||
.withDefaultActionsEnv()
|
||||
.withFeatures([Feature.AllowMergeConfigFiles])
|
||||
.withArgs(
|
||||
tmpDir,
|
||||
createTestInitConfigInputs({
|
||||
configInput: simpleConfigFileContents,
|
||||
configFile: configFilePath,
|
||||
workspacePath: tmpDir,
|
||||
}),
|
||||
);
|
||||
|
||||
await target
|
||||
.logs(
|
||||
t,
|
||||
`Using config from action input: ${expectedConfigPath}`,
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
.notLogs(t, "No configuration file was provided")
|
||||
.passes(t.deepEqual, {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo_file" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("loadUserConfig - loads local configuration files", async (t) => {
|
||||
await withTmpDir(async (workspaceDir) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
// Construct the test target.
|
||||
const loadUserConfig = (
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
filePath: string,
|
||||
) =>
|
||||
configUtils.loadUserConfig(
|
||||
actionState,
|
||||
filePath,
|
||||
workspaceDir,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
tmpDir,
|
||||
);
|
||||
const target = callee(loadUserConfig);
|
||||
|
||||
// `loadUserConfig` should load local configuration files if they are inside the workspace:
|
||||
const insideOfWorkspace = path.join(workspaceDir, "some-file.yml");
|
||||
fs.writeFileSync(insideOfWorkspace, "test-key: present", "utf8");
|
||||
|
||||
await target
|
||||
.withArgs(insideOfWorkspace)
|
||||
.passes(t.deepEqual, { "test-key": "present" });
|
||||
|
||||
// `loadUserConfig` should normally throw if the path is outside of the workspace:
|
||||
const outsideOfWorkspace = path.join(
|
||||
tmpDir,
|
||||
"not-the-generated-file.yml",
|
||||
);
|
||||
fs.writeFileSync(outsideOfWorkspace, "test-key: present", "utf8");
|
||||
|
||||
await target
|
||||
.withArgs(outsideOfWorkspace)
|
||||
.throws(t, { instanceOf: ConfigurationError });
|
||||
|
||||
// `loadUserConfig` does not throw if the path is the result of `userConfigFromActionPath`:
|
||||
const generatedPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
fs.writeFileSync(generatedPath, "test-key: present", "utf8");
|
||||
|
||||
await target
|
||||
.withArgs(generatedPath)
|
||||
.passes(t.deepEqual, { "test-key": "present" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.serial("loadUserConfig - loads remote configuration files", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const getRemoteConfig = sinon.stub(file, "getRemoteConfig").resolves({});
|
||||
|
||||
const remoteAddress = "owner/repo/file@ref";
|
||||
await callee(configUtils.loadUserConfig)
|
||||
.withArgs(remoteAddress, tmpDir, SAMPLE_DOTCOM_API_DETAILS, tmpDir)
|
||||
.passes(t.deepEqual, {});
|
||||
|
||||
t.true(
|
||||
getRemoteConfig.calledOnceWithExactly(
|
||||
sinon.match.any,
|
||||
remoteAddress,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"loadUserConfig - loads remote configuration files (new format, partial)",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const getRemoteConfig = sinon.stub(file, "getRemoteConfig").resolves({});
|
||||
|
||||
// Construct the basic test target.
|
||||
const target = callee(configUtils.loadUserConfig).withDefaultActionsEnv();
|
||||
|
||||
// Utility function to assert that `targetWithArgs` has identified
|
||||
// the input as a remote file address.
|
||||
const checkIsRemote =
|
||||
(address: string) =>
|
||||
async <R>(targetWithArgs: AssertableTarget<R>) => {
|
||||
// We have stubbed `getRemoteConfig` to resolve to `{}`, so we
|
||||
// expect that result.
|
||||
await targetWithArgs.passes(t.deepEqual, {});
|
||||
|
||||
// And `getRemoteConfig` should have been called exactly once.
|
||||
t.is(getRemoteConfig.callCount, 1);
|
||||
|
||||
// Get the arguments for the call and check that there were three.
|
||||
// We don't care about the first, but check that the other two
|
||||
// match our expectations. We break it down like this to get
|
||||
// more useful test output.
|
||||
const args = getRemoteConfig.getCalls()[0].args;
|
||||
t.is(args.length, 3);
|
||||
t.deepEqual(args[1], address);
|
||||
t.deepEqual(args[2], SAMPLE_DOTCOM_API_DETAILS);
|
||||
};
|
||||
|
||||
// Utility function to assert that `targetWithArgs` has not identified
|
||||
// the input as a remote file address.
|
||||
const checkIsNotRemote = async <R>(
|
||||
targetWithArgs: AssertableTarget<R>,
|
||||
) => {
|
||||
// We expect `loadUserConfig` to have thrown if it thinks the path is local,
|
||||
// since the inputs we provide aren't for files that exist.
|
||||
await targetWithArgs.throws(t);
|
||||
|
||||
// Additionally, we expect that `getRemoteConfig` wasn't called.
|
||||
t.is(getRemoteConfig.callCount, 0);
|
||||
};
|
||||
|
||||
// Utility function to add the explicit `REMOTE_PATH_PREFIX` to the input.
|
||||
const withExplicitPrefix = (str: string) =>
|
||||
`${file.REMOTE_PATH_PREFIX}${str}`;
|
||||
|
||||
// Utility to set up a call to `loadUserConfig` with the provided `address`
|
||||
// and pass it to `assertion`.
|
||||
const testTargetWith = async (
|
||||
address: string,
|
||||
assertion: (
|
||||
targetWithArgs: AssertableTarget<Promise<UserConfig>>,
|
||||
) => Promise<any>,
|
||||
) => {
|
||||
// Reset the stub's history since we re-use it.
|
||||
getRemoteConfig.resetHistory();
|
||||
|
||||
// Log the input we are testing so that, in the event of a failure,
|
||||
// it is easier to see which input was responsible.
|
||||
t.log(`testTargetWith("${address}")`);
|
||||
|
||||
// Prepare the test call to `loadUserConfig`.
|
||||
const targetWithArgs = target.withArgs(
|
||||
address,
|
||||
tmpDir,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
tmpDir,
|
||||
);
|
||||
|
||||
// Pass it to the provided assertion function.
|
||||
await assertion(targetWithArgs);
|
||||
};
|
||||
|
||||
// Since this input contains an '@' character, it is treated as a remote path
|
||||
// by the old logic even without the explicit prefix.
|
||||
const remoteWithoutPrefix = "repo@main";
|
||||
await testTargetWith(
|
||||
remoteWithoutPrefix,
|
||||
checkIsRemote(remoteWithoutPrefix),
|
||||
);
|
||||
await testTargetWith(
|
||||
withExplicitPrefix(remoteWithoutPrefix),
|
||||
checkIsRemote(remoteWithoutPrefix),
|
||||
);
|
||||
// It is only treated as a local path with the corresponding prefix.
|
||||
await testTargetWith(`./${remoteWithoutPrefix}`, checkIsNotRemote);
|
||||
|
||||
// The following test inputs are examples of ambiguous paths. They could refer to
|
||||
// valid local or remote paths. For each, we check that they are treated as remote
|
||||
// paths if the explicit remote file prefix is used and as local paths otherwise.
|
||||
const testInputs = ["repo:file", "input", "../input"];
|
||||
|
||||
for (const testInput of testInputs) {
|
||||
for (const addPrefix of [true, false]) {
|
||||
await testTargetWith(
|
||||
addPrefix ? withExplicitPrefix(testInput) : testInput,
|
||||
addPrefix ? checkIsRemote(testInput) : checkIsNotRemote,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
+198
-254
@@ -5,10 +5,12 @@ import { performance } from "perf_hooks";
|
||||
import * as core from "@actions/core";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
import { ActionState } from "./action-common";
|
||||
import {
|
||||
getActionVersion,
|
||||
getOptionalInput,
|
||||
isAnalyzingPullRequest,
|
||||
isDefaultSetup,
|
||||
isDynamicWorkflow,
|
||||
} from "./actions-util";
|
||||
import {
|
||||
@@ -18,15 +20,27 @@ import {
|
||||
getAnalysisConfig,
|
||||
} from "./analyses";
|
||||
import * as api from "./api-client";
|
||||
import { CachingKind, getCachingKind } from "./caching-utils";
|
||||
import { getCachingKind } from "./caching-utils";
|
||||
import { type CodeQL } from "./codeql";
|
||||
import { type Config } from "./config/action-config";
|
||||
import {
|
||||
calculateAugmentation,
|
||||
ExcludeQueryFilter,
|
||||
generateCodeScanningConfig,
|
||||
mergeDefaultSetupAndUserConfigs,
|
||||
parseUserConfig,
|
||||
UserConfig,
|
||||
} from "./config/db-config";
|
||||
import {
|
||||
getRemoteConfig,
|
||||
LOCAL_PATH_PREFIX,
|
||||
REMOTE_PATH_PREFIX,
|
||||
} from "./config/file";
|
||||
import {
|
||||
parseRegistries,
|
||||
type RegistryConfigNoCredentials,
|
||||
type RegistryConfigWithCredentials,
|
||||
} from "./config/pack-registries";
|
||||
import {
|
||||
addNoLanguageDiagnostic,
|
||||
makeTelemetryDiagnostic,
|
||||
@@ -79,6 +93,8 @@ import {
|
||||
isHostedRunner,
|
||||
} from "./util";
|
||||
|
||||
export { type Config } from "./config/action-config";
|
||||
|
||||
/**
|
||||
* The minimum available disk space (in MB) required to perform overlay analysis.
|
||||
* If the available disk space on the runner is below the threshold when deciding
|
||||
@@ -117,148 +133,6 @@ const OVERLAY_MINIMUM_MEMORY_MB = 5 * 1024;
|
||||
*/
|
||||
const CODEQL_VERSION_REDUCED_OVERLAY_MEMORY_USAGE = "2.24.3";
|
||||
|
||||
export type RegistryConfigWithCredentials = RegistryConfigNoCredentials & {
|
||||
// Token to use when downloading packs from this registry.
|
||||
token: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The list of registries and the associated pack globs that determine where each
|
||||
* pack can be downloaded from.
|
||||
*/
|
||||
export interface RegistryConfigNoCredentials {
|
||||
// URL of a package registry, eg- https://ghcr.io/v2/
|
||||
url: string;
|
||||
|
||||
// List of globs that determine which packs are associated with this registry.
|
||||
packages: string[] | string;
|
||||
|
||||
// Kind of registry, either "github" or "docker". Default is "docker".
|
||||
// "docker" refers specifically to the GitHub Container Registry, which is the usual way of sharing CodeQL packs.
|
||||
// "github" refers to packs published as content in a GitHub repository. This kind of registry is used in scenarios
|
||||
// where GHCR is not available, such as certain GHES environments.
|
||||
kind?: "github" | "docker";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format of the parsed config file.
|
||||
*/
|
||||
export interface Config {
|
||||
/**
|
||||
* The version of the CodeQL Action that the configuration is for.
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Set of analysis kinds that are enabled.
|
||||
*/
|
||||
analysisKinds: AnalysisKind[];
|
||||
/**
|
||||
* Set of languages to run analysis for.
|
||||
*/
|
||||
languages: Language[];
|
||||
/**
|
||||
* Build mode, if set. Currently only a single build mode is supported per job.
|
||||
*/
|
||||
buildMode: BuildMode | undefined;
|
||||
/**
|
||||
* A unaltered copy of the original user input.
|
||||
* Mainly intended to be used for status reporting.
|
||||
* If any field is useful for the actual processing
|
||||
* of the action then consider pulling it out to a
|
||||
* top-level field above.
|
||||
*/
|
||||
originalUserInput: UserConfig;
|
||||
/**
|
||||
* Directory to use for temporary files that should be
|
||||
* deleted at the end of the job.
|
||||
*/
|
||||
tempDir: string;
|
||||
/**
|
||||
* Path of the CodeQL executable.
|
||||
*/
|
||||
codeQLCmd: string;
|
||||
/**
|
||||
* Version of GitHub we are talking to.
|
||||
*/
|
||||
gitHubVersion: GitHubVersion;
|
||||
/**
|
||||
* The location where CodeQL databases should be stored.
|
||||
*/
|
||||
dbLocation: string;
|
||||
/**
|
||||
* Specifies whether we are debugging mode and should try to produce extra
|
||||
* output for debugging purposes when possible.
|
||||
*/
|
||||
debugMode: boolean;
|
||||
/**
|
||||
* Specifies the name of the debugging artifact if we are in debug mode.
|
||||
*/
|
||||
debugArtifactName: string;
|
||||
/**
|
||||
* Specifies the name of the database in the debugging artifact.
|
||||
*/
|
||||
debugDatabaseName: string;
|
||||
/**
|
||||
* The configuration we computed by combining `originalUserInput` with `augmentationProperties`,
|
||||
* as well as adjustments made to it based on unsupported or required options.
|
||||
*/
|
||||
computedConfig: UserConfig;
|
||||
|
||||
/**
|
||||
* Partial map from languages to locations of TRAP caches for that language.
|
||||
* If a key is omitted, then TRAP caching should not be used for that language.
|
||||
*/
|
||||
trapCaches: { [language: Language]: string };
|
||||
|
||||
/**
|
||||
* Time taken to download TRAP caches. Used for status reporting.
|
||||
*/
|
||||
trapCacheDownloadTime: number;
|
||||
|
||||
/** A value indicating how dependency caching should be used. */
|
||||
dependencyCachingEnabled: CachingKind;
|
||||
|
||||
/** The keys of caches that we restored, if any. */
|
||||
dependencyCachingRestoredKeys: string[];
|
||||
|
||||
/**
|
||||
* Extra query exclusions to append to the config.
|
||||
*/
|
||||
extraQueryExclusions: ExcludeQueryFilter[];
|
||||
|
||||
/**
|
||||
* The overlay database mode to use.
|
||||
*/
|
||||
overlayDatabaseMode: OverlayDatabaseMode;
|
||||
|
||||
/**
|
||||
* Whether to use caching for overlay databases. If it is true, the action
|
||||
* will upload the created overlay-base database to the actions cache, and
|
||||
* download an overlay-base database from the actions cache before it creates
|
||||
* a new overlay database. If it is false, the action assumes that the
|
||||
* workflow will be responsible for managing database storage and retrieval.
|
||||
*
|
||||
* This property has no effect unless `overlayDatabaseMode` is `Overlay` or
|
||||
* `OverlayBase`.
|
||||
*/
|
||||
useOverlayDatabaseCaching: boolean;
|
||||
|
||||
/**
|
||||
* Whether the overlay database mode was set explicitly.
|
||||
*/
|
||||
overlayModeSetExplicitly: boolean;
|
||||
|
||||
/**
|
||||
* A partial mapping from repository properties that affect us to their values.
|
||||
*/
|
||||
repositoryProperties: RepositoryProperties;
|
||||
|
||||
/**
|
||||
* Whether to enable file coverage information.
|
||||
*/
|
||||
enableFileCoverageInformation: boolean;
|
||||
}
|
||||
|
||||
async function getSupportedLanguageMap(
|
||||
codeql: CodeQL,
|
||||
logger: Logger,
|
||||
@@ -598,13 +472,22 @@ async function downloadCacheWithTime(
|
||||
return { trapCaches, trapCacheDownloadTime };
|
||||
}
|
||||
|
||||
async function loadUserConfig(
|
||||
logger: Logger,
|
||||
/**
|
||||
* Loads a CLI configuration file from `configFile`.
|
||||
*
|
||||
* @param actionState The Action state.
|
||||
* @param configFile The address of the configuration file.
|
||||
* @param workspacePath The workspace path, used to check that the configuration file exists relative to it.
|
||||
* @param apiDetails Information for how to access the API to fetch remote files.
|
||||
* @param tempDir The temporary directory which may contain a CodeQL Action-generated configuration file.
|
||||
* @returns The loaded configuration file, if successful.
|
||||
*/
|
||||
export async function loadUserConfig(
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
configFile: string,
|
||||
workspacePath: string,
|
||||
apiDetails: api.GitHubApiCombinedDetails,
|
||||
tempDir: string,
|
||||
validateConfig: boolean,
|
||||
): Promise<UserConfig> {
|
||||
if (isLocal(configFile)) {
|
||||
if (configFile !== userConfigFromActionPath(tempDir)) {
|
||||
@@ -617,14 +500,18 @@ async function loadUserConfig(
|
||||
);
|
||||
}
|
||||
}
|
||||
return getLocalConfig(logger, configFile, validateConfig);
|
||||
} else {
|
||||
return await getRemoteConfig(
|
||||
logger,
|
||||
configFile,
|
||||
apiDetails,
|
||||
validateConfig,
|
||||
const validateConfig = await actionState.features.getValue(
|
||||
Feature.ValidateDbConfig,
|
||||
);
|
||||
return getLocalConfig(actionState.logger, configFile, validateConfig);
|
||||
} else {
|
||||
// Drop the explicit prefix if it is present. Since `REMOTE_PATH_PREFIX` is chosen
|
||||
// to not conflict with permissible characters in "owner" or "repo" components,
|
||||
// this does not risk removing valid parts of either component by accident.
|
||||
if (isExplicitRemotePath(configFile)) {
|
||||
configFile = configFile.substring(REMOTE_PATH_PREFIX.length);
|
||||
}
|
||||
return await getRemoteConfig(actionState, configFile, apiDetails);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1071,7 +958,11 @@ function dbLocationOrDefault(
|
||||
return dbLocation || path.resolve(tempDir, "codeql_databases");
|
||||
}
|
||||
|
||||
function userConfigFromActionPath(tempDir: string): string {
|
||||
/**
|
||||
* Gets the path for the CodeQL Action-generated configuration file,
|
||||
* which is used to store the `config` input.
|
||||
*/
|
||||
export function userConfigFromActionPath(tempDir: string): string {
|
||||
return path.resolve(tempDir, "user-config-from-action.yml");
|
||||
}
|
||||
|
||||
@@ -1131,6 +1022,110 @@ export async function applyIncrementalAnalysisSettings(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines where to load the `UserConfig` for the CLI from and loads it.
|
||||
*
|
||||
* @param inputs The Action inputs. The `configFile` value will be mutated
|
||||
* if a CodeQL Action-generated file should be used.
|
||||
*
|
||||
* @returns The loaded `UserConfig`, which might be empty if no configuration
|
||||
* was specified.
|
||||
*/
|
||||
export async function determineUserConfig(
|
||||
action: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
tempDir: string,
|
||||
inputs: InitConfigInputs,
|
||||
): Promise<UserConfig> {
|
||||
const validateConfig = await action.features.getValue(
|
||||
Feature.ValidateDbConfig,
|
||||
);
|
||||
|
||||
// We have the following cases:
|
||||
// 1. A `config` or `config-file` input is provided, but not both: use the provided one.
|
||||
// 2. Both are provided and we are in an advanced workflow: ignore the `config-file` input.
|
||||
// 3. Both are provided and we are in Default Setup: the `config` input uses a limited
|
||||
// set of options, which are supported by `mergeDefaultSetupAndUserConfigs`,
|
||||
// and we merge the two configs.
|
||||
if (inputs.configInput) {
|
||||
const computedConfigPath = userConfigFromActionPath(tempDir);
|
||||
|
||||
// Get a function which enables us to determine whether the FF that allows us to
|
||||
// merge supported configuration file properties is enabled. We only execute
|
||||
// this lazily if the other checks pass.
|
||||
const allowMergeConfigs = () =>
|
||||
action.features.getValue(Feature.AllowMergeConfigFiles);
|
||||
|
||||
// Check whether we also have a `config-file` input and decide what to do.
|
||||
if (
|
||||
inputs.configFile &&
|
||||
isDefaultSetup(action.env) &&
|
||||
(await allowMergeConfigs())
|
||||
) {
|
||||
// If the FF is enabled and we are in Default Setup, combine the supported
|
||||
// configuration file properties and write the result to disk.
|
||||
const fromConfigInput = parseUserConfig(
|
||||
action.logger,
|
||||
"`config` input",
|
||||
inputs.configInput,
|
||||
validateConfig,
|
||||
);
|
||||
const fromConfigFile = await loadUserConfig(
|
||||
action,
|
||||
inputs.configFile,
|
||||
inputs.workspacePath,
|
||||
inputs.apiDetails,
|
||||
tempDir,
|
||||
);
|
||||
|
||||
// Write the merged configuration to disk so that it can be loaded subsequently by
|
||||
// the CLI or other CodeQL Action steps.
|
||||
const mergedConfig = mergeDefaultSetupAndUserConfigs(
|
||||
action.logger,
|
||||
fromConfigInput,
|
||||
fromConfigFile,
|
||||
);
|
||||
fs.writeFileSync(computedConfigPath, yaml.dump(mergedConfig));
|
||||
action.logger.debug(
|
||||
`Using merged configurations from 'config' input with configuration from '${inputs.configFile}': ${computedConfigPath}`,
|
||||
);
|
||||
|
||||
inputs.configFile = computedConfigPath;
|
||||
return mergedConfig;
|
||||
} else {
|
||||
// If we are in this branch and there is a `config-file` input, then it means
|
||||
// we didn't meet the conditions for merging the configurations. Warn the user
|
||||
// that the configuration file will be ignored.
|
||||
if (inputs.configFile) {
|
||||
action.logger.warning(
|
||||
`Both a config file and config input were provided. Ignoring config file.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Write the `config` input straight to disk.
|
||||
fs.writeFileSync(computedConfigPath, inputs.configInput);
|
||||
inputs.configFile = computedConfigPath;
|
||||
action.logger.debug(
|
||||
`Using config from action input: ${inputs.configFile}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Load whatever configuration file we have, if any.
|
||||
if (!inputs.configFile) {
|
||||
action.logger.debug("No configuration file was provided");
|
||||
return {};
|
||||
} else {
|
||||
action.logger.debug(`Using configuration file: ${inputs.configFile}`);
|
||||
return await loadUserConfig(
|
||||
action,
|
||||
inputs.configFile,
|
||||
inputs.workspacePath,
|
||||
inputs.apiDetails,
|
||||
tempDir,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and return the config.
|
||||
*
|
||||
@@ -1138,38 +1133,13 @@ export async function applyIncrementalAnalysisSettings(
|
||||
* a default config. The parsed config is then stored to a known location.
|
||||
*/
|
||||
export async function initConfig(
|
||||
features: FeatureEnablement,
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
inputs: InitConfigInputs,
|
||||
): Promise<Config> {
|
||||
const { logger, tempDir } = inputs;
|
||||
const { logger, features } = actionState;
|
||||
const { tempDir } = inputs;
|
||||
|
||||
// if configInput is set, it takes precedence over configFile
|
||||
if (inputs.configInput) {
|
||||
if (inputs.configFile) {
|
||||
logger.warning(
|
||||
`Both a config file and config input were provided. Ignoring config file.`,
|
||||
);
|
||||
}
|
||||
inputs.configFile = userConfigFromActionPath(tempDir);
|
||||
fs.writeFileSync(inputs.configFile, inputs.configInput);
|
||||
logger.debug(`Using config from action input: ${inputs.configFile}`);
|
||||
}
|
||||
|
||||
let userConfig: UserConfig = {};
|
||||
if (!inputs.configFile) {
|
||||
logger.debug("No configuration file was provided");
|
||||
} else {
|
||||
logger.debug(`Using configuration file: ${inputs.configFile}`);
|
||||
const validateConfig = await features.getValue(Feature.ValidateDbConfig);
|
||||
userConfig = await loadUserConfig(
|
||||
logger,
|
||||
inputs.configFile,
|
||||
inputs.workspacePath,
|
||||
inputs.apiDetails,
|
||||
tempDir,
|
||||
validateConfig,
|
||||
);
|
||||
}
|
||||
const userConfig = await determineUserConfig(actionState, tempDir, inputs);
|
||||
|
||||
const config = await initActionState(inputs, userConfig);
|
||||
|
||||
@@ -1317,39 +1287,61 @@ export async function initConfig(
|
||||
return config;
|
||||
}
|
||||
|
||||
function parseRegistries(
|
||||
registriesInput: string | undefined,
|
||||
): RegistryConfigWithCredentials[] | undefined {
|
||||
try {
|
||||
return registriesInput
|
||||
? (yaml.load(registriesInput) as RegistryConfigWithCredentials[])
|
||||
: undefined;
|
||||
} catch {
|
||||
throw new ConfigurationError(
|
||||
"Invalid registries input. Must be a YAML string.",
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Determines if `configPath` is explicitly local. That is, it starts with `LOCAL_PATH_PREFIX`.
|
||||
* A configuration file path that starts with `LOCAL_PATH_PREFIX` is always treated as a local path.
|
||||
*
|
||||
* @param configPath The path to test.
|
||||
*/
|
||||
function isExplicitLocalPath(configPath: string): boolean {
|
||||
return configPath.startsWith(LOCAL_PATH_PREFIX);
|
||||
}
|
||||
|
||||
export function parseRegistriesWithoutCredentials(
|
||||
registriesInput?: string,
|
||||
): RegistryConfigNoCredentials[] | undefined {
|
||||
return parseRegistries(registriesInput)?.map((r) => {
|
||||
const { url, packages, kind } = r;
|
||||
return { url, packages, kind };
|
||||
});
|
||||
/**
|
||||
* Determines if `configPath` starts with the prefix used to explicitly mark a path
|
||||
* as a remote path (`REMOTE_PATH_PREFIX`).
|
||||
*
|
||||
* @param configPath The path to test.
|
||||
*/
|
||||
function isExplicitRemotePath(configPath: string): boolean {
|
||||
return configPath.startsWith(REMOTE_PATH_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if `configPath` contains a '@' character.
|
||||
*
|
||||
* @param configPath The path to test.
|
||||
*/
|
||||
function containsAtRef(configPath: string): boolean {
|
||||
return configPath.includes("@");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if `configPath` refers to a local configuration file.
|
||||
*
|
||||
* @param configPath The path to test.
|
||||
* @returns True if it is local, or false otherwise.
|
||||
*/
|
||||
function isLocal(configPath: string): boolean {
|
||||
// If the path starts with ./, look locally
|
||||
if (configPath.indexOf("./") === 0) {
|
||||
// If the path starts with `LOCAL_PATH_PREFIX`, it is explicitly local.
|
||||
// This allows local paths that would otherwise contain '@'
|
||||
// to be used with a `LOCAL_PATH_PREFIX` prefix.
|
||||
if (isExplicitLocalPath(configPath)) {
|
||||
return true;
|
||||
}
|
||||
// If the path starts with `REMOTE_PATH_PREFIX`, it is explicitly remote.
|
||||
// This allows users to resolve ambiguity by specifying `REMOTE_PATH_PREFIX`.
|
||||
if (isExplicitRemotePath(configPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return configPath.indexOf("@") === -1;
|
||||
// Otherwise, the path is also local if it does not contain '@'.
|
||||
// This assumes the `OLD_REMOTE_ADDRESS_FORMAT` which must contain a '@'
|
||||
// character for remote addresses.
|
||||
return !containsAtRef(configPath);
|
||||
}
|
||||
|
||||
function getLocalConfig(
|
||||
export function getLocalConfig(
|
||||
logger: Logger,
|
||||
configFile: string,
|
||||
validateConfig: boolean,
|
||||
@@ -1369,54 +1361,6 @@ function getLocalConfig(
|
||||
);
|
||||
}
|
||||
|
||||
async function getRemoteConfig(
|
||||
logger: Logger,
|
||||
configFile: string,
|
||||
apiDetails: api.GitHubApiCombinedDetails,
|
||||
validateConfig: boolean,
|
||||
): Promise<UserConfig> {
|
||||
// retrieve the various parts of the config location, and ensure they're present
|
||||
const format = new RegExp(
|
||||
"(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)",
|
||||
);
|
||||
const pieces = format.exec(configFile);
|
||||
// 5 = 4 groups + the whole expression
|
||||
if (pieces?.groups === undefined || pieces.length < 5) {
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFileRepoFormatInvalidMessage(configFile),
|
||||
);
|
||||
}
|
||||
|
||||
const response = await api
|
||||
.getApiClientWithExternalAuth(apiDetails)
|
||||
.rest.repos.getContent({
|
||||
owner: pieces.groups.owner,
|
||||
repo: pieces.groups.repo,
|
||||
path: pieces.groups.path,
|
||||
ref: pieces.groups.ref,
|
||||
});
|
||||
|
||||
let fileContents: string;
|
||||
if ("content" in response.data && response.data.content !== undefined) {
|
||||
fileContents = response.data.content;
|
||||
} else if (Array.isArray(response.data)) {
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFileDirectoryGivenMessage(configFile),
|
||||
);
|
||||
} else {
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFileFormatInvalidMessage(configFile),
|
||||
);
|
||||
}
|
||||
|
||||
return parseUserConfig(
|
||||
logger,
|
||||
configFile,
|
||||
Buffer.from(fileContents, "base64").toString("binary"),
|
||||
validateConfig,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file path where the parsed config will be stored.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { AnalysisKind } from "../analyses";
|
||||
import type { CachingKind } from "../caching-utils";
|
||||
import type { RepositoryProperties } from "../feature-flags/properties";
|
||||
import type { Language } from "../languages";
|
||||
import type { OverlayDatabaseMode } from "../overlay/overlay-database-mode";
|
||||
import type { BuildMode, GitHubVersion } from "../util";
|
||||
|
||||
import type { ExcludeQueryFilter, UserConfig } from "./db-config";
|
||||
|
||||
/**
|
||||
* Format of the CodeQL Action configuration state that is persisted
|
||||
* between steps of the CodeQL Action in a CodeQL workflow.
|
||||
*/
|
||||
export interface Config {
|
||||
/**
|
||||
* The version of the CodeQL Action that the configuration is for.
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Set of analysis kinds that are enabled.
|
||||
*/
|
||||
analysisKinds: AnalysisKind[];
|
||||
/**
|
||||
* Set of languages to run analysis for.
|
||||
*/
|
||||
languages: Language[];
|
||||
/**
|
||||
* Build mode, if set. Currently only a single build mode is supported per job.
|
||||
*/
|
||||
buildMode: BuildMode | undefined;
|
||||
/**
|
||||
* A unaltered copy of the original user input.
|
||||
* Mainly intended to be used for status reporting.
|
||||
* If any field is useful for the actual processing
|
||||
* of the action then consider pulling it out to a
|
||||
* top-level field above.
|
||||
*/
|
||||
originalUserInput: UserConfig;
|
||||
/**
|
||||
* Directory to use for temporary files that should be
|
||||
* deleted at the end of the job.
|
||||
*/
|
||||
tempDir: string;
|
||||
/**
|
||||
* Path of the CodeQL executable.
|
||||
*/
|
||||
codeQLCmd: string;
|
||||
/**
|
||||
* Version of GitHub we are talking to.
|
||||
*/
|
||||
gitHubVersion: GitHubVersion;
|
||||
/**
|
||||
* The location where CodeQL databases should be stored.
|
||||
*/
|
||||
dbLocation: string;
|
||||
/**
|
||||
* Specifies whether we are debugging mode and should try to produce extra
|
||||
* output for debugging purposes when possible.
|
||||
*/
|
||||
debugMode: boolean;
|
||||
/**
|
||||
* Specifies the name of the debugging artifact if we are in debug mode.
|
||||
*/
|
||||
debugArtifactName: string;
|
||||
/**
|
||||
* Specifies the name of the database in the debugging artifact.
|
||||
*/
|
||||
debugDatabaseName: string;
|
||||
/**
|
||||
* The configuration we computed by combining `originalUserInput` with `augmentationProperties`,
|
||||
* as well as adjustments made to it based on unsupported or required options.
|
||||
*/
|
||||
computedConfig: UserConfig;
|
||||
|
||||
/**
|
||||
* Partial map from languages to locations of TRAP caches for that language.
|
||||
* If a key is omitted, then TRAP caching should not be used for that language.
|
||||
*/
|
||||
trapCaches: { [language: Language]: string };
|
||||
|
||||
/**
|
||||
* Time taken to download TRAP caches. Used for status reporting.
|
||||
*/
|
||||
trapCacheDownloadTime: number;
|
||||
|
||||
/** A value indicating how dependency caching should be used. */
|
||||
dependencyCachingEnabled: CachingKind;
|
||||
|
||||
/** The keys of caches that we restored, if any. */
|
||||
dependencyCachingRestoredKeys: string[];
|
||||
|
||||
/**
|
||||
* Extra query exclusions to append to the config.
|
||||
*/
|
||||
extraQueryExclusions: ExcludeQueryFilter[];
|
||||
|
||||
/**
|
||||
* The overlay database mode to use.
|
||||
*/
|
||||
overlayDatabaseMode: OverlayDatabaseMode;
|
||||
|
||||
/**
|
||||
* Whether to use caching for overlay databases. If it is true, the action
|
||||
* will upload the created overlay-base database to the actions cache, and
|
||||
* download an overlay-base database from the actions cache before it creates
|
||||
* a new overlay database. If it is false, the action assumes that the
|
||||
* workflow will be responsible for managing database storage and retrieval.
|
||||
*
|
||||
* This property has no effect unless `overlayDatabaseMode` is `Overlay` or
|
||||
* `OverlayBase`.
|
||||
*/
|
||||
useOverlayDatabaseCaching: boolean;
|
||||
|
||||
/**
|
||||
* Whether the overlay database mode was set explicitly.
|
||||
*/
|
||||
overlayModeSetExplicitly: boolean;
|
||||
|
||||
/**
|
||||
* A partial mapping from repository properties that affect us to their values.
|
||||
*/
|
||||
repositoryProperties: RepositoryProperties;
|
||||
|
||||
/**
|
||||
* Whether to enable file coverage information.
|
||||
*/
|
||||
enableFileCoverageInformation: boolean;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getRecordingLogger,
|
||||
LoggedMessage,
|
||||
makeMacro,
|
||||
RecordingLogger,
|
||||
} from "../testing-utils";
|
||||
import { ConfigurationError, prettyPrintPack } from "../util";
|
||||
|
||||
@@ -488,3 +489,139 @@ test("parseUserConfig - throws no ConfigurationError if validation should fail,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("mergeDefaultSetupAndUserConfigs - combines threat models", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{ "threat-models": ["a", "b"] },
|
||||
{ "threat-models": ["local", "remote"] },
|
||||
);
|
||||
|
||||
const threatModels = result["threat-models"];
|
||||
|
||||
if (t.truthy(threatModels)) {
|
||||
t.deepEqual(threatModels, ["a", "b", "local", "remote"]);
|
||||
}
|
||||
});
|
||||
|
||||
test("mergeDefaultSetupAndUserConfigs - warns if user-supplied config contains default setup key", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{},
|
||||
{ "default-setup": {} },
|
||||
);
|
||||
|
||||
// User-supplied value is ignored.
|
||||
t.deepEqual(result, {});
|
||||
|
||||
// Warning is logged.
|
||||
t.true(
|
||||
logger.hasMessage(
|
||||
"The 'default-setup' configuration key is not supported in user-supplied configuration files",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("mergeDefaultSetupAndUserConfigs - keeps default setup key from 'config' input", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const expected: dbConfig.DefaultSetupConfig = {
|
||||
org: { "model-packs": ["some-pack"] },
|
||||
};
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{ "default-setup": expected },
|
||||
{},
|
||||
);
|
||||
|
||||
// Result matches the input.
|
||||
t.deepEqual(result["default-setup"], expected);
|
||||
|
||||
// No warning is logged.
|
||||
t.false(
|
||||
logger.hasMessage(
|
||||
"The 'default-setup' configuration key is not supported in user-supplied configuration files",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("mergeDefaultSetupAndUserConfigs - keeps other properties from user-supplied configuration", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const configFile: dbConfig.UserConfig = {
|
||||
"query-filters": [{ exclude: { a: "b" } }],
|
||||
"paths-ignore": ["path"],
|
||||
};
|
||||
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{},
|
||||
configFile,
|
||||
);
|
||||
|
||||
t.deepEqual(result, configFile);
|
||||
});
|
||||
|
||||
test("mergeDefaultSetupAndUserConfigs - ignores, but warns about, unknown keys from Default Setup", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const configFile: dbConfig.UserConfig = {
|
||||
"query-filters": [{ exclude: { a: "b" } }],
|
||||
"paths-ignore": ["path"],
|
||||
};
|
||||
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{
|
||||
"default-setup": {
|
||||
borg: [],
|
||||
org: {
|
||||
unknown: "foo",
|
||||
"model-packs": [],
|
||||
},
|
||||
} as unknown as dbConfig.DefaultSetupConfig,
|
||||
"paths-ignore": ["other-path"],
|
||||
},
|
||||
configFile,
|
||||
);
|
||||
|
||||
t.deepEqual(result, {
|
||||
...configFile,
|
||||
"default-setup": { org: { "model-packs": [] } },
|
||||
});
|
||||
|
||||
const expectedUnrecognisedKeys = [
|
||||
".default-setup.org.unknown",
|
||||
".default-setup.borg",
|
||||
".paths-ignore",
|
||||
].join(", ");
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
`Unrecognised keys in Default Setup configuration: ${expectedUnrecognisedKeys}`,
|
||||
]);
|
||||
});
|
||||
|
||||
test("mergeDefaultSetupAndUserConfigs - warns about invalid keys from Default Setup", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const configFile: dbConfig.UserConfig = {};
|
||||
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{
|
||||
"default-setup": {
|
||||
org: {
|
||||
"model-packs": [123],
|
||||
},
|
||||
} as unknown as dbConfig.DefaultSetupConfig,
|
||||
},
|
||||
configFile,
|
||||
);
|
||||
|
||||
t.deepEqual(result, {
|
||||
...configFile,
|
||||
"default-setup": { org: { "model-packs": [123] } },
|
||||
});
|
||||
|
||||
const expectedInvalidKeys = [".default-setup.org.model-packs[0]"].join(", ");
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
`Invalid keys in Default Setup configuration: ${expectedInvalidKeys}`,
|
||||
]);
|
||||
});
|
||||
@@ -4,11 +4,16 @@ import * as yaml from "js-yaml";
|
||||
import * as jsonschema from "jsonschema";
|
||||
import * as semver from "semver";
|
||||
|
||||
import {
|
||||
addNoLanguageDiagnostic,
|
||||
makeTelemetryDiagnostic,
|
||||
} from "../diagnostics";
|
||||
import * as errorMessages from "../error-messages";
|
||||
import {
|
||||
RepositoryProperties,
|
||||
RepositoryPropertyName,
|
||||
} from "../feature-flags/properties";
|
||||
import * as json from "../json";
|
||||
import { Language } from "../languages";
|
||||
import { Logger } from "../logging";
|
||||
import { cloneObject, ConfigurationError, prettyPrintPack } from "../util";
|
||||
@@ -28,6 +33,21 @@ export interface QuerySpec {
|
||||
uses: string;
|
||||
}
|
||||
|
||||
const ORG_SCHEMA = {
|
||||
/** An array of model pack names. */
|
||||
"model-packs": json.optional(json.array(json.string)),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Not intended to be provided directly by a user. */
|
||||
export type OrgType = json.FromSchema<typeof ORG_SCHEMA>;
|
||||
|
||||
const DEFAULT_SETUP_SCHEMA = {
|
||||
org: json.optional<OrgType>(json.object(ORG_SCHEMA)),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Not intended to be provided directly by a user. */
|
||||
export type DefaultSetupConfig = json.FromSchema<typeof DEFAULT_SETUP_SCHEMA>;
|
||||
|
||||
/**
|
||||
* Format of the config file supplied by the user.
|
||||
*/
|
||||
@@ -46,6 +66,119 @@ export interface UserConfig {
|
||||
// Set of query filters to include and exclude extra queries based on
|
||||
// codeql query suite `include` and `exclude` properties
|
||||
"query-filters"?: QueryFilter[];
|
||||
|
||||
/** An array (possibly empty or absent) of threat models to use. */
|
||||
"threat-models"?: string[];
|
||||
|
||||
/**
|
||||
* Configuration options that are reserved for us in Default Setup and
|
||||
* not intended to be supplied directly by users.
|
||||
*/
|
||||
"default-setup"?: DefaultSetupConfig;
|
||||
}
|
||||
|
||||
/** A subset of the `UserConfig` schema that is used by Default Setup. */
|
||||
const DEFAULT_SETUP_CONFIG_SCHEMA = {
|
||||
"threat-models": json.optional(json.array(json.string)),
|
||||
"default-setup": json.optional<DefaultSetupConfig>(
|
||||
json.object(DEFAULT_SETUP_SCHEMA),
|
||||
),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/**
|
||||
* Merges supported properties from two configuration files. This is intended only for
|
||||
* use with merging the `config` input provided by Default Setup with a potentially
|
||||
* richer configuration file provided by a user.
|
||||
*
|
||||
* @param logger The logger to use.
|
||||
* @param fromConfigInput The configuration from Default Setup.
|
||||
* @param fromConfigFile The user-supplied configuration.
|
||||
* @returns The combination of both configuration files.
|
||||
*/
|
||||
export function mergeDefaultSetupAndUserConfigs(
|
||||
logger: Logger,
|
||||
fromConfigInput: UserConfig,
|
||||
fromConfigFile: UserConfig,
|
||||
): UserConfig {
|
||||
logger.debug(
|
||||
"Combining configuration files from 'config' and 'config-file' inputs",
|
||||
);
|
||||
|
||||
// Check for unexpected keys in the configuration from the `config` input
|
||||
// that was provided by Default Setup. This should only contain the keys
|
||||
// we would expect to receive from Default Setup.
|
||||
const schemaCheckResult = json.checkSchema(
|
||||
DEFAULT_SETUP_CONFIG_SCHEMA,
|
||||
fromConfigInput as json.UnvalidatedObject<any>,
|
||||
);
|
||||
|
||||
// Report any invalid or unrecognised keys.
|
||||
if (schemaCheckResult.invalidKeys.length > 0) {
|
||||
logger.warning(
|
||||
`Invalid keys in Default Setup configuration: ${schemaCheckResult.invalidKeys.join(", ")}`,
|
||||
);
|
||||
addNoLanguageDiagnostic(
|
||||
undefined,
|
||||
makeTelemetryDiagnostic(
|
||||
"codeql-action/invalid-default-setup-config-keys",
|
||||
"Invalid Default Setup configuration keys",
|
||||
{
|
||||
invalidKeys: schemaCheckResult.invalidKeys,
|
||||
},
|
||||
["internal-error"],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (schemaCheckResult.unknownKeys.length > 0) {
|
||||
logger.warning(
|
||||
`Unrecognised keys in Default Setup configuration: ${schemaCheckResult.unknownKeys.join(", ")}`,
|
||||
);
|
||||
addNoLanguageDiagnostic(
|
||||
undefined,
|
||||
makeTelemetryDiagnostic(
|
||||
"codeql-action/unrecognised-default-setup-config-keys",
|
||||
"Unrecognised Default Setup configuration keys",
|
||||
{
|
||||
unrecognisedKeys: schemaCheckResult.unknownKeys,
|
||||
},
|
||||
["internal-error"],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Combine all specified threat models from both sources.
|
||||
const threatModels = new Set(fromConfigInput["threat-models"] || []);
|
||||
for (const configFileThreatModel of fromConfigFile["threat-models"] || []) {
|
||||
threatModels.add(configFileThreatModel);
|
||||
}
|
||||
|
||||
// Warn if there is a 'default-setup' configuration key in the user-supplied configuration,
|
||||
// since it is not meant to be used and we therefore ignore it here.
|
||||
if (fromConfigFile["default-setup"]) {
|
||||
logger.warning(
|
||||
`The 'default-setup' configuration key is not supported in user-supplied configuration files and will be ignored.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Since we expect the `fromConfigInput` configuration to be provided by Default Setup,
|
||||
// we expect a limited set of options. Therefore, we base the overall configuration on
|
||||
// the one provided via the `config-file` input, which may be richer.
|
||||
const result = { ...fromConfigFile };
|
||||
delete result["threat-models"];
|
||||
delete result["default-setup"];
|
||||
|
||||
if (fromConfigInput["default-setup"]?.org?.["model-packs"]) {
|
||||
result["default-setup"] = {
|
||||
org: {
|
||||
"model-packs": fromConfigInput["default-setup"].org["model-packs"],
|
||||
},
|
||||
};
|
||||
}
|
||||
if (threatModels.size > 0) {
|
||||
result["threat-models"] = Array.from(threatModels);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+96
-72
@@ -1,22 +1,26 @@
|
||||
import * as github from "@actions/github";
|
||||
import test from "ava";
|
||||
import sinon from "sinon";
|
||||
|
||||
import * as api from "../api-client";
|
||||
import { RegistryProxyVars } from "../environment";
|
||||
import { Feature } from "../feature-flags";
|
||||
import { RepositoryPropertyName } from "../feature-flags/properties";
|
||||
import {
|
||||
getTestActionsEnv,
|
||||
RecordingLogger,
|
||||
callee,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
setupTests,
|
||||
} from "../testing-utils";
|
||||
|
||||
import { getConfigFileInput } from "./file";
|
||||
import { getConfigFileInput, getRemoteConfig } from "./file";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test("getConfigFileInput returns undefined by default", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const actionsEnv = getTestActionsEnv();
|
||||
const result = getConfigFileInput(logger, actionsEnv, {}, true);
|
||||
t.is(result, undefined);
|
||||
await callee(getConfigFileInput)
|
||||
.withArgs({})
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.passes(t.is, undefined);
|
||||
});
|
||||
|
||||
const repositoryProperties = {
|
||||
@@ -24,86 +28,106 @@ const repositoryProperties = {
|
||||
};
|
||||
|
||||
test("getConfigFileInput returns input value", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const actionsEnv = getTestActionsEnv();
|
||||
const testInput = "/some/path";
|
||||
sinon
|
||||
.stub(actionsEnv, "getOptionalInput")
|
||||
.withArgs("config-file")
|
||||
.returns(testInput);
|
||||
|
||||
// Even though both an input and repository property are configured,
|
||||
// we prefer the direct input to the Action.
|
||||
const result = getConfigFileInput(
|
||||
logger,
|
||||
actionsEnv,
|
||||
repositoryProperties,
|
||||
true,
|
||||
);
|
||||
t.is(result, testInput);
|
||||
|
||||
// Check for the expected log message.
|
||||
t.true(logger.hasMessage("Using configuration file input from workflow"));
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.withActions((actionsEnv) => {
|
||||
sinon
|
||||
.stub(actionsEnv, "getOptionalInput")
|
||||
.withArgs("config-file")
|
||||
.returns(testInput);
|
||||
})
|
||||
.withArgs(repositoryProperties)
|
||||
.logs(t, "Using configuration file input from workflow")
|
||||
.passes(t.is, testInput);
|
||||
});
|
||||
|
||||
test("getConfigFileInput returns repository property value", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const actionsEnv = getTestActionsEnv();
|
||||
|
||||
// Since there is no direct input, we should use the repository property.
|
||||
const result = getConfigFileInput(
|
||||
logger,
|
||||
actionsEnv,
|
||||
repositoryProperties,
|
||||
true,
|
||||
);
|
||||
t.is(result, repositoryProperties[RepositoryPropertyName.CONFIG_FILE]);
|
||||
|
||||
// Check for the expected log message.
|
||||
t.true(
|
||||
logger.hasMessage(
|
||||
"Using configuration file input from repository property",
|
||||
),
|
||||
);
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.withArgs(repositoryProperties)
|
||||
.logs(t, "Using configuration file input from repository property")
|
||||
.passes(t.is, repositoryProperties[RepositoryPropertyName.CONFIG_FILE]);
|
||||
});
|
||||
|
||||
test("getConfigFileInput ignores empty repository property value", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const actionsEnv = getTestActionsEnv();
|
||||
|
||||
// Since the repository property value is an empty/whitespace string, we should ignore it.
|
||||
const result = getConfigFileInput(
|
||||
logger,
|
||||
actionsEnv,
|
||||
{
|
||||
[RepositoryPropertyName.CONFIG_FILE]: " ",
|
||||
},
|
||||
true,
|
||||
);
|
||||
t.is(result, undefined);
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.withArgs({ [RepositoryPropertyName.CONFIG_FILE]: " " })
|
||||
.passes(t.is, undefined);
|
||||
});
|
||||
|
||||
test("getConfigFileInput ignores repository property value when FF is off", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const actionsEnv = getTestActionsEnv();
|
||||
|
||||
// Since the FF is off, we should ignore the repository property value.
|
||||
const result = getConfigFileInput(
|
||||
logger,
|
||||
actionsEnv,
|
||||
repositoryProperties,
|
||||
false,
|
||||
);
|
||||
t.is(result, undefined);
|
||||
|
||||
t.false(
|
||||
logger.hasMessage(
|
||||
"Using configuration file input from repository property",
|
||||
),
|
||||
);
|
||||
t.true(
|
||||
logger.hasMessage(
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([])
|
||||
.withArgs(repositoryProperties)
|
||||
.notLogs(t, "Using configuration file input from repository property")
|
||||
.logs(
|
||||
t,
|
||||
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.",
|
||||
),
|
||||
);
|
||||
)
|
||||
.passes(t.is, undefined);
|
||||
});
|
||||
|
||||
test.serial("getRemoteConfig uses proxy when it is supposed to", async (t) => {
|
||||
const client = github.getOctokit("123");
|
||||
const response = {
|
||||
data: {
|
||||
content: Buffer.from("disable-default-queries: false").toString("base64"),
|
||||
},
|
||||
};
|
||||
sinon
|
||||
.stub(client.rest.repos, "getContent")
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
.resolves(response as any);
|
||||
|
||||
// We stub `getApiClientWithExternalAuth` so that it throws if no
|
||||
// proxy is provided and returns the client otherwise. This allows us
|
||||
// to verify the result in the following test cases.
|
||||
const errorMessage = "No `proxy` was provided by the caller.";
|
||||
sinon
|
||||
.stub(api, "getApiClientWithExternalAuth")
|
||||
.callsFake((_details, proxy) => {
|
||||
// Throw if proxy isn't defined.
|
||||
if (proxy === undefined) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
// Otherwise return the client object.
|
||||
return client;
|
||||
});
|
||||
|
||||
const target = callee(getRemoteConfig)
|
||||
.withDefaultActionsEnv()
|
||||
.withArgs("file.yml", SAMPLE_DOTCOM_API_DETAILS);
|
||||
|
||||
// Should use it when the FF is enabled and the environment variables are set.
|
||||
await target
|
||||
.withFeatures([Feature.ProxyApiRequests])
|
||||
.withEnv((env) => {
|
||||
env.set(RegistryProxyVars.PROXY_HOST, "localhost");
|
||||
env.set(RegistryProxyVars.PROXY_PORT, "1234");
|
||||
})
|
||||
.logs(t, "Using private registry proxy at 'http://localhost:1234'")
|
||||
.passes(t.truthy);
|
||||
|
||||
// But not when the FF is not enabled.
|
||||
await target
|
||||
.withEnv((env) => {
|
||||
env.set(RegistryProxyVars.PROXY_HOST, "localhost");
|
||||
env.set(RegistryProxyVars.PROXY_PORT, "1234");
|
||||
})
|
||||
.notLogs(t, "Using private registry proxy at 'http://localhost:1234'")
|
||||
.throws(t, { message: errorMessage });
|
||||
|
||||
// And not when the environment variables aren't set.
|
||||
await target
|
||||
.withFeatures([Feature.ProxyApiRequests])
|
||||
.notLogs(t, "Using private registry proxy at 'http://localhost:1234'")
|
||||
.throws(t, { message: errorMessage });
|
||||
});
|
||||
+88
-7
@@ -1,19 +1,40 @@
|
||||
import { ActionsEnv } from "../actions-util";
|
||||
import { ActionState } from "../action-common";
|
||||
import * as api from "../api-client";
|
||||
import * as errorMessages from "../error-messages";
|
||||
import { Feature } from "../feature-flags";
|
||||
import {
|
||||
RepositoryProperties,
|
||||
RepositoryPropertyName,
|
||||
} from "../feature-flags/properties";
|
||||
import { Logger } from "../logging";
|
||||
import { ConfigurationError } from "../util";
|
||||
|
||||
import { parseUserConfig, UserConfig } from "./db-config";
|
||||
import { parseRemoteFileAddress } from "./remote-file";
|
||||
|
||||
/**
|
||||
* The prefix that can be specified to indicate that a path should be treated as a local file address.
|
||||
*/
|
||||
export const LOCAL_PATH_PREFIX = "./";
|
||||
|
||||
/**
|
||||
* The prefix that can be specified to indicate that a path should be treated as a remote file address.
|
||||
* The new remote file address format must start with either an owner or repository name. Both
|
||||
* are restricted to ASCII characters, '.', and '-'. The prefix chosen here does not interfere with
|
||||
* those (since it contains an `=`) and is _unlikely_ (but not impossible) to appear in a local file path.
|
||||
*/
|
||||
export const REMOTE_PATH_PREFIX = "remote=";
|
||||
|
||||
/**
|
||||
* Gets the value that is configured for the configuration file, if any.
|
||||
*/
|
||||
export function getConfigFileInput(
|
||||
logger: Logger,
|
||||
actions: ActionsEnv,
|
||||
export async function getConfigFileInput(
|
||||
{
|
||||
logger,
|
||||
actions,
|
||||
features,
|
||||
}: ActionState<["Logger", "Actions", "FeatureFlags"]>,
|
||||
repositoryProperties: Partial<RepositoryProperties>,
|
||||
useRepositoryProperty: boolean,
|
||||
): string | undefined {
|
||||
): Promise<string | undefined> {
|
||||
const input = actions.getOptionalInput("config-file");
|
||||
|
||||
if (input !== undefined) {
|
||||
@@ -26,6 +47,10 @@ export function getConfigFileInput(
|
||||
|
||||
if (propertyValue !== undefined && propertyValue.trim().length > 0) {
|
||||
// Only use the repository property value if the FF is enabled.
|
||||
const useRepositoryProperty = await features.getValue(
|
||||
Feature.ConfigFileRepositoryProperty,
|
||||
);
|
||||
|
||||
if (useRepositoryProperty) {
|
||||
logger.info(
|
||||
`Using configuration file input from repository property: ${propertyValue}`,
|
||||
@@ -40,3 +65,59 @@ export function getConfigFileInput(
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to fetch a `UserConfig` from a remote `address`.
|
||||
*
|
||||
* @param actionState The current Action state.
|
||||
* @param configFile The remote address of the configuration file.
|
||||
* @param apiDetails Information about how to connect to the API.
|
||||
*
|
||||
* @returns The `UserConfig`, if it could be fetched and parsed successfully.
|
||||
*/
|
||||
export async function getRemoteConfig(
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
configFile: string,
|
||||
apiDetails: api.GitHubApiCombinedDetails,
|
||||
): Promise<UserConfig> {
|
||||
const address = await parseRemoteFileAddress(actionState, configFile);
|
||||
|
||||
const shouldProxyRequest = await actionState.features.getValue(
|
||||
Feature.ProxyApiRequests,
|
||||
);
|
||||
const proxy = shouldProxyRequest
|
||||
? api.getRegistryProxy(actionState)
|
||||
: undefined;
|
||||
|
||||
const response = await api
|
||||
.getApiClientWithExternalAuth(apiDetails, proxy)
|
||||
.rest.repos.getContent({
|
||||
owner: address.owner,
|
||||
repo: address.repo,
|
||||
path: address.path,
|
||||
ref: address.ref,
|
||||
});
|
||||
|
||||
let fileContents: string;
|
||||
if ("content" in response.data && response.data.content !== undefined) {
|
||||
fileContents = response.data.content;
|
||||
} else if (Array.isArray(response.data)) {
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFileDirectoryGivenMessage(configFile),
|
||||
);
|
||||
} else {
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFileFormatInvalidMessage(configFile),
|
||||
);
|
||||
}
|
||||
|
||||
const validateConfig = await actionState.features.getValue(
|
||||
Feature.ValidateDbConfig,
|
||||
);
|
||||
return parseUserConfig(
|
||||
actionState.logger,
|
||||
configFile,
|
||||
Buffer.from(fileContents, "base64").toString("binary"),
|
||||
validateConfig,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
import { ConfigurationError } from "../util";
|
||||
|
||||
export type RegistryConfigWithCredentials = RegistryConfigNoCredentials & {
|
||||
// Token to use when downloading packs from this registry.
|
||||
token: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The list of registries and the associated pack globs that determine where each
|
||||
* pack can be downloaded from.
|
||||
*/
|
||||
export interface RegistryConfigNoCredentials {
|
||||
// URL of a package registry, eg- https://ghcr.io/v2/
|
||||
url: string;
|
||||
|
||||
// List of globs that determine which packs are associated with this registry.
|
||||
packages: string[] | string;
|
||||
|
||||
// Kind of registry, either "github" or "docker". Default is "docker".
|
||||
// "docker" refers specifically to the GitHub Container Registry, which is the usual way of sharing CodeQL packs.
|
||||
// "github" refers to packs published as content in a GitHub repository. This kind of registry is used in scenarios
|
||||
// where GHCR is not available, such as certain GHES environments.
|
||||
kind?: "github" | "docker";
|
||||
}
|
||||
|
||||
export function parseRegistries(
|
||||
registriesInput: string | undefined,
|
||||
): RegistryConfigWithCredentials[] | undefined {
|
||||
try {
|
||||
return registriesInput
|
||||
? (yaml.load(registriesInput) as RegistryConfigWithCredentials[])
|
||||
: undefined;
|
||||
} catch {
|
||||
throw new ConfigurationError(
|
||||
"Invalid registries input. Must be a YAML string.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRegistriesWithoutCredentials(
|
||||
registriesInput?: string,
|
||||
): RegistryConfigNoCredentials[] | undefined {
|
||||
return parseRegistries(registriesInput)?.map((r) => {
|
||||
const { url, packages, kind } = r;
|
||||
return { url, packages, kind };
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import test from "ava";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { ActionsEnvVars } from "../environment";
|
||||
import { callee } from "../testing-utils";
|
||||
import { ConfigurationError } from "../util";
|
||||
|
||||
import {
|
||||
DEFAULT_CONFIG_FILE_NAME,
|
||||
DEFAULT_CONFIG_FILE_REF,
|
||||
parseRemoteFileAddress,
|
||||
RemoteFileAddress,
|
||||
} from "./remote-file";
|
||||
|
||||
type ParseRemoteFileAddressTest = {
|
||||
input: string;
|
||||
expected: RemoteFileAddress;
|
||||
};
|
||||
|
||||
test("parseRemoteFileAddress accepts full remote addresses", async (t) => {
|
||||
const target = callee(parseRemoteFileAddress);
|
||||
|
||||
const expected: RemoteFileAddress = {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
path: "path",
|
||||
ref: "ref",
|
||||
};
|
||||
|
||||
const oldFormatInputs: ParseRemoteFileAddressTest[] = [
|
||||
{ input: "owner/repo/path@ref", expected },
|
||||
{ input: "owner /repo/path@ref", expected },
|
||||
{ input: "owner/ repo/path@ref", expected },
|
||||
{ input: "owner/repo /path@ref", expected },
|
||||
{ input: "owner/repo/ path@ref", expected },
|
||||
{ input: "owner/repo/path @ref", expected },
|
||||
{ input: "owner/repo/path@ ref", expected },
|
||||
{
|
||||
input: "owner/repo/path/to/codeql.yml@ref/feature",
|
||||
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
|
||||
},
|
||||
{
|
||||
input: " owner/repo/path/to/codeql.yml@ref/feature ",
|
||||
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
|
||||
},
|
||||
];
|
||||
|
||||
for (const oldFormatInput of oldFormatInputs) {
|
||||
await target
|
||||
.withArgs(oldFormatInput.input)
|
||||
.passes(t.deepEqual, oldFormatInput.expected);
|
||||
}
|
||||
|
||||
// New format.
|
||||
const newFormatInputs: ParseRemoteFileAddressTest[] = [
|
||||
{ input: "owner/repo@ref:path", expected },
|
||||
{ input: "owner /repo@ref:path", expected },
|
||||
{ input: "owner/ repo@ref:path", expected },
|
||||
{ input: "owner/repo @ref:path", expected },
|
||||
{ input: "owner/repo@ ref:path", expected },
|
||||
{ input: "owner/repo@ref :path", expected },
|
||||
{ input: "owner/repo@ref: path", expected },
|
||||
{
|
||||
input: "owner/repo@ref/feature:path/to/codeql.yml",
|
||||
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
|
||||
},
|
||||
{
|
||||
input: " owner/repo@ref/feature:path/to/codeql.yml ",
|
||||
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
|
||||
},
|
||||
];
|
||||
|
||||
for (const newFormatInput of newFormatInputs) {
|
||||
const targetWithArgs = target.withArgs(newFormatInput.input);
|
||||
|
||||
await targetWithArgs.passes(t.deepEqual, newFormatInput.expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress accepts remote address without an owner", async (t) => {
|
||||
const owner = "test-owner";
|
||||
const target = callee(parseRemoteFileAddress).withEnv((env) => {
|
||||
const getRequired = sinon.stub(env, "getRequired");
|
||||
getRequired
|
||||
.withArgs(ActionsEnvVars.GITHUB_REPOSITORY)
|
||||
.returns(`${owner}/current-repo`);
|
||||
});
|
||||
|
||||
const testCases: ParseRemoteFileAddressTest[] = [
|
||||
{
|
||||
input: "repo@ref:path.yml",
|
||||
expected: {
|
||||
owner,
|
||||
repo: "repo",
|
||||
path: "path.yml",
|
||||
ref: "ref",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "repo@ref",
|
||||
expected: {
|
||||
owner,
|
||||
repo: "repo",
|
||||
path: DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: "ref",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "repo:path.yml",
|
||||
expected: {
|
||||
owner,
|
||||
repo: "repo",
|
||||
path: "path.yml",
|
||||
ref: DEFAULT_CONFIG_FILE_REF,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "repo",
|
||||
expected: {
|
||||
owner,
|
||||
repo: "repo",
|
||||
path: DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: DEFAULT_CONFIG_FILE_REF,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const targetWithArgs = target.withArgs(testCase.input);
|
||||
|
||||
await targetWithArgs.passes(t.deepEqual, testCase.expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress throws for invalid `GITHUB_REPOSITORY`", async (t) => {
|
||||
const getRequired: sinon.SinonStub = sinon.stub();
|
||||
getRequired.withArgs(ActionsEnvVars.GITHUB_REPOSITORY).returns(`not-valid`);
|
||||
|
||||
const target = callee(parseRemoteFileAddress)
|
||||
.withArgs("repo@ref")
|
||||
.withEnv((env) => {
|
||||
sinon.define(env, "getRequired", getRequired);
|
||||
});
|
||||
|
||||
await target.throws(t, { instanceOf: Error });
|
||||
|
||||
t.assert(getRequired.calledOnceWith(ActionsEnvVars.GITHUB_REPOSITORY));
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress accepts remote address without a path", async (t) => {
|
||||
const target = callee(parseRemoteFileAddress);
|
||||
|
||||
const testCases: ParseRemoteFileAddressTest[] = [
|
||||
{
|
||||
input: "owner/repo@ref",
|
||||
expected: {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
path: DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: "ref",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "owner/repo",
|
||||
expected: {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
path: DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: DEFAULT_CONFIG_FILE_REF,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const targetWithArgs = target.withArgs(testCase.input);
|
||||
|
||||
await targetWithArgs.passes(t.deepEqual, testCase.expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress accepts remote address without a ref", async (t) => {
|
||||
const target = callee(parseRemoteFileAddress).withArgs("owner/repo:path");
|
||||
|
||||
await target.passes(t.deepEqual, {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
path: "path",
|
||||
ref: DEFAULT_CONFIG_FILE_REF,
|
||||
} satisfies RemoteFileAddress);
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress rejects invalid values", async (t) => {
|
||||
const owner = "owner";
|
||||
const target = callee(parseRemoteFileAddress).withEnv((env) => {
|
||||
const getRequired = sinon.stub(env, "getRequired");
|
||||
getRequired
|
||||
.withArgs(ActionsEnvVars.GITHUB_REPOSITORY)
|
||||
.returns(`${owner}/current-repo`);
|
||||
});
|
||||
|
||||
const testInputs = [
|
||||
" ",
|
||||
"repo//absolute",
|
||||
"repo:/absolute",
|
||||
"/repo@ref",
|
||||
" /repo@ref",
|
||||
"repo@",
|
||||
"repo:",
|
||||
"repo/",
|
||||
"/repo",
|
||||
":path",
|
||||
"@ref",
|
||||
"@ref:path",
|
||||
"owner/@ref:path",
|
||||
"owner/@ref",
|
||||
"owner/:path",
|
||||
];
|
||||
|
||||
for (const testInput of testInputs) {
|
||||
const targetWithArgs = target.withArgs(testInput);
|
||||
|
||||
await targetWithArgs.throws(t, {
|
||||
// When the new format is accepted, there are some more specific
|
||||
// errors in some cases. It is sufficient for us to check that
|
||||
// an exception is thrown.
|
||||
instanceOf: ConfigurationError,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
import { ActionState } from "../action-common";
|
||||
import { ActionsEnvVars, ReadOnlyEnv } from "../environment";
|
||||
import * as errorMessages from "../error-messages";
|
||||
import { ConfigurationError, Failure, Result, Success } from "../util";
|
||||
|
||||
/** Represents remote file addresses. */
|
||||
export interface RemoteFileAddress {
|
||||
/** The owner of the repository. */
|
||||
owner: string;
|
||||
/** The repository name. */
|
||||
repo: string;
|
||||
/** The path of the file. */
|
||||
path: string;
|
||||
/** The ref of the repository. */
|
||||
ref: string;
|
||||
}
|
||||
|
||||
/** The default file path to use in configuration file shorthands. */
|
||||
export const DEFAULT_CONFIG_FILE_NAME = ".github/codeql-action.yaml";
|
||||
|
||||
/** The default ref to use in configuration file shorthands. */
|
||||
export const DEFAULT_CONFIG_FILE_REF = "main";
|
||||
|
||||
/** Extracts the owner from the `GITHUB_REPOSITORY` environment variable. */
|
||||
function getDefaultOwner(env: ReadOnlyEnv): string {
|
||||
const currentRepoNwo = env.getRequired(ActionsEnvVars.GITHUB_REPOSITORY);
|
||||
const nwoParts = currentRepoNwo.split("/");
|
||||
|
||||
if (nwoParts.length !== 2 || nwoParts[0].trim().length === 0) {
|
||||
// This shouldn't happen, so we should throw if `GITHUB_REPOSITORY` doesn't match
|
||||
// our expectations.
|
||||
throw new Error(
|
||||
`Expected ${ActionsEnvVars.GITHUB_REPOSITORY} to contain a name with owner, but got '${currentRepoNwo}'.`,
|
||||
);
|
||||
}
|
||||
|
||||
return nwoParts[0].trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* The old remote address format that's always been supported for the `config-file` input.
|
||||
* All the components are required. Unchanged from the previous implementation.
|
||||
*/
|
||||
const OLD_REMOTE_ADDRESS_FORMAT = new RegExp(
|
||||
"(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)",
|
||||
);
|
||||
|
||||
/**
|
||||
* Attempts to parse `input` as a `RemoteFileAddress` using the old format.
|
||||
*
|
||||
* @param input The input to try and parse.
|
||||
* @returns A `RemoteFileAddress` value if successful or `undefined` otherwise.
|
||||
*/
|
||||
function parseOldRemoteFileAddress(
|
||||
input: string,
|
||||
): Result<RemoteFileAddress, undefined> {
|
||||
const pieces = OLD_REMOTE_ADDRESS_FORMAT.exec(input);
|
||||
|
||||
// 5 = 4 groups + the whole expression
|
||||
if (pieces?.groups === undefined || pieces.length < 5) {
|
||||
return new Failure(undefined);
|
||||
}
|
||||
|
||||
return new Success({
|
||||
owner: pieces.groups.owner.trim(),
|
||||
repo: pieces.groups.repo.trim(),
|
||||
path: pieces.groups.path.trim(),
|
||||
ref: pieces.groups.ref.trim(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse `input` as a `RemoteFileAddress` using the new format.
|
||||
*
|
||||
* @param env The read-only environment to obtain the owner name from if needed.
|
||||
* @param configFile The input to try and parse.
|
||||
* @returns A `RemoteFileAddress` value if successful or `undefined` otherwise.
|
||||
*/
|
||||
export function parseNewRemoteFileAddress(
|
||||
env: ReadOnlyEnv,
|
||||
configFile: string,
|
||||
): Result<RemoteFileAddress, undefined> {
|
||||
// retrieve the various parts of the config location, and ensure they're present
|
||||
const format = new RegExp(
|
||||
"^((?<owner>[^:@/]+)/)?(?<repo>[^:@/]+)(@(?<ref>[^:]+))?(:(?<path>.+))?$",
|
||||
);
|
||||
const pieces = format.exec(configFile.trim());
|
||||
|
||||
const repo: string | undefined = pieces?.groups?.repo?.trim();
|
||||
|
||||
// Check that the regular expression matched and that we have at least the repo name.
|
||||
if (!pieces?.groups || !repo || repo.length === 0) {
|
||||
return new Failure(undefined);
|
||||
}
|
||||
|
||||
const owner: string | undefined = pieces.groups.owner?.trim();
|
||||
const path: string | undefined = pieces.groups.path?.trim();
|
||||
const ref: string | undefined = pieces.groups.ref?.trim();
|
||||
|
||||
return new Success({
|
||||
owner: owner || getDefaultOwner(env),
|
||||
repo,
|
||||
path: path || DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: ref || DEFAULT_CONFIG_FILE_REF,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse `configFile` into an array of `RemoteFileAddress` components.
|
||||
*
|
||||
* @param actionState The current Action state.
|
||||
* @param configFile The string to try and parse.
|
||||
* @returns The successful result of executing the regex.
|
||||
* @throws `ConfigurationError` if the format of `configFile` is not valid.
|
||||
*/
|
||||
export async function parseRemoteFileAddress(
|
||||
actionState: ActionState<["FeatureFlags", "Env"]>,
|
||||
configFile: string,
|
||||
): Promise<RemoteFileAddress> {
|
||||
// Try to parse the input using the old format. If successful, return the
|
||||
// resulting `RemoteFileAddress`. Otherwise, continue using the new format.
|
||||
const oldFormatAddressResult = parseOldRemoteFileAddress(configFile);
|
||||
|
||||
if (oldFormatAddressResult.isSuccess()) {
|
||||
return oldFormatAddressResult.value;
|
||||
}
|
||||
|
||||
// retrieve the various parts of the config location, and ensure they're present
|
||||
const newFormatAddressResult = parseNewRemoteFileAddress(
|
||||
actionState.env,
|
||||
configFile,
|
||||
);
|
||||
|
||||
if (newFormatAddressResult.isFailure()) {
|
||||
// Neither the old format nor the new format worked. Throw an error that
|
||||
// explains the format we accept. We only mention the new format, since that's
|
||||
// what we want to be used going forward.
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFileRepoFormatInvalidMessage(configFile),
|
||||
);
|
||||
}
|
||||
|
||||
const address = newFormatAddressResult.value;
|
||||
|
||||
// Ensure that the path is a relative path.
|
||||
if (address.path.startsWith("/")) {
|
||||
throw new ConfigurationError(
|
||||
`The path component of '${configFile}' cannot be an absolute path.`,
|
||||
);
|
||||
}
|
||||
|
||||
return address;
|
||||
}
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.25.6",
|
||||
"cliVersion": "2.25.6",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.5",
|
||||
"priorCliVersion": "2.25.5"
|
||||
"bundleVersion": "codeql-bundle-v2.26.1",
|
||||
"cliVersion": "2.26.1",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.0",
|
||||
"priorCliVersion": "2.26.0"
|
||||
}
|
||||
+52
-19
@@ -6,24 +6,45 @@ import { Language } from "./languages";
|
||||
import { getActionsLogger } from "./logging";
|
||||
import { getCodeQLDatabasePath } from "./util";
|
||||
|
||||
/** Represents a diagnostic message for the tool status page, etc. */
|
||||
export interface DiagnosticMessage {
|
||||
/**
|
||||
* Known tags for diagnostics. There is currently only "internal-error",
|
||||
* but others may be added in the future.
|
||||
*/
|
||||
export type DiagnosticTag = "internal-error";
|
||||
|
||||
/** Optional information about the origin of a diagnostic. */
|
||||
export type DiagnosticSourceOptions = {
|
||||
/**
|
||||
* Name of the CodeQL extractor. This is used to identify which tool component the reporting
|
||||
* descriptor object should be nested under in SARIF.
|
||||
*/
|
||||
extractorName?: string;
|
||||
/** An array of tags for the diagnostic. */
|
||||
tags?: DiagnosticTag[];
|
||||
};
|
||||
|
||||
/** Represents information about the origin of a diagnostic. */
|
||||
export type DiagnosticSource = {
|
||||
/**
|
||||
* An identifier under which it makes sense to group this diagnostic message.
|
||||
* This is used to build the SARIF reporting descriptor object.
|
||||
*/
|
||||
id: string;
|
||||
/** Display name for the ID. This is used to build the SARIF reporting descriptor object. */
|
||||
name: string;
|
||||
} & DiagnosticSourceOptions;
|
||||
|
||||
/**
|
||||
* Represents a diagnostic message for the tool status page, etc.
|
||||
*
|
||||
* Unlike {@link DiagnosticMessage}, properties which can automatically
|
||||
* be populated are optional in this type.
|
||||
*/
|
||||
export type DiagnosticMessageOptions = {
|
||||
/** ISO 8601 timestamp */
|
||||
timestamp: string;
|
||||
source: {
|
||||
/**
|
||||
* An identifier under which it makes sense to group this diagnostic message.
|
||||
* This is used to build the SARIF reporting descriptor object.
|
||||
*/
|
||||
id: string;
|
||||
/** Display name for the ID. This is used to build the SARIF reporting descriptor object. */
|
||||
name: string;
|
||||
/**
|
||||
* Name of the CodeQL extractor. This is used to identify which tool component the reporting
|
||||
* descriptor object should be nested under in SARIF.
|
||||
*/
|
||||
extractorName?: string;
|
||||
};
|
||||
timestamp?: string;
|
||||
/** Information about the origin of the diagnostic. */
|
||||
source?: DiagnosticSourceOptions;
|
||||
/** GitHub flavored Markdown formatted message. Should include inline links to any help pages. */
|
||||
markdownMessage?: string;
|
||||
/** Plain text message. Used by components where the string processing needed to support Markdown is cumbersome. */
|
||||
@@ -53,7 +74,15 @@ export interface DiagnosticMessage {
|
||||
};
|
||||
/** Structured metadata about the diagnostic message */
|
||||
attributes?: { [key: string]: any };
|
||||
}
|
||||
};
|
||||
|
||||
/** Represents a diagnostic message for the tool status page, etc. */
|
||||
export type DiagnosticMessage = DiagnosticMessageOptions & {
|
||||
/** ISO 8601 timestamp */
|
||||
timestamp: string;
|
||||
/** Information about the origin of the diagnostic. */
|
||||
source: DiagnosticSource;
|
||||
};
|
||||
|
||||
/** Represents a diagnostic message that has not yet been written to the database. */
|
||||
interface UnwrittenDiagnostic {
|
||||
@@ -90,7 +119,7 @@ let diagnosticCounter = 0;
|
||||
export function makeDiagnostic(
|
||||
id: string,
|
||||
name: string,
|
||||
data: Partial<DiagnosticMessage> | undefined = undefined,
|
||||
data: DiagnosticMessageOptions | undefined = undefined,
|
||||
): DiagnosticMessage {
|
||||
return {
|
||||
...data,
|
||||
@@ -243,6 +272,7 @@ export function makeTelemetryDiagnostic(
|
||||
id: string,
|
||||
name: string,
|
||||
attributes: { [key: string]: any },
|
||||
tags?: DiagnosticTag[],
|
||||
): DiagnosticMessage {
|
||||
return makeDiagnostic(id, name, {
|
||||
attributes,
|
||||
@@ -251,5 +281,8 @@ export function makeTelemetryDiagnostic(
|
||||
statusPage: false,
|
||||
telemetry: true,
|
||||
},
|
||||
source: {
|
||||
tags,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,13 @@
|
||||
/**
|
||||
* Environment variables used by Default Setup to communicate the private registry proxy configuration.
|
||||
*/
|
||||
export enum RegistryProxyVars {
|
||||
PROXY_HOST = "CODEQL_PROXY_HOST",
|
||||
PROXY_PORT = "CODEQL_PROXY_PORT",
|
||||
PROXY_CA_CERTIFICATE = "CODEQL_PROXY_CA_CERTIFICATE",
|
||||
PROXY_URLS = "CODEQL_PROXY_URLS",
|
||||
}
|
||||
|
||||
/**
|
||||
* Environment variables used by the CodeQL Action.
|
||||
*
|
||||
@@ -17,6 +27,18 @@ export enum EnvVar {
|
||||
*/
|
||||
CLI_VERBOSITY = "CODEQL_VERBOSITY",
|
||||
|
||||
/**
|
||||
* Set by Default Setup to the base branch of the PR being analysed, if analysing a PR.
|
||||
* This is needed because the `pull_request` context is not available for `dynamic` events.
|
||||
*/
|
||||
CODE_SCANNING_BASE_BRANCH = "CODE_SCANNING_BASE_BRANCH",
|
||||
|
||||
/**
|
||||
* Set by Default Setup to the full ref being analysed, if analysing a PR.
|
||||
* This is needed because the `pull_request` context is not available for `dynamic` events.
|
||||
*/
|
||||
CODE_SCANNING_REF = "CODE_SCANNING_REF",
|
||||
|
||||
/**
|
||||
* `PersistedVersionInfo` for the CodeQL CLI, so later Actions steps can reuse it instead of
|
||||
* invoking `codeql version` again.
|
||||
@@ -83,6 +105,9 @@ export enum EnvVar {
|
||||
/** Whether to suppress the warning if the current CLI will soon be unsupported. */
|
||||
SUPPRESS_DEPRECATED_SOON_WARNING = "CODEQL_ACTION_SUPPRESS_DEPRECATED_SOON_WARNING",
|
||||
|
||||
/** Used to dictate or persist the temporary directory used by the CodeQL Action. */
|
||||
TEMP = "CODEQL_ACTION_TEMP",
|
||||
|
||||
/** Whether to disable uploading SARIF results or status reports to the GitHub API */
|
||||
TEST_MODE = "CODEQL_ACTION_TEST_MODE",
|
||||
|
||||
@@ -160,3 +185,130 @@ export enum EnvVar {
|
||||
/** Used by Code Scanning Risk Assessment to communicate the assessment ID to the CodeQL Action. */
|
||||
RISK_ASSESSMENT_ID = "CODEQL_ACTION_RISK_ASSESSMENT_ID",
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerates known GitHub Actions environment variables that we expect
|
||||
* to be set in a GitHub Actions environment.
|
||||
*/
|
||||
export enum ActionsEnvVars {
|
||||
GITHUB_ACTION_REPOSITORY = "GITHUB_ACTION_REPOSITORY",
|
||||
GITHUB_API_URL = "GITHUB_API_URL",
|
||||
GITHUB_EVENT_NAME = "GITHUB_EVENT_NAME",
|
||||
GITHUB_EVENT_PATH = "GITHUB_EVENT_PATH",
|
||||
GITHUB_JOB = "GITHUB_JOB",
|
||||
GITHUB_REF = "GITHUB_REF",
|
||||
GITHUB_REPOSITORY = "GITHUB_REPOSITORY",
|
||||
GITHUB_RUN_ATTEMPT = "GITHUB_RUN_ATTEMPT",
|
||||
GITHUB_RUN_ID = "GITHUB_RUN_ID",
|
||||
GITHUB_SERVER_URL = "GITHUB_SERVER_URL",
|
||||
GITHUB_SHA = "GITHUB_SHA",
|
||||
GITHUB_WORKFLOW = "GITHUB_WORKFLOW",
|
||||
GITHUB_WORKSPACE = "GITHUB_WORKSPACE",
|
||||
RUNNER_ENVIRONMENT = "RUNNER_ENVIRONMENT",
|
||||
RUNNER_NAME = "RUNNER_NAME",
|
||||
RUNNER_OS = "RUNNER_OS",
|
||||
RUNNER_TEMP = "RUNNER_TEMP",
|
||||
RUNNER_TOOL_CACHE = "RUNNER_TOOL_CACHE",
|
||||
}
|
||||
|
||||
/** A type representing all known environment variables. */
|
||||
export type KnownEnvVar = EnvVar | ActionsEnvVars | RegistryProxyVars;
|
||||
|
||||
/**
|
||||
* Gets an environment variable, but throws an error if it is not set.
|
||||
*/
|
||||
function getRequiredEnvVar(env: NodeJS.ProcessEnv, paramName: string): string {
|
||||
const value = env[paramName];
|
||||
if (value === undefined || value.length === 0) {
|
||||
throw new Error(`${paramName} environment variable must be set`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an environment parameter, but throw an error if it is not set.
|
||||
*
|
||||
* @deprecated Use `getRequired` of a `ReadOnlyEnv` or `Env` instance instead.
|
||||
*/
|
||||
export function getRequiredEnvParam(paramName: string): string {
|
||||
return getRequiredEnvVar(process.env, paramName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an environment variable, but returns `undefined` if it is not set or empty.
|
||||
*/
|
||||
function getOptionalEnvVarFrom(
|
||||
env: NodeJS.ProcessEnv,
|
||||
paramName: string,
|
||||
): string | undefined {
|
||||
const value = env[paramName];
|
||||
if (value?.trim().length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an environment variable, but return `undefined` if it is not set or empty.
|
||||
*
|
||||
* @deprecated Use `getOptional` of a `ReadOnlyEnv` or `Env` instance instead.
|
||||
*/
|
||||
export function getOptionalEnvVar(paramName: string): string | undefined {
|
||||
return getOptionalEnvVarFrom(process.env, paramName);
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstraction around read-only environment variables, to allow abstracting away from `process.env`
|
||||
* in tests, while clearly signalling in regular code that the consumer of the `ReadOnlyEnv` instance
|
||||
* will only read from it.
|
||||
*/
|
||||
export class ReadOnlyEnv<T extends string | undefined = string | undefined> {
|
||||
constructor(protected readonly vars: Record<string, T>) {}
|
||||
|
||||
/** Clones the object while detaching the underlying environment from the original. */
|
||||
public clone(): this {
|
||||
return Object.create(this, { vars: { value: { ...this.vars } } }) as this;
|
||||
}
|
||||
|
||||
/** Tries to get the value for `name` and throws if there isn't one. */
|
||||
public getRequired(name: string): string {
|
||||
return getRequiredEnvVar(this.vars, name);
|
||||
}
|
||||
|
||||
/** Gets the value for `name`, or `undefined` if it isn't set or empty. */
|
||||
public getOptional(name: string): string | undefined {
|
||||
return getOptionalEnvVarFrom(this.vars, name);
|
||||
}
|
||||
|
||||
/** Gets the entries of the underlying `ProcessEnv`. */
|
||||
public entries(): Array<[string, T]> {
|
||||
return Object.entries(this.vars);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around an environment, to allow abstracting away from `process.env` in tests.
|
||||
* Use `ReadOnlyEnv` instead if you only plan to read from the environment.
|
||||
* This type allows writing to the environment.
|
||||
*/
|
||||
export class Env<
|
||||
T extends string | undefined = string | undefined,
|
||||
> extends ReadOnlyEnv<T> {
|
||||
private changed: boolean = false;
|
||||
|
||||
/** Sets an environment variable. */
|
||||
public set(name: string, value: T): void {
|
||||
this.vars[name] = value;
|
||||
this.changed = true;
|
||||
}
|
||||
|
||||
/** Gets a value indicating whether `set` was called at least once. */
|
||||
public hasChanged(): boolean {
|
||||
return this.changed;
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets an `Env` instance for `env`, which is `process.env` by default. */
|
||||
export function getEnv(env: NodeJS.ProcessEnv = process.env): Env {
|
||||
return new Env(env);
|
||||
}
|
||||
+10
-1
@@ -30,7 +30,7 @@ export function getInvalidConfigFileMessage(
|
||||
return `The configuration file "${configFile}" is invalid: ${messages.slice(0, 10).join(", ")}${andMore}`;
|
||||
}
|
||||
|
||||
export function getConfigFileRepoFormatInvalidMessage(
|
||||
export function getConfigFileRepoOldFormatInvalidMessage(
|
||||
configFile: string,
|
||||
): string {
|
||||
let error = `The configuration file "${configFile}" is not a supported remote file reference.`;
|
||||
@@ -39,6 +39,15 @@ export function getConfigFileRepoFormatInvalidMessage(
|
||||
return error;
|
||||
}
|
||||
|
||||
export function getConfigFileRepoFormatInvalidMessage(
|
||||
configFile: string,
|
||||
): string {
|
||||
let error = `The configuration file "${configFile}" is not a supported remote file reference.`;
|
||||
error += " Expected format [<owner>/]<repository>[@<ref>][:<file-path>]";
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
export function getConfigFileFormatInvalidMessage(configFile: string): string {
|
||||
return `The configuration file "${configFile}" could not be read`;
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ export interface CodeQLDefaultVersionInfo {
|
||||
* Legacy features should end with `_enabled`.
|
||||
*/
|
||||
export enum Feature {
|
||||
/** Allows supported properties of configuration files to be merged. */
|
||||
AllowMergeConfigFiles = "allow_merge_config_files",
|
||||
/** Controls whether we allow multiple values for the `analysis-kinds` input. */
|
||||
AllowMultipleAnalysisKinds = "allow_multiple_analysis_kinds",
|
||||
AllowToolcacheInput = "allow_toolcache_input",
|
||||
@@ -134,6 +136,8 @@ export enum Feature {
|
||||
/** Controls whether overlay build failures on the default branch are stored in the Actions cache. */
|
||||
OverlayAnalysisStatusSave = "overlay_analysis_status_save",
|
||||
QaTelemetryEnabled = "qa_telemetry_enabled",
|
||||
/** Routes (some) API requests through the registry proxy. */
|
||||
ProxyApiRequests = "proxy_api_requests",
|
||||
/** Note that this currently only disables baseline file coverage information. */
|
||||
SkipFileCoverageOnPrs = "skip_file_coverage_on_prs",
|
||||
StartProxyUseFeaturesRelease = "start_proxy_use_features_release",
|
||||
@@ -171,6 +175,11 @@ export type FeatureConfig = {
|
||||
};
|
||||
|
||||
export const featureConfig = {
|
||||
[Feature.AllowMergeConfigFiles]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_ALLOW_MERGE_CONFIG_FILES",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.AllowMultipleAnalysisKinds]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_ALLOW_MULTIPLE_ANALYSIS_KINDS",
|
||||
@@ -375,6 +384,11 @@ export const featureConfig = {
|
||||
legacyApi: true,
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.ProxyApiRequests]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_PROXY_API_REQUESTS",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.SkipFileCoverageOnPrs]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS",
|
||||
|
||||
+18
-27
@@ -7,9 +7,9 @@ import * as io from "@actions/io";
|
||||
import * as semver from "semver";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import {
|
||||
FileCmdNotFoundError,
|
||||
getActionsEnv,
|
||||
getActionVersion,
|
||||
getFileType,
|
||||
getOptionalInput,
|
||||
@@ -56,7 +56,7 @@ import {
|
||||
runDatabaseInitCluster,
|
||||
} from "./init";
|
||||
import { JavaEnvVars, BuiltInLanguage } from "./languages";
|
||||
import { getActionsLogger, Logger, withGroupAsync } from "./logging";
|
||||
import { Logger, withGroupAsync } from "./logging";
|
||||
import {
|
||||
downloadOverlayBaseDatabaseFromCache,
|
||||
OverlayBaseDatabaseDownloadStats,
|
||||
@@ -73,7 +73,6 @@ import {
|
||||
createStatusReportBase,
|
||||
getActionsStatus,
|
||||
sendStatusReport,
|
||||
sendUnhandledErrorStatusReport,
|
||||
} from "./status-report";
|
||||
import { ZstdAvailability } from "./tar";
|
||||
import { ToolsDownloadStatusReport } from "./tools-download";
|
||||
@@ -204,12 +203,14 @@ async function sendCompletedStatusReport(
|
||||
}
|
||||
}
|
||||
|
||||
async function run(startedAt: Date) {
|
||||
async function run(
|
||||
actionState: ActionState<["Base", "Logger", "Env", "Actions"]>,
|
||||
) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
const logger = getActionsLogger();
|
||||
const actionsEnv = getActionsEnv();
|
||||
const startedAt = actionState.startedAt;
|
||||
const logger = actionState.logger;
|
||||
|
||||
let apiDetails: GitHubApiCombinedDetails;
|
||||
let config: configUtils.Config | undefined;
|
||||
@@ -263,14 +264,10 @@ async function run(startedAt: Date) {
|
||||
|
||||
core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true");
|
||||
|
||||
const useConfigFileProperty = await features.getValue(
|
||||
Feature.ConfigFileRepositoryProperty,
|
||||
);
|
||||
configFile = getConfigFileInput(
|
||||
logger,
|
||||
actionsEnv,
|
||||
const actionStateWithFeatures = { ...actionState, features };
|
||||
configFile = await getConfigFileInput(
|
||||
actionStateWithFeatures,
|
||||
repositoryProperties,
|
||||
useConfigFileProperty,
|
||||
);
|
||||
|
||||
// path.resolve() respects the intended semantics of source-root. If
|
||||
@@ -369,7 +366,7 @@ async function run(startedAt: Date) {
|
||||
repositoryProperties,
|
||||
);
|
||||
|
||||
config = await initConfig(features, {
|
||||
config = await initConfig(actionStateWithFeatures, {
|
||||
analysisKinds,
|
||||
languagesInput: getOptionalInput("languages"),
|
||||
queriesInput: getOptionalInput("queries"),
|
||||
@@ -854,19 +851,13 @@ async function recordZstdAvailability(
|
||||
);
|
||||
}
|
||||
|
||||
/** Defines the `init` Action. */
|
||||
const init: Action = {
|
||||
name: ActionName.Init,
|
||||
run,
|
||||
};
|
||||
|
||||
export async function runWrapper() {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
try {
|
||||
await run(startedAt);
|
||||
} catch (error) {
|
||||
core.setFailed(`init action failed: ${getErrorMessage(error)}`);
|
||||
await sendUnhandledErrorStatusReport(
|
||||
ActionName.Init,
|
||||
startedAt,
|
||||
error,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
await runInActions(init);
|
||||
await checkForTimeout();
|
||||
}
|
||||
+2
-1
@@ -8,6 +8,7 @@ import * as sinon from "sinon";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { createStubCodeQL } from "./codeql";
|
||||
import { ActionsEnvVars } from "./environment";
|
||||
import { Feature } from "./feature-flags";
|
||||
import {
|
||||
checkPacksForOverlayCompatibility,
|
||||
@@ -84,7 +85,7 @@ for (const { runnerEnv, ErrorConstructor, message } of [
|
||||
`cleanupDatabaseClusterDirectory throws a ${ErrorConstructor.name} when cleanup fails on ${runnerEnv} runner`,
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir: string) => {
|
||||
process.env["RUNNER_ENVIRONMENT"] = runnerEnv;
|
||||
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = runnerEnv;
|
||||
|
||||
const dbLocation = path.resolve(tmpDir, "dbs");
|
||||
fs.mkdirSync(dbLocation, { recursive: true });
|
||||
|
||||
+3
-2
@@ -7,6 +7,7 @@ import * as github from "@actions/github";
|
||||
import * as io from "@actions/io";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
import { ActionState } from "./action-common";
|
||||
import {
|
||||
getOptionalInput,
|
||||
isAnalyzingPullRequest,
|
||||
@@ -81,11 +82,11 @@ export async function initCodeQL(
|
||||
}
|
||||
|
||||
export async function initConfig(
|
||||
features: FeatureEnablement,
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
inputs: configUtils.InitConfigInputs,
|
||||
): Promise<configUtils.Config> {
|
||||
return await withGroupAsync("Load language configuration", async () => {
|
||||
return await configUtils.initConfig(features, inputs);
|
||||
return await configUtils.initConfig(actionState, inputs);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+102
-6
@@ -10,8 +10,8 @@ const testSchema = {
|
||||
requiredKey: json.string,
|
||||
};
|
||||
|
||||
const optionalSchema = {
|
||||
optionalKey: json.optional(json.string),
|
||||
const optionalOrNullSchema = {
|
||||
optionalKey: json.optionalOrNull(json.string),
|
||||
};
|
||||
|
||||
test("validateSchema - required properties are required", async (t) => {
|
||||
@@ -28,13 +28,36 @@ test("validateSchema - required properties are required", async (t) => {
|
||||
t.true(json.validateSchema(testSchema, { requiredKey: "foo" }));
|
||||
});
|
||||
|
||||
test("validateSchema - optional properties are optional", async (t) => {
|
||||
test("validateSchema - optionalOrNullSchema properties are optional or null", async (t) => {
|
||||
// Optional fields may be absent
|
||||
t.true(json.validateSchema(optionalSchema, {}));
|
||||
t.true(json.validateSchema(optionalSchema, { optionalKey: undefined }));
|
||||
t.true(json.validateSchema(optionalSchema, { optionalKey: null }));
|
||||
t.true(json.validateSchema(optionalOrNullSchema, {}));
|
||||
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: undefined }));
|
||||
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: null }));
|
||||
|
||||
// But, if present, should have the expected type
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: 0 }));
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: 123 }));
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: false }));
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: true }));
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: [] }));
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: {} }));
|
||||
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: "" }));
|
||||
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: "foo" }));
|
||||
});
|
||||
|
||||
const optionalSchema = {
|
||||
optionalKey: json.optional(json.string),
|
||||
};
|
||||
|
||||
test("validateSchema - optional properties are optional", async (t) => {
|
||||
// Optional fields may be absent or explicitly undefined
|
||||
t.true(json.validateSchema(optionalSchema, {}));
|
||||
t.true(json.validateSchema(optionalSchema, { optionalKey: undefined }));
|
||||
|
||||
// But should reject null
|
||||
t.false(json.validateSchema(optionalSchema, { optionalKey: null }));
|
||||
|
||||
// And, if present, should have the expected type
|
||||
t.false(json.validateSchema(optionalSchema, { optionalKey: 0 }));
|
||||
t.false(json.validateSchema(optionalSchema, { optionalKey: 123 }));
|
||||
t.false(json.validateSchema(optionalSchema, { optionalKey: false }));
|
||||
@@ -44,3 +67,76 @@ test("validateSchema - optional properties are optional", async (t) => {
|
||||
t.true(json.validateSchema(optionalSchema, { optionalKey: "" }));
|
||||
t.true(json.validateSchema(optionalSchema, { optionalKey: "foo" }));
|
||||
});
|
||||
|
||||
const arraySchema = {
|
||||
arrayKey: json.array(json.number),
|
||||
};
|
||||
|
||||
test("validateSchema - validates arrays", async (t) => {
|
||||
// Arrays of numeric elements are accepted.
|
||||
t.true(json.validateSchema(arraySchema, { arrayKey: [] }));
|
||||
t.true(json.validateSchema(arraySchema, { arrayKey: [4] }));
|
||||
t.true(json.validateSchema(arraySchema, { arrayKey: [4, 8] }));
|
||||
t.true(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15] }));
|
||||
|
||||
// Other array elements are not accepted.
|
||||
t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15, "bar"] }));
|
||||
t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, undefined] }));
|
||||
t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15, null] }));
|
||||
});
|
||||
|
||||
const objectSchema = {
|
||||
objectKey: json.object(arraySchema),
|
||||
};
|
||||
|
||||
test("validateSchema - validates objects", async (t) => {
|
||||
// Objects of the given schema are accepted.
|
||||
t.true(json.validateSchema(objectSchema, { objectKey: { arrayKey: [] } }));
|
||||
t.true(json.validateSchema(objectSchema, { objectKey: { arrayKey: [4] } }));
|
||||
|
||||
// Other values are not accepted.
|
||||
t.false(json.validateSchema(objectSchema, {}));
|
||||
t.false(json.validateSchema(objectSchema, { objectKey: [] }));
|
||||
t.false(json.validateSchema(objectSchema, { objectKey: undefined }));
|
||||
t.false(json.validateSchema(objectSchema, { objectKey: null }));
|
||||
t.false(json.validateSchema(objectSchema, { objectKey: "foo" }));
|
||||
t.false(json.validateSchema(objectSchema, { objectKey: 123 }));
|
||||
});
|
||||
|
||||
const checkSchemaTestSchema = {
|
||||
rootKey: json.object(objectSchema),
|
||||
};
|
||||
|
||||
test("checkSchema - reports unknown keys", async (t) => {
|
||||
const result = json.checkSchema(checkSchemaTestSchema, {
|
||||
rootKey: {
|
||||
objectKey: {
|
||||
arrayKey: [],
|
||||
},
|
||||
nestedExtraKey: "foo",
|
||||
},
|
||||
extraKey: "bar",
|
||||
});
|
||||
|
||||
t.true(result.valid);
|
||||
t.deepEqual(
|
||||
result.unknownKeys.sort(),
|
||||
[".extraKey", ".rootKey.nestedExtraKey"].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
test("checkSchema - reports invalid keys", async (t) => {
|
||||
const result = json.checkSchema(checkSchemaTestSchema, {
|
||||
rootKey: {
|
||||
objectKey: {
|
||||
arrayKey: ["foo"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
t.false(result.valid);
|
||||
t.deepEqual(
|
||||
result.invalidKeys.sort(),
|
||||
[".rootKey.objectKey.arrayKey[0]"].sort(),
|
||||
);
|
||||
});
|
||||
+241
-13
@@ -30,6 +30,11 @@ export function isString(value: unknown): value is string {
|
||||
return typeof value === "string";
|
||||
}
|
||||
|
||||
/** Asserts that `value` is a number. */
|
||||
export function isNumber(value: unknown): value is number {
|
||||
return typeof value === "number";
|
||||
}
|
||||
|
||||
/** Asserts that `value` is either a string or undefined. */
|
||||
export function isStringOrUndefined(
|
||||
value: unknown,
|
||||
@@ -43,28 +48,146 @@ export function isStringOrUndefined(
|
||||
*/
|
||||
export type Validator<T> = {
|
||||
validate: (val: unknown) => val is T;
|
||||
check: (
|
||||
val: unknown,
|
||||
opts: CheckSchemaOptions,
|
||||
path: string,
|
||||
) => CheckSchemaResult;
|
||||
required: boolean;
|
||||
};
|
||||
|
||||
function defaultCheck(
|
||||
validate: (val: unknown) => val is any,
|
||||
): (arg: unknown) => CheckSchemaResult {
|
||||
return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate(arg) });
|
||||
}
|
||||
|
||||
function makeValidator<T>(
|
||||
validate: (arg: unknown) => arg is T,
|
||||
required: boolean = true,
|
||||
) {
|
||||
return {
|
||||
validate,
|
||||
check: defaultCheck(validate),
|
||||
required,
|
||||
} as const satisfies Validator<T>;
|
||||
}
|
||||
|
||||
/** Extracts `T` from `Validator<T>`. */
|
||||
export type UnwrapValidator<V> = V extends Validator<infer A> ? A : never;
|
||||
|
||||
/** A validator for string fields in schemas. */
|
||||
export const string = {
|
||||
validate: isString,
|
||||
required: true,
|
||||
} as const satisfies Validator<string>;
|
||||
export const string = makeValidator(isString);
|
||||
|
||||
/** Transforms a validator to be optional. */
|
||||
export function optional<T>(validator: Validator<T>) {
|
||||
/** A validator for number fields in schemas. */
|
||||
export const number = makeValidator(isNumber);
|
||||
|
||||
/** A validator for arrays. */
|
||||
export function array<T>(validator: Validator<T>) {
|
||||
const validate = (val: unknown) => {
|
||||
return isArray(val) && val.every((e) => validator.validate(e));
|
||||
};
|
||||
return {
|
||||
validate,
|
||||
check: (val: unknown, opts: CheckSchemaOptions, path: string) => {
|
||||
const result: CheckSchemaResult = successfulCheckSchema();
|
||||
|
||||
// The value must be an array.
|
||||
if (!isArray(val)) {
|
||||
result.valid = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Validate all elements of the array.
|
||||
let index = 0;
|
||||
for (const e of val) {
|
||||
const elementPath = `${path}[${index}]`;
|
||||
const eResult = validator.check(e, opts, `${elementPath}`);
|
||||
|
||||
result.invalidKeys.push(...eResult.invalidKeys);
|
||||
result.unknownKeys.push(...eResult.unknownKeys);
|
||||
index++;
|
||||
|
||||
if (!eResult.valid) {
|
||||
result.valid = false;
|
||||
|
||||
// Add the element path to `invalidKeys` if we didn't get
|
||||
// any more specific ones from the element validator.
|
||||
if (eResult.invalidKeys.length === 0) {
|
||||
result.invalidKeys.push(elementPath);
|
||||
}
|
||||
|
||||
if (opts.failFast) {
|
||||
return result;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
required: true,
|
||||
} as const satisfies Validator<T[]>;
|
||||
}
|
||||
|
||||
/** A validator for objects. */
|
||||
export function object<
|
||||
S extends Schema,
|
||||
T extends UnvalidatedObject<any> = FromSchema<S>,
|
||||
>(schema: S) {
|
||||
return {
|
||||
validate: (val: unknown) => {
|
||||
return isObject(val) && validateSchema<S, T>(schema, val);
|
||||
},
|
||||
check: (val, opts, path) => {
|
||||
if (!isObject(val)) {
|
||||
return invalidCheckSchema();
|
||||
}
|
||||
return checkSchema(schema, val, opts, path);
|
||||
},
|
||||
required: true,
|
||||
} as const satisfies Validator<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a validator to be optional, accepting `undefined` or `null` for an
|
||||
* absent value.
|
||||
*/
|
||||
export function optionalOrNull<T>(validator: Validator<T>) {
|
||||
return {
|
||||
validate: (val: unknown) => {
|
||||
return val === undefined || val === null || validator.validate(val);
|
||||
},
|
||||
check: (val, opts, path) => {
|
||||
if (val === undefined || val === null) {
|
||||
return successfulCheckSchema();
|
||||
}
|
||||
return validator.check(val, opts, path);
|
||||
},
|
||||
required: false,
|
||||
} as const satisfies Validator<T | undefined | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a validator to be optional, accepting `undefined` for an absent
|
||||
* value but, unlike `optionalOrNull`, rejecting `null`.
|
||||
*/
|
||||
export function optional<T>(validator: Validator<T>) {
|
||||
return {
|
||||
validate: (val: unknown): val is T | undefined => {
|
||||
return val === undefined || validator.validate(val);
|
||||
},
|
||||
check: (val, opts, path) => {
|
||||
if (val === undefined) {
|
||||
return successfulCheckSchema();
|
||||
}
|
||||
return validator.check(val, opts, path);
|
||||
},
|
||||
required: false,
|
||||
} as const satisfies Validator<T | undefined>;
|
||||
}
|
||||
|
||||
/** Represents an arbitrary object schema. */
|
||||
export type Schema = Record<string, Validator<any>>;
|
||||
|
||||
@@ -90,28 +213,133 @@ export type FromSchema<S extends Schema> = {
|
||||
* @param obj The object to validate.
|
||||
* @returns Asserts that `obj` is of the `schema`'s type if validation is successful.
|
||||
*/
|
||||
export function validateSchema<S extends Schema>(
|
||||
export function validateSchema<
|
||||
S extends Schema,
|
||||
T extends UnvalidatedObject<any> = FromSchema<S>,
|
||||
>(schema: S, obj: UnvalidatedObject<any>): obj is T {
|
||||
const result = checkSchema(schema, obj, { failFast: true });
|
||||
return result.valid;
|
||||
}
|
||||
|
||||
export interface CheckSchemaOptions {
|
||||
/** Whether to stop validation after the first error. */
|
||||
failFast?: boolean;
|
||||
}
|
||||
|
||||
export interface CheckSchemaResult {
|
||||
/** Whether the `obj` satisfies the schema. */
|
||||
valid: boolean;
|
||||
/** Unknown keys that were found during validation. */
|
||||
unknownKeys: string[];
|
||||
/** Known keys that failed validation. */
|
||||
invalidKeys: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to produce a `CheckSchemaResult` where `valid: true`.
|
||||
*/
|
||||
function successfulCheckSchema(): CheckSchemaResult {
|
||||
return {
|
||||
valid: true,
|
||||
unknownKeys: [],
|
||||
invalidKeys: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to produce a `CheckSchemaResult` where `valid: false`.
|
||||
*/
|
||||
function invalidCheckSchema(): CheckSchemaResult {
|
||||
return {
|
||||
valid: false,
|
||||
unknownKeys: [],
|
||||
invalidKeys: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function checkSchema<S extends Schema>(
|
||||
schema: S,
|
||||
obj: UnvalidatedObject<any>,
|
||||
): obj is FromSchema<S> {
|
||||
options: CheckSchemaOptions = {},
|
||||
path: string = "",
|
||||
): CheckSchemaResult {
|
||||
const result: CheckSchemaResult = successfulCheckSchema();
|
||||
|
||||
// Track the set of input keys. We remove keys from this set as we recognise them
|
||||
// during validation.
|
||||
const inputKeys = new Set(Object.keys(obj));
|
||||
|
||||
// Track keys that have failed validation, starting with the empty set.
|
||||
const invalidKeys = new Set();
|
||||
|
||||
// Loop through all keys in the object schema and validate that the given object
|
||||
// satisfies the schema key.
|
||||
for (const [key, validator] of Object.entries(schema)) {
|
||||
const hasKey = key in obj;
|
||||
|
||||
// Remove key from set of unrecognised keys.
|
||||
inputKeys.delete(key);
|
||||
|
||||
// Add the key to the set of invalid keys. We remove it later once
|
||||
// it passes validation.
|
||||
invalidKeys.add(key);
|
||||
|
||||
// If the property is required, but absent, fail.
|
||||
if (validator.required && !hasKey) {
|
||||
return false;
|
||||
result.valid = false;
|
||||
|
||||
if (options.failFast) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the property is required, but undefined or null, fail.
|
||||
if (validator.required && (obj[key] === undefined || obj[key] === null)) {
|
||||
return false;
|
||||
result.valid = false;
|
||||
|
||||
if (options.failFast) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the property is present, validate it.
|
||||
if (hasKey && !validator.validate(obj[key])) {
|
||||
return false;
|
||||
if (hasKey) {
|
||||
const checkResult = validator.check(obj[key], options, `${path}.${key}`);
|
||||
|
||||
result.unknownKeys.push(...checkResult.unknownKeys);
|
||||
result.invalidKeys.push(...checkResult.invalidKeys);
|
||||
|
||||
// If we have invalid keys from the validator, then that means that
|
||||
// we have a more specific key than `key`. Remove `key` from the results.
|
||||
if (checkResult.invalidKeys.length > 0) {
|
||||
invalidKeys.delete(key);
|
||||
}
|
||||
|
||||
if (!checkResult.valid) {
|
||||
result.valid = false;
|
||||
|
||||
if (options.failFast) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach this point, the key has been successfully validated.
|
||||
invalidKeys.delete(key);
|
||||
}
|
||||
|
||||
return true;
|
||||
// If there are any remaining keys in `inputKeys`, add them to `unknownKeys`.
|
||||
for (const remainingKey of inputKeys) {
|
||||
result.unknownKeys.push(`${path}.${remainingKey}`);
|
||||
}
|
||||
|
||||
// If there are any remaining keys in `invalidKeys`, add them to the result.
|
||||
for (const invalidKey of invalidKeys) {
|
||||
result.invalidKeys.push(`${path}.${invalidKey}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
+13
-19
@@ -1,6 +1,7 @@
|
||||
import * as core from "@actions/core";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import {
|
||||
getActionVersion,
|
||||
getOptionalInput,
|
||||
@@ -14,7 +15,7 @@ import { getRawLanguagesNoAutodetect } from "./config-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import { initFeatures } from "./feature-flags";
|
||||
import { initCodeQL } from "./init";
|
||||
import { getActionsLogger, Logger } from "./logging";
|
||||
import { Logger } from "./logging";
|
||||
import { getRepositoryNwo } from "./repository";
|
||||
import { ToolsSource } from "./setup-codeql";
|
||||
import {
|
||||
@@ -24,7 +25,6 @@ import {
|
||||
createStatusReportBase,
|
||||
getActionsStatus,
|
||||
sendStatusReport,
|
||||
sendUnhandledErrorStatusReport,
|
||||
} from "./status-report";
|
||||
import { ToolsDownloadStatusReport } from "./tools-download";
|
||||
import {
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
ConfigurationError,
|
||||
wrapError,
|
||||
checkActionVersion,
|
||||
getErrorMessage,
|
||||
} from "./util";
|
||||
|
||||
/**
|
||||
@@ -88,12 +87,13 @@ async function sendCompletedStatusReport(
|
||||
}
|
||||
|
||||
/** The main behaviour of this action. */
|
||||
async function run(startedAt: Date): Promise<void> {
|
||||
async function run({
|
||||
startedAt,
|
||||
logger,
|
||||
}: ActionState<["Base", "Logger"]>): Promise<void> {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
const logger = getActionsLogger();
|
||||
|
||||
let codeql: CodeQL;
|
||||
let toolsDownloadStatusReport: ToolsDownloadStatusReport | undefined;
|
||||
let toolsFeatureFlagsValid: boolean | undefined;
|
||||
@@ -195,20 +195,14 @@ async function run(startedAt: Date): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/** Defines the `setup-codeql` Action. */
|
||||
const setupCodeQL: Action = {
|
||||
name: ActionName.SetupCodeQL,
|
||||
run,
|
||||
};
|
||||
|
||||
/** Run the action and catch any unhandled errors. */
|
||||
export async function runWrapper(): Promise<void> {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
try {
|
||||
await run(startedAt);
|
||||
} catch (error) {
|
||||
core.setFailed(`setup-codeql action failed: ${getErrorMessage(error)}`);
|
||||
await sendUnhandledErrorStatusReport(
|
||||
ActionName.SetupCodeQL,
|
||||
startedAt,
|
||||
error,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
await runInActions(setupCodeQL);
|
||||
await checkForTimeout();
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import * as sinon from "sinon";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import { EnvVar } from "./environment";
|
||||
import { Feature } from "./feature-flags";
|
||||
import { getRunnerLogger } from "./logging";
|
||||
import { getCacheRestoreKeyPrefix } from "./overlay/caching";
|
||||
@@ -637,8 +638,8 @@ test.serial(
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
process.env["CODE_SCANNING_REF"] = "refs/heads/feature-branch";
|
||||
process.env["CODE_SCANNING_BASE_BRANCH"] = "main";
|
||||
process.env[EnvVar.CODE_SCANNING_REF] = "refs/heads/feature-branch";
|
||||
process.env[EnvVar.CODE_SCANNING_BASE_BRANCH] = "main";
|
||||
|
||||
sinon.stub(api, "getAutomationID").resolves("test/");
|
||||
const listStub = sinon.stub(api, "listActionsCaches").resolves([
|
||||
|
||||
+55
-6
@@ -120,9 +120,20 @@ const mixedCredentials = [
|
||||
{ type: "maven_repository", host: "maven.pkg.github.com", token: "def" },
|
||||
{ type: "nuget_feed", host: "nuget.pkg.github.com", token: "ghi" },
|
||||
{ type: "goproxy_server", host: "goproxy.example.com", token: "jkl" },
|
||||
{ type: "git_source", host: "github.com/github", token: "mno" },
|
||||
];
|
||||
|
||||
const gitSourceCredential = {
|
||||
type: "git_source",
|
||||
host: "github.com/github",
|
||||
token: "mno",
|
||||
};
|
||||
|
||||
const dockerRegistryCredential = {
|
||||
type: "docker_registry",
|
||||
host: "https://registry.example.com",
|
||||
token: "pqr",
|
||||
};
|
||||
|
||||
test("getCredentials prefers registriesCredentials over registrySecrets", async (t) => {
|
||||
const registryCredentials = Buffer.from(
|
||||
JSON.stringify([
|
||||
@@ -241,7 +252,7 @@ test("getCredentials returns all for a language when specified", async (t) => {
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
toEncodedJSON(mixedCredentials),
|
||||
toEncodedJSON([...mixedCredentials, gitSourceCredential]),
|
||||
BuiltInLanguage.go,
|
||||
);
|
||||
t.is(credentials.length, 2);
|
||||
@@ -284,7 +295,7 @@ test("getCredentials returns all maven_repositories for Java when specified", as
|
||||
host: "maven2.pkg.github.com",
|
||||
token: "token2",
|
||||
},
|
||||
{ type: "git_source", host: "github.com/github", token: "mno" },
|
||||
{ type: "goproxy_server", host: "github.com/github", token: "mno" },
|
||||
];
|
||||
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
@@ -624,8 +635,12 @@ test("getCredentials validates 'replaces-base' correctly", async (t) => {
|
||||
);
|
||||
});
|
||||
|
||||
test("getCredentials returns no credentials for Actions", async (t) => {
|
||||
const credentialsInput = toEncodedJSON(mixedCredentials);
|
||||
test("getCredentials returns only ALWAYS_ENABLED_REGISTRY_TYPE credentials for Actions", async (t) => {
|
||||
const credentialsInput = toEncodedJSON([
|
||||
...mixedCredentials,
|
||||
gitSourceCredential,
|
||||
dockerRegistryCredential,
|
||||
]);
|
||||
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
@@ -633,7 +648,41 @@ test("getCredentials returns no credentials for Actions", async (t) => {
|
||||
credentialsInput,
|
||||
BuiltInLanguage.actions,
|
||||
);
|
||||
t.deepEqual(credentials, []);
|
||||
|
||||
for (const credential of credentials) {
|
||||
t.true(
|
||||
startProxyExports.ALWAYS_ENABLED_REGISTRY_TYPE.some(
|
||||
(ty) => ty === credential.type,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("getCredentials always returns ALWAYS_ENABLED_REGISTRY_TYPE credentials for all languages", async (t) => {
|
||||
const alwaysEnabledCredentials: startProxyExports.Credential[] = [];
|
||||
|
||||
for (const alwaysEnabled of startProxyExports.ALWAYS_ENABLED_REGISTRY_TYPE) {
|
||||
alwaysEnabledCredentials.push({
|
||||
type: alwaysEnabled,
|
||||
host: `host-${alwaysEnabled}`,
|
||||
token: `bar-${alwaysEnabled}`,
|
||||
url: `url-${alwaysEnabled}`,
|
||||
});
|
||||
}
|
||||
|
||||
const credentialsInput = toEncodedJSON(alwaysEnabledCredentials);
|
||||
|
||||
// Test all languages.
|
||||
for (const language of Object.values(BuiltInLanguage)) {
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
credentialsInput,
|
||||
language,
|
||||
);
|
||||
|
||||
t.deepEqual(credentials, alwaysEnabledCredentials);
|
||||
}
|
||||
});
|
||||
|
||||
function mockGetApiClient(endpoints: any) {
|
||||
|
||||
+20
-5
@@ -187,9 +187,19 @@ function isPAT(value: string) {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of always-enabled registry types. The registry types in this list are always
|
||||
* enabled, because generic CodeQL workflow components may use them rather than just
|
||||
* language-specific components.
|
||||
*/
|
||||
export const ALWAYS_ENABLED_REGISTRY_TYPE = [
|
||||
"git_source",
|
||||
"docker_registry",
|
||||
] as const;
|
||||
|
||||
type RegistryMapping = Partial<Record<BuiltInLanguage, string[]>>;
|
||||
|
||||
const LANGUAGE_TO_REGISTRY_TYPE: Required<RegistryMapping> = {
|
||||
export const LANGUAGE_TO_REGISTRY_TYPE: Required<RegistryMapping> = {
|
||||
actions: [],
|
||||
cpp: [],
|
||||
java: ["maven_repository"],
|
||||
@@ -233,9 +243,11 @@ function getRegistryAddress(
|
||||
}
|
||||
}
|
||||
|
||||
// getCredentials returns registry credentials from action inputs.
|
||||
// It prefers `registries_credentials` over `registry_secrets`.
|
||||
// If neither is set, it returns an empty array.
|
||||
/**
|
||||
* Returns registry credentials from action inputs.
|
||||
* It prefers `registriesCredentials` over `registrySecrets`.
|
||||
* If neither is set, it returns an empty array.
|
||||
*/
|
||||
export function getCredentials(
|
||||
logger: Logger,
|
||||
registrySecrets: string | undefined,
|
||||
@@ -291,8 +303,11 @@ export function getCredentials(
|
||||
const address = getRegistryAddress(e);
|
||||
|
||||
// Filter credentials based on language if specified. `type` is the registry type.
|
||||
// E.g., "maven_feed" for Java/Kotlin, "nuget_repository" for C#.
|
||||
// E.g., "maven_repository" for Java/Kotlin, "nuget_feed" for C#.
|
||||
// We always allow types in `ALWAYS_ENABLED_REGISTRY_TYPE` since they can be used by
|
||||
// other parts of the workflow.
|
||||
if (
|
||||
!ALWAYS_ENABLED_REGISTRY_TYPE.some((t) => t === e.type) &&
|
||||
registryTypeForLanguage &&
|
||||
!registryTypeForLanguage.some((t) => t === e.type)
|
||||
) {
|
||||
|
||||
@@ -12,7 +12,7 @@ export type RawCredential = UnvalidatedObject<Credential>;
|
||||
/** A schema for credential objects with a username. */
|
||||
export const usernameSchema = {
|
||||
/** The username needed to authenticate to the package registry, if any. */
|
||||
username: json.optional(json.string),
|
||||
username: json.optionalOrNull(json.string),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Usernames may be present for both authentication with tokens or passwords. */
|
||||
@@ -29,7 +29,7 @@ export function hasUsername(config: AuthConfig): config is Username {
|
||||
/** A schema for credential objects with a username and password. */
|
||||
export const usernamePasswordSchema = {
|
||||
/** The password needed to authenticate to the package registry, if any. */
|
||||
password: json.optional(json.string),
|
||||
password: json.optionalOrNull(json.string),
|
||||
...usernameSchema,
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
@@ -52,7 +52,7 @@ export function hasUsernameAndPassword(
|
||||
/** A schema for credential objects for token-based authentication. */
|
||||
export const tokenSchema = {
|
||||
/** The token needed to authenticate to the package registry, if any. */
|
||||
token: json.optional(json.string),
|
||||
token: json.optionalOrNull(json.string),
|
||||
...usernameSchema,
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
@@ -100,7 +100,7 @@ export const awsConfigSchema = {
|
||||
"role-name": json.string,
|
||||
domain: json.string,
|
||||
"domain-owner": json.string,
|
||||
audience: json.optional(json.string),
|
||||
audience: json.optionalOrNull(json.string),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Configuration for AWS OIDC. */
|
||||
@@ -116,8 +116,8 @@ export function isAWSConfig(
|
||||
/** A schema for JFrog OIDC configurations. */
|
||||
export const jfrogConfigSchema = {
|
||||
"jfrog-oidc-provider-name": json.string,
|
||||
audience: json.optional(json.string),
|
||||
"identity-mapping-name": json.optional(json.string),
|
||||
audience: json.optionalOrNull(json.string),
|
||||
"identity-mapping-name": json.optionalOrNull(json.string),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Configuration for JFrog OIDC. */
|
||||
@@ -150,8 +150,8 @@ export function isCloudsmithConfig(
|
||||
/** A schema for GCP OIDC configurations. */
|
||||
export const gcpConfigSchema = {
|
||||
"workload-identity-provider": json.string,
|
||||
"service-account": json.optional(json.string),
|
||||
audience: json.optional(json.string),
|
||||
"service-account": json.optionalOrNull(json.string),
|
||||
audience: json.optionalOrNull(json.string),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Configuration for GCP OIDC. */
|
||||
|
||||
+17
-5
@@ -12,15 +12,16 @@ import {
|
||||
isSelfHostedRunner,
|
||||
} from "./actions-util";
|
||||
import { getAnalysisKey, getApiClient } from "./api-client";
|
||||
import { parseRegistriesWithoutCredentials, type Config } from "./config-utils";
|
||||
import { DependencyCacheRestoreStatusReport } from "./dependency-caching";
|
||||
import type { Config } from "./config/action-config";
|
||||
import { parseRegistriesWithoutCredentials } from "./config/pack-registries";
|
||||
import type { DependencyCacheRestoreStatusReport } from "./dependency-caching";
|
||||
import { DocUrl } from "./doc-url";
|
||||
import { EnvVar } from "./environment";
|
||||
import { getRef } from "./git-utils";
|
||||
import { Logger } from "./logging";
|
||||
import { OverlayBaseDatabaseDownloadStats } from "./overlay/caching";
|
||||
import type { Logger } from "./logging";
|
||||
import type { OverlayBaseDatabaseDownloadStats } from "./overlay/caching";
|
||||
import { getRepositoryNwo } from "./repository";
|
||||
import { ToolsSource } from "./setup-codeql";
|
||||
import type { ToolsSource } from "./setup-codeql";
|
||||
import {
|
||||
ConfigurationError,
|
||||
getRequiredEnvParam,
|
||||
@@ -46,6 +47,17 @@ export enum ActionName {
|
||||
UploadSarif = "upload-sarif",
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an `ActionName` to its display name. Usually that is the same, except
|
||||
* for `ActionName.Analyze` where it is `"analyze"` instead of `"finish"`.
|
||||
*/
|
||||
export function getDisplayActionName(actionName: ActionName): string {
|
||||
if (actionName === ActionName.Analyze) {
|
||||
return "analyze";
|
||||
}
|
||||
return actionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a boolean indicating whether the analysis is considered to be first party.
|
||||
*
|
||||
|
||||
+342
-7
@@ -3,6 +3,8 @@ import path from "path";
|
||||
|
||||
import * as github from "@actions/github";
|
||||
import test, {
|
||||
type ThrownError,
|
||||
type ThrowsExpectation,
|
||||
type ExecutionContext,
|
||||
type MacroDeclarationOptions,
|
||||
type TestFn,
|
||||
@@ -10,6 +12,7 @@ import test, {
|
||||
import nock from "nock";
|
||||
import * as sinon from "sinon";
|
||||
|
||||
import { ActionState, StateFeature } from "./action-common";
|
||||
import { ActionsEnv, getActionVersion } from "./actions-util";
|
||||
import { AnalysisKind } from "./analyses";
|
||||
import * as apiClient from "./api-client";
|
||||
@@ -18,6 +21,7 @@ import { CachingKind } from "./caching-utils";
|
||||
import * as codeql from "./codeql";
|
||||
import { Config } from "./config-utils";
|
||||
import * as defaults from "./defaults.json";
|
||||
import { Env, ActionsEnvVars } from "./environment";
|
||||
import {
|
||||
CodeQLDefaultVersionInfo,
|
||||
Feature,
|
||||
@@ -26,9 +30,11 @@ import {
|
||||
} from "./feature-flags";
|
||||
import { Logger } from "./logging";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
import { ActionName } from "./status-report";
|
||||
import {
|
||||
DEFAULT_DEBUG_ARTIFACT_NAME,
|
||||
DEFAULT_DEBUG_DATABASE_NAME,
|
||||
getEnv,
|
||||
GitHubVariant,
|
||||
GitHubVersion,
|
||||
HTTPError,
|
||||
@@ -172,6 +178,10 @@ export function makeMacro<Args extends unknown[]>(
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
export function getTestEnv(testEnv: NodeJS.ProcessEnv = {}): Env {
|
||||
return getEnv(testEnv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an `ActionsEnv` instance for use in tests.
|
||||
*/
|
||||
@@ -181,6 +191,297 @@ export function getTestActionsEnv(): ActionsEnv {
|
||||
};
|
||||
}
|
||||
|
||||
/** For testing purposes, we make all available state features accessible in `TestEnv`. */
|
||||
type AllState = [
|
||||
"Base",
|
||||
"Logger",
|
||||
"Env",
|
||||
"ReadOnlyEnv",
|
||||
"Actions",
|
||||
"Api",
|
||||
"FeatureFlags",
|
||||
];
|
||||
|
||||
/** Initialise a fresh `ActionState<AllState>` value. */
|
||||
export function initAllState(
|
||||
overrides?: Partial<ActionState<AllState>>,
|
||||
): ActionState<AllState> {
|
||||
return {
|
||||
name: ActionName.Init,
|
||||
startedAt: new Date(),
|
||||
logger: new RecordingLogger(),
|
||||
env: getTestEnv(),
|
||||
actions: getTestActionsEnv(),
|
||||
apiClient: github.getOctokit("123"),
|
||||
features: createFeatures([]),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
type DelayedCheck<
|
||||
Args extends readonly any[],
|
||||
R,
|
||||
Fs extends ReadonlyArray<AllState[number]>,
|
||||
> = (env: Readonly<BaseEnvBuilder<Args, R, Fs>>) => Promise<any>;
|
||||
|
||||
export type ValueOrMutation<T> = T | ((val: T) => void);
|
||||
|
||||
/**
|
||||
* Wraps a function that accepts an `ActionState` for testing in different environments.
|
||||
*/
|
||||
abstract class BaseEnvBuilder<
|
||||
Args extends readonly any[],
|
||||
R,
|
||||
Fs extends ReadonlyArray<AllState[number]>,
|
||||
> {
|
||||
protected readonly fn: (state: ActionState<Fs>, ...args: Args) => R;
|
||||
private logger: RecordingLogger;
|
||||
protected state: ActionState<AllState>;
|
||||
protected checks: Array<DelayedCheck<Args, R, Fs>>;
|
||||
|
||||
constructor(
|
||||
fn: (state: ActionState<Fs>, ...args: Args) => R,
|
||||
cloneFrom?: BaseEnvBuilder<Args, R, Fs>,
|
||||
) {
|
||||
this.fn = fn;
|
||||
this.logger = new RecordingLogger();
|
||||
this.state =
|
||||
cloneFrom !== undefined
|
||||
? ({
|
||||
...cloneFrom.state,
|
||||
env: cloneFrom.state.env.clone(),
|
||||
actions: Object.create(cloneFrom.state.actions),
|
||||
logger: this.logger,
|
||||
} satisfies ActionState<AllState>)
|
||||
: initAllState({ logger: this.logger });
|
||||
this.checks = [...(cloneFrom?.checks ?? [])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a clone of this object. Used internally.
|
||||
* Must be overridden by subclasses.
|
||||
*/
|
||||
protected abstract clone(): this;
|
||||
|
||||
public getLogger(): RecordingLogger {
|
||||
return this.logger;
|
||||
}
|
||||
|
||||
public getState(): ActionState<AllState> {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public withArgs(...args: Args): CallableEnvBuilder<Args, R, Fs> {
|
||||
const result = new CallableEnvBuilder(this.fn, args, this.clone());
|
||||
return result;
|
||||
}
|
||||
|
||||
public withFeatures(enabled: Feature[]): this {
|
||||
const result = this.clone();
|
||||
result.state.features = createFeatures(enabled);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets environment variables that are always available to GitHub Actions,
|
||||
* excluding some that are expected to be set to paths.
|
||||
*
|
||||
* @param overrides Overrides for the defaults.
|
||||
*/
|
||||
public withDefaultActionsEnv(overrides?: ActionVarOverrides): this {
|
||||
const result = this.clone();
|
||||
setupBaseActionsVars(overrides, result.state.env);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets environment variables that are always available to GitHub Actions.
|
||||
* @param tempDir A value for `RUNNER_TEMP` and `GITHUB_WORKSPACE`.
|
||||
* @param toolsDir A value for `RUNNER_TOOL_CACHE`.
|
||||
* @param overrides Overrides for the defaults.
|
||||
*/
|
||||
public withActionsEnv(
|
||||
tempDir: string,
|
||||
toolsDir: string,
|
||||
overrides?: ActionVarOverrides,
|
||||
): this {
|
||||
const result = this.clone();
|
||||
setupActionsVars(tempDir, toolsDir, overrides, result.state.env);
|
||||
return result;
|
||||
}
|
||||
|
||||
public withEnv(arg: ValueOrMutation<Env>): this {
|
||||
const result = this.clone();
|
||||
if (typeof arg === "function") {
|
||||
arg(result.state.env);
|
||||
} else {
|
||||
result.state.env = arg;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public withActions(arg: ValueOrMutation<ActionsEnv>): this {
|
||||
const result = this.clone();
|
||||
if (typeof arg === "function") {
|
||||
arg(result.state.actions);
|
||||
} else {
|
||||
result.state.actions = arg;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a delayed check that `messages` are logged. The check will be
|
||||
* performed after the main assertion passes.
|
||||
*/
|
||||
public logs(t: ExecutionContext<unknown>, ...messages: string[]): this {
|
||||
const result = this.clone();
|
||||
result.checks.push(async (env) => {
|
||||
checkExpectedLogMessages(t, env.getLogger().messages, messages);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a delayed check that `messages` are not logged. The check will be
|
||||
* performed after the main assertion passes.
|
||||
*/
|
||||
public notLogs(t: ExecutionContext<unknown>, ...messages: string[]): this {
|
||||
const result = this.clone();
|
||||
result.checks.push(async (env) => {
|
||||
checkUnexpectedLogMessages(t, env.getLogger().messages, messages);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class EnvBuilder<
|
||||
Args extends readonly any[],
|
||||
R,
|
||||
Fs extends ReadonlyArray<AllState[number]>,
|
||||
> extends BaseEnvBuilder<Args, R, Fs> {
|
||||
protected clone(): this {
|
||||
return new EnvBuilder(this.fn, this) as this;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PassedAssertion<R, T> {
|
||||
result: Awaited<R>;
|
||||
assertionResult: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* A more minimal, exported interface for `CallableEnvBuilder`. This makes it easier to
|
||||
* define helper functions in tests which expect a value of a compatible type.
|
||||
*/
|
||||
export interface AssertableTarget<R> {
|
||||
passes<AArgs extends readonly any[], AResult>(
|
||||
assertion: (val: Awaited<R>, ...assertionArgs: AArgs) => AResult,
|
||||
...assertionArgs: AArgs
|
||||
): Promise<PassedAssertion<R, AResult>>;
|
||||
|
||||
throws<ErrorType extends ErrorConstructor | Error>(
|
||||
t: ExecutionContext<unknown>,
|
||||
expectations?: ThrowsExpectation<ErrorType>,
|
||||
): Promise<ThrownError<ErrorType>>;
|
||||
}
|
||||
|
||||
class CallableEnvBuilder<
|
||||
Args extends readonly any[],
|
||||
R,
|
||||
Fs extends ReadonlyArray<AllState[number]>,
|
||||
>
|
||||
extends BaseEnvBuilder<Args, R, Fs>
|
||||
implements AssertableTarget<R>
|
||||
{
|
||||
private args: Args;
|
||||
|
||||
constructor(
|
||||
fn: (state: ActionState<Fs>, ...args: Args) => R,
|
||||
args: Args,
|
||||
cloneFrom?: BaseEnvBuilder<Args, R, Fs>,
|
||||
) {
|
||||
super(fn, cloneFrom);
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
protected clone(): this {
|
||||
return new CallableEnvBuilder(this.fn, this.args, this) as this;
|
||||
}
|
||||
|
||||
public getArgs(): Args {
|
||||
return this.args;
|
||||
}
|
||||
|
||||
call(): R {
|
||||
return this.fn(this.state as unknown as ActionState<Fs>, ...this.args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the underlying function in the configured environment and passes
|
||||
* the result to `assertion` along with extra `assertionArgs`.
|
||||
*
|
||||
* @param assertion The assertion to apply to the result.
|
||||
* @param assertionArgs Extra arguments for the assertion.
|
||||
* @returns The result of the assertion.
|
||||
*/
|
||||
public async passes<AArgs extends readonly any[], AResult>(
|
||||
assertion: (val: Awaited<R>, ...assertionArgs: AArgs) => AResult,
|
||||
...assertionArgs: AArgs
|
||||
): Promise<PassedAssertion<R, AResult>> {
|
||||
// this.call() may or may not return a promise,
|
||||
// `Promise.resolve` turns the result into one if it isn't already,
|
||||
// and we then await it. That ensures that `result` is an `Awaited<R>`.
|
||||
const result = await Promise.resolve(this.call());
|
||||
|
||||
// Run the main assertion on the `result`.
|
||||
const assertionResult = await assertion(result, ...assertionArgs);
|
||||
|
||||
// Run other delayed checks.
|
||||
for (const delayedCheck of this.checks) {
|
||||
await delayedCheck(this);
|
||||
}
|
||||
|
||||
// Return the results of the function call and the main assertion.
|
||||
return { result, assertionResult };
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that calling the underlying function should throw an exception.
|
||||
*
|
||||
* @param t The execution context for the assertion.
|
||||
* @param expectations Expectations for the error.
|
||||
* @returns The error that was thrown.
|
||||
*/
|
||||
public async throws<ErrorType extends ErrorConstructor | Error>(
|
||||
t: ExecutionContext<unknown>,
|
||||
expectations?: ThrowsExpectation<ErrorType>,
|
||||
): Promise<ThrownError<ErrorType>> {
|
||||
// Run the main assertion.
|
||||
const error = await t.throwsAsync(
|
||||
async () => Promise.resolve(this.call()),
|
||||
expectations,
|
||||
);
|
||||
|
||||
// Run other delayed checks.
|
||||
for (const delayedCheck of this.checks) {
|
||||
await delayedCheck(this);
|
||||
}
|
||||
|
||||
// Return the error.
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
/** Utility function to construct a `TestEnv`. */
|
||||
export function callee<
|
||||
Args extends readonly any[],
|
||||
R,
|
||||
Fs extends readonly StateFeature[],
|
||||
>(fn: (state: ActionState<Fs>, ...args: Args) => R): EnvBuilder<Args, R, Fs> {
|
||||
return new EnvBuilder(fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default values for environment variables typically set in an Actions
|
||||
* environment. Tests can override individual variables by passing them in the
|
||||
@@ -200,7 +501,7 @@ export const DEFAULT_ACTIONS_VARS = {
|
||||
GITHUB_WORKFLOW: "test-workflow",
|
||||
RUNNER_NAME: "my-runner",
|
||||
RUNNER_OS: "Linux",
|
||||
} as const satisfies Record<string, string>;
|
||||
} as const satisfies Partial<Record<ActionsEnvVars, string>>;
|
||||
|
||||
/** Partial mappings from GitHub Actions environment variables to values. */
|
||||
export type ActionVarOverrides = Partial<
|
||||
@@ -212,11 +513,15 @@ export type ActionVarOverrides = Partial<
|
||||
* excluding some that are expected to be set to paths. See `setupActionsVars`.
|
||||
*
|
||||
* @param overrides Overrides for the defaults.
|
||||
* @param env The environment to set the variables for.
|
||||
*/
|
||||
export function setupBaseActionsVars(overrides?: ActionVarOverrides) {
|
||||
export function setupBaseActionsVars(
|
||||
overrides?: ActionVarOverrides,
|
||||
env: Env = getEnv(),
|
||||
) {
|
||||
const vars = { ...DEFAULT_ACTIONS_VARS, ...overrides };
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
process.env[key] = value;
|
||||
env.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,16 +531,18 @@ export function setupBaseActionsVars(overrides?: ActionVarOverrides) {
|
||||
* @param tempDir A value for `RUNNER_TEMP` and `GITHUB_WORKSPACE`.
|
||||
* @param toolsDir A value for `RUNNER_TOOL_CACHE`.
|
||||
* @param overrides Overrides for the defaults.
|
||||
* @param env The environment to set the variables for.
|
||||
*/
|
||||
export function setupActionsVars(
|
||||
tempDir: string,
|
||||
toolsDir: string,
|
||||
overrides?: ActionVarOverrides,
|
||||
env: Env = getEnv(),
|
||||
) {
|
||||
setupBaseActionsVars(overrides);
|
||||
process.env["RUNNER_TEMP"] = tempDir;
|
||||
process.env["RUNNER_TOOL_CACHE"] = toolsDir;
|
||||
process.env["GITHUB_WORKSPACE"] = tempDir;
|
||||
setupBaseActionsVars(overrides, env);
|
||||
env.set(ActionsEnvVars.RUNNER_TEMP, tempDir);
|
||||
env.set(ActionsEnvVars.RUNNER_TOOL_CACHE, toolsDir);
|
||||
env.set(ActionsEnvVars.GITHUB_WORKSPACE, tempDir);
|
||||
}
|
||||
|
||||
type LogLevel = "debug" | "info" | "warning" | "error";
|
||||
@@ -369,6 +676,34 @@ export function checkExpectedLogMessages(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that `messages` contains none of `unexpectedMessages`.
|
||||
*/
|
||||
export function checkUnexpectedLogMessages(
|
||||
t: ExecutionContext<any>,
|
||||
messages: LoggedMessage[],
|
||||
unexpectedMessages: string[],
|
||||
) {
|
||||
const presentMessages: string[] = [];
|
||||
|
||||
for (const unexpectedMessage of unexpectedMessages) {
|
||||
if (hasLoggedMessage(messages, unexpectedMessage)) {
|
||||
presentMessages.push(unexpectedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (presentMessages.length > 0) {
|
||||
const listify = (lines: string[]) =>
|
||||
lines.map((m) => ` - '${m}'`).join("\n");
|
||||
|
||||
t.fail(
|
||||
`Did not expect\n\n${listify(presentMessages)}\n\nin the logger output, but found them in:\n\n${messages.map((m) => ` - '${m.message}'`).join("\n")}`,
|
||||
);
|
||||
} else {
|
||||
t.pass();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `message` should not have been logged to `logger`.
|
||||
*/
|
||||
|
||||
+10
-22
@@ -1,17 +1,17 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { getActionVersion, getTemporaryDirectory } from "./actions-util";
|
||||
import * as analyses from "./analyses";
|
||||
import { getGitHubVersion } from "./api-client";
|
||||
import { initFeatures } from "./feature-flags";
|
||||
import { Logger, getActionsLogger } from "./logging";
|
||||
import { Logger } from "./logging";
|
||||
import { getRepositoryNwo } from "./repository";
|
||||
import { InvalidSarifUploadError } from "./sarif";
|
||||
import {
|
||||
createStatusReportBase,
|
||||
sendStatusReport,
|
||||
sendUnhandledErrorStatusReport,
|
||||
StatusReportBase,
|
||||
getActionsStatus,
|
||||
ActionName,
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
ConfigurationError,
|
||||
checkActionVersion,
|
||||
checkDiskUsage,
|
||||
getErrorMessage,
|
||||
initializeEnvironment,
|
||||
shouldSkipSarifUpload,
|
||||
wrapError,
|
||||
@@ -55,12 +54,9 @@ async function sendSuccessStatusReport(
|
||||
}
|
||||
}
|
||||
|
||||
async function run(startedAt: Date) {
|
||||
async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
const logger = getActionsLogger();
|
||||
|
||||
try {
|
||||
initializeEnvironment(getActionVersion());
|
||||
|
||||
@@ -165,20 +161,12 @@ async function run(startedAt: Date) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Defines the `upload-sarif` Action. */
|
||||
const uploadSarif: Action = {
|
||||
name: ActionName.UploadSarif,
|
||||
run,
|
||||
};
|
||||
|
||||
export async function runWrapper() {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
try {
|
||||
await run(startedAt);
|
||||
} catch (error) {
|
||||
core.setFailed(
|
||||
`codeql/upload-sarif action failed: ${getErrorMessage(error)}`,
|
||||
);
|
||||
await sendUnhandledErrorStatusReport(
|
||||
ActionName.UploadSarif,
|
||||
startedAt,
|
||||
error,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
await runInActions(uploadSarif);
|
||||
}
|
||||
+4
-23
@@ -13,11 +13,14 @@ import * as apiCompatibility from "./api-compatibility.json";
|
||||
import type { CodeQL, VersionInfo } from "./codeql";
|
||||
import type { Pack } from "./config/db-config";
|
||||
import type { Config } from "./config-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import { EnvVar, getRequiredEnvParam } from "./environment";
|
||||
import * as json from "./json";
|
||||
import { Language } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
|
||||
// Re-export for backwards compatibility to avoid updating a lot of imports elsewhere.
|
||||
export { getRequiredEnvParam, getOptionalEnvVar, getEnv } from "./environment";
|
||||
|
||||
/**
|
||||
* The name of the file containing the base database OIDs, as stored in the
|
||||
* root of the database location.
|
||||
@@ -566,28 +569,6 @@ export function initializeEnvironment(version: string) {
|
||||
core.exportVariable(EnvVar.VERSION, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an environment parameter, but throw an error if it is not set.
|
||||
*/
|
||||
export function getRequiredEnvParam(paramName: string): string {
|
||||
const value = process.env[paramName];
|
||||
if (value === undefined || value.length === 0) {
|
||||
throw new Error(`${paramName} environment variable must be set`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an environment variable, but return `undefined` if it is not set or empty.
|
||||
*/
|
||||
export function getOptionalEnvVar(paramName: string): string | undefined {
|
||||
const value = process.env[paramName];
|
||||
if (value?.trim().length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export class HTTPError extends Error {
|
||||
public status: number;
|
||||
|
||||
|
||||
Reference in new issue
Block a user