Compare commits

...

56 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 4869139f44 Address code review feedback
- Remove awkward blank lines left after removing eslint-disable comments
- Add baseUrl to tsconfig.json for proper paths resolution

Co-authored-by: henrymercer <14129055+henrymercer@users.noreply.github.com>
2026-01-28 18:51:48 +00:00
copilot-swe-agent[bot] 712b51a568 Also disable no-unsafe-argument rule
This rule is also triggered by stricter types in @actions/github v9

Co-authored-by: henrymercer <14129055+henrymercer@users.noreply.github.com>
2026-01-28 18:46:47 +00:00
copilot-swe-agent[bot] 0d61442a70 Fix ESLint errors caused by @actions/github v9 upgrade
- Add @actions/github to ESLint import/ignore list to avoid parse errors
- Disable @typescript-eslint/no-unsafe-call and no-unsafe-return rules
- Add TypeScript paths mapping for @octokit/core/types
- Remove unused eslint-disable directives

Co-authored-by: henrymercer <14129055+henrymercer@users.noreply.github.com>
2026-01-28 18:45:04 +00:00
copilot-swe-agent[bot] c4b6a67406 Initial plan 2026-01-28 18:38:23 +00:00
dependabot[bot] 90b2d28dad Bump @actions/github from 8.0.0 to 9.0.0
Bumps [@actions/github](https://github.com/actions/toolkit/tree/HEAD/packages/github) from 8.0.0 to 9.0.0.
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/github/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/github)

