mirror of
https://github.com/github/codeql-action.git
synced 2026-10-10 12:51:40 +00:00
Compare commits
26 Commits
mbg/use-co
...
codeql-bun
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18420e3271 | ||
|
|
7e8d8970f0 | ||
|
|
2d4c474c2c | ||
|
|
98c05a17d3 | ||
|
|
8289a49271 | ||
|
|
2a8731cc06 | ||
|
|
3434fbbc53 | ||
|
|
3013ac07bd | ||
|
|
74b15aa2c6 | ||
|
|
f00f809405 | ||
|
|
0953dc00da | ||
|
|
cbad145443 | ||
|
|
ab44eb939d | ||
|
|
961b583f9a | ||
|
|
d71461774b | ||
|
|
027ac05d3b | ||
|
|
66a6f42f0a | ||
|
|
c5d621238d | ||
|
|
adba0868a4 | ||
|
|
916098aa8d | ||
|
|
093dce6cc2 | ||
|
|
57eb44123f | ||
|
|
5901394530 | ||
|
|
b69467ce8b | ||
|
|
85d157095f | ||
|
|
2d14f71964 |
11
.github/actions/release-initialise/action.yml
vendored
11
.github/actions/release-initialise/action.yml
vendored
@@ -25,17 +25,6 @@ runs:
|
||||
shell: bash
|
||||
run: npm ci
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install PyGithub==2.3.0 requests
|
||||
shell: bash
|
||||
|
||||
- name: Update git config
|
||||
run: |
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
1
.github/workflows/codeql.yml
vendored
1
.github/workflows/codeql.yml
vendored
@@ -113,7 +113,6 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
- language: python
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
8
.github/workflows/post-release-mergeback.yml
vendored
8
.github/workflows/post-release-mergeback.yml
vendored
@@ -51,9 +51,9 @@ jobs:
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install JavaScript dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Update git config
|
||||
run: |
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
env:
|
||||
PARTIAL_CHANGELOG: "${{ runner.temp }}/partial_changelog.md"
|
||||
run: |
|
||||
python .github/workflows/script/prepare_changelog.py CHANGELOG.md > $PARTIAL_CHANGELOG
|
||||
npx tsx pr-checks/prepare-changelog.ts --output="$PARTIAL_CHANGELOG"
|
||||
|
||||
echo "::group::Partial CHANGELOG"
|
||||
cat $PARTIAL_CHANGELOG
|
||||
|
||||
6
.github/workflows/rollback-release.yml
vendored
6
.github/workflows/rollback-release.yml
vendored
@@ -93,7 +93,7 @@ jobs:
|
||||
LATEST_TAG: ${{ needs.prepare.outputs.latest_tag }}
|
||||
VERSION: "${{ needs.prepare.outputs.version }}"
|
||||
run: |
|
||||
python .github/workflows/script/rollback_changelog.py \
|
||||
npx tsx pr-checks/rollback-changelog.ts \
|
||||
--target-version "${ROLLBACK_TAG:1}" \
|
||||
--rollback-version "${LATEST_TAG:1}" \
|
||||
--new-version "$VERSION" > $NEW_CHANGELOG
|
||||
@@ -128,7 +128,9 @@ jobs:
|
||||
NEW_CHANGELOG: "${{ runner.temp }}/new_changelog.md"
|
||||
PARTIAL_CHANGELOG: "${{ runner.temp }}/partial_changelog.md"
|
||||
run: |
|
||||
python .github/workflows/script/prepare_changelog.py $NEW_CHANGELOG > $PARTIAL_CHANGELOG
|
||||
npx tsx pr-checks/prepare-changelog.ts \
|
||||
--changelog="$NEW_CHANGELOG" \
|
||||
--output="$PARTIAL_CHANGELOG"
|
||||
|
||||
echo "::group::Partial CHANGELOG"
|
||||
cat $PARTIAL_CHANGELOG
|
||||
|
||||
23
.github/workflows/script/bundle_changelog.py
vendored
23
.github/workflows/script/bundle_changelog.py
vendored
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import re
|
||||
|
||||
cli_version = os.environ['CLI_VERSION']
|
||||
|
||||
# The GitHub Release for the new bundle version.
|
||||
bundle_release_url = f"https://github.com/github/codeql-action/releases/tag/codeql-bundle-v{cli_version}"
|
||||
# Get the PR number from the PR URL.
|
||||
pr_number = os.environ['PR_URL'].split('/')[-1]
|
||||
changelog_note = f"- Update default CodeQL bundle version to [{cli_version}]({bundle_release_url}). [#{pr_number}]({os.environ['PR_URL']})"
|
||||
|
||||
# If the "[UNRELEASED]" section starts with "no user facing changes", remove that line.
|
||||
with open('CHANGELOG.md', 'r') as f:
|
||||
changelog = f.read()
|
||||
|
||||
changelog = changelog.replace('## [UNRELEASED]\n\nNo user facing changes.', '## [UNRELEASED]\n')
|
||||
|
||||
# Add the changelog note to the bottom of the "[UNRELEASED]" section.
|
||||
changelog = re.sub(r'\n## (\d+\.\d+\.\d+)', f'{changelog_note}\n\n## \\1', changelog, count=1)
|
||||
|
||||
with open('CHANGELOG.md', 'w') as f:
|
||||
f.write(changelog)
|
||||
35
.github/workflows/script/prepare_changelog.py
vendored
35
.github/workflows/script/prepare_changelog.py
vendored
@@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
EMPTY_CHANGELOG = 'No changes.\n\n'
|
||||
|
||||
# Prepare the changelog for the new release
|
||||
# This function will extract the part of the changelog that
|
||||
# we want to include in the new release.
|
||||
def extract_changelog_snippet(changelog_file):
|
||||
output = ''
|
||||
if (not os.path.exists(changelog_file)):
|
||||
output = EMPTY_CHANGELOG
|
||||
|
||||
else:
|
||||
with open(changelog_file, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Include only the contents of the first section
|
||||
found_first_section = False
|
||||
for line in lines:
|
||||
if line.startswith('## '):
|
||||
if found_first_section:
|
||||
break
|
||||
found_first_section = True
|
||||
elif found_first_section:
|
||||
output += line
|
||||
|
||||
return output.strip()
|
||||
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
raise Exception('Expecting argument: changelog_file')
|
||||
changelog_file = sys.argv[1]
|
||||
print(extract_changelog_snippet(changelog_file))
|
||||
62
.github/workflows/script/rollback_changelog.py
vendored
62
.github/workflows/script/rollback_changelog.py
vendored
@@ -1,62 +0,0 @@
|
||||
import datetime
|
||||
import os
|
||||
import argparse
|
||||
|
||||
EMPTY_CHANGELOG = """# CodeQL Action Changelog
|
||||
|
||||
"""
|
||||
|
||||
def get_today_string():
|
||||
today = datetime.datetime.today()
|
||||
return '{:%d %b %Y}'.format(today)
|
||||
|
||||
# Include everything up to and after the first heading,
|
||||
# but not the first heading and body.
|
||||
def drop_unreleased_section(lines: list[str]):
|
||||
before_first_section = ''
|
||||
after_first_section = ''
|
||||
found_first_section = False
|
||||
skipped_first_section = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith('## ') and not found_first_section:
|
||||
found_first_section = True
|
||||
elif line.startswith('## ') and found_first_section:
|
||||
skipped_first_section = True
|
||||
|
||||
if not found_first_section:
|
||||
before_first_section += line
|
||||
if skipped_first_section:
|
||||
after_first_section += line
|
||||
|
||||
return (before_first_section, after_first_section)
|
||||
|
||||
def update_changelog(target_version, rollback_version, new_version):
|
||||
before_first_section = EMPTY_CHANGELOG
|
||||
after_first_section = ''
|
||||
|
||||
if (os.path.exists('CHANGELOG.md')):
|
||||
with open('CHANGELOG.md', 'r') as f:
|
||||
(before_first_section, after_first_section) = drop_unreleased_section(f.readlines())
|
||||
|
||||
newHeader = f'## {new_version} - {get_today_string()}\n'
|
||||
|
||||
print(before_first_section, end="")
|
||||
print(newHeader)
|
||||
print(f"This release rolls back {rollback_version} due to issues with that release. It is identical to {target_version}.\n")
|
||||
print(after_first_section)
|
||||
|
||||
# We expect three version strings as input:
|
||||
#
|
||||
# - target_version: the version that we are re-releasing as `new_version`
|
||||
# - rollback_version: the version that we are rolling back, typically the one that followed `target_version`
|
||||
# - new_version: the new version that we are releasing `target_version` as, typically the one that follows `rollback_version`
|
||||
#
|
||||
# Example: python3 .github/workflows/script/rollback_changelog.py --target-version "1.2.3" --rollback-version "1.2.4" --new-version "1.2.5"
|
||||
parser = argparse.ArgumentParser(description="Update CHANGELOG.md for a rollback release.")
|
||||
parser.add_argument("--target-version", "-t", required=True, help="Version to re-release as new_version.")
|
||||
parser.add_argument("--rollback-version", "-r", required=True, help="Version being rolled back.")
|
||||
parser.add_argument("--new-version", "-n", required=True, help="New version to publish for target_version.")
|
||||
args = parser.parse_args()
|
||||
|
||||
update_changelog(args.target_version, args.rollback_version, args.new_version)
|
||||
7
.github/workflows/update-bundle.yml
vendored
7
.github/workflows/update-bundle.yml
vendored
@@ -40,11 +40,6 @@ jobs:
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
@@ -120,7 +115,7 @@ jobs:
|
||||
|
||||
- name: Create changelog note
|
||||
run: |
|
||||
python .github/workflows/script/bundle_changelog.py
|
||||
npx tsx pr-checks/bundle-changelog.ts
|
||||
|
||||
- name: Push changelog note
|
||||
run: |
|
||||
|
||||
@@ -22,11 +22,6 @@ jobs:
|
||||
pull-requests: write # needed to create pull request
|
||||
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Checkout CodeQL Action
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
|
||||
181
lib/entry-points.js
generated
181
lib/entry-points.js
generated
@@ -146550,7 +146550,7 @@ async function sendStatusReport(statusReport) {
|
||||
);
|
||||
}
|
||||
}
|
||||
async function createInitWithConfigStatusReport(config, initStatusReport, configFileInput, totalCacheSize, overlayBaseDatabaseStats, dependencyCachingResults) {
|
||||
async function createInitWithConfigStatusReport(config, initStatusReport, configFile, totalCacheSize, overlayBaseDatabaseStats, dependencyCachingResults) {
|
||||
const languages = config.languages.join(",");
|
||||
const paths = (config.originalUserInput.paths || []).join(",");
|
||||
const pathsIgnore = (config.originalUserInput["paths-ignore"] || []).join(
|
||||
@@ -146576,7 +146576,7 @@ async function createInitWithConfigStatusReport(config, initStatusReport, config
|
||||
}
|
||||
return {
|
||||
...initStatusReport,
|
||||
config_file: configFileInput?.value ?? "",
|
||||
config_file: configFile ?? "",
|
||||
disable_default_queries: disableDefaultQueries,
|
||||
paths,
|
||||
paths_ignore: pathsIgnore,
|
||||
@@ -146794,11 +146794,6 @@ var featureConfig = {
|
||||
envVar: "CODEQL_ACTION_ALLOW_MULTIPLE_ANALYSIS_KINDS",
|
||||
minimumVersion: void 0
|
||||
},
|
||||
["allow_toolcache_input" /* AllowToolcacheInput */]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_ALLOW_TOOLCACHE_INPUT",
|
||||
minimumVersion: void 0
|
||||
},
|
||||
["cleanup_trap_caches" /* CleanupTrapCaches */]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_CLEANUP_TRAP_CACHES",
|
||||
@@ -147996,17 +147991,13 @@ var stringProperty = {
|
||||
validate: isString2,
|
||||
parse: parseStringRepositoryProperty
|
||||
};
|
||||
var nonEmptyStringProperty = {
|
||||
...stringProperty,
|
||||
parse: parseNonEmptyStringRepositoryProperty
|
||||
};
|
||||
var booleanProperty = {
|
||||
// The value from the API should come as a string, which we then parse into a boolean.
|
||||
validate: isString2,
|
||||
parse: parseBooleanRepositoryProperty
|
||||
};
|
||||
var repositoryPropertyParsers = {
|
||||
["github-codeql-config-file" /* CONFIG_FILE */]: nonEmptyStringProperty,
|
||||
["github-codeql-config-file" /* CONFIG_FILE */]: stringProperty,
|
||||
["github-codeql-disable-overlay" /* DISABLE_OVERLAY */]: booleanProperty,
|
||||
["github-codeql-extra-queries" /* EXTRA_QUERIES */]: stringProperty,
|
||||
["github-codeql-file-coverage-on-prs" /* FILE_COVERAGE_ON_PRS */]: booleanProperty,
|
||||
@@ -148084,12 +148075,6 @@ function parseBooleanRepositoryProperty(name, value, logger) {
|
||||
function parseStringRepositoryProperty(_name, value) {
|
||||
return value;
|
||||
}
|
||||
function parseNonEmptyStringRepositoryProperty(_name, value) {
|
||||
if (value.trim().length === 0) {
|
||||
return void 0;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
var KNOWN_REPOSITORY_PROPERTY_NAMES = new Set(
|
||||
Object.values(RepositoryPropertyName)
|
||||
);
|
||||
@@ -148433,50 +148418,6 @@ function parseUserConfig(logger, pathInput, contents, validateConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
// src/config/inputs.ts
|
||||
async function getComputedInput(action, repositoryProperties, name, options) {
|
||||
const input = action.actions.getOptionalInput(name);
|
||||
const propertyValue = repositoryProperties[options.repositoryPropertyName];
|
||||
if (options.repositoryPropertyFeatureEnabled && options.allowForcedRepositoryPropertyValue && propertyValue?.startsWith("!")) {
|
||||
action.logger.info(
|
||||
`Using ${name} input from repository property (enforced): ${propertyValue}`
|
||||
);
|
||||
return {
|
||||
// Drop the '!' from the value.
|
||||
value: propertyValue.substring(1),
|
||||
source: "repository-property" /* RepositoryProperty */
|
||||
};
|
||||
}
|
||||
if (input !== void 0) {
|
||||
action.logger.info(`Using ${name} input from workflow: ${input}`);
|
||||
return { value: input, source: "workflow" /* Workflow */ };
|
||||
}
|
||||
if (options.repositoryPropertyFeatureEnabled && propertyValue !== void 0) {
|
||||
action.logger.info(
|
||||
`Using ${name} input from repository property: ${propertyValue}`
|
||||
);
|
||||
return {
|
||||
value: propertyValue,
|
||||
source: "repository-property" /* RepositoryProperty */
|
||||
};
|
||||
} else if (propertyValue !== void 0) {
|
||||
action.logger.info(
|
||||
`Ignoring ${name} input from repository property, because the corresponding feature flag is disabled.`
|
||||
);
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
async function getToolsInput(action, repositoryProperties) {
|
||||
const allowRepositoryProperty = await action.features.getValue(
|
||||
"tools_repository_property" /* ToolsRepositoryProperty */
|
||||
);
|
||||
return getComputedInput(action, repositoryProperties, "tools" /* Tools */, {
|
||||
repositoryPropertyFeatureEnabled: allowRepositoryProperty,
|
||||
allowForcedRepositoryPropertyValue: true,
|
||||
repositoryPropertyName: "github-codeql-tools" /* TOOLS */
|
||||
});
|
||||
}
|
||||
|
||||
// src/config/remote-file.ts
|
||||
var DEFAULT_CONFIG_FILE_NAME = ".github/codeql-action.yaml";
|
||||
var DEFAULT_CONFIG_FILE_REF = "main";
|
||||
@@ -148550,15 +148491,38 @@ async function parseRemoteFileAddress(actionState, configFile) {
|
||||
// src/config/file.ts
|
||||
var LOCAL_PATH_PREFIX = "./";
|
||||
var REMOTE_PATH_PREFIX = "remote=";
|
||||
async function getConfigFileInput(action, repositoryProperties) {
|
||||
const useRepositoryProperty = await action.features.getValue(
|
||||
"config_file_repository_property" /* ConfigFileRepositoryProperty */
|
||||
);
|
||||
return getComputedInput(action, repositoryProperties, "config-file" /* ConfigFile */, {
|
||||
repositoryPropertyFeatureEnabled: useRepositoryProperty,
|
||||
allowForcedRepositoryPropertyValue: false,
|
||||
repositoryPropertyName: "github-codeql-config-file" /* CONFIG_FILE */
|
||||
});
|
||||
async function getConfigFileInput({
|
||||
logger,
|
||||
actions,
|
||||
features
|
||||
}, repositoryProperties, analysisKinds) {
|
||||
const input = actions.getOptionalInput("config-file");
|
||||
if (input !== void 0) {
|
||||
logger.info(`Using configuration file input from workflow: ${input}`);
|
||||
return input;
|
||||
}
|
||||
const propertyValue = repositoryProperties["github-codeql-config-file" /* CONFIG_FILE */];
|
||||
const analysisKindSupported = analysisKinds === void 0 || analysisKinds.includes("code-scanning" /* CodeScanning */) && analysisKinds.length === 1;
|
||||
if (propertyValue !== void 0 && propertyValue.trim().length > 0) {
|
||||
const useRepositoryProperty = await features.getValue(
|
||||
"config_file_repository_property" /* ConfigFileRepositoryProperty */
|
||||
);
|
||||
if (analysisKindSupported && useRepositoryProperty) {
|
||||
logger.info(
|
||||
`Using configuration file input from repository property: ${propertyValue}`
|
||||
);
|
||||
return propertyValue;
|
||||
} else if (!analysisKindSupported) {
|
||||
logger.info(
|
||||
"Ignoring configuration file input from repository property, because it is unsupported for the current analysis kind."
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled."
|
||||
);
|
||||
}
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
async function getRemoteConfig(actionState, configFile, apiDetails) {
|
||||
const address = await parseRemoteFileAddress(actionState, configFile);
|
||||
@@ -151016,10 +150980,7 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO
|
||||
}
|
||||
} else if (toolsInput !== void 0 && toolsInput === CODEQL_TOOLCACHE_INPUT) {
|
||||
let latestToolcacheVersion;
|
||||
const allowToolcacheValueFF = await features.getValue(
|
||||
"allow_toolcache_input" /* AllowToolcacheInput */
|
||||
);
|
||||
const allowToolcacheValue = allowToolcacheValueFF && (isDynamicWorkflow() || isInTestMode());
|
||||
const allowToolcacheValue = isDynamicWorkflow() || isInTestMode();
|
||||
if (allowToolcacheValue) {
|
||||
logger.info(
|
||||
`Attempting to use the latest CodeQL CLI version in the toolcache, as requested by 'tools: ${toolsInput}'.`
|
||||
@@ -151035,15 +150996,9 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO
|
||||
`Found no CodeQL CLI in the toolcache, ignoring 'tools: ${toolsInput}'...`
|
||||
);
|
||||
} else {
|
||||
if (allowToolcacheValueFF) {
|
||||
logger.warning(
|
||||
`Ignoring 'tools: ${toolsInput}' because the workflow was not triggered dynamically.`
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Ignoring 'tools: ${toolsInput}' because the feature is not enabled.`
|
||||
);
|
||||
}
|
||||
logger.warning(
|
||||
`Ignoring 'tools: ${toolsInput}' because the workflow was not triggered dynamically.`
|
||||
);
|
||||
}
|
||||
const version = await resolveDefaultCliVersion(
|
||||
defaultCliVersion,
|
||||
@@ -160383,6 +160338,40 @@ var core21 = __toESM(require_core());
|
||||
var io7 = __toESM(require_io());
|
||||
var semver10 = __toESM(require_semver2());
|
||||
|
||||
// src/config/inputs.ts
|
||||
async function getToolsInput(action, repositoryProperties) {
|
||||
const name = "tools" /* Tools */;
|
||||
const input = action.actions.getOptionalInput(name);
|
||||
const propertyValue = repositoryProperties["github-codeql-tools" /* TOOLS */];
|
||||
const allowRepositoryProperty = await action.features.getValue(
|
||||
"tools_repository_property" /* ToolsRepositoryProperty */
|
||||
);
|
||||
if (allowRepositoryProperty && propertyValue?.startsWith("!")) {
|
||||
action.logger.info(
|
||||
`Using ${name} input from repository property (enforced): ${propertyValue}`
|
||||
);
|
||||
return {
|
||||
// Drop the '!' from the value.
|
||||
value: propertyValue.substring(1),
|
||||
source: "repository-property" /* RepositoryProperty */
|
||||
};
|
||||
}
|
||||
if (input !== void 0) {
|
||||
action.logger.info(`Using ${name} input from workflow: ${input}`);
|
||||
return { value: input, source: "workflow" /* Workflow */ };
|
||||
}
|
||||
if (allowRepositoryProperty && propertyValue !== void 0) {
|
||||
action.logger.info(
|
||||
`Using ${name} input from repository property: ${propertyValue}`
|
||||
);
|
||||
return {
|
||||
value: propertyValue,
|
||||
source: "repository-property" /* RepositoryProperty */
|
||||
};
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
|
||||
// src/workflow.ts
|
||||
var fs27 = __toESM(require("fs"));
|
||||
var path23 = __toESM(require("path"));
|
||||
@@ -160673,7 +160662,7 @@ async function sendStartingStatusReport(startedAt, config, logger) {
|
||||
await sendStatusReport(statusReportBase);
|
||||
}
|
||||
}
|
||||
async function sendCompletedStatusReport2(startedAt, config, configFileInput, toolsInput, toolsDownloadStatusReport, toolsFeatureFlagsValid, toolsSource, toolsVersion, overlayBaseDatabaseStats, dependencyCachingResults, logger, error3) {
|
||||
async function sendCompletedStatusReport2(startedAt, config, configFile, toolsInput, toolsDownloadStatusReport, toolsFeatureFlagsValid, toolsSource, toolsVersion, overlayBaseDatabaseStats, dependencyCachingResults, logger, error3) {
|
||||
const statusReportBase = await createStatusReportBase(
|
||||
"init" /* Init */,
|
||||
getActionsStatus(error3),
|
||||
@@ -160698,9 +160687,6 @@ async function sendCompletedStatusReport2(startedAt, config, configFileInput, to
|
||||
if (toolsInput !== void 0) {
|
||||
initStatusReport.computed_inputs.tools = toolsInput;
|
||||
}
|
||||
if (configFileInput !== void 0) {
|
||||
initStatusReport.computed_inputs["config-file"] = configFileInput;
|
||||
}
|
||||
const initToolsDownloadFields = {};
|
||||
if (toolsDownloadStatusReport?.downloadDurationMs !== void 0) {
|
||||
initToolsDownloadFields.tools_download_duration_ms = toolsDownloadStatusReport.downloadDurationMs;
|
||||
@@ -160712,7 +160698,7 @@ async function sendCompletedStatusReport2(startedAt, config, configFileInput, to
|
||||
const initWithConfigStatusReport = await createInitWithConfigStatusReport(
|
||||
config,
|
||||
initStatusReport,
|
||||
configFileInput,
|
||||
configFile,
|
||||
Math.round(
|
||||
await getTotalCacheSize(Object.values(config.trapCaches), logger)
|
||||
),
|
||||
@@ -160732,7 +160718,7 @@ async function run3(actionState) {
|
||||
const logger = actionState.logger;
|
||||
let apiDetails;
|
||||
let config;
|
||||
let configFileInput;
|
||||
let configFile;
|
||||
let codeql;
|
||||
let features;
|
||||
let sourceRoot;
|
||||
@@ -160769,11 +160755,6 @@ async function run3(actionState) {
|
||||
logger.info(`Job run UUID is ${jobRunUuid}.`);
|
||||
core21.exportVariable("JOB_RUN_UUID" /* JOB_RUN_UUID */, jobRunUuid);
|
||||
core21.exportVariable("CODEQL_ACTION_INIT_HAS_RUN" /* INIT_ACTION_HAS_RUN */, "true");
|
||||
const actionStateWithFeatures = { ...actionState, features };
|
||||
configFileInput = await getConfigFileInput(
|
||||
actionStateWithFeatures,
|
||||
repositoryProperties
|
||||
);
|
||||
sourceRoot = path24.resolve(
|
||||
getRequiredEnvParam("GITHUB_WORKSPACE"),
|
||||
getOptionalInput("source-root") || ""
|
||||
@@ -160786,6 +160767,12 @@ async function run3(actionState) {
|
||||
`Failed to parse analysis kinds for 'starting' status report: ${getErrorMessage(err)}`
|
||||
);
|
||||
}
|
||||
const actionStateWithFeatures = { ...actionState, features };
|
||||
configFile = await getConfigFileInput(
|
||||
actionStateWithFeatures,
|
||||
repositoryProperties,
|
||||
analysisKinds
|
||||
);
|
||||
await sendStartingStatusReport(startedAt, { analysisKinds }, logger);
|
||||
if (process.env["CODEQL_ACTION_SETUP_CODEQL_HAS_RUN" /* SETUP_CODEQL_ACTION_HAS_RUN */] === "true") {
|
||||
throw new ConfigurationError(
|
||||
@@ -160851,7 +160838,7 @@ async function run3(actionState) {
|
||||
packsInput: getOptionalInput("packs"),
|
||||
buildModeInput: getOptionalInput("build-mode"),
|
||||
ramInput: getOptionalInput("ram"),
|
||||
configFile: configFileInput?.value,
|
||||
configFile,
|
||||
dbLocation: getOptionalInput("db-location"),
|
||||
configInput: getOptionalInput("config"),
|
||||
dependencyCachingEnabled: getDependencyCachingEnabled(),
|
||||
@@ -161138,7 +161125,7 @@ exec ${goBinaryPath} "$@"`
|
||||
await sendCompletedStatusReport2(
|
||||
startedAt,
|
||||
config,
|
||||
configFileInput,
|
||||
configFile,
|
||||
toolsInput,
|
||||
toolsDownloadStatusReport,
|
||||
toolsFeatureFlagsValid,
|
||||
|
||||
142
pr-checks/bundle-changelog.test.ts
Normal file
142
pr-checks/bundle-changelog.test.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Tests for `bundle-changelog.ts`.
|
||||
*/
|
||||
|
||||
import * as assert from "node:assert/strict";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterEach, beforeEach, describe, it } from "node:test";
|
||||
|
||||
import {
|
||||
CLI_VERSION_ENV_VAR,
|
||||
getCLIVersion,
|
||||
getPRNumber,
|
||||
getPRUrl,
|
||||
PR_URL_ENV_VAR,
|
||||
updateChangelog,
|
||||
} from "./bundle-changelog";
|
||||
import {
|
||||
EMPTY_CHANGELOG,
|
||||
NO_CHANGES_STR,
|
||||
UNRELEASED_PLACEHOLDER,
|
||||
} from "./changelog";
|
||||
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
// Set up a temporary directory for testing
|
||||
testDir = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-changelog-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
/** Clean up temporary directories. */
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("getCLIVersion", async () => {
|
||||
await it("throws if the environment variable is not set", async () => {
|
||||
delete process.env[CLI_VERSION_ENV_VAR];
|
||||
assert.throws(() => getCLIVersion());
|
||||
});
|
||||
|
||||
await it("throws if the environment variable is empty", async () => {
|
||||
process.env[CLI_VERSION_ENV_VAR] = " ";
|
||||
assert.throws(() => getCLIVersion());
|
||||
});
|
||||
|
||||
await it("returns value of the environment variable if set", async () => {
|
||||
const testValue = "1.2.3";
|
||||
process.env[CLI_VERSION_ENV_VAR] = testValue;
|
||||
assert.deepEqual(getCLIVersion(), testValue);
|
||||
});
|
||||
});
|
||||
|
||||
const testPrUrl = "https://github.com/github/codeql-action/pulls/42";
|
||||
|
||||
describe("getPRUrl", async () => {
|
||||
await it("throws if the environment variable is not set", async () => {
|
||||
delete process.env[PR_URL_ENV_VAR];
|
||||
assert.throws(() => getPRUrl());
|
||||
});
|
||||
|
||||
await it("throws if the environment variable is empty", async () => {
|
||||
process.env[PR_URL_ENV_VAR] = " ";
|
||||
assert.throws(() => getPRUrl());
|
||||
});
|
||||
|
||||
await it("returns value of the environment variable if set", async () => {
|
||||
process.env[PR_URL_ENV_VAR] = testPrUrl;
|
||||
assert.deepEqual(getPRUrl(), testPrUrl);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPRNumber", async () => {
|
||||
await it("throws if the last part of the input is not a number", async () => {
|
||||
assert.throws(() => getPRNumber(`${testPrUrl}/foo`));
|
||||
});
|
||||
|
||||
await it("throws if the last part of the input is not a positive number", async () => {
|
||||
assert.throws(() => getPRNumber(`${testPrUrl}/-100`));
|
||||
});
|
||||
|
||||
await it("returns the PR number from an URL", async () => {
|
||||
assert.equal(getPRNumber(testPrUrl), 42);
|
||||
});
|
||||
});
|
||||
|
||||
const testChangelog = `${EMPTY_CHANGELOG.trimEnd()}
|
||||
|
||||
## 4.23.7
|
||||
|
||||
- Other change
|
||||
|
||||
## 4.23.6
|
||||
|
||||
${NO_CHANGES_STR}`;
|
||||
|
||||
const expectedChangelog = `# CodeQL Action Changelog
|
||||
|
||||
## ${UNRELEASED_PLACEHOLDER}
|
||||
|
||||
- Update default CodeQL bundle version to
|
||||
|
||||
## 4.23.7
|
||||
|
||||
- Other change
|
||||
|
||||
## 4.23.6
|
||||
|
||||
${NO_CHANGES_STR}`;
|
||||
|
||||
describe("updateChangelog", async () => {
|
||||
await it("removes `NO_CHANGES_STR` if present in [UNRELEASED] section", async () => {
|
||||
const result = updateChangelog(EMPTY_CHANGELOG, "");
|
||||
assert.ok(!result.includes(NO_CHANGES_STR.trim()));
|
||||
});
|
||||
|
||||
await it("doesn't remove `NO_CHANGES_STR` if present in versioned section", async () => {
|
||||
const result = updateChangelog(
|
||||
EMPTY_CHANGELOG.replace(UNRELEASED_PLACEHOLDER, "1.2.3"),
|
||||
"",
|
||||
);
|
||||
assert.ok(result.includes(NO_CHANGES_STR.trim()));
|
||||
});
|
||||
|
||||
await it("throws if there are no sections", async () => {
|
||||
assert.throws(() => {
|
||||
updateChangelog(
|
||||
"# CodeQL Action Changelog",
|
||||
"- Update default CodeQL bundle version to",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await it("adds note at the end of the first section", async () => {
|
||||
const result = updateChangelog(
|
||||
testChangelog,
|
||||
"- Update default CodeQL bundle version to",
|
||||
);
|
||||
assert.deepEqual(result, expectedChangelog);
|
||||
});
|
||||
});
|
||||
127
pr-checks/bundle-changelog.ts
Executable file
127
pr-checks/bundle-changelog.ts
Executable file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Updates the changelog with a change note for an updated CodeQL CLI bundle.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
|
||||
import {
|
||||
parseChangelog,
|
||||
renderChangelog,
|
||||
UNRELEASED_PLACEHOLDER,
|
||||
} from "./changelog";
|
||||
import { CHANGELOG_FILE, CLI_BUNDLE_RELEASE_URL_PREFIX } from "./config";
|
||||
import { getErrorMessage } from "./util";
|
||||
|
||||
export const CLI_VERSION_ENV_VAR = "CLI_VERSION";
|
||||
export const PR_URL_ENV_VAR = "PR_URL";
|
||||
|
||||
/** Gets the CLI version from the environment. */
|
||||
export function getCLIVersion() {
|
||||
const cliVersion = process.env[CLI_VERSION_ENV_VAR];
|
||||
|
||||
if (cliVersion === undefined || cliVersion.trim() === "") {
|
||||
throw new Error(`No CLI version was set in '${CLI_VERSION_ENV_VAR}'.`);
|
||||
}
|
||||
|
||||
return cliVersion;
|
||||
}
|
||||
|
||||
/** Gets the PR URL from the environment. */
|
||||
export function getPRUrl() {
|
||||
const prUrl = process.env[PR_URL_ENV_VAR];
|
||||
|
||||
if (prUrl === undefined || prUrl.trim() === "") {
|
||||
throw new Error(`No PR URL was set in '${PR_URL_ENV_VAR}'.`);
|
||||
}
|
||||
|
||||
return prUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the PR number from something like a PR URL.
|
||||
*/
|
||||
export function getPRNumber(prUrl: string) {
|
||||
const prUrlParts = prUrl.split("/");
|
||||
const prNumberStr = prUrlParts[prUrlParts.length - 1];
|
||||
|
||||
const prNumber = Number.parseInt(prNumberStr, 10);
|
||||
|
||||
if (!Number.isInteger(prNumber) || prNumber <= 0) {
|
||||
throw new Error(
|
||||
`Invalid PR URL '${prUrl}': last part is not a positive number`,
|
||||
);
|
||||
}
|
||||
|
||||
return prNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates `changelog` by adding `changelogNote` to the first section.
|
||||
*
|
||||
* @param contents The existing changelog contents.
|
||||
* @param changelogNote The note to add to the first section.
|
||||
*/
|
||||
export function updateChangelog(contents: string, changelogNote: string) {
|
||||
// If the "[UNRELEASED]" section starts with "no user facing changes", remove that line.
|
||||
contents = contents.replace(
|
||||
`## ${UNRELEASED_PLACEHOLDER}\n\nNo user facing changes.`,
|
||||
`## ${UNRELEASED_PLACEHOLDER}\n`,
|
||||
);
|
||||
|
||||
const changelog = parseChangelog(contents);
|
||||
|
||||
if (changelog.sections.length === 0) {
|
||||
throw new Error("The changelog contains no existing sections.");
|
||||
}
|
||||
|
||||
// Add the changelog note to the bottom of the first section.
|
||||
const firstSection = changelog.sections[0];
|
||||
const lastLine = firstSection.bodyLines.pop();
|
||||
|
||||
if (lastLine !== undefined && lastLine.trim() !== "") {
|
||||
// We expect the last line to be empty. If it isn't for some reason,
|
||||
// add it back.
|
||||
firstSection.bodyLines.push(lastLine);
|
||||
}
|
||||
|
||||
firstSection.bodyLines.push(changelogNote);
|
||||
|
||||
// If the last line is empty as expected, then add it back in after the new note.
|
||||
if (lastLine?.trim() === "") {
|
||||
firstSection.bodyLines.push(lastLine);
|
||||
}
|
||||
|
||||
return renderChangelog(changelog);
|
||||
}
|
||||
|
||||
function main() {
|
||||
try {
|
||||
const cliVersion = getCLIVersion();
|
||||
const prUrl = getPRUrl();
|
||||
|
||||
// The GitHub Release for the new bundle version.
|
||||
const bundleReleaseUrl = `${CLI_BUNDLE_RELEASE_URL_PREFIX}${cliVersion}`;
|
||||
|
||||
// Get the PR number from the PR URL.
|
||||
const prNumber = getPRNumber(prUrl);
|
||||
const changelogNote = `- Update default CodeQL bundle version to [${cliVersion}](${bundleReleaseUrl}). [#${prNumber}](${prUrl})`;
|
||||
|
||||
let changelog = fs.readFileSync(CHANGELOG_FILE, "utf-8");
|
||||
|
||||
changelog = updateChangelog(changelog, changelogNote);
|
||||
|
||||
fs.writeFileSync(CHANGELOG_FILE, changelog);
|
||||
|
||||
return 0;
|
||||
} catch (err) {
|
||||
console.error(`Failed to bundle changelog: ${getErrorMessage(err)}`);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Only call `main` if this script was run directly.
|
||||
if (require.main === module) {
|
||||
process.exit(main());
|
||||
}
|
||||
@@ -5,14 +5,18 @@
|
||||
*/
|
||||
|
||||
import * as assert from "node:assert/strict";
|
||||
import * as fs from "node:fs";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import {
|
||||
EMPTY_CHANGELOG,
|
||||
getReleaseDateString,
|
||||
parseChangelog,
|
||||
processChangelogForBackports,
|
||||
renderChangelog,
|
||||
setVersionAndDate,
|
||||
} from "./changelog";
|
||||
import { CHANGELOG_FILE } from "./config";
|
||||
|
||||
const testDate = new Date(2026, 7, 14);
|
||||
|
||||
@@ -37,6 +41,14 @@ describe("setVersionAndDate", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseChangelog + renderChangelog", async () => {
|
||||
await it("renderChangelog(parseChangelog(c)) == c", async () => {
|
||||
const actualChangelog = fs.readFileSync(CHANGELOG_FILE, "utf-8");
|
||||
const roundtrip = renderChangelog(parseChangelog(actualChangelog));
|
||||
assert.deepEqual(roundtrip.split("\n"), actualChangelog.split("\n"));
|
||||
});
|
||||
});
|
||||
|
||||
const testChangelog = `# CodeQL Action Changelog
|
||||
|
||||
## 4.12.3 - 14 Aug 2026
|
||||
|
||||
@@ -2,14 +2,34 @@ import * as fs from "node:fs";
|
||||
|
||||
import { CHANGELOG_FILE, DryRunOption } from "./config";
|
||||
|
||||
/** The placeholder in the header for unreleased changes. */
|
||||
export const UNRELEASED_PLACEHOLDER = "[UNRELEASED]";
|
||||
|
||||
/** The default contents for a section in the changelog. */
|
||||
export const NO_CHANGES_STR = "No user facing changes.\n\n";
|
||||
|
||||
/** Placeholder changelog content for a new release. */
|
||||
export const EMPTY_CHANGELOG = `# CodeQL Action Changelog
|
||||
|
||||
## [UNRELEASED]
|
||||
## ${UNRELEASED_PLACEHOLDER}
|
||||
|
||||
No user facing changes.
|
||||
${NO_CHANGES_STR}`;
|
||||
|
||||
`;
|
||||
/**
|
||||
* Represents sections in a changelog.
|
||||
*/
|
||||
export interface ChangelogSection {
|
||||
headerLine: string;
|
||||
bodyLines: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a changelog.
|
||||
*/
|
||||
export interface Changelog {
|
||||
preamble: string[];
|
||||
sections: ChangelogSection[];
|
||||
}
|
||||
|
||||
/** Returns `date` formatted as `DD Mon YYYY`. */
|
||||
export function getReleaseDateString(today: Date = new Date()): string {
|
||||
@@ -53,7 +73,76 @@ export function setVersionAndDate(
|
||||
date: Date = new Date(),
|
||||
): string {
|
||||
const versionAndDate = `${version} - ${getReleaseDateString(date)}`;
|
||||
return content.replace("[UNRELEASED]", versionAndDate);
|
||||
return content.replace(UNRELEASED_PLACEHOLDER, versionAndDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses `content` into a structured representation of a changelog.
|
||||
*
|
||||
* @param content The contents of the changelog file.
|
||||
*/
|
||||
export function parseChangelog(content: string): Changelog {
|
||||
const lines = content.split("\n");
|
||||
let i = 0;
|
||||
|
||||
const preamble: string[] = [];
|
||||
const sections: ChangelogSection[] = [];
|
||||
let currentSection: ChangelogSection | undefined = undefined;
|
||||
|
||||
// Process all lines of the input file.
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
|
||||
// Sections of the changelog start with `## `.
|
||||
if (line.startsWith("## ")) {
|
||||
// We have discovered a new section. If `currentSection` is already defined,
|
||||
// then this marks the end of that section. Push it to the array of sections
|
||||
// in the changelog.
|
||||
if (currentSection !== undefined) {
|
||||
sections.push(currentSection);
|
||||
}
|
||||
|
||||
// Initialise the new section.
|
||||
currentSection = { headerLine: line, bodyLines: [] };
|
||||
} else if (currentSection !== undefined) {
|
||||
// Add lines between the section header and the next to the current section.
|
||||
currentSection.bodyLines.push(line);
|
||||
} else {
|
||||
// This is neither a section header nor are we in a section already,
|
||||
// so this line is part of the preamble.
|
||||
preamble.push(line);
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
// Push the current section to the array of completed sections, if there is
|
||||
// still one unfinished.
|
||||
if (currentSection !== undefined) {
|
||||
sections.push(currentSection);
|
||||
}
|
||||
|
||||
return { preamble, sections };
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines an array of lines into a single string by adding line breaks.
|
||||
*/
|
||||
export function unlines(lines: string[]): string {
|
||||
return `${lines.join("\n")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a given changelog to a string.
|
||||
*/
|
||||
export function renderChangelog(changelog: Changelog): string {
|
||||
let result = unlines(changelog.preamble);
|
||||
|
||||
for (const section of changelog.sections) {
|
||||
result += `\n${section.headerLine}\n${unlines(section.bodyLines)}`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,70 +155,58 @@ export function processChangelogForBackports(
|
||||
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;
|
||||
// Parse the changelog.
|
||||
const changelog = parseChangelog(content);
|
||||
|
||||
// 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) {
|
||||
if (changelog.sections.length === 0) {
|
||||
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";
|
||||
// Filter out changelog entries that only apply to newer versions and
|
||||
// update the section headings with the backport major version for
|
||||
// sections we keep.
|
||||
for (const section of changelog.sections) {
|
||||
// Update the section headings with the backport major version.
|
||||
section.headerLine = section.headerLine.replace(
|
||||
`## ${sourceBranchMajorVersion}`,
|
||||
`## ${targetBranchMajorVersion}`,
|
||||
);
|
||||
|
||||
while (i < lines.length) {
|
||||
let line = lines[i];
|
||||
i++;
|
||||
const filteredEntries: string[] = [];
|
||||
let foundContent = false;
|
||||
|
||||
// Filter out changelog entries that only apply to newer versions.
|
||||
const match = someVersionsOnlyRegex.exec(line);
|
||||
if (match) {
|
||||
for (const line of section.bodyLines) {
|
||||
// Skip the entry if `someVersionsOnlyRegex` matches and the major version
|
||||
// of the target branch is smaller than the required version.
|
||||
const match = someVersionsOnlyRegex.exec(line);
|
||||
if (
|
||||
match &&
|
||||
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 {
|
||||
// Keep the line.
|
||||
filteredEntries.push(line);
|
||||
|
||||
// Set `foundContent` to `true` if the line is not empty.
|
||||
if (line.trim() !== "") {
|
||||
foundContent = true;
|
||||
output += `${line}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the section with the retained entries.
|
||||
section.bodyLines = filteredEntries;
|
||||
|
||||
// Add an entry if we didn't keep any.
|
||||
if (!foundContent) {
|
||||
section.bodyLines.push(NO_CHANGES_STR.trim());
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
return renderChangelog(changelog);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ export const API_COMPATIBILITY_FILE = path.join(
|
||||
"api-compatibility.json",
|
||||
);
|
||||
|
||||
/** The prefix of CodeQL CLI bundle release URLs. */
|
||||
export const CLI_BUNDLE_RELEASE_URL_PREFIX =
|
||||
"https://github.com/github/codeql-action/releases/tag/codeql-bundle-v";
|
||||
|
||||
/** A common interface for operations that support dry runs. */
|
||||
export interface DryRunOption {
|
||||
/** A value indicating whether to perform operations with side effects. */
|
||||
|
||||
54
pr-checks/prepare-changelog.test.ts
Normal file
54
pr-checks/prepare-changelog.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Tests for `prepare-changelog.ts`.
|
||||
*/
|
||||
|
||||
import * as assert from "node:assert/strict";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterEach, beforeEach, describe, it } from "node:test";
|
||||
|
||||
import { EMPTY_CHANGELOG, NO_CHANGES_STR } from "./changelog";
|
||||
import { extractChangelogSnippet } from "./prepare-changelog";
|
||||
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
// Set up a temporary directory for testing
|
||||
testDir = fs.mkdtempSync(path.join(os.tmpdir(), "prepare-changelog-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
/** Clean up temporary directories. */
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const testBody = `- Test change`;
|
||||
const testChangelog = `${EMPTY_CHANGELOG.replace(NO_CHANGES_STR, testBody)}
|
||||
|
||||
## Another section
|
||||
|
||||
- Other change`;
|
||||
|
||||
describe("extractChangelogSnippet", async () => {
|
||||
await it("returns the default body if the input doesn't exist", async () => {
|
||||
const result = extractChangelogSnippet(path.join(testDir, "not-here.md"));
|
||||
assert.deepEqual(result, NO_CHANGES_STR);
|
||||
});
|
||||
|
||||
await it("returns the first section if the input exists", async () => {
|
||||
const changelogPath = path.join(testDir, "test-readme.md");
|
||||
fs.writeFileSync(changelogPath, testChangelog);
|
||||
|
||||
const result = extractChangelogSnippet(changelogPath);
|
||||
assert.deepEqual(result, testBody);
|
||||
});
|
||||
|
||||
await it("returns an empty string if there is no first section", async () => {
|
||||
const changelogPath = path.join(testDir, "test-readme.md");
|
||||
fs.writeFileSync(changelogPath, "# CodeQL Action Changelog\n");
|
||||
|
||||
const result = extractChangelogSnippet(changelogPath);
|
||||
assert.deepEqual(result, "");
|
||||
});
|
||||
});
|
||||
82
pr-checks/prepare-changelog.ts
Executable file
82
pr-checks/prepare-changelog.ts
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Extracts the body of the first changelog section and outputs it to either
|
||||
* stdout or a file.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import { NO_CHANGES_STR, parseChangelog } from "./changelog";
|
||||
import { CHANGELOG_FILE } from "./config";
|
||||
import { getErrorMessage } from "./util";
|
||||
|
||||
/**
|
||||
* Prepare the changelog for the new release
|
||||
* This function will extract the part of the changelog that
|
||||
* we want to include in the new release.
|
||||
*
|
||||
* @param changelogPath The path to the changelog file.
|
||||
*/
|
||||
export function extractChangelogSnippet(changelogPath: string) {
|
||||
try {
|
||||
const content = fs.readFileSync(changelogPath, "utf-8");
|
||||
const changelog = parseChangelog(content);
|
||||
|
||||
// Return an empty string if we couldn't find the first section.
|
||||
if (changelog.sections.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return changelog.sections[0].bodyLines.join("\n").trim();
|
||||
} catch (err) {
|
||||
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
||||
console.error(`Changelog file at '${changelogPath}' does not exist.`);
|
||||
return NO_CHANGES_STR;
|
||||
} else {
|
||||
throw Error(
|
||||
`Failed to open changelog file at '${changelogPath}': ${getErrorMessage(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
try {
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
changelog: {
|
||||
type: "string",
|
||||
short: "f",
|
||||
default: CHANGELOG_FILE,
|
||||
},
|
||||
output: {
|
||||
type: "string",
|
||||
short: "o",
|
||||
},
|
||||
},
|
||||
strict: true,
|
||||
});
|
||||
|
||||
const body = extractChangelogSnippet(values.changelog);
|
||||
|
||||
// If no `output` argument was provided, output to stdout. Otherwise,
|
||||
// write a file to the specified path.
|
||||
if (values.output === undefined) {
|
||||
console.info(body);
|
||||
} else {
|
||||
fs.writeFileSync(values.output, body);
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (err) {
|
||||
console.error(`Failed to prepare changelog: ${getErrorMessage(err)}`);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Only call `main` if this script was run directly.
|
||||
if (require.main === module) {
|
||||
process.exit(main());
|
||||
}
|
||||
45
pr-checks/rollback-changelog.test.ts
Normal file
45
pr-checks/rollback-changelog.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Tests for `rollback-changelog.ts`.
|
||||
*/
|
||||
|
||||
import * as assert from "node:assert/strict";
|
||||
import * as fs from "node:fs";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { getReleaseDateString, parseChangelog } from "./changelog";
|
||||
import { CHANGELOG_FILE } from "./config";
|
||||
import { updateChangelog } from "./rollback-changelog";
|
||||
|
||||
describe("updateChangelog", async () => {
|
||||
await it("replaces the first section with one for the rollback release", async () => {
|
||||
const actualChangelog = parseChangelog(
|
||||
fs.readFileSync(CHANGELOG_FILE, "utf-8"),
|
||||
);
|
||||
const existingFirstSection = actualChangelog.sections[0];
|
||||
|
||||
const today = new Date();
|
||||
updateChangelog(actualChangelog, {
|
||||
"new-version": "Test.1.3",
|
||||
"rollback-version": "Test.1.2",
|
||||
"target-version": "Test.1.1",
|
||||
today,
|
||||
});
|
||||
|
||||
// Check that the old, first section is gone.
|
||||
for (const section of actualChangelog.sections) {
|
||||
assert.notDeepEqual(section, existingFirstSection);
|
||||
}
|
||||
|
||||
// Check that the new, first section matches our expectations.
|
||||
const newFirstSection = actualChangelog.sections[0];
|
||||
assert.deepEqual(
|
||||
newFirstSection.headerLine,
|
||||
`## Test.1.3 - ${getReleaseDateString(today)}`,
|
||||
);
|
||||
assert.equal(newFirstSection.bodyLines.length, 3);
|
||||
assert.deepEqual(
|
||||
newFirstSection.bodyLines[1],
|
||||
`This release rolls back Test.1.2 due to issues with that release. It is identical to Test.1.1.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
84
pr-checks/rollback-changelog.ts
Executable file
84
pr-checks/rollback-changelog.ts
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Replaces the current, first section of the changelog with a new one for the rollback release.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import {
|
||||
Changelog,
|
||||
ChangelogSection,
|
||||
getReleaseDateString,
|
||||
parseChangelog,
|
||||
renderChangelog,
|
||||
} from "./changelog";
|
||||
import { CHANGELOG_FILE } from "./config";
|
||||
import { getErrorMessage } from "./util";
|
||||
|
||||
export interface RollbackChangelogInputs {
|
||||
"target-version": string;
|
||||
"rollback-version": string;
|
||||
"new-version": string;
|
||||
today?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the current, first section of the changelog with a new one for the rollback release.
|
||||
*/
|
||||
export function updateChangelog(
|
||||
changelog: Changelog,
|
||||
versions: RollbackChangelogInputs,
|
||||
) {
|
||||
// Drop the existing first section.
|
||||
changelog.sections.shift();
|
||||
|
||||
// Construct the section for the rollback version.
|
||||
const newSection: ChangelogSection = {
|
||||
headerLine: `## ${versions["new-version"]} - ${getReleaseDateString(versions.today)}`,
|
||||
bodyLines: [
|
||||
"",
|
||||
`This release rolls back ${versions["rollback-version"]} due to issues with that release. It is identical to ${versions["target-version"]}.`,
|
||||
"",
|
||||
],
|
||||
};
|
||||
|
||||
// Add the new section at the top of the changelog.
|
||||
changelog.sections.unshift(newSection);
|
||||
}
|
||||
|
||||
function main() {
|
||||
try {
|
||||
const options = {
|
||||
"target-version": { type: "string", short: "t" },
|
||||
"rollback-version": { type: "string", short: "r" },
|
||||
"new-version": { type: "string", short: "n" },
|
||||
} as const;
|
||||
|
||||
const { values } = parseArgs({ options, strict: true });
|
||||
|
||||
for (const key of Object.keys(options)) {
|
||||
const val = values[key as keyof typeof values];
|
||||
if (val === undefined || val.trim() === "") {
|
||||
throw new Error(`Argument '--${key}' is required.`);
|
||||
}
|
||||
}
|
||||
|
||||
const changelog = parseChangelog(fs.readFileSync(CHANGELOG_FILE, "utf-8"));
|
||||
updateChangelog(changelog, values as RollbackChangelogInputs);
|
||||
console.info(renderChangelog(changelog));
|
||||
|
||||
return 0;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Failed to prepare rollback changelog: ${getErrorMessage(err)}`,
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Only call `main` if this script was run directly.
|
||||
if (require.main === module) {
|
||||
process.exit(main());
|
||||
}
|
||||
9
pr-checks/util.ts
Normal file
9
pr-checks/util.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Returns an appropriate message for the error.
|
||||
*
|
||||
* If the error is an `Error` instance, this returns the error message without
|
||||
* an `Error: ` prefix.
|
||||
*/
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import * as github from "@actions/github";
|
||||
import test from "ava";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { AnalysisKind } from "../analyses";
|
||||
import * as api from "../api-client";
|
||||
import { RegistryProxyVars } from "../environment";
|
||||
import { Feature } from "../feature-flags";
|
||||
@@ -13,13 +14,12 @@ import {
|
||||
} from "../testing-utils";
|
||||
|
||||
import { getConfigFileInput, getRemoteConfig } from "./file";
|
||||
import { InputSource } from "./inputs";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test("getConfigFileInput returns undefined by default", async (t) => {
|
||||
await callee(getConfigFileInput)
|
||||
.withArgs({})
|
||||
.withArgs({}, undefined)
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.passes(t.is, undefined);
|
||||
});
|
||||
@@ -41,32 +41,69 @@ test("getConfigFileInput returns input value", async (t) => {
|
||||
.withArgs("config-file")
|
||||
.returns(testInput);
|
||||
})
|
||||
.withArgs(repositoryProperties)
|
||||
.logs(t, "Using config-file input from workflow")
|
||||
.passes(t.deepEqual, { value: testInput, source: InputSource.Workflow });
|
||||
.withArgs(repositoryProperties, undefined)
|
||||
.logs(t, "Using configuration file input from workflow")
|
||||
.passes(t.is, testInput);
|
||||
});
|
||||
|
||||
test("getConfigFileInput returns repository property value", async (t) => {
|
||||
// Since there is no direct input, we should use the repository property.
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.withArgs(repositoryProperties)
|
||||
.logs(t, "Using config-file input from repository property")
|
||||
.passes(t.deepEqual, {
|
||||
value: repositoryProperties[RepositoryPropertyName.CONFIG_FILE],
|
||||
source: InputSource.RepositoryProperty,
|
||||
});
|
||||
.withArgs(repositoryProperties, undefined)
|
||||
.logs(t, "Using configuration file input from repository property")
|
||||
.passes(t.is, repositoryProperties[RepositoryPropertyName.CONFIG_FILE]);
|
||||
});
|
||||
|
||||
test("getConfigFileInput returns repository property value for Code Scanning", async (t) => {
|
||||
// Since there is no direct input, we should use the repository property.
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.withArgs(repositoryProperties, [AnalysisKind.CodeScanning])
|
||||
.logs(t, "Using configuration file input from repository property")
|
||||
.passes(t.is, repositoryProperties[RepositoryPropertyName.CONFIG_FILE]);
|
||||
});
|
||||
|
||||
test("getConfigFileInput ignores repository property for other analysis kinds", async (t) => {
|
||||
const unsupportedCases = [
|
||||
[AnalysisKind.CodeQuality],
|
||||
[AnalysisKind.RiskAssessment],
|
||||
[AnalysisKind.CodeScanning, AnalysisKind.CodeQuality],
|
||||
];
|
||||
|
||||
const target = callee(getConfigFileInput).withFeatures([
|
||||
Feature.ConfigFileRepositoryProperty,
|
||||
]);
|
||||
|
||||
for (const unsupportedCase of unsupportedCases) {
|
||||
// Since the analysis kind is unsupported, we should ignore the repository property.
|
||||
await target
|
||||
.withArgs(repositoryProperties, unsupportedCase)
|
||||
.logs(
|
||||
t,
|
||||
"Ignoring configuration file input from repository property, because it is unsupported for the current analysis kind.",
|
||||
)
|
||||
.passes(t.is, undefined);
|
||||
}
|
||||
});
|
||||
|
||||
test("getConfigFileInput ignores empty repository property value", async (t) => {
|
||||
// Since the repository property value is an empty/whitespace string, we should ignore it.
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.withArgs({ [RepositoryPropertyName.CONFIG_FILE]: " " }, undefined)
|
||||
.passes(t.is, undefined);
|
||||
});
|
||||
|
||||
test("getConfigFileInput ignores repository property value when FF is off", async (t) => {
|
||||
// Since the FF is off, we should ignore the repository property value.
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([])
|
||||
.withArgs(repositoryProperties)
|
||||
.notLogs(t, "Using config-file input from repository property")
|
||||
.withArgs(repositoryProperties, undefined)
|
||||
.notLogs(t, "Using configuration file input from repository property")
|
||||
.logs(
|
||||
t,
|
||||
"Ignoring config-file input from repository property, because the corresponding feature flag is disabled.",
|
||||
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.",
|
||||
)
|
||||
.passes(t.is, undefined);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ActionState } from "../action-common";
|
||||
import { AnalysisKind } from "../analyses";
|
||||
import * as api from "../api-client";
|
||||
import * as errorMessages from "../error-messages";
|
||||
import { Feature } from "../feature-flags";
|
||||
@@ -9,7 +10,6 @@ import {
|
||||
import { ConfigurationError } from "../util";
|
||||
|
||||
import { parseUserConfig, UserConfig } from "./db-config";
|
||||
import { getComputedInput, InputName, type ComputedInput } from "./inputs";
|
||||
import { parseRemoteFileAddress } from "./remote-file";
|
||||
|
||||
/**
|
||||
@@ -29,19 +29,55 @@ export const REMOTE_PATH_PREFIX = "remote=";
|
||||
* Gets the value that is configured for the configuration file, if any.
|
||||
*/
|
||||
export async function getConfigFileInput(
|
||||
action: ActionState<["Logger", "Actions", "FeatureFlags"]>,
|
||||
{
|
||||
logger,
|
||||
actions,
|
||||
features,
|
||||
}: ActionState<["Logger", "Actions", "FeatureFlags"]>,
|
||||
repositoryProperties: Partial<RepositoryProperties>,
|
||||
): Promise<ComputedInput | undefined> {
|
||||
// Only use the repository property value if the FF is enabled.
|
||||
const useRepositoryProperty = await action.features.getValue(
|
||||
Feature.ConfigFileRepositoryProperty,
|
||||
);
|
||||
analysisKinds: AnalysisKind[] | undefined,
|
||||
): Promise<string | undefined> {
|
||||
const input = actions.getOptionalInput("config-file");
|
||||
|
||||
return getComputedInput(action, repositoryProperties, InputName.ConfigFile, {
|
||||
repositoryPropertyFeatureEnabled: useRepositoryProperty,
|
||||
allowForcedRepositoryPropertyValue: false,
|
||||
repositoryPropertyName: RepositoryPropertyName.CONFIG_FILE,
|
||||
});
|
||||
if (input !== undefined) {
|
||||
logger.info(`Using configuration file input from workflow: ${input}`);
|
||||
return input;
|
||||
}
|
||||
|
||||
const propertyValue =
|
||||
repositoryProperties[RepositoryPropertyName.CONFIG_FILE];
|
||||
|
||||
// Only allow the repository property to be used for standard Code Scanning analyses,
|
||||
// since we don't currently support some customisation options for Code Quality.
|
||||
// We don't expect customisations for Risk Assessments either.
|
||||
const analysisKindSupported =
|
||||
analysisKinds === undefined ||
|
||||
(analysisKinds.includes(AnalysisKind.CodeScanning) &&
|
||||
analysisKinds.length === 1);
|
||||
|
||||
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 (analysisKindSupported && useRepositoryProperty) {
|
||||
logger.info(
|
||||
`Using configuration file input from repository property: ${propertyValue}`,
|
||||
);
|
||||
return propertyValue;
|
||||
} else if (!analysisKindSupported) {
|
||||
logger.info(
|
||||
"Ignoring configuration file input from repository property, because it is unsupported for the current analysis kind.",
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,12 +3,10 @@ import { Feature } from "../feature-flags";
|
||||
import {
|
||||
RepositoryProperties,
|
||||
RepositoryPropertyName,
|
||||
StringRepositoryPropertyNames,
|
||||
} from "../feature-flags/properties";
|
||||
|
||||
/** Enumerates input names. */
|
||||
export enum InputName {
|
||||
ConfigFile = "config-file",
|
||||
Tools = "tools",
|
||||
}
|
||||
|
||||
@@ -30,50 +28,26 @@ export type ComputedInput = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents options for how to compute an input.
|
||||
*/
|
||||
export interface ComputedInputOptions {
|
||||
/**
|
||||
* Whether the FF for the repository property (if any) is enabled.
|
||||
*/
|
||||
repositoryPropertyFeatureEnabled: boolean;
|
||||
/**
|
||||
* The name of the repository property to try and get the input value from.
|
||||
*/
|
||||
repositoryPropertyName: StringRepositoryPropertyNames;
|
||||
/**
|
||||
* Whether the repository property value may start with `!` to take precedence
|
||||
* over any input value provided in the workflow file.
|
||||
*/
|
||||
allowForcedRepositoryPropertyValue: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the computed input for `name`. This comes from either the workflow or
|
||||
* Gets the computed `tools` input. This comes from either the workflow or
|
||||
* the repository property.
|
||||
*
|
||||
* @param action The Action state.
|
||||
* @param repositoryProperties The values of known repository properties.
|
||||
* @param name The name of the input to compute.
|
||||
* @param options Options for how to compute the input value.
|
||||
*
|
||||
* @returns The computed input or `undefined` if there is no input.
|
||||
*/
|
||||
export async function getComputedInput(
|
||||
export async function getToolsInput(
|
||||
action: ActionState<["Logger", "Actions", "FeatureFlags"]>,
|
||||
repositoryProperties: RepositoryProperties,
|
||||
name: InputName,
|
||||
options: ComputedInputOptions,
|
||||
repositoryProperties: Partial<RepositoryProperties>,
|
||||
): Promise<ComputedInput | undefined> {
|
||||
const name = InputName.Tools;
|
||||
const input = action.actions.getOptionalInput(name);
|
||||
const propertyValue = repositoryProperties[options.repositoryPropertyName];
|
||||
const propertyValue = repositoryProperties[RepositoryPropertyName.TOOLS];
|
||||
const allowRepositoryProperty = await action.features.getValue(
|
||||
Feature.ToolsRepositoryProperty,
|
||||
);
|
||||
|
||||
// The repository property takes precedence if it starts with an '!'.
|
||||
if (
|
||||
options.repositoryPropertyFeatureEnabled &&
|
||||
options.allowForcedRepositoryPropertyValue &&
|
||||
propertyValue?.startsWith("!")
|
||||
) {
|
||||
if (allowRepositoryProperty && propertyValue?.startsWith("!")) {
|
||||
action.logger.info(
|
||||
`Using ${name} input from repository property (enforced): ${propertyValue}`,
|
||||
);
|
||||
@@ -91,7 +65,7 @@ export async function getComputedInput(
|
||||
}
|
||||
|
||||
// Use the repository property if there's no workflow input.
|
||||
if (options.repositoryPropertyFeatureEnabled && propertyValue !== undefined) {
|
||||
if (allowRepositoryProperty && propertyValue !== undefined) {
|
||||
action.logger.info(
|
||||
`Using ${name} input from repository property: ${propertyValue}`,
|
||||
);
|
||||
@@ -99,34 +73,8 @@ export async function getComputedInput(
|
||||
value: propertyValue,
|
||||
source: InputSource.RepositoryProperty,
|
||||
};
|
||||
} else if (propertyValue !== undefined) {
|
||||
action.logger.info(
|
||||
`Ignoring ${name} input from repository property, because the corresponding feature flag is disabled.`,
|
||||
);
|
||||
}
|
||||
|
||||
// There's no input.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the computed `tools` input. This comes from either the workflow or
|
||||
* the repository property.
|
||||
*
|
||||
* @param action The Action state.
|
||||
* @param repositoryProperties The values of known repository properties.
|
||||
* @returns The computed input or `undefined` if there is no input.
|
||||
*/
|
||||
export async function getToolsInput(
|
||||
action: ActionState<["Logger", "Actions", "FeatureFlags"]>,
|
||||
repositoryProperties: Partial<RepositoryProperties>,
|
||||
): Promise<ComputedInput | undefined> {
|
||||
const allowRepositoryProperty = await action.features.getValue(
|
||||
Feature.ToolsRepositoryProperty,
|
||||
);
|
||||
return getComputedInput(action, repositoryProperties, InputName.Tools, {
|
||||
repositoryPropertyFeatureEnabled: allowRepositoryProperty,
|
||||
allowForcedRepositoryPropertyValue: true,
|
||||
repositoryPropertyName: RepositoryPropertyName.TOOLS,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,7 +74,6 @@ export enum Feature {
|
||||
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",
|
||||
CleanupTrapCaches = "cleanup_trap_caches",
|
||||
/** Whether to allow the `config-file` input to be specified via a repository property. */
|
||||
ConfigFileRepositoryProperty = "config_file_repository_property",
|
||||
@@ -187,11 +186,6 @@ export const featureConfig = {
|
||||
envVar: "CODEQL_ACTION_ALLOW_MULTIPLE_ANALYSIS_KINDS",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.AllowToolcacheInput]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_ALLOW_TOOLCACHE_INPUT",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.CleanupTrapCaches]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_CLEANUP_TRAP_CACHES",
|
||||
|
||||
@@ -176,30 +176,6 @@ test.serial(
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"loadPropertiesFromApi returns undefined if non-empty string property is empty",
|
||||
async (t) => {
|
||||
sinon.stub(api, "getRepositoryProperties").resolves({
|
||||
headers: {},
|
||||
status: 200,
|
||||
url: "",
|
||||
data: [{ property_name: "github-codeql-config-file", value: "" }],
|
||||
});
|
||||
const logger = getRunnerLogger(true);
|
||||
const mockRepositoryNwo = parseRepositoryNwo("owner/repo");
|
||||
|
||||
// `github-codeql-config-file` is a `nonEmptyStringProperty`, so we expect to
|
||||
// get `undefined` instead of the empty string
|
||||
const response = await properties.loadPropertiesFromApi(
|
||||
logger,
|
||||
mockRepositoryNwo,
|
||||
);
|
||||
t.deepEqual(response, {
|
||||
"github-codeql-config-file": undefined,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"loadPropertiesFromApi warns if boolean property has unexpected value",
|
||||
async (t) => {
|
||||
|
||||
@@ -22,25 +22,13 @@ export enum RepositoryPropertyName {
|
||||
|
||||
/** Parsed types of the known repository properties. */
|
||||
export type AllRepositoryProperties = {
|
||||
[RepositoryPropertyName.CONFIG_FILE]: string | undefined;
|
||||
[RepositoryPropertyName.CONFIG_FILE]: string;
|
||||
[RepositoryPropertyName.DISABLE_OVERLAY]: boolean;
|
||||
[RepositoryPropertyName.EXTRA_QUERIES]: string;
|
||||
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: boolean;
|
||||
[RepositoryPropertyName.TOOLS]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The subset of known repository properties which are of type `string`.
|
||||
* We tolerate `undefined` for `string`-typed properties that are empty.
|
||||
*/
|
||||
export type StringRepositoryPropertyNames = keyof {
|
||||
[K in keyof AllRepositoryProperties as AllRepositoryProperties[K] extends
|
||||
| string
|
||||
| undefined
|
||||
? K
|
||||
: never]: AllRepositoryProperties[K];
|
||||
};
|
||||
|
||||
/** Parsed repository properties. */
|
||||
export type RepositoryProperties = Partial<AllRepositoryProperties>;
|
||||
|
||||
@@ -84,12 +72,6 @@ const stringProperty = {
|
||||
parse: parseStringRepositoryProperty,
|
||||
};
|
||||
|
||||
/** A repository property that we expect to contain a non-empty string value. */
|
||||
const nonEmptyStringProperty = {
|
||||
...stringProperty,
|
||||
parse: parseNonEmptyStringRepositoryProperty,
|
||||
};
|
||||
|
||||
/** A repository property that we expect to contain a boolean value. */
|
||||
const booleanProperty = {
|
||||
// The value from the API should come as a string, which we then parse into a boolean.
|
||||
@@ -101,7 +83,7 @@ const booleanProperty = {
|
||||
const repositoryPropertyParsers: {
|
||||
[K in RepositoryPropertyName]: PropertyInfo<K>;
|
||||
} = {
|
||||
[RepositoryPropertyName.CONFIG_FILE]: nonEmptyStringProperty,
|
||||
[RepositoryPropertyName.CONFIG_FILE]: stringProperty,
|
||||
[RepositoryPropertyName.DISABLE_OVERLAY]: booleanProperty,
|
||||
[RepositoryPropertyName.EXTRA_QUERIES]: stringProperty,
|
||||
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: booleanProperty,
|
||||
@@ -246,17 +228,6 @@ function parseStringRepositoryProperty(_name: string, value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Parse a non-empty string repository property. */
|
||||
function parseNonEmptyStringRepositoryProperty(
|
||||
_name: string,
|
||||
value: string,
|
||||
): string | undefined {
|
||||
if (value.trim().length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Set of known repository property names, for fast lookups. */
|
||||
const KNOWN_REPOSITORY_PROPERTY_NAMES = new Set<string>(
|
||||
Object.values(RepositoryPropertyName),
|
||||
|
||||
@@ -129,7 +129,7 @@ async function sendStartingStatusReport(
|
||||
async function sendCompletedStatusReport(
|
||||
startedAt: Date,
|
||||
config: configUtils.Config | undefined,
|
||||
configFileInput: ComputedInput | undefined,
|
||||
configFile: string | undefined,
|
||||
toolsInput: ComputedInput | undefined,
|
||||
toolsDownloadStatusReport: ToolsDownloadStatusReport | undefined,
|
||||
toolsFeatureFlagsValid: boolean | undefined,
|
||||
@@ -168,9 +168,6 @@ async function sendCompletedStatusReport(
|
||||
if (toolsInput !== undefined) {
|
||||
initStatusReport.computed_inputs.tools = toolsInput;
|
||||
}
|
||||
if (configFileInput !== undefined) {
|
||||
initStatusReport.computed_inputs["config-file"] = configFileInput;
|
||||
}
|
||||
|
||||
const initToolsDownloadFields: InitToolsDownloadFields = {};
|
||||
|
||||
@@ -188,7 +185,7 @@ async function sendCompletedStatusReport(
|
||||
await createInitWithConfigStatusReport(
|
||||
config,
|
||||
initStatusReport,
|
||||
configFileInput,
|
||||
configFile,
|
||||
Math.round(
|
||||
await getTotalCacheSize(Object.values(config.trapCaches), logger),
|
||||
),
|
||||
@@ -215,7 +212,7 @@ async function run(
|
||||
|
||||
let apiDetails: GitHubApiCombinedDetails;
|
||||
let config: configUtils.Config | undefined;
|
||||
let configFileInput: ComputedInput | undefined;
|
||||
let configFile: string | undefined;
|
||||
let codeql: CodeQL;
|
||||
let features: FeatureEnablement;
|
||||
let sourceRoot: string;
|
||||
@@ -265,12 +262,6 @@ async function run(
|
||||
|
||||
core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true");
|
||||
|
||||
const actionStateWithFeatures = { ...actionState, features };
|
||||
configFileInput = await getConfigFileInput(
|
||||
actionStateWithFeatures,
|
||||
repositoryProperties,
|
||||
);
|
||||
|
||||
// path.resolve() respects the intended semantics of source-root. If
|
||||
// source-root is relative, it is relative to the GITHUB_WORKSPACE. If
|
||||
// source-root is absolute, it is used as given.
|
||||
@@ -293,6 +284,14 @@ async function run(
|
||||
);
|
||||
}
|
||||
|
||||
// Compute the value of the `config-file` input.
|
||||
const actionStateWithFeatures = { ...actionState, features };
|
||||
configFile = await getConfigFileInput(
|
||||
actionStateWithFeatures,
|
||||
repositoryProperties,
|
||||
analysisKinds,
|
||||
);
|
||||
|
||||
// Send a status report indicating that an analysis is starting.
|
||||
await sendStartingStatusReport(startedAt, { analysisKinds }, logger);
|
||||
|
||||
@@ -379,7 +378,7 @@ async function run(
|
||||
packsInput: getOptionalInput("packs"),
|
||||
buildModeInput: getOptionalInput("build-mode"),
|
||||
ramInput: getOptionalInput("ram"),
|
||||
configFile: configFileInput?.value,
|
||||
configFile,
|
||||
dbLocation: getOptionalInput("db-location"),
|
||||
configInput: getOptionalInput("config"),
|
||||
dependencyCachingEnabled: getDependencyCachingEnabled(),
|
||||
@@ -785,7 +784,7 @@ async function run(
|
||||
await sendCompletedStatusReport(
|
||||
startedAt,
|
||||
config,
|
||||
configFileInput,
|
||||
configFile,
|
||||
toolsInput,
|
||||
toolsDownloadStatusReport,
|
||||
toolsFeatureFlagsValid,
|
||||
|
||||
@@ -451,7 +451,7 @@ test.serial(
|
||||
async (t) => {
|
||||
const loggedMessages: LoggedMessage[] = [];
|
||||
const logger = getRecordingLogger(loggedMessages);
|
||||
const features = createFeatures([Feature.AllowToolcacheInput]);
|
||||
const features = createFeatures([]);
|
||||
|
||||
const latestToolcacheVersion = "3.2.1";
|
||||
const latestVersionPath = "/path/to/latest";
|
||||
@@ -580,7 +580,7 @@ const toolcacheInputFallbackMacro = makeMacro({
|
||||
|
||||
toolcacheInputFallbackMacro.serial(
|
||||
"the toolcache doesn't have a CodeQL CLI when tools == toolcache",
|
||||
[Feature.AllowToolcacheInput],
|
||||
[],
|
||||
{ GITHUB_EVENT_NAME: "dynamic" },
|
||||
[],
|
||||
[
|
||||
@@ -591,7 +591,7 @@ toolcacheInputFallbackMacro.serial(
|
||||
|
||||
toolcacheInputFallbackMacro.serial(
|
||||
"the workflow trigger is not `dynamic`",
|
||||
[Feature.AllowToolcacheInput],
|
||||
[],
|
||||
{ GITHUB_EVENT_NAME: "pull_request" },
|
||||
[],
|
||||
[
|
||||
@@ -599,14 +599,6 @@ toolcacheInputFallbackMacro.serial(
|
||||
],
|
||||
);
|
||||
|
||||
toolcacheInputFallbackMacro.serial(
|
||||
"the feature flag is not enabled",
|
||||
[],
|
||||
{ GITHUB_EVENT_NAME: "dynamic" },
|
||||
[],
|
||||
[`Ignoring 'tools: toolcache' because the feature is not enabled.`],
|
||||
);
|
||||
|
||||
test.serial(
|
||||
'tryGetTagNameFromUrl extracts the right tag name for a repo name containing "codeql-bundle"',
|
||||
(t) => {
|
||||
|
||||
@@ -533,11 +533,7 @@ export async function getCodeQLSource(
|
||||
// We only allow `toolsInput === "toolcache"` for `dynamic` events. In general, using `toolsInput === "toolcache"`
|
||||
// can lead to alert wobble and so it shouldn't be used for an analysis where results are intended to be uploaded.
|
||||
// We also allow this in test mode.
|
||||
const allowToolcacheValueFF = await features.getValue(
|
||||
Feature.AllowToolcacheInput,
|
||||
);
|
||||
const allowToolcacheValue =
|
||||
allowToolcacheValueFF && (isDynamicWorkflow() || util.isInTestMode());
|
||||
const allowToolcacheValue = isDynamicWorkflow() || util.isInTestMode();
|
||||
if (allowToolcacheValue) {
|
||||
// If `toolsInput === "toolcache"`, try to find the latest version of the CLI that's available in the toolcache
|
||||
// and use that. We perform this check here since we can set `cliVersion` directly and don't want to default to
|
||||
@@ -558,15 +554,9 @@ export async function getCodeQLSource(
|
||||
`Found no CodeQL CLI in the toolcache, ignoring 'tools: ${toolsInput}'...`,
|
||||
);
|
||||
} else {
|
||||
if (allowToolcacheValueFF) {
|
||||
logger.warning(
|
||||
`Ignoring 'tools: ${toolsInput}' because the workflow was not triggered dynamically.`,
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Ignoring 'tools: ${toolsInput}' because the feature is not enabled.`,
|
||||
);
|
||||
}
|
||||
logger.warning(
|
||||
`Ignoring 'tools: ${toolsInput}' because the workflow was not triggered dynamically.`,
|
||||
);
|
||||
}
|
||||
|
||||
const version = await resolveDefaultCliVersion(
|
||||
|
||||
@@ -554,7 +554,7 @@ export interface InitToolsDownloadFields {
|
||||
*
|
||||
* @param config The CodeQL Action configuration whose values should be added to the base status report.
|
||||
* @param initStatusReport The base status report.
|
||||
* @param configFileInput Optionally, the filename of the configuration file that was read.
|
||||
* @param configFile Optionally, the filename of the configuration file that was read.
|
||||
* @param totalCacheSize The computed total TRAP cache size.
|
||||
* @param overlayBaseDatabaseStats Statistics about the overlay database, if any.
|
||||
* @returns
|
||||
@@ -562,7 +562,7 @@ export interface InitToolsDownloadFields {
|
||||
export async function createInitWithConfigStatusReport(
|
||||
config: Config,
|
||||
initStatusReport: InitStatusReport,
|
||||
configFileInput: ComputedInput | undefined,
|
||||
configFile: string | undefined,
|
||||
totalCacheSize: number,
|
||||
overlayBaseDatabaseStats: OverlayBaseDatabaseDownloadStats | undefined,
|
||||
dependencyCachingResults: DependencyCacheRestoreStatusReport | undefined,
|
||||
@@ -601,7 +601,7 @@ export async function createInitWithConfigStatusReport(
|
||||
|
||||
return {
|
||||
...initStatusReport,
|
||||
config_file: configFileInput?.value ?? "",
|
||||
config_file: configFile ?? "",
|
||||
disable_default_queries: disableDefaultQueries,
|
||||
paths,
|
||||
paths_ignore: pathsIgnore,
|
||||
|
||||
Reference in New Issue
Block a user