Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:
- '**/*.tsx'
- package.json
- package-lock.json
- .github/workflows/codeql.yml
- '.github/workflows/**'
pull_request:
branches: [main]
paths:
Expand All @@ -24,31 +24,40 @@ on:
- '**/*.tsx'
- package.json
- package-lock.json
- .github/workflows/codeql.yml
- '.github/workflows/**'
schedule:
- cron: '31 7 * * 1'

permissions:
contents: read

# Not a required status check, so it is safe to skip entirely (rather than
# just cancel) when a PR touches no JavaScript/TypeScript source: an
# unmatched path means the workflow never starts, and nothing here gates the
# merge queue on it. The weekly schedule run still scans the full tree
# regardless of what changed most recently.
# just cancel) when a PR touches neither JavaScript/TypeScript source nor a
# workflow: an unmatched path means the workflow never starts, and nothing
# here gates the merge queue on it. The weekly schedule run still scans the
# full tree regardless of what changed most recently.
concurrency:
group: codeql-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
analyze:
name: Analyze JavaScript
name: Analyze ${{ matrix.language }}
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
security-events: write
actions: read
strategy:
# One language failing must not hide the other language's results.
fail-fast: false
matrix:
# `actions` analyses the workflow files themselves: script injection
# through ${{ github.event.* }} in `run:` blocks, over-broad
# GITHUB_TOKEN permissions, un-pinned third-party actions. Nothing else
# in this repository reads the workflows statically.
language: ['javascript-typescript', 'actions']
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -57,9 +66,9 @@ jobs:
- name: Initialize CodeQL
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
languages: javascript-typescript
languages: ${{ matrix.language }}
build-mode: none
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
category: '/language:javascript-typescript'
category: '/language:${{ matrix.language }}'
65 changes: 53 additions & 12 deletions tests/codeql-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,7 @@ test('required returns a present value and names the fault in the rest', () => {
});

test('the analyze job may upload its results to the security tab', () => {
const jobs = Object.entries(required(workflow.jobs, 'jobs'));
assert.equal(jobs.length, 1, 'codeql.yml declares more than one job');
const [jobName, job] = jobs[0];
const [jobName, job] = analyzeJob();
const permissions = required(
job.permissions,
`permissions for job ${jobName}`,
Expand All @@ -125,18 +123,58 @@ test('init and analyze come from the same codeql-action release', () => {
);
});

test('the analysis declares a language set and the category that names it', () => {
// The languages CodeQL must analyse here. `javascript-typescript` is the site
// and its scripts; `actions` is the workflow files themselves, which nothing
// else in this repository reads statically. Dropping either from the matrix
// silently narrows the scan, so both are pinned by name.
const REQUIRED_LANGUAGES = ['actions', 'javascript-typescript'];

function analyzeJob() {
const jobs = Object.entries(required(workflow.jobs, 'jobs'));
assert.equal(jobs.length, 1, 'codeql.yml declares more than one job');
return jobs[0];
}

test('the analysis fans out over every required language', () => {
const [jobName, job] = analyzeJob();
const strategy = required(job.strategy, `strategy for job ${jobName}`);
const languages = required(
required(strategy.matrix, `matrix for job ${jobName}`).language,
`matrix.language for job ${jobName}`,
);
assert.ok(
Array.isArray(languages),
`codeql.yml matrix.language is not a list: ${JSON.stringify(languages)}`,
);
const missing = REQUIRED_LANGUAGES.filter(
(language) => !languages.includes(language),
);
assert.deepEqual(
missing,
[],
`codeql.yml matrix omits languages the scan must cover: ${missing.join(', ')}`,
);
// A failure in one language's analysis must not cancel the other's upload.
assert.equal(
strategy['fail-fast'],
false,
'codeql.yml matrix does not set fail-fast: false, so one language failing hides the other',
);
});

test('init and analyze read the language from the matrix and name it in the category', () => {
const languages = required(
required(codeqlStep('init').with, '`with` block for codeql-action/init')
.languages,
'languages for codeql-action/init',
);
assert.equal(
typeof languages,
'string',
'codeql-action/init declares no languages',
languages,
'${{ matrix.language }}',
`codeql-action/init languages ${JSON.stringify(
languages,
)} does not read the matrix, so the fan-out above analyses nothing`,
);
assert.ok(languages.length > 0, 'codeql-action/init languages is empty');
const category = required(
codeqlStep('analyze').with,
'`with` block for codeql-action/analyze',
Expand All @@ -146,7 +184,7 @@ test('the analysis declares a language set and the category that names it', () =
`/language:${languages}`,
`codeql-action/analyze category ${JSON.stringify(
category,
)} does not name the analysed languages ${JSON.stringify(languages)}`,
)} does not name the analysed language ${JSON.stringify(languages)}`,
);
});

Expand Down Expand Up @@ -179,11 +217,14 @@ test('every scanned source extension in the tree is matched by the path filters'
);
});

test('a change to the scanner configuration triggers a scan', () => {
test('a change to any workflow, including the scanner itself, triggers a scan', () => {
// `actions` analyses the workflow files, so the filter has to match all of
// them rather than only codeql.yml; the glob also covers codeql.yml, so
// narrowing the scan still runs it.
const paths = pathFilters('push');
assert.ok(
paths.includes('.github/workflows/codeql.yml'),
'codeql.yml path filters omit codeql.yml, so narrowing the scan does not run it',
paths.includes('.github/workflows/**'),
'codeql.yml path filters omit .github/workflows/**, so a workflow edit is never analysed and narrowing the scan does not run it',
);
});

Expand Down
Loading