@sasjs/lint 4.3.2 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/package.json +22 -3
- package/rules/file/hasDoxygenHeader.d.ts +0 -3
- package/rules/file/hasDoxygenHeader.js +88 -37
- package/rules/file/hasDoxygenHeader.js.map +1 -1
- package/rules/file/index.d.ts +1 -0
- package/rules/file/index.js +3 -1
- package/rules/file/index.js.map +1 -1
- package/rules/file/noNestedCommentsInHeader.d.ts +6 -0
- package/rules/file/noNestedCommentsInHeader.js +105 -0
- package/rules/file/noNestedCommentsInHeader.js.map +1 -0
- package/sasjslint-schema.json +17 -0
- package/types/LintConfig.js +3 -0
- package/types/LintConfig.js.map +1 -1
- package/utils/getLintConfig.d.ts +1 -0
- package/utils/getLintConfig.js +1 -0
- package/utils/getLintConfig.js.map +1 -1
- package/utils/getPosition.d.ts +14 -0
- package/utils/getPosition.js +25 -0
- package/utils/getPosition.js.map +1 -0
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/index.js.map +1 -1
- package/.all-contributorsrc +0 -123
- package/.git-hooks/commit-msg +0 -18
- package/.git-hooks/pre-commit +0 -18
- package/.github/CONTRIBUTING.md +0 -201
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -21
- package/.github/dependabot.yml +0 -7
- package/.github/workflows/audit-watch.yml +0 -68
- package/.github/workflows/build.yml +0 -66
- package/.github/workflows/mutation-pr.yml +0 -69
- package/.github/workflows/mutation-weekly.yml +0 -92
- package/.github/workflows/publish.yml +0 -169
- package/.gitpod.yml +0 -8
- package/.pre-commit-config.yaml +0 -16
- package/.prettierrc +0 -6
- package/.sasjslint +0 -13
- package/jest.config.js +0 -62
- package/jest.stryker.config.js +0 -17
- package/mutation-baseline.json +0 -30
- package/scripts/audit-watch.mjs +0 -249
- package/scripts/generate-macro-names.mjs +0 -101
- package/scripts/mutation-gate.mjs +0 -390
- package/stryker.conf.json +0 -19
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
name: Mutation Watch
|
|
2
|
-
|
|
3
|
-
# The per-PR mutation gate compares a branch's changed files with a baseline, so
|
|
4
|
-
# the baseline is the thing that has to stay honest: if it is never refreshed,
|
|
5
|
-
# files that improve keep their old count and the gate stops asking for more.
|
|
6
|
-
#
|
|
7
|
-
# This runs the whole source tree weekly, prints the new baseline, and fails when
|
|
8
|
-
# any file carries MORE surviving mutants than the recorded one - which means a
|
|
9
|
-
# PR was merged without the gate seeing it (a push to main, a config change, a
|
|
10
|
-
# file the gate skipped). Failure files an issue, as audit-watch.yml does, and
|
|
11
|
-
# only a genuine regression does so.
|
|
12
|
-
|
|
13
|
-
on:
|
|
14
|
-
schedule:
|
|
15
|
-
- cron: '0 7 * * 1' # Mondays, 07:00 UTC
|
|
16
|
-
workflow_dispatch:
|
|
17
|
-
|
|
18
|
-
jobs:
|
|
19
|
-
watch:
|
|
20
|
-
runs-on: ubuntu-latest
|
|
21
|
-
# the whole tree takes ~20-30 minutes; bounded so a hang cannot hold a runner
|
|
22
|
-
# for the six-hour default
|
|
23
|
-
timeout-minutes: 90
|
|
24
|
-
permissions:
|
|
25
|
-
contents: read
|
|
26
|
-
issues: write
|
|
27
|
-
|
|
28
|
-
steps:
|
|
29
|
-
- uses: actions/checkout@v4
|
|
30
|
-
|
|
31
|
-
- name: Use Node.js
|
|
32
|
-
uses: actions/setup-node@v4
|
|
33
|
-
with:
|
|
34
|
-
node-version: 22
|
|
35
|
-
cache: npm
|
|
36
|
-
|
|
37
|
-
- name: Install Dependencies
|
|
38
|
-
run: npm ci
|
|
39
|
-
|
|
40
|
-
- name: Mutate the whole source tree
|
|
41
|
-
id: watch
|
|
42
|
-
env:
|
|
43
|
-
MUTATION_WATCH_REPORT: ${{ runner.temp }}/mutation-watch-report.md
|
|
44
|
-
run: node scripts/mutation-gate.mjs weekly
|
|
45
|
-
|
|
46
|
-
- name: Keep the report
|
|
47
|
-
if: always()
|
|
48
|
-
uses: actions/upload-artifact@v4
|
|
49
|
-
with:
|
|
50
|
-
name: mutation-watch-report
|
|
51
|
-
path: |
|
|
52
|
-
reports/mutation/mutation.json
|
|
53
|
-
${{ runner.temp }}/mutation-watch-report.md
|
|
54
|
-
if-no-files-found: ignore
|
|
55
|
-
|
|
56
|
-
report:
|
|
57
|
-
needs: watch
|
|
58
|
-
# a failure files an issue; a cancelled run does so only when the schedule
|
|
59
|
-
# cancelled it (the 90-minute timeout), so cancelling a manual run by hand
|
|
60
|
-
# does not claim a regression that never happened
|
|
61
|
-
if: >-
|
|
62
|
-
always() &&
|
|
63
|
-
(needs.watch.result == 'failure' ||
|
|
64
|
-
(needs.watch.result == 'cancelled' && github.event_name == 'schedule'))
|
|
65
|
-
runs-on: ubuntu-latest
|
|
66
|
-
timeout-minutes: 5
|
|
67
|
-
permissions:
|
|
68
|
-
contents: read
|
|
69
|
-
issues: write
|
|
70
|
-
|
|
71
|
-
steps:
|
|
72
|
-
- name: File an issue
|
|
73
|
-
env:
|
|
74
|
-
GH_TOKEN: ${{ github.token }}
|
|
75
|
-
run: |
|
|
76
|
-
title='Mutation watch: a file carries more surviving mutants than the baseline'
|
|
77
|
-
existing=$(gh issue list --state open --search "\"$title\" in:title" \
|
|
78
|
-
--json number --jq '.[0].number // empty')
|
|
79
|
-
|
|
80
|
-
if [ -n "$existing" ]; then
|
|
81
|
-
echo "already tracked in #$existing"
|
|
82
|
-
exit 0
|
|
83
|
-
fi
|
|
84
|
-
|
|
85
|
-
gh issue create --title "$title" --body \
|
|
86
|
-
"The weekly mutation run found a file carrying more surviving mutants than mutation-baseline.json records, so behaviour was merged without a test that fails on it.
|
|
87
|
-
|
|
88
|
-
The run names the file and each surviving mutant (file:line:column, mutator, replacement), and prints the refreshed baseline:
|
|
89
|
-
|
|
90
|
-
$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID
|
|
91
|
-
|
|
92
|
-
Close this once a spec kills them and mutation-baseline.json is updated from that run."
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
|
|
2
|
-
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
|
|
3
|
-
|
|
4
|
-
name: SASjs Lint Build and Publish
|
|
5
|
-
|
|
6
|
-
on:
|
|
7
|
-
push:
|
|
8
|
-
branches:
|
|
9
|
-
- main
|
|
10
|
-
workflow_dispatch:
|
|
11
|
-
|
|
12
|
-
# One release at a time. Both triggers publish, and the push trigger fires on
|
|
13
|
-
# every release commit @semantic-release/git pushes back to main, so a
|
|
14
|
-
# dispatched recovery run can interleave with a push-triggered one. Two
|
|
15
|
-
# semantic-release processes racing the same tag and the same npm publish is a
|
|
16
|
-
# new way to strand a release - the state the retry below exists to recover.
|
|
17
|
-
# `cancel-in-progress: false` queues the second run rather than cancelling the
|
|
18
|
-
# first, because cancelling a run midway through a release is itself a way to
|
|
19
|
-
# leave a tag pushed and the release missing.
|
|
20
|
-
concurrency:
|
|
21
|
-
group: publish
|
|
22
|
-
cancel-in-progress: false
|
|
23
|
-
|
|
24
|
-
jobs:
|
|
25
|
-
build:
|
|
26
|
-
runs-on: ubuntu-latest
|
|
27
|
-
permissions:
|
|
28
|
-
contents: write
|
|
29
|
-
id-token: write # required for npm provenance (trusted publisher)
|
|
30
|
-
issues: write # optional: lets @semantic-release/github comment on issues
|
|
31
|
-
pull-requests: write # optional: lets @semantic-release/github comment on PRs
|
|
32
|
-
|
|
33
|
-
strategy:
|
|
34
|
-
matrix:
|
|
35
|
-
node-version: [22]
|
|
36
|
-
|
|
37
|
-
steps:
|
|
38
|
-
# Mint a short-lived GitHub App token (bypass-capable)
|
|
39
|
-
- name: Generate token
|
|
40
|
-
id: app-token
|
|
41
|
-
uses: actions/create-github-app-token@v1
|
|
42
|
-
with:
|
|
43
|
-
app-id: ${{ secrets.APP_ID }}
|
|
44
|
-
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
45
|
-
|
|
46
|
-
# Checkout using the app token so the release commit can be pushed
|
|
47
|
-
- uses: actions/checkout@v4
|
|
48
|
-
with:
|
|
49
|
-
token: ${{ steps.app-token.outputs.token }}
|
|
50
|
-
persist-credentials: true
|
|
51
|
-
fetch-depth: 0 # semantic-release needs full history + tags
|
|
52
|
-
|
|
53
|
-
- name: Use Node.js ${{ matrix.node-version }}
|
|
54
|
-
uses: actions/setup-node@v4
|
|
55
|
-
with:
|
|
56
|
-
node-version: ${{ matrix.node-version }}
|
|
57
|
-
registry-url: https://registry.npmjs.org
|
|
58
|
-
|
|
59
|
-
# Node 22 ships npm 10.x — OIDC trusted publishing needs npm >= 11.5.1
|
|
60
|
-
- name: Update npm
|
|
61
|
-
run: npm install -g npm@latest
|
|
62
|
-
|
|
63
|
-
- name: Install Dependencies
|
|
64
|
-
run: npm ci
|
|
65
|
-
|
|
66
|
-
- name: Check Code Style
|
|
67
|
-
run: npm run lint
|
|
68
|
-
|
|
69
|
-
- name: Run Unit Tests
|
|
70
|
-
run: npm test
|
|
71
|
-
|
|
72
|
-
- name: Build Project
|
|
73
|
-
run: npm run build
|
|
74
|
-
|
|
75
|
-
- name: Clean up ready for publishing
|
|
76
|
-
run: npm run publishInit
|
|
77
|
-
|
|
78
|
-
- name: Semantic Release
|
|
79
|
-
# The release toolchain is deliberately not a project dependency: every
|
|
80
|
-
# @semantic-release plugin declares a `semantic-release` peer, which npm
|
|
81
|
-
# auto-installs together with the `npm` package semantic-release bundles
|
|
82
|
-
# - and that bundled npm carries advisories nothing here can fix.
|
|
83
|
-
# Installing it here, pinned, keeps the dependency tree clean. Keep
|
|
84
|
-
# these versions in step with `release.plugins` in package.json.
|
|
85
|
-
#
|
|
86
|
-
# semantic-release pushes twice: @semantic-release/git pushes the release
|
|
87
|
-
# commit to the branch, then core pushes the tag in a second, tags-only
|
|
88
|
-
# push. When that second push is rejected the branch keeps the release
|
|
89
|
-
# commit - package.json at the new version - with no tag: no GitHub
|
|
90
|
-
# release, no npm publish, and every later run dying in the exec plugin's
|
|
91
|
-
# `npm version` with "Version not changed", a state only a hand repair
|
|
92
|
-
# clears. sasjs/adapter hit exactly that on 2026-10-17.
|
|
93
|
-
#
|
|
94
|
-
# The tag push runs after the tag exists locally and after the branch is
|
|
95
|
-
# updated, so one more attempt has only the tag to push. Retry exactly
|
|
96
|
-
# that case: a local tag alone does not identify it, because the tag is
|
|
97
|
-
# also there when the failure came later (the GitHub release), and
|
|
98
|
-
# retrying that finds the tag restored by semantic-release's own fetch,
|
|
99
|
-
# decides there is nothing to release, exits 0 and leaves the step green
|
|
100
|
-
# with the release still missing - so require the tag to be absent from
|
|
101
|
-
# the remote as well.
|
|
102
|
-
#
|
|
103
|
-
# Only a definitive answer counts. `git ls-remote --exit-code` exits 2 for
|
|
104
|
-
# an absent ref but 128 when the query itself fails (network, auth,
|
|
105
|
-
# unreachable remote), and treating every non-zero exit as "absent" would
|
|
106
|
-
# retry on the transient GitHub-side failures that are the very class of
|
|
107
|
-
# incident this retry repairs. So 2 retries, 0 and anything else fail the
|
|
108
|
-
# step.
|
|
109
|
-
run: |
|
|
110
|
-
run_release() {
|
|
111
|
-
npx --yes \
|
|
112
|
-
--package semantic-release@25.0.9 \
|
|
113
|
-
--package @semantic-release/exec@7.1.0 \
|
|
114
|
-
--package @semantic-release/git@11.0.1 \
|
|
115
|
-
semantic-release
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if run_release; then
|
|
119
|
-
exit 0
|
|
120
|
-
fi
|
|
121
|
-
|
|
122
|
-
version=$(node -p "require('./package.json').version")
|
|
123
|
-
|
|
124
|
-
if ! git rev-parse -q --verify "refs/tags/v$version" >/dev/null; then
|
|
125
|
-
echo "No local v$version tag, so the failure came before the tag was created - not retrying." >&2
|
|
126
|
-
exit 1
|
|
127
|
-
fi
|
|
128
|
-
|
|
129
|
-
ls_rc=0
|
|
130
|
-
git ls-remote --exit-code --tags origin "refs/tags/v$version" >/dev/null 2>&1 || ls_rc=$?
|
|
131
|
-
|
|
132
|
-
if [ "$ls_rc" -eq 0 ]; then
|
|
133
|
-
echo "v$version is already on the remote, so the failure came after the tag push - not retrying." >&2
|
|
134
|
-
exit 1
|
|
135
|
-
fi
|
|
136
|
-
|
|
137
|
-
if [ "$ls_rc" -ne 2 ]; then
|
|
138
|
-
echo "Could not establish whether v$version is on the remote (git ls-remote exited $ls_rc) - not retrying." >&2
|
|
139
|
-
exit 1
|
|
140
|
-
fi
|
|
141
|
-
|
|
142
|
-
echo "Retrying: v$version exists locally but not on the remote, so the tag push is what failed."
|
|
143
|
-
git tag -d "v$version"
|
|
144
|
-
run_release
|
|
145
|
-
env:
|
|
146
|
-
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} # <-- app token, not secrets.GITHUB_TOKEN
|
|
147
|
-
|
|
148
|
-
# semantic-release only releases when it finds a releasable commit, and the
|
|
149
|
-
# angular convention this repo is configured with does not treat `chore:` or
|
|
150
|
-
# `docs:` as one. This step ran on `if: success()` regardless, so a merge with
|
|
151
|
-
# no releasable commit published the version already in package.json, which
|
|
152
|
-
# npm rejects with "You cannot publish over the previously published
|
|
153
|
-
# versions". Compare against the registry instead, and publish only when
|
|
154
|
-
# there is something new.
|
|
155
|
-
- name: Publish to npm with trusted publisher
|
|
156
|
-
id: publish
|
|
157
|
-
if: success()
|
|
158
|
-
run: |
|
|
159
|
-
local_version=$(node -p "require('./package.json').version")
|
|
160
|
-
published_version=$(npm view @sasjs/lint version 2>/dev/null || echo "")
|
|
161
|
-
|
|
162
|
-
if [ "$local_version" = "$published_version" ]; then
|
|
163
|
-
echo "released=false" >> "$GITHUB_OUTPUT"
|
|
164
|
-
echo "Nothing to publish - $local_version is already on npm"
|
|
165
|
-
exit 0
|
|
166
|
-
fi
|
|
167
|
-
|
|
168
|
-
npm publish --access public
|
|
169
|
-
echo "released=true" >> "$GITHUB_OUTPUT"
|
package/.gitpod.yml
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
# This configuration file was automatically generated by Gitpod.
|
|
2
|
-
# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
|
|
3
|
-
# and commit this file to your remote git repository to share the goodness with others.
|
|
4
|
-
|
|
5
|
-
tasks:
|
|
6
|
-
- init: npm install && npm run build
|
|
7
|
-
|
|
8
|
-
|
package/.pre-commit-config.yaml
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# Hooks for the pre-commit framework - https://pre-commit.com
|
|
2
|
-
#
|
|
3
|
-
# The repo also ships native hooks in .git-hooks/, covering the secret scan, a
|
|
4
|
-
# 2MB commit size limit and Conventional Commits. But .npmrc sets
|
|
5
|
-
# ignore-scripts=true, so the `prepare` script that would activate them never
|
|
6
|
-
# runs on `npm i` - a fresh clone commits unchecked until the one-time command
|
|
7
|
-
# in .github/CONTRIBUTING.md is run by hand.
|
|
8
|
-
#
|
|
9
|
-
# This file is the standard route instead: install the framework once with
|
|
10
|
-
# `pre-commit install`, and the secret scan runs on every commit. The hook is
|
|
11
|
-
# pinned to an exact gitleaks release tag.
|
|
12
|
-
repos:
|
|
13
|
-
- repo: https://github.com/gitleaks/gitleaks
|
|
14
|
-
rev: v8.30.1
|
|
15
|
-
hooks:
|
|
16
|
-
- id: gitleaks
|
package/.prettierrc
DELETED
package/.sasjslint
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"noTrailingSpaces": true,
|
|
3
|
-
"noEncodedPasswords": true,
|
|
4
|
-
"hasDoxygenHeader": true,
|
|
5
|
-
"noSpacesInFileNames": true,
|
|
6
|
-
"maxLineLength": 80,
|
|
7
|
-
"lowerCaseFileNames": true,
|
|
8
|
-
"noTabIndentation": true,
|
|
9
|
-
"indentationMultiple": 2,
|
|
10
|
-
"hasMacroNameInMend": true,
|
|
11
|
-
"noNestedMacros": true,
|
|
12
|
-
"hasMacroParentheses": true
|
|
13
|
-
}
|
package/jest.config.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
preset: 'ts-jest',
|
|
3
|
-
testEnvironment: 'node',
|
|
4
|
-
// A Stryker run keeps a sandbox under .stryker-tmp with a copy of src/ in it.
|
|
5
|
-
// Without this, any jest run that overlaps a mutation run collects every spec
|
|
6
|
-
// twice (measured: 946 tests instead of 464) and reports failures from files
|
|
7
|
-
// that are being mutated. Anchored to <rootDir> on purpose: inside the sandbox
|
|
8
|
-
// the whole project IS under .stryker-tmp, and an unanchored pattern would make
|
|
9
|
-
// Stryker's own dry run collect nothing ("No tests were executed").
|
|
10
|
-
testPathIgnorePatterns: [
|
|
11
|
-
'/node_modules/',
|
|
12
|
-
'<rootDir>/.stryker-tmp/',
|
|
13
|
-
'<rootDir>/build/'
|
|
14
|
-
],
|
|
15
|
-
coverageThreshold: {
|
|
16
|
-
global: {
|
|
17
|
-
branches: 80,
|
|
18
|
-
functions: 80,
|
|
19
|
-
lines: 80,
|
|
20
|
-
statements: -10
|
|
21
|
-
},
|
|
22
|
-
// Per-path floors, so a change that hollows out one area cannot hide behind
|
|
23
|
-
// the aggregate. Every number is the coverage measured when this was written,
|
|
24
|
-
// rounded down, so the block is a ratchet: raise one when the area improves,
|
|
25
|
-
// never lower one to get a branch green. `statements` is a PERCENTAGE unless
|
|
26
|
-
// it is negative, where `-N` reads as "at most N uncovered statements" - so a
|
|
27
|
-
// plain `0` asserts nothing at all (0% coverage passes), which is why the two
|
|
28
|
-
// fully-covered areas ask for 100 and the three with known gaps carry a budget
|
|
29
|
-
// measured from the tree on the day this landed.
|
|
30
|
-
'./src/utils/': {
|
|
31
|
-
branches: 95,
|
|
32
|
-
functions: 100,
|
|
33
|
-
lines: 99,
|
|
34
|
-
statements: -4
|
|
35
|
-
},
|
|
36
|
-
'./src/rules/': {
|
|
37
|
-
branches: 95,
|
|
38
|
-
functions: 100,
|
|
39
|
-
lines: 100,
|
|
40
|
-
statements: 100
|
|
41
|
-
},
|
|
42
|
-
'./src/lint/': {
|
|
43
|
-
branches: 85,
|
|
44
|
-
functions: 100,
|
|
45
|
-
lines: 100,
|
|
46
|
-
statements: -2
|
|
47
|
-
},
|
|
48
|
-
'./src/format/': {
|
|
49
|
-
branches: 90,
|
|
50
|
-
functions: 100,
|
|
51
|
-
lines: 100,
|
|
52
|
-
statements: 100
|
|
53
|
-
},
|
|
54
|
-
'./src/types/': {
|
|
55
|
-
branches: 95,
|
|
56
|
-
functions: 100,
|
|
57
|
-
lines: 96,
|
|
58
|
-
statements: -4
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
collectCoverageFrom: ['src/**/{!(index|formatExample|lintExample),}.ts']
|
|
62
|
-
}
|
package/jest.stryker.config.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Jest config for Stryker runs.
|
|
3
|
-
*
|
|
4
|
-
* The only difference from jest.config.js is that coverage is off: Stryker
|
|
5
|
-
* instruments the code itself, and the coverage thresholds would fail a
|
|
6
|
-
* single-mutant run. The suite itself runs in full, so the weekly baseline is
|
|
7
|
-
* built on every test - a skipped spec here would record mutants it covers as
|
|
8
|
-
* legitimate survivors and let them into the baseline permanently.
|
|
9
|
-
*/
|
|
10
|
-
const base = require('./jest.config.js')
|
|
11
|
-
|
|
12
|
-
module.exports = {
|
|
13
|
-
...base,
|
|
14
|
-
collectCoverage: false,
|
|
15
|
-
coverageThreshold: undefined,
|
|
16
|
-
collectCoverageFrom: undefined
|
|
17
|
-
}
|
package/mutation-baseline.json
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_comment": "Surviving-mutant counts per file, used by scripts/mutation-gate.mjs. Refresh from the output of npm run mutation:watch; raise a count only with a reason in the PR that raises it.",
|
|
3
|
-
"generatedAt": "2026-10-25T20:06:38Z",
|
|
4
|
-
"files": {
|
|
5
|
-
"src/rules/file/noUnusedMacros.ts": {
|
|
6
|
-
"score": 100.0,
|
|
7
|
-
"survived": 0,
|
|
8
|
-
"killed": 37,
|
|
9
|
-
"timeout": 0
|
|
10
|
-
},
|
|
11
|
-
"src/utils/getIndicesOf.ts": {
|
|
12
|
-
"score": 100.0,
|
|
13
|
-
"survived": 0,
|
|
14
|
-
"killed": 15,
|
|
15
|
-
"timeout": 6
|
|
16
|
-
},
|
|
17
|
-
"src/utils/isIgnored.ts": {
|
|
18
|
-
"score": 100.0,
|
|
19
|
-
"survived": 0,
|
|
20
|
-
"killed": 25,
|
|
21
|
-
"timeout": 0
|
|
22
|
-
},
|
|
23
|
-
"src/utils/parseMacroCalls.ts": {
|
|
24
|
-
"score": 92.98,
|
|
25
|
-
"survived": 25,
|
|
26
|
-
"killed": 293,
|
|
27
|
-
"timeout": 38
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
package/scripts/audit-watch.mjs
DELETED
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Watches the dev-tree audit exemption declared in `.github/workflows/build.yml`.
|
|
3
|
-
*
|
|
4
|
-
* That workflow gates on `npm audit --omit=dev` because the dev tree carries one
|
|
5
|
-
* advisory nothing in this repo can fix: GHSA-hp3w-g68c-fv3c (sprintf-js,
|
|
6
|
-
* reached through @jest/transform -> babel-plugin-istanbul ->
|
|
7
|
-
* @istanbuljs/load-nyc-config -> js-yaml 3 -> argparse). Every package in that
|
|
8
|
-
* chain is at its latest published version and the advisory lists no patched
|
|
9
|
-
* release, so gating on it would mean a permanently red build.
|
|
10
|
-
*
|
|
11
|
-
* An exemption with no expiry is how a scoped gate becomes a permanent one, so
|
|
12
|
-
* this script re-checks it weekly and reports when it is out of date:
|
|
13
|
-
*
|
|
14
|
-
* - a dev-tree advisory outside EXEMPTIONS appears - the exemption is too narrow
|
|
15
|
-
* - an EXEMPTIONS advisory no longer appears in the tree - the exemption is dead
|
|
16
|
-
* - a WATCHED_FOR_RELEASE package publishes a version newer than the one
|
|
17
|
-
* recorded here - a fix may now exist, so re-evaluate and drop the exemption
|
|
18
|
-
* - an EXEMPTIONS id is missing from the build.yml comment - the gate and this
|
|
19
|
-
* script disagree about what is exempt
|
|
20
|
-
*
|
|
21
|
-
* Any of those exits 1, failing the scheduled run and making the workflow file an
|
|
22
|
-
* issue. The known advisory on its own does NOT exit 1: it is expected, and a job
|
|
23
|
-
* that is red every week trains people to ignore red.
|
|
24
|
-
*
|
|
25
|
-
* Run with `node scripts/audit-watch.mjs`.
|
|
26
|
-
*/
|
|
27
|
-
import { execFileSync } from 'node:child_process'
|
|
28
|
-
import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'
|
|
29
|
-
import path from 'node:path'
|
|
30
|
-
import { fileURLToPath } from 'node:url'
|
|
31
|
-
|
|
32
|
-
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
33
|
-
const BUILD_WORKFLOW = path.join('.github', 'workflows', 'build.yml')
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Advisories the dev-scoped gate in build.yml is allowed to leave unaddressed.
|
|
37
|
-
* Each one must also be named in that workflow's comment - the check below
|
|
38
|
-
* enforces it, so the gate and this list cannot drift apart.
|
|
39
|
-
*/
|
|
40
|
-
const EXEMPTIONS = ['GHSA-hp3w-g68c-fv3c']
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Packages whose published version decides whether an exemption is still
|
|
44
|
-
* justified. `newestKnown` is the newest release at the time the exemption was
|
|
45
|
-
* written; a newer one on the registry means a fix may exist and the exemption
|
|
46
|
-
* deserves a fresh look rather than another year of silence.
|
|
47
|
-
*/
|
|
48
|
-
const WATCHED_FOR_RELEASE = [{ name: 'sprintf-js', newestKnown: '1.1.3' }]
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* `npm audit --json` exits 1 whenever it reports anything, so the exit code says
|
|
52
|
-
* nothing about whether the command worked - parse the report instead.
|
|
53
|
-
*/
|
|
54
|
-
const runAudit = () => {
|
|
55
|
-
let stdout
|
|
56
|
-
try {
|
|
57
|
-
stdout = execFileSync('npm', ['audit', '--json'], {
|
|
58
|
-
encoding: 'utf8',
|
|
59
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
60
|
-
})
|
|
61
|
-
} catch (error) {
|
|
62
|
-
stdout = error.stdout
|
|
63
|
-
}
|
|
64
|
-
if (!stdout || !stdout.trim()) {
|
|
65
|
-
throw new Error('npm audit produced no JSON - run `npm ci` first')
|
|
66
|
-
}
|
|
67
|
-
return JSON.parse(stdout)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* The advisories the report names, keyed by GHSA id. A `via` entry is a bare
|
|
72
|
-
* package name when the advisory is inherited from a dependency, and an object
|
|
73
|
-
* carrying the GHSA url when it is the root of the chain - only the latter are
|
|
74
|
-
* advisories in their own right.
|
|
75
|
-
*/
|
|
76
|
-
const collectAdvisories = (report) => {
|
|
77
|
-
const advisories = new Map()
|
|
78
|
-
|
|
79
|
-
for (const [packageName, info] of Object.entries(report?.vulnerabilities ?? {})) {
|
|
80
|
-
for (const via of info?.via ?? []) {
|
|
81
|
-
if (typeof via !== 'object' || via === null) continue
|
|
82
|
-
const id = String(via.url ?? '').split('/').pop()
|
|
83
|
-
if (!id?.startsWith('GHSA-')) continue
|
|
84
|
-
|
|
85
|
-
const entry = advisories.get(id) ?? {
|
|
86
|
-
severity: via.severity,
|
|
87
|
-
title: via.title,
|
|
88
|
-
packages: []
|
|
89
|
-
}
|
|
90
|
-
entry.packages.push(packageName)
|
|
91
|
-
advisories.set(id, entry)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return advisories
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** The version the registry currently resolves `latest` to, or null if offline. */
|
|
99
|
-
const latestPublished = (name) => {
|
|
100
|
-
try {
|
|
101
|
-
return execFileSync('npm', ['view', name, 'version'], {
|
|
102
|
-
encoding: 'utf8',
|
|
103
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
104
|
-
}).trim()
|
|
105
|
-
} catch {
|
|
106
|
-
return null
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Numeric dotted comparison, so `1.1.10` sorts above `1.1.9`. A prerelease
|
|
112
|
-
* suffix is dropped from both sides first: `1.1.3-beta.1` is not newer than
|
|
113
|
-
* `1.1.3`, and a prerelease that happens to be tagged `latest` must not raise
|
|
114
|
-
* the alarm by itself.
|
|
115
|
-
*/
|
|
116
|
-
const isNewer = (candidate, baseline) => {
|
|
117
|
-
const a = candidate.split('-')[0].split('.')
|
|
118
|
-
const b = baseline.split('-')[0].split('.')
|
|
119
|
-
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
120
|
-
const left = a[i] ?? '0'
|
|
121
|
-
const right = b[i] ?? '0'
|
|
122
|
-
if (left === right) continue
|
|
123
|
-
const leftNumber = Number(left)
|
|
124
|
-
const rightNumber = Number(right)
|
|
125
|
-
if (Number.isNaN(leftNumber) || Number.isNaN(rightNumber)) {
|
|
126
|
-
return left > right
|
|
127
|
-
}
|
|
128
|
-
return leftNumber > rightNumber
|
|
129
|
-
}
|
|
130
|
-
return false
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Write the report to the step summary and to the file the workflow reads.
|
|
135
|
-
* Wrapped so a failure to report cannot mask the failure being reported - and
|
|
136
|
-
* so the workflow's `gh issue create --body-file` always has a file, including
|
|
137
|
-
* on the path below where the watch itself broke.
|
|
138
|
-
*/
|
|
139
|
-
const writeReport = (summary) => {
|
|
140
|
-
try {
|
|
141
|
-
if (process.env.GITHUB_STEP_SUMMARY) {
|
|
142
|
-
appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary)
|
|
143
|
-
}
|
|
144
|
-
if (process.env.AUDIT_WATCH_REPORT) {
|
|
145
|
-
writeFileSync(process.env.AUDIT_WATCH_REPORT, summary)
|
|
146
|
-
}
|
|
147
|
-
} catch (error) {
|
|
148
|
-
console.error(`could not write the report: ${error.message}`)
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const main = () => {
|
|
153
|
-
const report = runAudit()
|
|
154
|
-
const advisories = collectAdvisories(report)
|
|
155
|
-
const problems = []
|
|
156
|
-
|
|
157
|
-
for (const [id, advisory] of advisories) {
|
|
158
|
-
if (EXEMPTIONS.includes(id)) continue
|
|
159
|
-
problems.push(
|
|
160
|
-
`new dev-tree advisory ${id} (${advisory.severity}): ${advisory.title} - ` +
|
|
161
|
-
`packages: ${[...new Set(advisory.packages)].sort().join(', ')}. ` +
|
|
162
|
-
'Fix it, or add it to EXEMPTIONS and name it in the build.yml comment.'
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
for (const id of EXEMPTIONS) {
|
|
167
|
-
if (advisories.has(id)) continue
|
|
168
|
-
problems.push(
|
|
169
|
-
`exempted advisory ${id} no longer appears in the tree - the exemption is ` +
|
|
170
|
-
'dead weight. Remove it from EXEMPTIONS and from the build.yml comment, ' +
|
|
171
|
-
'and restore the full `npm audit` gate.'
|
|
172
|
-
)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
for (const { name, newestKnown } of WATCHED_FOR_RELEASE) {
|
|
176
|
-
const latest = latestPublished(name)
|
|
177
|
-
if (latest === null) {
|
|
178
|
-
console.log(`skipped the registry check for ${name} - npm view failed`)
|
|
179
|
-
continue
|
|
180
|
-
}
|
|
181
|
-
if (isNewer(latest, newestKnown)) {
|
|
182
|
-
problems.push(
|
|
183
|
-
`${name} has published ${latest}, newer than the ${newestKnown} recorded ` +
|
|
184
|
-
'with the exemption - a fix may exist. Re-evaluate the exemption, bump ' +
|
|
185
|
-
'the chain if it clears, and drop the exemption when it does.'
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const buildWorkflow = readFileSync(path.join(ROOT, BUILD_WORKFLOW), 'utf8')
|
|
191
|
-
for (const id of EXEMPTIONS) {
|
|
192
|
-
if (buildWorkflow.includes(id)) continue
|
|
193
|
-
problems.push(
|
|
194
|
-
`exemption ${id} is not named in ${BUILD_WORKFLOW} - the gate and this ` +
|
|
195
|
-
'script disagree about what is exempt.'
|
|
196
|
-
)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const { moderate = 0, high = 0, critical = 0, total = 0 } =
|
|
200
|
-
report?.metadata?.vulnerabilities ?? {}
|
|
201
|
-
|
|
202
|
-
const summary = [
|
|
203
|
-
'## Dev-tree audit watch',
|
|
204
|
-
'',
|
|
205
|
-
`Full audit: ${total} advisories (${critical} critical, ${high} high, ${moderate} moderate).`,
|
|
206
|
-
'',
|
|
207
|
-
'| advisory | severity | packages | exempt |',
|
|
208
|
-
'| --- | --- | --- | --- |',
|
|
209
|
-
...[...advisories].map(
|
|
210
|
-
([id, advisory]) =>
|
|
211
|
-
`| ${id} | ${advisory.severity} | ${[...new Set(advisory.packages)].sort().join(', ')} | ` +
|
|
212
|
-
`${EXEMPTIONS.includes(id) ? 'yes' : '**no**'} |`
|
|
213
|
-
),
|
|
214
|
-
'',
|
|
215
|
-
problems.length
|
|
216
|
-
? ['### The exemption needs attention', '', ...problems.map((p) => `- ${p}`), ''].join('\n')
|
|
217
|
-
: 'The exemption still matches the tree - nothing to do.',
|
|
218
|
-
''
|
|
219
|
-
].join('\n')
|
|
220
|
-
|
|
221
|
-
console.log(summary)
|
|
222
|
-
writeReport(summary)
|
|
223
|
-
|
|
224
|
-
process.exitCode = problems.length ? 1 : 0
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
main()
|
|
229
|
-
} catch (error) {
|
|
230
|
-
// The watch itself broke, so the exemption went unchecked. Say so in the
|
|
231
|
-
// report rather than leaving the workflow to fail on a missing file.
|
|
232
|
-
const summary = [
|
|
233
|
-
'## Dev-tree audit watch',
|
|
234
|
-
'',
|
|
235
|
-
'The watch did not complete, so the dev-tree audit exemption was **not**',
|
|
236
|
-
'checked against the tree. Nothing is known about the advisories right now.',
|
|
237
|
-
'',
|
|
238
|
-
'```',
|
|
239
|
-
error.message,
|
|
240
|
-
'```',
|
|
241
|
-
'',
|
|
242
|
-
'Investigate the failing step, then re-run the workflow.',
|
|
243
|
-
''
|
|
244
|
-
].join('\n')
|
|
245
|
-
|
|
246
|
-
console.error(`audit watch failed: ${error.message}`)
|
|
247
|
-
writeReport(summary)
|
|
248
|
-
process.exit(1)
|
|
249
|
-
}
|