---
updated-dependencies:
- dependency-name: "@actions/github"
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-28 17:53:43 +00:00
Michael B. Gale 1314d3d17e Merge pull request #3439 from github/mbg/fix-proxy-test
Remove `gh` setup from global proxy test
2026-01-28 13:58:28 +00:00
Michael B. Gale f7f9d3f341 Remove gh setup from global proxy test 2026-01-28 13:35:59 +00:00
Henry Mercer eb5bd2d0b9 Merge pull request #3421 from github/henrymercer/only-request-properties-for-orgs
Tolerate errors loading repository properties
2026-01-28 04:00:31 -08:00
Henry Mercer 9aa0515f67 Update comments 2026-01-28 11:33:06 +00:00
Henry Mercer 0720e13f0b Log repository owner type explicitly 2026-01-28 11:30:35 +00:00
Henry Mercer 38ba96d2aa Merge branch 'main' into henrymercer/only-request-properties-for-orgs 2026-01-27 18:28:33 +00:00
Henry Mercer 679da45cc3 Add basic unit tests for Result class 2026-01-27 15:19:17 +00:00
Henry Mercer d5dd165f8b Ensure default value is assignable if we have a Failure 2026-01-27 15:18:50 +00:00
Henry Mercer fbf75ebd7b Merge branch 'main' into henrymercer/only-request-properties-for-orgs 2026-01-27 15:17:20 +00:00
Henry Mercer 6a50972d16 Introduce addNoLanguageDiagnostic 2026-01-27 15:14:32 +00:00
Henry Mercer 5cb12c41c2 Include "Result" in name 2026-01-27 15:11:48 +00:00
Henry Mercer e8f487178f Add some doc for loadRepositoryProperties 2026-01-27 15:11:01 +00:00
Henry Mercer a0671be58e Add doc for Result 2026-01-27 15:07:46 +00:00
Henry Mercer 9ea34c5169 Result: Make use of type hint 2026-01-27 15:05:03 +00:00
Henry Mercer 9fda641d8d Prefer accessing context via @actions/github 2026-01-27 15:00:52 +00:00
Henry Mercer b126facd4e Merge pull request #3434 from github/mbg/dependabot/cooldown
Add `cooldown` settings for Dependabot
2026-01-27 06:57:30 -08:00
Henry Mercer 835dadecbf Merge pull request #3420 from github/henrymercer/compute-job-status-if-no-config
Simplify computation of job status
2026-01-27 06:51:55 -08:00
Henry Mercer a02edfe319 Merge pull request #3424 from github/henrymercer/feature-skip-file-coverage-info-prs
Add feature flag to skip computing baseline file coverage information on PRs
2026-01-27 06:49:29 -08:00
Michael B. Gale 173919c9d5 Merge pull request #3436 from github/mbg/rebuild-js-es2022
Update JS for ES2022
2026-01-27 14:46:54 +00:00
Henry Mercer 6095dc4d51 Merge branch 'main' into henrymercer/compute-job-status-if-no-config 2026-01-27 14:31:51 +00:00
Henry Mercer b333fc6f5b Split up getFinalJobStatus 2026-01-27 14:30:42 +00:00
Henry Mercer 60b658ed10 Update comment 2026-01-27 14:26:31 +00:00
Michael B. Gale e4e324705e Update JS 2026-01-27 14:13:28 +00:00
github-actions[bot] faf7a50b01 Rebuild 2026-01-27 14:13:05 +00:00
Michael B. Gale 2591c2031f Add cooldown settings for Dependabot 2026-01-27 14:08:27 +00:00
Michael B. Gale 34cae51104 Merge pull request #3422 from github/mbg/start-proxy/warn-if-pat-without-username
Warn if a private registry configuration uses a PAT, but has no username
2026-01-27 14:07:06 +00:00
Henry Mercer 9308bcd6bb Add unit tests for file coverage enablement 2026-01-27 13:55:22 +00:00
Michael B. Gale c656a11252 Use .match in isAuthToken and add repeated call to test 2026-01-27 11:45:03 +00:00
Michael B. Gale 0a0c3a2e09 Merge branch 'main' into mbg/start-proxy/warn-if-pat-without-username 2026-01-27 11:27:31 +00:00
Henry Mercer bf20b3e07b Exclude PR check from feature flag 2026-01-26 18:04:37 +00:00
Henry Mercer f1aa4f497a Explain why we ignore extra baseline files options 2026-01-26 18:02:58 +00:00
Henry Mercer 9a55d5bc5f Improve log message 2026-01-26 18:00:34 +00:00
Henry Mercer 17cd475099 Move to separate function 2026-01-26 17:55:17 +00:00
Henry Mercer 18c2cfc765 Indulge caniuse-lite to avoid build warnings 2026-01-26 11:42:13 +00:00
Henry Mercer 1996ca9f5d Log when file coverage info is disabled 2026-01-26 11:42:13 +00:00
Henry Mercer 12c4c7d0e9 Don't log empty summaries 2026-01-26 11:42:13 +00:00
Henry Mercer 919e8aaa40 Mention caveat in feature JSDoc 2026-01-26 11:00:04 +00:00
Henry Mercer 4918026b93 Use FF to disable baseline file coverage 2026-01-26 11:00:04 +00:00
Henry Mercer e8c164b902 Remove unused database print-baseline 2026-01-26 11:00:03 +00:00
Michael B. Gale 9fccf271ff Warn if a private registry configuration uses a PAT, but has no username 2026-01-24 13:02:41 +00:00
Michael B. Gale c12cf8d49a Move makeTestToken to testing-utils 2026-01-24 12:55:32 +00:00
Michael B. Gale 0fcbec3eec Add isAuthToken function, with tests 2026-01-24 12:38:14 +00:00
Michael B. Gale 0ae8b05d08 Extend unit tests to cover all token types 2026-01-24 12:25:40 +00:00
Michael B. Gale 49cdf744d9 Use enum for token types 2026-01-24 11:58:10 +00:00
Michael B. Gale aac4202424 Add fine-grained tokens to GITHUB_TOKEN_PATTERNS 2026-01-24 11:52:53 +00:00
Henry Mercer e7ece62b96 Add feature flag to skip file coverage information on PRs 2026-01-23 18:41:24 +00:00
Henry Mercer d9e374ef85 Tolerate failures loading repository properties 2026-01-23 17:51:41 +00:00
Henry Mercer f4b47e7013 Add result type 2026-01-23 17:51:02 +00:00
Henry Mercer 4e14537b54 Improve logging when no known repository properties found 2026-01-23 17:29:15 +00:00
Henry Mercer e142eee9b4 Only load repository properties for repos owned by orgs 2026-01-23 17:20:30 +00:00
Henry Mercer dcd1b12beb Simplify computation of job status
- Move it out of the failed SARIF reporting so we compute the job status
whether or not we have a CodeQL config.
- Add comments to clarify what happens in the case that the CodeQL
config is absent.
2026-01-23 17:07:21 +00:00
46 changed files with 280144 additions and 57855 deletions
+8
View File
@@ -4,6 +4,10 @@ updates:
directory: "/"
schedule:
interval: weekly
cooldown:
default-days: 7
exclude:
- "@actions/*"
labels:
- Rebuild
# Ignore incompatible dependency updates
@@ -25,6 +29,10 @@ updates:
- "/.github/actions"
schedule:
interval: weekly
cooldown:
default-days: 7
exclude:
- "actions/*"
labels:
- Rebuild
groups:
@@ -125,5 +125,6 @@ jobs:
fi
done
env:
CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS: false
CODEQL_ACTION_SUBLANGUAGE_FILE_COVERAGE: true
CODEQL_ACTION_TEST_MODE: true
-12
View File
@@ -48,18 +48,6 @@ jobs:
timeout-minutes: 45
runs-on: ${{ matrix.os }}
steps:
# These steps are required to initialise the `gh` cli in a container that doesn't
# come pre-installed with it. The reason for that is that this is later
# needed by the `prepare-test` workflow to find the latest release of CodeQL.
- name: Set up GitHub CLI
run: |
apt update
apt install -y curl libreadline8 gnupg2 software-properties-common zstd
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
apt-key add /usr/share/keyrings/githubcli-archive-keyring.gpg
apt-add-repository https://cli.github.com/packages
apt install -y gh
env: {}
- name: Check out repository
uses: actions/checkout@v6
- name: Prepare test
+4 -1
View File
@@ -78,7 +78,7 @@ export default [
typescript: {},
},
"import/ignore": ["sinon", "uuid", "@octokit/plugin-retry", "del", "get-folder-size"],
"import/ignore": ["sinon", "uuid", "@octokit/plugin-retry", "del", "get-folder-size", "@actions/github"],
},
rules: {
@@ -152,9 +152,12 @@ export default [
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-enum-comparison": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/prefer-regexp-exec": "off",
"@typescript-eslint/require-await": "off",
+23497 -5023
View File
File diff suppressed because one or more lines are too long
+23095 -4621
View File
File diff suppressed because one or more lines are too long
+23078 -4610
View File
File diff suppressed because one or more lines are too long
+23975 -5493
View File
File diff suppressed because one or more lines are too long
+23383 -4826
View File
File diff suppressed because one or more lines are too long
+23078 -4610
View File
File diff suppressed because one or more lines are too long
+23078 -4610
View File
File diff suppressed because one or more lines are too long
+23488 -5011
View File
File diff suppressed because one or more lines are too long
+23129 -4604
View File
File diff suppressed because one or more lines are too long
+23077 -4610
View File
File diff suppressed because one or more lines are too long
+23488 -5011
View File
File diff suppressed because one or more lines are too long
+23080 -4613
View File
File diff suppressed because one or more lines are too long
+30 -10
View File
@@ -14,7 +14,7 @@
"@actions/cache": "^5.0.3",
"@actions/core": "^2.0.2",
"@actions/exec": "^2.0.0",
"@actions/github": "^8.0.0",
"@actions/github": "^9.0.0",
"@actions/glob": "^0.5.0",
"@actions/http-client": "^3.0.0",
"@actions/io": "^2.0.0",
@@ -395,18 +395,28 @@
}
},
"node_modules/@actions/github": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-8.0.0.tgz",
"integrity": "sha512-bBukvVRvIf7NshWEXFVnBaXs/ZyGzDvx+jbB5AdJij9CUG4VFEYJJD6T6y06dDkuULalQQUNvKXIH71yqw7SoQ==",
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-9.0.0.tgz",
"integrity": "sha512-yJ0RoswsAaKcvkmpCE4XxBRiy/whH2SdTBHWzs0gi4wkqTDhXMChjSdqBz/F4AeiDlP28rQqL33iHb+kjAMX6w==",
"license": "MIT",
"dependencies": {
"@actions/http-client": "^3.0.1",
"@actions/http-client": "^3.0.2",
"@octokit/core": "^7.0.6",
"@octokit/plugin-paginate-rest": "^14.0.0",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
"@octokit/request": "^10.0.7",
"@octokit/request-error": "^7.1.0",
"undici": "^5.28.5"
"undici": "^6.23.0"
}
},
"node_modules/@actions/github/node_modules/@actions/http-client": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.2.tgz",
"integrity": "sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^6.23.0"
}
},
"node_modules/@actions/github/node_modules/@octokit/plugin-paginate-rest": {
@@ -439,6 +449,15 @@
"@octokit/core": ">=6"
}
},
"node_modules/@actions/github/node_modules/undici": {
"version": "6.23.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/@actions/glob": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.5.0.tgz",
@@ -3792,9 +3811,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"version": "1.0.30001766",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
"integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
"dev": true,
"funding": [
{
@@ -3809,7 +3828,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
],
"license": "CC-BY-4.0"
},
"node_modules/cbor": {
"version": "10.0.9",
+1 -1
View File
@@ -29,7 +29,7 @@
"@actions/cache": "^5.0.3",
"@actions/core": "^2.0.2",
"@actions/exec": "^2.0.0",
"@actions/github": "^8.0.0",
"@actions/github": "^9.0.0",
"@actions/glob": "^0.5.0",
"@actions/http-client": "^3.0.0",
"@actions/io": "^2.0.0",
@@ -5,6 +5,7 @@ versions: ["nightly-latest"]
installGo: true
installDotNet: true
env:
CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS: false
CODEQL_ACTION_SUBLANGUAGE_FILE_COVERAGE: true
steps:
- uses: ./../action/init
-13
View File
@@ -3,19 +3,6 @@ description: "Tests using a proxy specified by the https_proxy environment varia
versions: ["linked", "nightly-latest"]
container:
image: ubuntu:22.04
container-init-steps:
# These steps are required to initialise the `gh` cli in a container that doesn't
# come pre-installed with it. The reason for that is that this is later
# needed by the `prepare-test` workflow to find the latest release of CodeQL.
name: Set up GitHub CLI
run: |
apt update
apt install -y curl libreadline8 gnupg2 software-properties-common zstd
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
apt-key add /usr/share/keyrings/githubcli-archive-keyring.gpg
apt-add-repository https://cli.github.com/packages
apt install -y gh
env: {}
services:
squid-proxy:
image: ubuntu/squid:latest
-1
View File
@@ -87,7 +87,6 @@ test("status report fields", async (t) => {
);
return "";
},
databasePrintBaseline: async () => "",
});
const config = createTestConfig({
+10 -2
View File
@@ -495,10 +495,18 @@ export async function runQueries(
endTimeInterpretResults.getTime() - startTimeInterpretResults.getTime();
logger.endGroup();
logger.info(analysisSummary);
if (qualityAnalysisSummary) {
if (analysisSummary.trim()) {
logger.info(analysisSummary);
}
if (qualityAnalysisSummary?.trim()) {
logger.info(qualityAnalysisSummary);
}
if (!config.enableFileCoverageInformation) {
logger.info(
"To speed up pull request analysis, file coverage information is only enabled when analyzing " +
"the default branch and protected branches.",
);
}
if (await features.getValue(Feature.QaTelemetryEnabled)) {
// Note: QA adds the `code-quality` query suite to the `queries` input,
-1
View File
@@ -52,7 +52,6 @@ function mockGetMetaVersionHeader(
};
const spyGetContents = sinon
.stub(client.rest.meta, "get")
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
.resolves(response as any);
sinon.stub(api, "getApiClient").value(() => client);
return spyGetContents;
-1
View File
@@ -116,7 +116,6 @@ export async function getGitHubVersionFromApi(
// Doesn't strictly have to be the meta endpoint as we're only
// using the response headers which are available on every request.
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const response = await apiClient.rest.meta.get();
// This happens on dotcom, although we expect to have already returned in that
+112 -27
View File
@@ -4,37 +4,122 @@ import * as path from "path";
import test from "ava";
import { scanArtifactsForTokens } from "./artifact-scanner";
import {
GITHUB_PAT_CLASSIC_PATTERN,
isAuthToken,
scanArtifactsForTokens,
TokenType,
} from "./artifact-scanner";
import { getRunnerLogger } from "./logging";
import { getRecordingLogger, LoggedMessage } from "./testing-utils";
import {
checkExpectedLogMessages,
getRecordingLogger,
LoggedMessage,
makeTestToken,
} from "./testing-utils";
test("scanArtifactsForTokens detects GitHub tokens in files", async (t) => {
const logger = getRunnerLogger(true);
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "scanner-test-"));
try {
// Create a test file with a fake GitHub token
const testFile = path.join(tempDir, "test.txt");
fs.writeFileSync(
testFile,
"This is a test file with token ghp_1234567890123456789012345678901234AB",
);
const error = await t.throwsAsync(
async () => await scanArtifactsForTokens([testFile], logger),
);
t.regex(
error?.message || "",
/Found 1 potential GitHub token.*Personal Access Token/,
);
t.regex(error?.message || "", /test\.txt/);
} finally {
// Clean up
fs.rmSync(tempDir, { recursive: true, force: true });
}
test("makeTestToken", (t) => {
t.is(makeTestToken().length, 36);
t.is(makeTestToken(255).length, 255);
});
test("isAuthToken", (t) => {
// Undefined for strings that aren't tokens
t.is(isAuthToken("some string"), undefined);
t.is(isAuthToken("ghp_"), undefined);
t.is(isAuthToken("ghp_123"), undefined);
// Token types for strings that are tokens.
t.is(isAuthToken(`ghp_${makeTestToken()}`), TokenType.PersonalAccessClassic);
t.is(isAuthToken(`ghp_${makeTestToken()}`), TokenType.PersonalAccessClassic);
t.is(
isAuthToken(`ghs_${makeTestToken(255)}`),
TokenType.AppInstallationAccess,
);
t.is(
isAuthToken(`github_pat_${makeTestToken(22)}_${makeTestToken(59)}`),
TokenType.PersonalAccessFineGrained,
);
// With a custom pattern set
t.is(
isAuthToken(`ghp_${makeTestToken()}`, [GITHUB_PAT_CLASSIC_PATTERN]),
TokenType.PersonalAccessClassic,
);
t.is(
isAuthToken(`github_pat_${makeTestToken(22)}_${makeTestToken(59)}`, [
GITHUB_PAT_CLASSIC_PATTERN,
]),
undefined,
);
});
const testTokens = [
{
type: TokenType.PersonalAccessClassic,
value: `ghp_${makeTestToken()}`,
checkPattern: "Personal Access Token",
},
{
type: TokenType.PersonalAccessFineGrained,
value:
"github_pat_1234567890ABCDEFGHIJKL_MNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHI",
checkPattern: "Personal Access Token",
},
{
type: TokenType.OAuth,
value: `gho_${makeTestToken()}`,
},
{
type: TokenType.UserToServer,
value: `ghu_${makeTestToken()}`,
},
{
type: TokenType.ServerToServer,
value: `ghs_${makeTestToken()}`,
},
{
type: TokenType.Refresh,
value: `ghr_${makeTestToken()}`,
},
{
type: TokenType.AppInstallationAccess,
value: `ghs_${makeTestToken(255)}`,
},
];
for (const { type, value, checkPattern } of testTokens) {
test(`scanArtifactsForTokens detects GitHub ${type} tokens in files`, async (t) => {
const logMessages = [];
const logger = getRecordingLogger(logMessages, { logToConsole: false });
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "scanner-test-"));
try {
// Create a test file with a fake GitHub token
const testFile = path.join(tempDir, "test.txt");
fs.writeFileSync(testFile, `This is a test file with token ${value}`);
const error = await t.throwsAsync(
async () => await scanArtifactsForTokens([testFile], logger),
);
t.regex(
error?.message || "",
new RegExp(`Found 1 potential GitHub token.*${checkPattern || type}`),
);
t.regex(error?.message || "", /test\.txt/);
checkExpectedLogMessages(t, logMessages, [
"Starting best-effort check",
`Found 1 ${type}`,
]);
} finally {
// Clean up
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
}
test("scanArtifactsForTokens handles files without tokens", async (t) => {
const logger = getRunnerLogger(true);
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "scanner-test-"));
+60 -13
View File
@@ -7,33 +7,62 @@ import * as exec from "@actions/exec";
import { Logger } from "./logging";
import { getErrorMessage } from "./util";
/**
* Enumerates known types of GitHub token formats.
*/
export enum TokenType {
PersonalAccessClassic = "Personal Access Token (Classic)",
PersonalAccessFineGrained = "Personal Access Token (Fine-grained)",
OAuth = "OAuth Access Token",
UserToServer = "User-to-Server Token",
ServerToServer = "Server-to-Server Token",
Refresh = "Refresh Token",
AppInstallationAccess = "App Installation Access Token",
}
/** A value of this type associates a token type with its pattern. */
export interface TokenPattern {
type: TokenType;
pattern: RegExp;
}
/** The pattern for PATs (Classic) */
export const GITHUB_PAT_CLASSIC_PATTERN: TokenPattern = {
type: TokenType.PersonalAccessClassic,
pattern: /\bghp_[a-zA-Z0-9]{36}\b/g,
};
/** The pattern for PATs (Fine-grained) */
export const GITHUB_PAT_FINE_GRAINED_PATTERN: TokenPattern = {
type: TokenType.PersonalAccessFineGrained,
pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g,
};
/**
* GitHub token patterns to scan for.
* These patterns match various GitHub token formats.
*/
const GITHUB_TOKEN_PATTERNS = [
const GITHUB_TOKEN_PATTERNS: TokenPattern[] = [
GITHUB_PAT_CLASSIC_PATTERN,
GITHUB_PAT_FINE_GRAINED_PATTERN,
{
name: "Personal Access Token",
pattern: /\bghp_[a-zA-Z0-9]{36}\b/g,
},
{
name: "OAuth Access Token",
type: TokenType.OAuth,
pattern: /\bgho_[a-zA-Z0-9]{36}\b/g,
},
{
name: "User-to-Server Token",
type: TokenType.UserToServer,
pattern: /\bghu_[a-zA-Z0-9]{36}\b/g,
},
{
name: "Server-to-Server Token",
type: TokenType.ServerToServer,
pattern: /\bghs_[a-zA-Z0-9]{36}\b/g,
},
{
name: "Refresh Token",
type: TokenType.Refresh,
pattern: /\bghr_[a-zA-Z0-9]{36}\b/g,
},
{
name: "App Installation Access Token",
type: TokenType.AppInstallationAccess,
pattern: /\bghs_[a-zA-Z0-9]{255}\b/g,
},
];
@@ -48,6 +77,24 @@ interface ScanResult {
findings: TokenFinding[];
}
/**
* Checks whether `value` matches any token `patterns`.
* @param value The value to match against.
* @param patterns The patterns to check.
* @returns The type of the first matching pattern, or `undefined` if none match.
*/
export function isAuthToken(
value: string,
patterns: TokenPattern[] = GITHUB_TOKEN_PATTERNS,
) {
for (const { type, pattern } of patterns) {
if (value.match(pattern)) {
return type;
}
}
return undefined;
}
/**
* Scans a file for GitHub tokens.
*
@@ -65,13 +112,13 @@ function scanFileForTokens(
try {
const content = fs.readFileSync(filePath, "utf8");
for (const { name, pattern } of GITHUB_TOKEN_PATTERNS) {
for (const { type, pattern } of GITHUB_TOKEN_PATTERNS) {
const matches = content.match(pattern);
if (matches) {
for (let i = 0; i < matches.length; i++) {
findings.push({ tokenType: name, filePath: relativePath });
findings.push({ tokenType: type, filePath: relativePath });
}
logger.debug(`Found ${matches.length} ${name}(s) in ${relativePath}`);
logger.debug(`Found ${matches.length} ${type}(s) in ${relativePath}`);
}
}
+12 -20
View File
@@ -186,10 +186,6 @@ export interface CodeQL {
config: Config,
features: FeatureEnablement,
): Promise<string>;
/**
* Run 'codeql database print-baseline'.
*/
databasePrintBaseline(databasePath: string): Promise<string>;
/**
* Run 'codeql database export-diagnostics'
*
@@ -493,10 +489,6 @@ export function createStubCodeQL(partialCodeql: Partial<CodeQL>): CodeQL {
partialCodeql,
"databaseInterpretResults",
),
databasePrintBaseline: resolveFunction(
partialCodeql,
"databasePrintBaseline",
),
databaseExportDiagnostics: resolveFunction(
partialCodeql,
"databaseExportDiagnostics",
@@ -628,6 +620,13 @@ async function getCodeQLForCmd(
extraArgs.push("--overlay-base");
}
const baselineFilesOptions = config.enableFileCoverageInformation
? [
"--calculate-language-specific-baseline",
"--sublanguage-file-coverage",
]
: ["--no-calculate-baseline"];
await runCli(
cmd,
[
@@ -639,12 +638,14 @@ async function getCodeQLForCmd(
"--db-cluster",
config.dbLocation,
`--source-root=${sourceRoot}`,
"--calculate-language-specific-baseline",
...baselineFilesOptions,
"--extractor-include-aliases",
"--sublanguage-file-coverage",
...extraArgs,
...getExtraOptionsFromEnv(["database", "init"], {
ignoringOptions: ["--overwrite"],
// Some user configs specify `--no-calculate-baseline` as an additional
// argument to `codeql database init`. Therefore ignore the baseline file
// options here to avoid specifying the same argument twice and erroring.
ignoringOptions: ["--overwrite", ...baselineFilesOptions],
}),
],
{ stdin: externalRepositoryToken },
@@ -885,15 +886,6 @@ async function getCodeQLForCmd(
noStreamStdout: true,
});
},
async databasePrintBaseline(databasePath: string): Promise<string> {
const codeqlArgs = [
"database",
"print-baseline",
...getExtraOptionsFromEnv(["database", "print-baseline"]),
databasePath,
];
return await runCli(cmd, codeqlArgs);
},
async databaseCleanupCluster(
config: Config,
cleanupLevel: CleanupLevel,
+1 -2
View File
@@ -89,6 +89,7 @@ function createTestInitConfigInputs(
},
features: createFeatures([]),
repositoryProperties: {},
enableFileCoverageInformation: true,
logger: getRunnerLogger(true),
} satisfies configUtils.InitConfigInputs,
overrides,
@@ -114,7 +115,6 @@ function mockGetContents(
};
const spyGetContents = sinon
.stub(client.rest.repos, "getContent")
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
.resolves(response as any);
sinon.stub(api, "getApiClient").value(() => client);
sinon.stub(api, "getApiClientWithExternalAuth").value(() => client);
@@ -130,7 +130,6 @@ function mockListLanguages(languages: string[]) {
for (const language of languages) {
response.data[language] = 123;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
sinon.stub(client.rest.repos, "listLanguages").resolves(response as any);
sinon.stub(api, "getApiClient").value(() => client);
}
+14 -9
View File
@@ -26,7 +26,10 @@ import {
parseUserConfig,
UserConfig,
} from "./config/db-config";
import { addDiagnostic, makeTelemetryDiagnostic } from "./diagnostics";
import {
addNoLanguageDiagnostic,
makeTelemetryDiagnostic,
} from "./diagnostics";
import { shouldPerformDiffInformedAnalysis } from "./diff-informed-analysis-utils";
import { EnvVar } from "./environment";
import * as errorMessages from "./error-messages";
@@ -214,6 +217,11 @@ export interface Config {
* 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(
@@ -433,6 +441,7 @@ export interface InitConfigInputs {
apiDetails: api.GitHubApiCombinedDetails;
features: FeatureEnablement;
repositoryProperties: RepositoryProperties;
enableFileCoverageInformation: boolean;
analysisKinds: AnalysisKind[];
logger: Logger;
}
@@ -462,6 +471,7 @@ export async function initActionState(
repositoryProperties,
analysisKinds,
logger,
enableFileCoverageInformation,
}: InitConfigInputs,
userConfig: UserConfig,
): Promise<Config> {
@@ -542,6 +552,7 @@ export async function initActionState(
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
repositoryProperties,
enableFileCoverageInformation,
};
}
@@ -1408,11 +1419,8 @@ async function logGitVersionTelemetry(
gitVersion: GitVersionInfo,
): Promise<void> {
if (config.languages.length > 0) {
addDiagnostic(
addNoLanguageDiagnostic(
config,
// Arbitrarily choose the first language. We could also choose all languages, but that
// increases the risk of misinterpreting the data.
config.languages[0],
makeTelemetryDiagnostic(
"codeql-action/git-version-telemetry",
"Git version telemetry",
@@ -1438,11 +1446,8 @@ async function logGeneratedFilesTelemetry(
return;
}
addDiagnostic(
addNoLanguageDiagnostic(
config,
// Arbitrarily choose the first language. We could also choose all languages, but that
// increases the risk of misinterpreting the data.
config.languages[0],
makeTelemetryDiagnostic(
"codeql-action/generated-files-telemetry",
"Generated files telemetry",
+14
View File
@@ -117,6 +117,20 @@ export function addDiagnostic(
}
}
/** Adds a diagnostic that is not specific to any language. */
export function addNoLanguageDiagnostic(
config: Config,
diagnostic: DiagnosticMessage,
) {
addDiagnostic(
config,
// Arbitrarily choose the first language. We could also choose all languages, but that
// increases the risk of misinterpreting the data.
config.languages[0],
diagnostic,
);
}
/**
* Writes the given diagnostic to the database.
*
+7 -11
View File
@@ -259,17 +259,13 @@ for (const feature of Object.keys(featureConfig)) {
// The type system should prevent this happening, but test that if we
// bypass it we get the expected error.
await t.throwsAsync(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
async () => features.getValue(feature as any),
{
message: `Internal error: A ${
featureConfig[feature].minimumVersion !== undefined
? "minimum version"
: "required tools feature"
} is specified for feature ${feature}, but no instance of CodeQL was provided.`,
},
);
await t.throwsAsync(async () => features.getValue(feature as any), {
message: `Internal error: A ${
featureConfig[feature].minimumVersion !== undefined
? "minimum version"
: "required tools feature"
} is specified for feature ${feature}, but no instance of CodeQL was provided.`,
});
});
});
}
+11
View File
@@ -70,6 +70,8 @@ export enum Feature {
OverlayAnalysisSwift = "overlay_analysis_swift",
PythonDefaultIsToNotExtractStdlib = "python_default_is_to_not_extract_stdlib",
QaTelemetryEnabled = "qa_telemetry_enabled",
/** Note that this currently only disables baseline file coverage information. */
SkipFileCoverageOnPrs = "skip_file_coverage_on_prs",
UploadOverlayDbToApi = "upload_overlay_db_to_api",
UseRepositoryProperties = "use_repository_properties",
ValidateDbConfig = "validate_db_config",
@@ -286,6 +288,15 @@ export const featureConfig = {
legacyApi: true,
minimumVersion: undefined,
},
[Feature.SkipFileCoverageOnPrs]: {
defaultValue: false,
envVar: "CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS",
// For testing, this is not behind a CLI version check yet. However
// before rolling this out externally, we should set a minimum version here
// since current versions of the CodeQL CLI will log if baseline information
// cannot be found when interpreting results.
minimumVersion: undefined,
},
[Feature.UploadOverlayDbToApi]: {
defaultValue: false,
envVar: "CODEQL_ACTION_UPLOAD_OVERLAY_DB_TO_API",
+11 -5
View File
@@ -78,11 +78,17 @@ export async function loadPropertiesFromApi(
}
}
logger.debug("Loaded the following values for the repository properties:");
for (const [property, value] of Object.entries(properties).sort(
([nameA], [nameB]) => nameA.localeCompare(nameB),
)) {
logger.debug(` ${property}: ${value}`);
if (Object.keys(properties).length === 0) {
logger.debug("No known repository properties were found.");
} else {
logger.debug(
"Loaded the following values for the repository properties:",
);
for (const [property, value] of Object.entries(properties).sort(
([nameA], [nameB]) => nameA.localeCompare(nameB),
)) {
logger.debug(` ${property}: ${value}`);
}
}
return properties;
+20 -55
View File
@@ -1,6 +1,5 @@
import * as fs from "fs";
import * as core from "@actions/core";
import * as github from "@actions/github";
import * as actionsUtil from "./actions-util";
@@ -129,48 +128,31 @@ export async function tryUploadSarifIfRunFailed(
features: FeatureEnablement,
logger: Logger,
): Promise<UploadFailedSarifResult> {
if (process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] !== "true") {
// If analyze didn't complete successfully and the job status hasn't
// already been set to Failure/ConfigurationError previously, this
// means that something along the way failed in a step that is not
// owned by the Action, for example a manual build step. We
// consider this a configuration error.
core.exportVariable(
EnvVar.JOB_STATUS,
process.env[EnvVar.JOB_STATUS] ?? JobStatus.ConfigErrorStatus,
);
// If the only enabled analysis kind is `code-quality`, then we shouldn't
// upload the failed SARIF to Code Scanning.
if (!isCodeScanningEnabled(config)) {
return {
upload_failed_run_skipped_because: "Code Scanning is not enabled.",
};
}
try {
return await maybeUploadFailedSarif(
config,
repositoryNwo,
features,
logger,
);
} catch (e) {
logger.debug(
`Failed to upload a SARIF file for this failed CodeQL code scanning run. ${e}`,
);
return createFailedUploadFailedSarifResult(e);
}
} else {
core.exportVariable(
EnvVar.JOB_STATUS,
process.env[EnvVar.JOB_STATUS] ?? JobStatus.SuccessStatus,
);
// Only upload the failed SARIF to Code scanning if Code scanning is enabled.
if (!isCodeScanningEnabled(config)) {
return {
upload_failed_run_skipped_because: "Code Scanning is not enabled.",
};
}
if (process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] === "true") {
return {
upload_failed_run_skipped_because:
"Analyze Action completed successfully",
};
}
try {
return await maybeUploadFailedSarif(
config,
repositoryNwo,
features,
logger,
);
} catch (e) {
logger.debug(
`Failed to upload a SARIF file for this failed CodeQL code scanning run. ${e}`,
);
return createFailedUploadFailedSarifResult(e);
}
}
export async function run(
@@ -335,20 +317,3 @@ async function removeUploadedSarif(
);
}
}
/**
* Returns the final job status sent in the `init-post` Action, based on the
* current value of the JOB_STATUS environment variable. If the variable is
* unset, or if its value is not one of the JobStatus enum values, returns
* Unknown. Otherwise it returns the status set in the environment variable.
*/
export function getFinalJobStatus(): JobStatus {
const jobStatusFromEnvironment = process.env[EnvVar.JOB_STATUS];
if (
!jobStatusFromEnvironment ||
!Object.values(JobStatus).includes(jobStatusFromEnvironment as JobStatus)
) {
return JobStatus.UnknownStatus;
}
return jobStatusFromEnvironment as JobStatus;
}
+72 -4
View File
@@ -14,12 +14,13 @@ import {
import { getGitHubVersion } from "./api-client";
import { CachingKind } from "./caching-utils";
import { getCodeQL } from "./codeql";
import { Config, getConfig } from "./config-utils";
import { type Config, getConfig } from "./config-utils";
import * as debugArtifacts from "./debug-artifacts";
import {
DependencyCachingUsageReport,
getDependencyCacheUsage,
} from "./dependency-caching";
import { EnvVar } from "./environment";
import { Features } from "./feature-flags";
import * as gitUtils from "./git-utils";
import * as initActionPostHelper from "./init-action-post-helper";
@@ -33,6 +34,7 @@ import {
getActionsStatus,
ActionName,
getJobStatusDisplayName,
JobStatus,
} from "./status-report";
import { checkDiskUsage, checkGitHubVersionInRange, wrapError } from "./util";
@@ -85,7 +87,7 @@ async function run(startedAt: Date) {
logger,
);
// If we are analysing the default branch and some kind of caching is enabled,
// If we are analyzing the default branch and some kind of caching is enabled,
// then try to determine our overall cache usage for dependency caches. We only
// do this under these circumstances to avoid slowing down analyses for PRs
// and where caching may not be enabled.
@@ -115,7 +117,7 @@ async function run(startedAt: Date) {
}
return;
}
const jobStatus = initActionPostHelper.getFinalJobStatus();
const jobStatus = getFinalJobStatus(config);
logger.info(`CodeQL job status was ${getJobStatusDisplayName(jobStatus)}.`);
const statusReportBase = await createStatusReportBase(
@@ -130,7 +132,7 @@ async function run(startedAt: Date) {
const statusReport: InitPostStatusReport = {
...statusReportBase,
...uploadFailedSarifResult,
job_status: initActionPostHelper.getFinalJobStatus(),
job_status: jobStatus,
dependency_caching_usage: dependencyCachingUsage,
};
logger.info("Sending status report for init-post step.");
@@ -139,6 +141,72 @@ async function run(startedAt: Date) {
}
}
/**
* Determine the final job status to be reported in the status report.
*
* If the job status has already been set by another step, we use that.
* Otherwise, we determine the job status based on whether the analyze step
* completed successfully and whether we have a valid CodeQL config.
*/
function getFinalJobStatus(config: Config | undefined): JobStatus {
const existingJobStatus = getJobStatusFromEnvironment();
if (existingJobStatus !== undefined) {
return existingJobStatus;
}
let jobStatus: JobStatus;
if (process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] === "true") {
core.exportVariable(EnvVar.JOB_STATUS, JobStatus.SuccessStatus);
jobStatus = JobStatus.SuccessStatus;
} else if (config !== undefined) {
// - We have computed a CodeQL config
// - Analyze didn't complete successfully
// - The job status hasn't already been set to Failure/ConfigurationError
//
// This means that something along the way failed in a step that is not
// owned by the Action, for example a manual build step. We consider this a
// configuration error.
jobStatus = JobStatus.ConfigErrorStatus;
} else {
// If we didn't manage to compute a CodeQL config, it is unclear at this
// point why the analyze Action didn't complete.
// - One possibility is that the workflow run was cancelled. We could
// consider determining workflow cancellation using the GitHub API, but
// for now we treat all these cases as unknown.
// - Another possibility is that we're running a workflow that only runs
// `init`, for instance a workflow that was created before `setup-codeql`
// was available and uses `init` just to set up the CodeQL tools.
jobStatus = JobStatus.UnknownStatus;
}
// This shouldn't be necessary, but in the odd case that we run more than one
// `init` post step, ensure the job status is consistent between them.
core.exportVariable(EnvVar.JOB_STATUS, jobStatus);
return jobStatus;
}
/**
* Get the job status from the environment variable, if it has been set.
*
* If the job status is invalid, return `UnknownStatus`.
*/
function getJobStatusFromEnvironment(): JobStatus | undefined {
const jobStatusFromEnvironment = process.env[EnvVar.JOB_STATUS];
if (jobStatusFromEnvironment !== undefined) {
// Validate the job status from the environment. If it is invalid, return unknown.
if (
Object.values(JobStatus).includes(jobStatusFromEnvironment as JobStatus)
) {
return jobStatusFromEnvironment as JobStatus;
}
return JobStatus.UnknownStatus;
}
return undefined;
}
async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
+82 -18
View File
@@ -2,6 +2,7 @@ import * as fs from "fs";
import * as path from "path";
import * as core from "@actions/core";
import * as github from "@actions/github";
import * as io from "@actions/io";
import * as semver from "semver";
import { v4 as uuidV4 } from "uuid";
@@ -30,18 +31,23 @@ import {
} from "./dependency-caching";
import {
addDiagnostic,
addNoLanguageDiagnostic,
flushDiagnostics,
logUnwrittenDiagnostics,
makeDiagnostic,
makeTelemetryDiagnostic,
} from "./diagnostics";
import { EnvVar } from "./environment";
import { Feature, Features } from "./feature-flags";
import { loadPropertiesFromApi } from "./feature-flags/properties";
import { Feature, FeatureEnablement, Features } from "./feature-flags";
import {
loadPropertiesFromApi,
RepositoryProperties,
} from "./feature-flags/properties";
import {
checkInstallPython311,
checkPacksForOverlayCompatibility,
cleanupDatabaseClusterDirectory,
getFileCoverageInformationEnabled,
initCodeQL,
initConfig,
runDatabaseInitCluster,
@@ -53,7 +59,7 @@ import {
OverlayBaseDatabaseDownloadStats,
OverlayDatabaseMode,
} from "./overlay-database-utils";
import { getRepositoryNwo } from "./repository";
import { getRepositoryNwo, RepositoryNwo } from "./repository";
import { ToolsSource } from "./setup-codeql";
import {
ActionName,
@@ -87,6 +93,8 @@ import {
checkActionVersion,
getErrorMessage,
BuildMode,
GitHubVersion,
Result,
} from "./util";
import { checkWorkflow } from "./workflow";
@@ -237,12 +245,12 @@ async function run(startedAt: Date) {
);
// Fetch the values of known repository properties that affect us.
const enableRepoProps = await features.getValue(
Feature.UseRepositoryProperties,
const repositoryPropertiesResult = await loadRepositoryProperties(
repositoryNwo,
gitHubVersion,
features,
logger,
);
const repositoryProperties = enableRepoProps
? await loadPropertiesFromApi(gitHubVersion, logger, repositoryNwo)
: {};
// Create a unique identifier for this run.
const jobRunUuid = uuidV4();
@@ -334,6 +342,7 @@ async function run(startedAt: Date) {
}
analysisKinds = await getAnalysisKinds(logger);
const debugMode = getOptionalInput("debug") === "true" || core.isDebug();
config = await initConfig(features, {
analysisKinds,
languagesInput: getOptionalInput("languages"),
@@ -350,7 +359,7 @@ async function run(startedAt: Date) {
// - The `init` Action is passed `debug: true`.
// - Actions step debugging is enabled (e.g. by [enabling debug logging for a rerun](https://docs.github.com/en/actions/managing-workflow-runs/re-running-workflows-and-jobs#re-running-all-the-jobs-in-a-workflow),
// or by setting the `ACTIONS_STEP_DEBUG` secret to `true`).
debugMode: getOptionalInput("debug") === "true" || core.isDebug(),
debugMode,
debugArtifactName:
getOptionalInput("debug-artifact-name") || DEFAULT_DEBUG_ARTIFACT_NAME,
debugDatabaseName:
@@ -363,10 +372,28 @@ async function run(startedAt: Date) {
githubVersion: gitHubVersion,
apiDetails,
features,
repositoryProperties,
repositoryProperties: repositoryPropertiesResult.orElse({}),
enableFileCoverageInformation: await getFileCoverageInformationEnabled(
debugMode,
repositoryNwo,
features,
),
logger,
});
if (repositoryPropertiesResult.isFailure()) {
addNoLanguageDiagnostic(
config,
makeTelemetryDiagnostic(
"codeql-action/repository-properties-load-failure",
"Failed to load repository properties",
{
error: getErrorMessage(repositoryPropertiesResult.value),
},
),
);
}
await checkInstallPython311(config.languages, codeql);
} catch (unwrappedError) {
const error = wrapError(unwrappedError);
@@ -429,11 +456,8 @@ async function run(startedAt: Date) {
// Log CodeQL download telemetry, if appropriate
if (toolsDownloadStatusReport) {
addDiagnostic(
addNoLanguageDiagnostic(
config,
// Arbitrarily choose the first language. We could also choose all languages, but that
// increases the risk of misinterpreting the data.
config.languages[0],
makeTelemetryDiagnostic(
"codeql-action/bundle-download-telemetry",
"CodeQL bundle download telemetry",
@@ -775,6 +799,49 @@ async function run(startedAt: Date) {
);
}
/**
* Loads [repository properties](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization) if applicable.
*/
async function loadRepositoryProperties(
repositoryNwo: RepositoryNwo,
gitHubVersion: GitHubVersion,
features: FeatureEnablement,
logger: Logger,
): Promise<Result<RepositoryProperties, unknown>> {
// See if we can skip loading repository properties early. In particular,
// repositories owned by users cannot have repository properties, so we can
// skip the API call entirely in that case.
const repositoryOwnerType = github.context.payload.repository?.owner.type;
logger.debug(
`Repository owner type is '${repositoryOwnerType ?? "unknown"}'.`,
);
if (repositoryOwnerType === "User") {
logger.debug(
"Skipping loading repository properties because the repository is owned by a user and " +
"therefore cannot have repository properties.",
);
return Result.success({});
}
if (!(await features.getValue(Feature.UseRepositoryProperties))) {
logger.debug(
"Skipping loading repository properties because the UseRepositoryProperties feature flag is disabled.",
);
return Result.success({});
}
try {
return Result.success(
await loadPropertiesFromApi(gitHubVersion, logger, repositoryNwo),
);
} catch (error) {
logger.warning(
`Failed to load repository properties: ${getErrorMessage(error)}`,
);
return Result.failure(error);
}
}
function getTrapCachingEnabled(): boolean {
// If the workflow specified something always respect that
const trapCaching = getOptionalInput("trap-caching");
@@ -791,11 +858,8 @@ async function recordZstdAvailability(
config: configUtils.Config,
zstdAvailability: ZstdAvailability,
) {
addDiagnostic(
addNoLanguageDiagnostic(
config,
// Arbitrarily choose the first language. We could also choose all languages, but that
// increases the risk of misinterpreting the data.
config.languages[0],
makeTelemetryDiagnostic(
"codeql-action/zstd-availability",
"Zstandard availability",
+64
View File
@@ -2,14 +2,20 @@ import * as fs from "fs";
import path from "path";
import test, { ExecutionContext } from "ava";
import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import { createStubCodeQL } from "./codeql";
import { Feature } from "./feature-flags";
import {
checkPacksForOverlayCompatibility,
cleanupDatabaseClusterDirectory,
getFileCoverageInformationEnabled,
} from "./init";
import { KnownLanguage } from "./languages";
import { parseRepositoryNwo } from "./repository";
import {
createFeatures,
LoggedMessage,
createTestConfig,
getRecordingLogger,
@@ -442,3 +448,61 @@ test(
expectedResult: true,
},
);
test("file coverage information enabled when debugMode is true", async (t) => {
t.true(
await getFileCoverageInformationEnabled(
true, // debugMode
parseRepositoryNwo("github/codeql-action"),
createFeatures([Feature.SkipFileCoverageOnPrs]),
),
);
});
test("file coverage information enabled when not analyzing a pull request", async (t) => {
sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(false);
t.true(
await getFileCoverageInformationEnabled(
false, // debugMode
parseRepositoryNwo("github/codeql-action"),
createFeatures([Feature.SkipFileCoverageOnPrs]),
),
);
});
test("file coverage information enabled when owner is not 'github'", async (t) => {
sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(true);
t.true(
await getFileCoverageInformationEnabled(
false, // debugMode
parseRepositoryNwo("other-org/some-repo"),
createFeatures([Feature.SkipFileCoverageOnPrs]),
),
);
});
test("file coverage information enabled when feature flag is not enabled", async (t) => {
sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(true);
t.true(
await getFileCoverageInformationEnabled(
false, // debugMode
parseRepositoryNwo("github/codeql-action"),
createFeatures([]),
),
);
});
test("file coverage information disabled when all conditions for skipping are met", async (t) => {
sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(true);
t.false(
await getFileCoverageInformationEnabled(
false, // debugMode
parseRepositoryNwo("github/codeql-action"),
createFeatures([Feature.SkipFileCoverageOnPrs]),
),
);
});
+29 -2
View File
@@ -5,13 +5,22 @@ import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as io from "@actions/io";
import * as yaml from "js-yaml";
import { getOptionalInput, isSelfHostedRunner } from "./actions-util";
import {
getOptionalInput,
isAnalyzingPullRequest,
isSelfHostedRunner,
} from "./actions-util";
import { GitHubApiDetails } from "./api-client";
import { CodeQL, setupCodeQL } from "./codeql";
import * as configUtils from "./config-utils";
import { CodeQLDefaultVersionInfo, FeatureEnablement } from "./feature-flags";
import {
CodeQLDefaultVersionInfo,
Feature,
FeatureEnablement,
} from "./feature-flags";
import { KnownLanguage, Language } from "./languages";
import { Logger, withGroupAsync } from "./logging";
import { RepositoryNwo } from "./repository";
import { ToolsSource } from "./setup-codeql";
import { ZstdAvailability } from "./tar";
import { ToolsDownloadStatusReport } from "./tools-download";
@@ -288,3 +297,21 @@ export function cleanupDatabaseClusterDirectory(
}
}
}
export async function getFileCoverageInformationEnabled(
debugMode: boolean,
repositoryNwo: RepositoryNwo,
features: FeatureEnablement,
): Promise<boolean> {
return (
// Always enable file coverage information in debug mode
debugMode ||
// We're most interested in speeding up PRs, and we want to keep
// submitting file coverage information for the default branch since
// it is used to populate the status page.
!isAnalyzingPullRequest() ||
// For now, restrict this feature to the GitHub org
repositoryNwo.owner !== "github" ||
!(await features.getValue(Feature.SkipFileCoverageOnPrs))
);
}
-1
View File
@@ -34,7 +34,6 @@ const expectedFeatureEnablement: FeatureEnablement = initializeFeatures(
true,
) as FeatureEnablement;
expectedFeatureEnablement.getValue = function (feature: Feature) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return expectedFeatureEnablement[feature];
};
test.beforeEach(() => {
+37 -2
View File
@@ -7,7 +7,12 @@ import { KnownLanguage } from "./languages";
import { getRunnerLogger } from "./logging";
import * as startProxyExports from "./start-proxy";
import { parseLanguage } from "./start-proxy";
import { setupTests } from "./testing-utils";
import {
checkExpectedLogMessages,
getRecordingLogger,
makeTestToken,
setupTests,
} from "./testing-utils";
setupTests(test);
@@ -174,6 +179,37 @@ test("getCredentials throws an error when non-printable characters are used", as
}
});
test("getCredentials logs a warning when a PAT is used without a username", async (t) => {
const loggedMessages = [];
const logger = getRecordingLogger(loggedMessages);
const likelyWrongCredentials = toEncodedJSON([
{
type: "git_server",
host: "https://github.com/",
password: `ghp_${makeTestToken()}`,
},
]);
const results = startProxyExports.getCredentials(
logger,
undefined,
likelyWrongCredentials,
undefined,
);
// The configuration should be accepted, despite the likely problem.
t.assert(results);
t.is(results.length, 1);
t.is(results[0].type, "git_server");
t.is(results[0].host, "https://github.com/");
t.assert(results[0].password?.startsWith("ghp_"));
// A warning should have been logged.
checkExpectedLogMessages(t, loggedMessages, [
"using a GitHub Personal Access Token (PAT), but no username was provided",
]);
});
test("parseLanguage", async (t) => {
// Exact matches
t.deepEqual(parseLanguage("csharp"), KnownLanguage.csharp);
@@ -213,7 +249,6 @@ function mockGetReleaseByTag(assets?: Array<{ name: string; url?: string }>) {
url: "GET /repos/:owner/:repo/releases/tags/:tag",
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
mockClient.returns({
rest: {
repos: {
+21
View File
@@ -1,6 +1,7 @@
import * as core from "@actions/core";
import { getApiClient } from "./api-client";
import * as artifactScanner from "./artifact-scanner";
import * as defaults from "./defaults.json";
import { KnownLanguage } from "./languages";
import { Logger } from "./logging";
@@ -62,6 +63,13 @@ export function parseLanguage(language: string): KnownLanguage | undefined {
return undefined;
}
function isPAT(value: string) {
return artifactScanner.isAuthToken(value, [
artifactScanner.GITHUB_PAT_CLASSIC_PATTERN,
artifactScanner.GITHUB_PAT_FINE_GRAINED_PATTERN,
]);
}
const LANGUAGE_TO_REGISTRY_TYPE: Partial<Record<KnownLanguage, string[]>> = {
java: ["maven_repository"],
csharp: ["nuget_feed"],
@@ -161,6 +169,19 @@ export function getCredentials(
);
}
// If the password or token looks like a GitHub PAT, warn if no username is configured.
if (
!isDefined(e.username) &&
((isDefined(e.password) && isPAT(e.password)) ||
(isDefined(e.token) && isPAT(e.token)))
) {
logger.warning(
`A ${e.type} private registry is configured for ${e.host || e.url} using a GitHub Personal Access Token (PAT), but no username was provided. ` +
`This may not work correctly. When configuring a private registry using a PAT, select "Username and password" and enter the username of the user ` +
`who generated the PAT.`,
);
}
out.push({
type: e.type,
host: e.host,
+7 -1
View File
@@ -269,7 +269,6 @@ export function mockLanguagesInRepo(languages: string[]) {
url: "GET /repos/:owner/:repo/languages",
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
mockClient.returns({
rest: {
repos: {
@@ -408,7 +407,14 @@ export function createTestConfig(overrides: Partial<Config>): Config {
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
repositoryProperties: {},
enableFileCoverageInformation: true,
} satisfies Config,
overrides,
);
}
export function makeTestToken(length: number = 36) {
const chars =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return chars.repeat(Math.ceil(length / chars.length)).slice(0, length);
}
-1
View File
@@ -1057,7 +1057,6 @@ function handleProcessingResultForUnsuccessfulExecution(
status === "failed" &&
Array.isArray(response.data.errors) &&
response.data.errors.length === 1 &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
response.data.errors[0].toString().startsWith("unsuccessful execution")
) {
logger.info(
+25
View File
@@ -563,3 +563,28 @@ test("joinAtMost - truncates list if array is > than limit", (t) => {
t.assert(result.includes("test5"));
t.false(result.includes("test6"));
});
test("Result.success creates a success result", (t) => {
const result = util.Result.success("test value");
t.true(result.isSuccess());
t.false(result.isFailure());
t.is(result.value, "test value");
});
test("Result.failure creates a failure result", (t) => {
const error = new Error("test error");
const result = util.Result.failure(error);
t.false(result.isSuccess());
t.true(result.isFailure());
t.is(result.value, error);
});
test("Result.orElse returns the value for a success result", (t) => {
const result = util.Result.success("success value");
t.is(result.orElse("default value"), "success value");
});
test("Result.orElse returns the default value for a failure result", (t) => {
const result = util.Result.failure(new Error("test error"));
t.is(result.orElse("default value"), "default value");
});
+40
View File
@@ -1292,3 +1292,43 @@ export function joinAtMost(
return array.join(separator);
}
/** A success result. */
type Success<T> = Result<T, never>;
/** A failure result. */
type Failure<E> = Result<never, E>;
/**
* A simple result type representing either a success or a failure.
*/
export class Result<T, E> {
private constructor(
private readonly _ok: boolean,
public readonly value: T | E,
) {}
/** Creates a success result. */
static success<T>(value: T): Success<T> {
return new Result(true, value) as Success<T>;
}
/** Creates a failure result. */
static failure<E>(value: E): Failure<E> {
return new Result(false, value) as Failure<E>;
}
/** Whether this result represents a success. */
isSuccess(): this is Success<T> {
return this._ok;
}
/** Whether this result represents a failure. */
isFailure(): this is Failure<E> {
return !this._ok;
}
/** Get the value if this is a success, or return the default value if this is a failure. */
orElse<U>(defaultValue: U): T | U {
return this.isSuccess() ? this.value : defaultValue;
}
}
+4
View File
@@ -25,8 +25,12 @@
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"baseUrl": ".", /* Base directory to resolve non-absolute module names. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"resolveJsonModule": true,
"paths": {
"@octokit/core/types": ["./node_modules/@octokit/core/dist-types/types.d.ts"]
},
},
"exclude": ["node_modules"]
}