-
Notifications
You must be signed in to change notification settings - Fork 0
fix(ci): write the Sonar exclusions as path patterns, not directory prefixes #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
amondnet
merged 5 commits into
main
from
amondnet/fix-ci-generated-skill-bundles-still-analysed-by
Sep 16, 2026
+140
−8
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
ff6f078
fix(ci): write the Sonar exclusions as path patterns, not directory p…
amondnet fddb91e
chore: apply AI code review suggestions
amondnet fa7036c
chore: clear the Codacy findings the guard test introduced
amondnet 8ab4a6a
chore: tolerate harmless reformatting in the config readers
amondnet 76599f8
chore: drop the dynamic RegExp from the properties reader
amondnet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,23 +1,31 @@ | ||
| # SonarQube Cloud runs here in Automatic Analysis mode — no workflow invokes a | ||
| # scanner, the GitHub App analyses every push on its own. That mode reads this | ||
| # file and ignores `sonar-project.properties`, and it accepts no wildcards in | ||
| # these values, so paths are written as plain directory prefixes. | ||
| # file and ignores `sonar-project.properties`. | ||
| # | ||
| # Two consequences worth knowing before editing: | ||
| # Three consequences worth knowing before editing: | ||
| # - Only the copy on the default branch takes effect. A change here does not | ||
| # alter the analysis of the pull request that makes it; it applies once | ||
| # merged. | ||
| # - Where this file and the SonarQube Cloud UI disagree, this file wins. An | ||
| # entry that matches nothing therefore cannot be corrected from the UI while | ||
| # it is still here. | ||
| # - The exclusion values below are path *patterns* matched against whole file | ||
| # paths, not directory prefixes: `*` matches within one path segment, `**` | ||
| # spans directories, and a whole subtree is written `dir/**/*`. A bare | ||
| # `dir/` matches no file and is discarded in silence. The Automatic Analysis | ||
| # page's "Wildcard patterns are not allowed" is about the plain path lists | ||
| # `sonar.sources` and `sonar.tests`; ee09832 read it as covering these too | ||
| # and wrote bare prefixes, which left the exclusion inert — `main` still | ||
| # indexed 5,018 lines of generated code and reported 359 findings in it. | ||
|
|
||
| # Generated by `bun run build:skill` from `scripts/*.ts`, committed because the | ||
| # `npx skills` install channel copies only the skill directory and runs no | ||
| # dependency install. Analysing them scores the same program twice: every line | ||
| # of `scripts/docs.ts` reappears in the bundle, which read as 7.7% duplication | ||
| # on new code, and the `var` declarations Bun emits cost a reliability rating | ||
| # that no edit can recover — `bun run build:skill:check` byte-compares the | ||
| # bundle against a fresh build, so hand-editing it fails CI. | ||
| # `eslint.config.js` ignores the same directory, for the same reason. | ||
| sonar.exclusions=skills/spring-docs/scripts/ | ||
| sonar.cpd.exclusions=skills/spring-docs/scripts/ | ||
| # bundle against a fresh build, so hand-editing it fails CI. `detect.mjs` also | ||
| # inlines `fast-xml-parser`, whose entity tables read as first-party source. | ||
| # `eslint.config.js` and `.codacy.yml` exclude the same directories. | ||
| sonar.exclusions=skills/*/scripts/**/* | ||
| sonar.cpd.exclusions=skills/*/scripts/**/* | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| /** | ||
| * Guards the analyser exclusions that keep `skills/<skill>/scripts/` out of Sonar, | ||
| * Codacy and ESLint. | ||
| * | ||
| * The exclusion in ee09832 was written as a bare directory prefix | ||
| * (`skills/spring-docs/scripts/`). Every analyser matches patterns against the | ||
| * whole file path, so that prefix matched nothing and was dropped without a | ||
| * warning — SonarCloud kept indexing 5,018 generated lines for two days before | ||
| * anyone noticed. A pattern that matches no file looks identical to a pattern | ||
| * that works, so assert the match instead of reading the config. | ||
| * | ||
| * Each reader below takes the *active* setting, never the file's raw text: a | ||
| * commented-out exclusion still contains a pattern that would match, so a text | ||
| * scan passes while the analyser receives nothing — the same blind spot one | ||
| * level down. Each reader therefore drops the file's comment lines before it | ||
| * takes anything from them. | ||
| * | ||
| * `Bun.Glob` stands in for three matchers it is not, so a pass is not proof | ||
| * that SonarCloud reads a pattern the same way. What it does catch is the | ||
| * failure that actually happened: a pattern that matches no bundle at all. | ||
| */ | ||
| import { join } from 'node:path' | ||
|
|
||
| import { describe, expect, test } from 'bun:test' | ||
|
|
||
| const ROOT = join(import.meta.dir, '..', '..') | ||
|
|
||
| /** The committed bundles every exclusion below is supposed to cover. */ | ||
| const BUNDLES = Array.from(new Bun.Glob('skills/*/scripts/*.mjs').scanSync(ROOT), p => p.replaceAll('\\', '/')) | ||
|
amondnet marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Split on `\r?\n`, not `\n`: the repository carries no `.gitattributes`, so a | ||
| * Windows checkout with `core.autocrlf=true` leaves a `\r` on every value, and | ||
| * a pattern ending in `\r` matches no bundle — a false failure that reads | ||
| * exactly like the misconfiguration this file exists to catch. | ||
| */ | ||
| async function lines(file: string): Promise<string[]> { | ||
| return (await Bun.file(join(ROOT, file)).text()).split(/\r?\n/) | ||
| } | ||
|
|
||
| /** | ||
| * The patterns `.sonarcloud.properties` sets for `key`, as a `key=a,b` line. | ||
| * | ||
| * Split each line on its first `=` and compare the trimmed key, rather than | ||
| * matching a prefix: `.properties` allows whitespace around the separator and | ||
| * before the key, and a reformatted file should not fail the guard. A | ||
| * commented-out setting keeps its `#`, so its key never compares equal. | ||
| */ | ||
| async function sonarExclusions(key: string): Promise<string[]> { | ||
| for (const line of await lines('.sonarcloud.properties')) { | ||
| const separator = line.indexOf('=') | ||
| if (separator < 0 || line.slice(0, separator).trim() !== key) | ||
| continue | ||
| return line.slice(separator + 1).split(',').map(v => v.trim()).filter(Boolean) | ||
| } | ||
| return [] | ||
| } | ||
|
|
||
| /** The `skills/` entries of `.codacy.yml`'s `exclude_paths:` block. */ | ||
| async function codacyExclusions(): Promise<string[]> { | ||
| const source = await lines('.codacy.yml') | ||
| const start = source.findIndex(l => l.startsWith('exclude_paths:')) | ||
| expect(start).toBeGreaterThanOrEqual(0) | ||
|
|
||
| const patterns: string[] = [] | ||
| for (const line of source.slice(start + 1)) { | ||
| const entry = line.trim() | ||
| if (entry.startsWith('- ')) { | ||
| // Strip the quotes YAML does not require here; single, double, or none. | ||
| patterns.push(entry.slice(2).trim().replace(/^["']|["']$/g, '')) | ||
| continue | ||
| } | ||
|
amondnet marked this conversation as resolved.
|
||
| // A comment or a blank line sits inside the block; anything else ends it. | ||
| if (entry !== '' && !entry.startsWith('#')) | ||
| break | ||
| } | ||
| return patterns.filter(p => p.startsWith('skills/')) | ||
| } | ||
|
|
||
| /** | ||
| * The `skills/` ignore patterns in `eslint.config.js`'s `ignores` array. | ||
| * | ||
| * Read as text with comment lines dropped, not by importing the module. The | ||
| * evaluated config would be the stronger source, but reaching it needs either a | ||
| * non-literal dynamic `import()` or `allowJs` in `tsconfig.json`, and neither is | ||
| * worth a project-wide change here. Dropping `//` lines closes the gap that | ||
| * matters: a commented-out ignore no longer supplies a pattern that passes. | ||
| */ | ||
| async function eslintIgnores(): Promise<string[]> { | ||
| return (await lines('eslint.config.js')) | ||
| .map(l => l.trim()) | ||
| .filter(l => !l.startsWith('//') && l.startsWith('\'skills/')) | ||
| .map(l => l.slice(1, l.indexOf('\'', 1))) | ||
| } | ||
|
|
||
| function expectCoversBundles(patterns: string[]): void { | ||
| expect(patterns.length).toBeGreaterThan(0) | ||
| for (const pattern of patterns) { | ||
| const glob = new Bun.Glob(pattern) | ||
| for (const bundle of BUNDLES) expect([pattern, bundle, glob.match(bundle)]).toEqual([pattern, bundle, true]) | ||
| } | ||
| } | ||
|
|
||
| describe('generated bundle exclusions', () => { | ||
| test('the bundles the exclusions target are committed', () => { | ||
| // A Set, because `Bun.Glob.scanSync` fixes no traversal order. | ||
| expect(new Set(BUNDLES)).toEqual(new Set(['skills/spring-docs/scripts/detect.mjs', 'skills/spring-docs/scripts/docs.mjs'])) | ||
| }) | ||
|
|
||
| test('.sonarcloud.properties excludes them from issues and duplication', async () => { | ||
| expectCoversBundles(await sonarExclusions('sonar.exclusions')) | ||
| expectCoversBundles(await sonarExclusions('sonar.cpd.exclusions')) | ||
| }) | ||
|
|
||
| test('.codacy.yml excludes them', async () => { | ||
| expectCoversBundles(await codacyExclusions()) | ||
| }) | ||
|
|
||
| test('eslint.config.js ignores them', async () => { | ||
| expectCoversBundles(await eslintIgnores()) | ||
| }) | ||
| }) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.