Production-readiness work for the Pipeline-Check VS Code extension. The pre-marketplace security and packaging review (C/H/M/L items below) is fully landed in v0.1.1. The in-depth code review of 2026-05-19 (R items at the bottom) is two-thirds landed across PRs #11–14.
| Layer | State |
|---|---|
| v0.1.0 → v0.1.1 | Shipped 2026-05-19. C1–C2, H1–H4, M1–M5, L1–L6 all closed. |
| v0.1.1 → v0.2.0 (in flight) | R1–R9, R12, R14, R16–R18, R20, R21, R24–R26 landed on stacked PRs #11–#14; merge them in order, then tag. |
| v0.2.0 → 1.0 (in flight) | R10/R15 (scan-workspace), R22 (eslint-flat-config), R29 (scan-on-save) landed; PVR + Discussions enabled on the repo. |
| Blocked | R11 (need suppression-comment syntax), R13/R27 (server-side change), R19 (interactive screenshot session), R23 (CodeQL setup). |
| Decided against | R28 (no telemetry — see SECURITY.md). |
These cannot land from a branch and have been queued since the production-readiness pass. Each one's failure mode is small enough that v0.2.0 can ship without them, but the listing improves once they're done.
- Resolve the CodeQL default-setup conflict. The advanced
.github/workflows/codeql.yml runs
security-extended; the org's default CodeQL setup conflicts and theanalyzecheck stays red. Settings → Code security → Code scanning → switch CodeQL from "Default" to "Advanced". If org policy forbids that, deletecodeql.ymland losesecurity-extended. - Enable Private Vulnerability Reporting. ✅ Enabled 2026-05-19 via the GitHub API; SECURITY.md's reporting link now resolves for external reporters.
- Enable Discussions. ✅ Enabled 2026-05-19 via the GitHub API;
the
qnalink in package.json now resolves on the marketplace listing. - Manual H4 smoke — F5 with the sample-workflow profile, open each provider's trigger file, confirm diagnostics still appear. The activation narrowing drops custom workflow paths intentionally but the regression risk is non-zero.
- Capture marketplace screenshots (R19). Highest-leverage conversion improvement still pending.
vsce ls against the current tree produces:
README.md
package.json
LICENSE
icon.png
CHANGELOG.md
out/extension.js
No node_modules/ — .vscodeignore excludes node_modules/**
and there is no bundler step. out/extension.js requires
vscode-languageclient/node at activation, so the published v0.1.0 listing
is almost certainly broken in a clean install. CI only verifies that the
.vsix packs; it never loads the bundle.
Plan
- Manual smoke the maintainer should run: install the
published v0.1.0 in a clean VS Code that doesn't have a sibling
pipeline-check-vscodecheckout and confirm it fails to activate. Either: (a) confirms the hypothesis and we cut a 0.1.1 hotfix from this branch, or (b) reveals a vsce behavior I don't know about (e.g. it auto-includes prod deps regardless of.vscodeignore) — in which case C1's CI smoke step still has value as defense-in-depth. - Add an esbuild bundle:
bundle:dev(sourcemap) andbundle:prod(minified).vscode:prepublishrunstypecheck && bundle:prod.compilerunstypecheck && bundle:devso F5 stays source-mappable. -
package.json#mainis now./dist/extension.js.out/**is excluded from the .vsix. -
.gitignorealready excludeddist/..vscodeignoreexcludesnode_modules/**andout/**; the bundle indist/is the only JS that ships. Confirmed viavsce ls:README.md package.json LICENSE icon.png CHANGELOG.md dist/extension.js - CI runs
npm run smoke(scripts/smoke.js) which stubs thevscodemodule, loads the bundle, and assertsactivate/deactivateare exported. Catches the missing-runtime-dep regression that purevsce packagecannot.
src/extension.ts:52-57 reads
pipelineCheck.serverCommand and pipelineCheck.serverArgs from workspace
configuration with no scope guard. A repo's .vscode/settings.json can set
serverCommand to any binary or serverArgs to
["-c", "<malicious python>"]. Once the user trusts the workspace and opens
any YAML / JSON / Dockerfile / Terraform / Groovy file, the extension
spawns it.
Plan
- Add
"scope": "machine-overridable"to bothserverCommandandserverArgsin package.json undercontributes.configuration.properties. - Add a workspace-trust capability so the extension stays inactive in
untrusted workspaces:
json "capabilities": { "untrustedWorkspaces": { "supported": "limited", "description": "Pipeline-Check spawns the configured Python interpreter to analyze workflow files. In untrusted workspaces the extension stays inactive until the workspace is trusted." }, "virtualWorkspaces": false } - Document the threat model in SECURITY.md (done as part of M1).
.github/workflows/publish.yml:70-87
runs npx --yes @vscode/vsce@latest and npx --yes ovsx@latest with
VSCE_PAT / OVSX_PAT in env. A compromise of either upstream package
between releases would exfiltrate both PATs.
- Pin
@vscode/vsce@3.9.1andovsx@0.10.12in publish.yml. - Pin the same
@vscode/vsce@3.9.1in ci.yml. - Moved
@vscode/vsceandovsxtodevDependenciesso the existing npm Dependabot config bumps them. Workflows now runnpx vsce/npx ovsxafternpm ci, so the pinned versions live inpackage-lock.jsonand there's no fresh registry fetch with PATs in env.
workflow_dispatch accepts a tag input and anyone with push access can
fire it. PATs are repo-scoped, so any workflow can read them.
- Maintainer created the
productionGitHub Environment with required reviewers;VSCE_PAT/OVSX_PATlive as environment secrets so any workflow that does not target this environment cannot read them. - Added
environment: productionto thepublishjob in .github/workflows/publish.yml. Aworkflow_dispatchor tag push now stalls at the environment gate until a reviewer approves the run.
A tag created on an arbitrary commit or a force-moved tag would still ship.
- Add a
git merge-base --is-ancestor "$REF_NAME" origin/maincheck to publish.yml before packaging.
package.json:41-47 used to activate on
onLanguage:yaml, onLanguage:json, etc., so opening an unrelated
package.json or mkdocs.yml would spawn Python.
- Replaced bare
onLanguage:*triggers withworkspaceContains:patterns for the providers we actually scan (.github/workflows/*,.gitlab-ci.yml,azure-pipelines.yml,bitbucket-pipelines.yml,.circleci/config.yml,cloudbuild.yaml,.buildkite/pipeline.yml,.drone.{yml,yaml},Jenkinsfile,Dockerfile,Containerfile). - Tightened the
documentSelectorin src/extension.ts topattern:globs matching the same files. The LSP only sees candidate documents — no more reliance on the server's content filter as a first line of defence, and no dependency on which language extension owns thegithub-actions-workflowlanguage ID. - Manual smoke the maintainer should run before merging this
branch: open each provider's fixture (GHA, GitLab, Azure,
Bitbucket, CircleCI, Cloud Build, Buildkite, Drone, Jenkins,
Dockerfile) and confirm diagnostics still appear. Custom
workflow paths (e.g.
pipelines/build.yml) will no longer activate the extension — that's the intent, but worth knowing before users surface it as a bug.
- M1 SECURITY.md added with GitHub Private Vulnerability Reporting as the disclosure channel, response SLAs, a threat-model section, and an out-of-scope list. Action item for the maintainer: enable Private Vulnerability Reporting on the repo (Settings → Code security → "Private vulnerability reporting"), otherwise the link in SECURITY.md 404s.
- M2 Narrow publish.yml permissions: workflow default is now
contents: read, and the publish job widens tocontents: writeonly for itself. (GitHub Actions doesn't support step-levelpermissions, so this is the tightest scope without splitting into two jobs.) - M3
npm audit --omit=dev --audit-level=highadded to ci.yml. - M4 publish.yml now refuses to ship a tag whose
CHANGELOG.md doesn't have a
## [X.Y.Z]header — protects the release-notes extraction that follows. - M5 (N/A today) When/if a sibling npm package ships, enable
--provenanceonnpm publishfrom GitHub-hosted runners. Nothing to do until an npm package is added. Park here as a reminder.
A vitest unit suite covers the pure-logic seams that user-facing
correctness depends on. The infrastructure (vitest, vi.mock("vscode", ...) for code that touches the editor namespace) is reusable — extend
it as more pure-logic modules are extracted.
- Severity threshold filter (src/severityFilter.test.ts) — 14 tests pinning down the invariants: missing/unknown severity is never silently dropped, an unknown threshold name falls back to LOW, CRITICAL survives every concrete threshold, INFO never does, order is preserved, no in-place mutation. The filter itself was extracted from src/extension.ts into src/severityFilter.ts so the test didn't need a vscode mock.
- Findings tree (src/findingsView.test.ts)
— 11 tests covering source filtering (only
pipeline-checkdiagnostics appear), the three group modes (severity,file,rule) with bucket ordering + counts + leaf labels +vscode.openreveal command, severity normalisation (lowercase → uppercase, unknown → INFO fallback), and the no-refresh-storm contract on a same-modesetGroupModecall. Usesvi.mock("vscode", ...)to stub the editor namespace. - VS Code integration tests with
@vscode/test-electrononce the surface stabilises. Useful for: real diagnostic publishing end-to-end, the tree view actually rendering in a VS Code host, and the workspace-trust prompt path. Held back because the payoff per test is high but the marginal cost of each test is also high (boot a real Electron + extension host), so the unit suite earns its keep first.
npm test runs the suite (configured in
vitest.config.ts); both ci.yml and publish.yml run
it as a gating step. Test files live next to the code they cover and
are stripped from the .vsix by src/** and **/*.ts in
.vscodeignore.
- L1 Recommend an absolute path for
serverCommandin itsmarkdownDescription(package.json:68). Mitigates WindowsCreateProcesscwd-search behavior. - L2 Set
"noEmitOnError": truein tsconfig.json so a localnpm run compilecan't emit broken JS intoout/. - L3 Reconcile
client.outputChannelaccess patterns: typed asvscode.OutputChannelat capture, optional chaining dropped at the use site. - L4 Kept the
?? SEVERITY_RANK.LOWfallback as defense-in-depth with a comment explaining the invariant: a diagnostic must clear the LOW bar, never silently disappear because of a hand-edited bogus value. - L5 Added
Othernext toLintersincategories. - L6 Added
qnapointing to the repo Discussions page. Action item for the maintainer: enable Discussions on the repo (Settings → General → Features), otherwise the link 404s. Skippedbugs.email— without a real disclosure address, a placeholder is worse than the existing GitHub-issues URL.
Captured from a frontend-design review of the Findings tree
panel (src/findingsView.ts,
media/pipeline-check.svg, and the
viewsContainers / views / viewsWelcome / menus blocks in
package.json). Items U1 – U8 land in this
branch; U9 – U11 are scoped as follow-ups so the surface
changes (commands, menu structure, view header) can be reviewed
on their own.
- U1 Replace the activity-bar SVG. Every CI security product (Snyk, Trivy, Checkov, GitGuardian, Bridgecrew, Wiz) ships a shield-with-checkmark; ours added an eighth shield to an activity bar that also carries the source-control fork and every git extension's variation on the same. Swapped for an inverted-Y pipeline glyph (top node solid, bottom-right node solid, bottom-left hollow) — speaks to pipeline + uneven posture in one glance and is uncrowded in the activity-bar neighbourhood.
- U2 Set a count badge on the TreeView. The panel-purpose
comment in src/findingsView.ts
claims "how many CRITICAL findings does this workspace have
right now?" as the question it answers, but the only way to
get the count was to expand every group and count rows.
Wired
treeView.badgeto the live total so the activity-bar icon carries the number when the panel is collapsed. - U3 Rewrite the empty-state copy. The previous copy led with "No findings in open files" and offered Restart / Show log as primary actions — denial-first framing pointing at dev-tools. Replaced with a one-sentence value proposition ("Pipeline-Check scans CI/CD configurations for OWASP Top 10 CI/CD risks…") and demoted the diagnostic links to a "Not seeing findings?" secondary line.
- U4 Restructure leaf rows. Previously: label was
GHA-001: <title>, description was the full workspace-relative path. The rule-ID prefix ate 7–8 characters of every label; the path duplicated the parent group's information in file-mode and got middle-truncated everywhere else (…templates/depl…). And the line number — the one piece of "where" information the user actually needs — was nowhere to be seen. Now: label = title only; description carriesRULE · file.yml:LINE(or the relevant subset for the current grouping). Matches thepath:lineform compilers emit, halves label width for the same information. - U5 Differentiate severity icons. Previously
CRITICAL and HIGH shared
error+errorForeground(same red icon, same red colour) — defensible for editor-gutter consistency but indistinguishable in the severity-grouped tree. Now CRITICAL renders asflame(still red), HIGH stayserror. Separately: INFO usedcircle-small-filled(a 6px glyph in a 16px slot, breaking the left-edge alignment) with no themed colour (defaulting to foreground — brighter than LOW's blue, inverting the severity gradient). Now usescircle-outlinethemed todescriptionForegroundso INFO is visibly the quietest row. - U6 Aggregate rule-group severity by max, not first.
src/findingsView.ts:301 was picking
items[0].severityafter a sort that ordered by file path then line number — totally unrelated to severity. A rule with one CRITICAL + four LOW findings rendered blue. Fixed to pick the maximum severity across the bucket. - U7 Drop the tooltip
---substitution and the dead leafgetChildrenbranch. The horizontal rule between paragraphs created three visually-separate cards in the tooltip — noisier than markdown's blank-line rhythm. The leafgetChildrenis unreachable because leaves are constructed withCollapsibleState.None. - U8 Compress command titles. "Refresh findings" →
"Refresh"; "Group findings by …" → "Group by …". The
categoryfield already prefixes "Pipeline-Check: " in the command palette, so the shorter form remains unambiguous globally and fits the title-bar tooltip without truncation.
- U9 Replaced the three group-mode title-bar buttons with
a single "Change Grouping" button that opens a Quick Pick.
The old "hide the active mode" radio pattern used elimination
as a state indicator — a first-time user saw two of three
modes and could not tell which was active or that there was
a third. The Quick Pick mirrors VS Code's own "Change
Language Mode" picker: each row carries the option name plus
a one-line description; the active mode is prefixed with
$(check). Dropped the threepipelineCheck.findings.groupBy.*commands in favour of onepipelineCheck.findings.changeGroupingcommand; menuwhenclauses that readpipelineCheck.groupModeare no longer required (the context key is still set so external keybindings / automation can query the current mode). - U11 Standardised group descriptions to count-only.
Severity, file, and rule groups all now show
"5"in the description column — a uniform right edge scans faster than the mixed"5"/"5 · workflows"/"5"shapes we had. The parent-dir disambiguator that used to live in the file-group description moved to the group's tooltip, soworkflows/release.ymlvspipelines/release.ymlcollisions are still distinguishable on hover.
- U10 Collapse the inner sub-view header band. Considered
eliding the
FINDINGSsub-header by leavingviews[].nameempty, so the activity-bar slot'sPIPELINE-CHECKtitle would serve as the only header. On second look this is the wrong call: every major single-purpose extension (GitLens, Test Explorer, Docker, Run and Debug, Live Share) keeps the two-bar layout, and the sub-header gives us room to add a second view in the same container later (a "Rule Browser" or "Scan History" panel) without restructuring the slot. The 50px of vertical space we'd save up front is small relative to the structural cost of having to add the header back the first time we want a second view.
The findings below came out of a holistic review of the codebase after v0.1.1 shipped. Categories cluster related work into reviewable PRs.
PR landing order (all stacked on main):
- #11
review-followups— R1–R9, R21 - #12
review-followups-batch-2— R12, R14, R16, R18, R20 - #13
review-followups-batch-3— R24, R25, R26 - #14
review-followups-batch-4— R17
Total: 19 of 29 review items landed; the rest are blocked on external inputs (suppression syntax, screenshots) or stacked branches (scan-workspace).
- R1 Reorder the
filterByThresholdimport in extension.ts up to the rest of the import block. (PR #11) - R2 "Restart language server" toast no longer fires when
startClient()failed. (PR #11) - R3
stopClient()races the LSP shutdown against a 2-second timer; dispose explicitly on timeout. (PR #11) - R4
groupByFilecarries the original Uri alongside the string key; noUri.parseround-trip. (PR #11) - R5
compareByLocationsorts onfsPath. (PR #11)
- R6
collectFindings()memoised behind a per-refresh cache. (PR #11) - R7
onDidChangeDiagnosticsskips refreshes whose URI batch doesn't touch a pipeline-check diagnostic — plus alastFindingUrisset so cleared findings still trigger a refresh. (PR #11)
- R8 Leaf tooltip appends a
$(book) <rule-id> documentationlink when the server publishesDiagnostic.code.target. (PR #11) - R9 Status bar item on the left at priority 100 showing the
top two non-zero severities (e.g.
$(shield) 3C 1H). (PR #11) - R10
pipelineCheck.findings.refreshnow callsscanWorkspace()rather than just re-painting the tree from already-published diagnostics. - R11
CodeActionprovider for suppression comments. (Blocked on the upstream pipeline-check CLI's suppression syntax.) - R12 Alt+F8 / Shift+Alt+F8 jump between findings, wrap at both ends. (PR #12)
- R13 Set
Diagnostic.tagsforDeprecated/Unnecessarywhere the rule indicates it. (Server-side change — file upstream.)
- R14 Trigger-pattern list extracted into
src/providers.ts(PROVIDERSmap +TRIGGER_PATTERNS). A regression test asserts the package.jsonactivationEventsstay in lockstep. (PR #12) - R15 Scan-workspace command shipped; covered by
workspaceContains:activation triggers +onStartupFinishedso the command is always reachable from the Findings welcome state and the title-bar button. - R16
[client] HH:MM:SS.mmm <level>logging into the LanguageClient's outputChannel.withTiming(label, fn)wraps thunks with start/ok/failed breadcrumbs. (PR #12)
- R17
@vscode/test-electronintegration suite covering activation, command registration, view registration, settings schema, and the workspace-trust capability. (PR #14) - R18
vi.mock("vscode")factory extracted intosrc/__testStubs__/vscode.ts. (PR #12)
- R19 Ship the screenshots the HTML comment in README.md has been waiting for since v0.1.0. (Needs an interactive VS Code session.) See docs/screenshots/README.md for the capture recipe.
- R20 CI fails the build if
package.json#descriptionexceeds 145 characters. Today's description is 141 chars. (PR #12)
- R21 Three-OS matrix:
[ubuntu-latest, windows-latest, macos-latest].npm auditand the vsix upload pinned to Linux. (PR #11) - R22 Migrated to eslint v9 flat config
(eslint.config.mjs); replaced
@typescript-eslint/eslint-plugin+parserwith the unifiedtypescript-eslintpackage. Rules carry over verbatim so the lint result is unchanged. Unblocks future eslint v9+ bumps. - R23 Resolve the CodeQL default-setup conflict — disable
default setup or delete
codeql.yml. (Needs repo-settings change.) - R24 Pre-release channel via tag naming
(
vX.Y.Z-rc.N→ pre-release). (PR #13)
- R25
pipelineCheck.disabledProviderssetting silences providers wholesale. (PR #13) - R26 Inline
CodeLenssummary at the top of each scanned file. (PR #13) - R27 Workspace-level config file (
.pipeline-check.toml) shared with the CLI. (Needs upstream coordination.) - R28 — decided against. No telemetry. For a security-tool audience, "we don't phone home" is a stronger trust signal than the prioritisation value an opt-in pixel would deliver. SECURITY.md carries the explicit no-telemetry promise so the policy is visible at the security-review surface researchers check first. (Decided 2026-05-19.)
- R29
pipelineCheck.scanOnSavesetting (defaultfalse). Saving a CI file kicks off a quiet workspace re-scan (status-bar spinner; no toast) so cross-file effects in unopened CI files get re-evaluated. In-flight guard collapses save-storms to a single scan.