sarif-to-comment 0.0.0 → 0.1.1

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +266 -0
  3. package/bin/sarif-to-comment.cjs +275 -0
  4. package/docs/api/index.md +32 -0
  5. package/docs/api/sarif-to-comment.iblockedoutcome.markdown.md +13 -0
  6. package/docs/api/sarif-to-comment.iblockedoutcome.md +81 -0
  7. package/docs/api/sarif-to-comment.iblockedoutcome.status.md +13 -0
  8. package/docs/api/sarif-to-comment.ipublishedoutcome.markdown.md +13 -0
  9. package/docs/api/sarif-to-comment.ipublishedoutcome.md +127 -0
  10. package/docs/api/sarif-to-comment.ipublishedoutcome.review.md +13 -0
  11. package/docs/api/sarif-to-comment.ipublishedoutcome.statepath.md +13 -0
  12. package/docs/api/sarif-to-comment.ipublishedoutcome.status.md +13 -0
  13. package/docs/api/sarif-to-comment.ipublishedreview.id.md +13 -0
  14. package/docs/api/sarif-to-comment.ipublishedreview.md +81 -0
  15. package/docs/api/sarif-to-comment.ipublishedreview.url.md +13 -0
  16. package/docs/api/sarif-to-comment.ipublishsarifreviewinput.destination.md +13 -0
  17. package/docs/api/sarif-to-comment.ipublishsarifreviewinput.md +207 -0
  18. package/docs/api/sarif-to-comment.ipublishsarifreviewinput.oldsourcecommit.md +13 -0
  19. package/docs/api/sarif-to-comment.ipublishsarifreviewinput.options.md +13 -0
  20. package/docs/api/sarif-to-comment.ipublishsarifreviewinput.reviewedcommit.md +13 -0
  21. package/docs/api/sarif-to-comment.ipublishsarifreviewinput.sarif.md +13 -0
  22. package/docs/api/sarif-to-comment.ipublishsarifreviewinput.sourcerooturi.md +13 -0
  23. package/docs/api/sarif-to-comment.ipublishsarifreviewinput.statepath.md +13 -0
  24. package/docs/api/sarif-to-comment.ipublishsarifreviewinput.token.md +13 -0
  25. package/docs/api/sarif-to-comment.ipublishsarifreviewoptions.ignoreapprovalhold.md +13 -0
  26. package/docs/api/sarif-to-comment.ipublishsarifreviewoptions.md +60 -0
  27. package/docs/api/sarif-to-comment.ipullrequestdestination.md +102 -0
  28. package/docs/api/sarif-to-comment.ipullrequestdestination.owner.md +13 -0
  29. package/docs/api/sarif-to-comment.ipullrequestdestination.pullnumber.md +13 -0
  30. package/docs/api/sarif-to-comment.ipullrequestdestination.repo.md +13 -0
  31. package/docs/api/sarif-to-comment.irejectedoutcome.markdown.md +13 -0
  32. package/docs/api/sarif-to-comment.irejectedoutcome.md +102 -0
  33. package/docs/api/sarif-to-comment.irejectedoutcome.statepath.md +13 -0
  34. package/docs/api/sarif-to-comment.irejectedoutcome.status.md +13 -0
  35. package/docs/api/sarif-to-comment.iuncertainoutcome.markdown.md +13 -0
  36. package/docs/api/sarif-to-comment.iuncertainoutcome.md +102 -0
  37. package/docs/api/sarif-to-comment.iuncertainoutcome.statepath.md +13 -0
  38. package/docs/api/sarif-to-comment.iuncertainoutcome.status.md +13 -0
  39. package/docs/api/sarif-to-comment.md +167 -0
  40. package/docs/api/sarif-to-comment.publishsarifreview.md +80 -0
  41. package/docs/api/sarif-to-comment.publishsarifreviewoutcome.md +15 -0
  42. package/docs/getting-started.md +170 -0
  43. package/package.json +69 -7
  44. package/src/github.cjs +1092 -0
  45. package/src/index.cjs +514 -0
  46. package/src/placement.cjs +586 -0
  47. package/src/prepare-review.cjs +1547 -0
  48. package/src/publication.cjs +994 -0
  49. package/src/replacements.cjs +463 -0
  50. package/types/index.d.ts +217 -0
  51. package/vendor/README.md +11 -0
  52. package/vendor/sarif-schema-2.1.0.json +3389 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # sarif-to-comment
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - e3a0c2a: Correct the release documentation shipped in the README.
8
+
9
+ - Provenance is described conditionally. Trusted publishing authenticates with OIDC from a private or public repository, but npm attaches a provenance attestation only when the source repository is public at publish time. Version 0.1.0 carries a verified attestation; a release from a private repository has none.
10
+ - Release commits are no longer said to be recorded as `gitHead`, which is absent from the registry metadata for 0.1.0. The commit is identified by the `publish.yml` run and, when present, the provenance attestation; tags remain optional.
11
+
12
+ ## 0.1.0
13
+
14
+ ### Minor Changes
15
+
16
+ - d13d144: First release: publish a ready SARIF 2.1.0 document as one GitHub draft pull request review.
17
+
18
+ - Library (`publishSarifReview`, in-memory SARIF) and CLI (`sarif-to-comment`) share one validation, placement and publication core.
19
+ - Whole-review validation: invalid, inconsistent, unsupported or held input blocks the entire review before any write.
20
+ - General feedback in the review body, exact inline comments on changed lines, and supported fixes as native suggestions, sent in one create-review request pinned to the reviewed commit.
21
+ - Durable, never-duplicating delivery: a caller-chosen state path records intent before sending, and retries confirm the review on GitHub instead of creating another.
22
+ - TypeScript declarations, a getting-started guide and generated API reference are included in the package.
package/README.md ADDED
@@ -0,0 +1,266 @@
1
+ # sarif-to-comment
2
+
3
+ Publish a ready [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html) document as **one GitHub draft pull request review**:
4
+
5
+ - general feedback in the review body;
6
+ - findings on changed lines as inline comments;
7
+ - supported fixes as native suggestions.
8
+
9
+ It is sent in a single create-review request.
10
+
11
+ The scope is deliberately narrow:
12
+
13
+ - **One-way.** SARIF goes to GitHub once. The tool never updates, reconciles, submits, restores or deletes a review afterwards. Supplying a new SARIF document (with a new state path) creates a separate review.
14
+ - **Drafts only.** The review is created pending. A person submits it on GitHub.
15
+ - **Whole review or nothing.** If any finding can't be published faithfully, nothing is published, and the tool explains why.
16
+ - **Never duplicated.** A durable state file makes retries confirm the existing review instead of creating another.
17
+
18
+ Requires Node.js 22 or later. There are two surfaces, the **library** (SARIF in memory) and the **CLI** (a SARIF file); both run the same code.
19
+
20
+ ## Documentation
21
+
22
+ These documents are included in the package. The links open them on unpkg (for the latest published version) and need no access to the source repository.
23
+
24
+ - **[Getting started](https://unpkg.com/sarif-to-comment/docs/getting-started.md):** credentials, the reviewed commit, source root and state path, a complete library example and a complete CLI example, and how to handle every outcome.
25
+ - **[API reference](https://unpkg.com/sarif-to-comment/docs/api/index.md):** generated by API Documenter from the package's TypeScript declarations.
26
+ - **[Changelog](https://unpkg.com/sarif-to-comment/CHANGELOG.md):** release notes produced by Changesets.
27
+
28
+ After installation, the same files are in `node_modules/sarif-to-comment/docs/` and `node_modules/sarif-to-comment/CHANGELOG.md`.
29
+
30
+ ## Installation
31
+
32
+ ```sh
33
+ npm install sarif-to-comment
34
+ npx sarif-to-comment --help
35
+ ```
36
+
37
+ The package ships:
38
+ - the library (CommonJS, usable from ES modules) with TypeScript declarations;
39
+ - the CLI;
40
+ - the vendored official SARIF schema;
41
+ - the documentation above.
42
+
43
+ It has three runtime dependencies (`ajv`, `ajv-draft-04`, `ajv-formats`).
44
+
45
+ ## Quick start: library
46
+
47
+ SARIF is passed as an in-memory object; no temporary file is needed.
48
+
49
+ ```js
50
+ import { publishSarifReview } from 'sarif-to-comment';
51
+
52
+ const outcome = await publishSarifReview({
53
+ sarif, // the parsed SARIF log (a plain JSON object)
54
+ destination: { owner: 'acme', repo: 'widgets', pullNumber: 42 },
55
+ reviewedCommit: 'c0dec0dec0dec0dec0dec0dec0dec0dec0dec0de', // full 40-character SHA
56
+ statePath: '/var/lib/my-linter/reviews/acme-widgets-42-run-1817.json', // absolute; keep it
57
+ token: process.env.GH_TOKEN, // a personal access token or user token
58
+ // sourceRootUri: 'file:///home/ci/work/widgets/', // optional: repo root in the producer's file system
59
+ // oldSourceCommit: '<full SHA>', // optional: see "Old-side source"
60
+ // options: { ignoreApprovalHold: true }, // optional: see "Approval hold"
61
+ });
62
+
63
+ console.log(outcome.markdown); // always a human-readable explanation
64
+ if (outcome.status === 'published') console.log(outcome.review.url);
65
+ ```
66
+
67
+ `status` is one of:
68
+ - `published`: the publication is complete, and `review` has the review's `id` and `url`. A repeated call answers from the state file without contacting GitHub, so a person may since have submitted, edited or deleted the review; the tool does not check.
69
+ - `blocked`: nothing was written anywhere.
70
+ - `uncertain`: retry with the same `statePath`.
71
+ - `rejected`: GitHub refused the request; it is never resent.
72
+
73
+ The contract is `status` plus `markdown`, and `review` or `statePath` where listed. Internal diagnostic codes are not part of it and may change. The [getting-started guide](https://unpkg.com/sarif-to-comment/docs/getting-started.md) has a complete, runnable version of this example with outcome handling.
74
+
75
+ The promise rejects for:
76
+ - invalid input (a `TypeError`, before any network request);
77
+ - a corrupt state file, or a state path reused for different input;
78
+ - operational failures, such as a network error or an unreachable pull request.
79
+
80
+ A rejection never contains the token.
81
+
82
+ The `sarif` value is copied when the call starts, so changing your object afterwards has no effect. Getters are never run. The copy refuses cycles and values JSON can't represent (functions, `undefined`, `NaN`, class instances and so on), rather than silently dropping them.
83
+
84
+ ## Quick start: CLI
85
+
86
+ ```sh
87
+ GH_TOKEN=... npx sarif-to-comment \
88
+ --sarif results.sarif \
89
+ --repo acme/widgets --pull 42 \
90
+ --commit c0dec0dec0dec0dec0dec0dec0dec0dec0dec0de \
91
+ --state /var/lib/my-linter/reviews/acme-widgets-42-run-1817.json
92
+ ```
93
+
94
+ - **Optional flags:** `--source-root FILE_URI`, `--old-source-commit FULLSHA` and `--ignore-approval-hold`. Run `sarif-to-comment --help` for details; it needs no token and makes no request.
95
+ - **Same core as the library:** the CLI reads the file, calls the same `publishSarifReview`, and prints the same Markdown to stdout.
96
+ - **File encoding:** the SARIF file must be UTF-8 JSON.
97
+ - A leading UTF-8 byte-order mark is ignored, so the CLI and a library caller passing the same parsed document produce the same publication.
98
+ - A file that is not valid UTF-8 (including UTF-16) is refused before any request is made. Its bytes are never silently replaced.
99
+
100
+ | Exit status | Meaning |
101
+ | --- | --- |
102
+ | 0 | published (or already published) |
103
+ | 2 | blocked: nothing was published |
104
+ | 3 | uncertain: retry with the same `--state` |
105
+ | 1 | usage error, unreadable or unparsable SARIF file, refused request, or operational failure (details on stderr) |
106
+
107
+ ## Credentials
108
+
109
+ Use a GitHub **personal access token or user token**. It needs permission to read the repository and to create pull request reviews; for a fine-grained token that is *Contents: Read* and *Pull requests: Read and write*. The CLI reads `GH_TOKEN`, or else `GITHUB_TOKEN`; there is no token flag.
110
+
111
+ Authentication uses the authenticated user's identity. GitHub App installation tokens, including the automatic `GITHUB_TOKEN` of GitHub Actions workflows, are **not supported**.
112
+
113
+ The token is never written to the state file, never included in any fingerprint, and never printed. It is redacted from errors.
114
+
115
+ This review credential is unrelated to how the package itself is published. npm releases use [trusted publishing](https://docs.npmjs.com/trusted-publishers/), a maintainer concern described under [Releasing](#releasing).
116
+
117
+ ## The state path: retries, recovery and concurrency
118
+
119
+ `statePath` (`--state`) is the durable identity of **one** publication. You choose it, and you must keep it.
120
+
121
+ - Before anything is sent, the tool writes the complete intended review and a hidden marker to that file and flushes it to disk. Only the process that creates the file sends, and it sends once.
122
+ - **Retry with the same state path.** A later run never sends again. It returns the recorded result, or checks GitHub for the marker and confirms the complete review. It doesn't need the branch to still exist or the SARIF to still apply, because it works from the saved request.
123
+ - **After an `uncertain` result, do not delete the state file.** Delivery could not be confirmed: the response may have been lost, or the review may not be visible yet. The file is the only record that a review may already exist, and deleting it risks a duplicate. Retry later with the same path. The tool never repairs or restores anything it finds.
124
+ - **A new state path means a new, separate review.** Use one only when you deliberately want another review, for example for new SARIF.
125
+ - Concurrent runs on the same state path are safe: exactly one sends and the others only check.
126
+ - **The state path is bound to its input.**
127
+ - Reusing it with different SARIF, a different pull request or commit, or a different source root or old-side candidate is refused.
128
+ - An unresolved publication also checks the authenticated account.
129
+ - Completed and rejected records return without authentication or network calls; the API still requires a token-shaped input.
130
+ - The state file is created with owner-only permissions. It requires a local file system that supports hard links.
131
+
132
+ ## One pending review per account
133
+
134
+ GitHub lets an account hold only **one pending (draft) review per pull request**, and refuses a second one with HTTP 422. If your account already has a draft on the pull request — made by a person or by an earlier run — publication is `rejected`. The tool never submits, edits or deletes an existing draft to make room. A person has to submit or delete it on GitHub, and then you publish again with a new state path.
135
+
136
+ A refused request is recorded in the state file. Later runs with that path report the refusal without contacting GitHub, and never resend it.
137
+
138
+ ## Supported SARIF (first-milestone profile)
139
+
140
+ - **Whole-review validation.** The document is validated against the official SARIF 2.1.0 schema, then checked for consistency against the pull request's actual source. Invalid input or an unsupported finding, source association or fix blocks the entire review. Every accepted finding is included; this is not a lossless rendering of all SARIF metadata.
141
+ - **General findings.** A result without a location goes in the review body.
142
+ - **Findings with a location.** A result with one physical location becomes an inline comment when it maps exactly onto a line of the pull request's diff at the reviewed commit. Otherwise it goes in the body with an exact permalink to that commit and a copy of the source. Nothing is ever placed on a nearby or different line.
143
+ - **Suggestions.** A fix with one text replacement on the reviewed head, inside the diff, becomes a native GitHub suggestion.
144
+ - A located result must refer to the same file and revision, with its lines contained in the replacement's lines. Otherwise this profile refuses the association; keep the correct finding location and separate the feedback from the unsupported fix.
145
+ - Cases GitHub doesn't apply faithfully are refused before anything is written: raw CR in the suggestion payload, nested triple-backtick fences, blank-only replacements and unsafe final-line deletions.
146
+ - **Refused features.** Multiple locations, related locations, code flows, graphs, stacks, attachments, suppressions, alternative or multi-file fixes, and file creation or deletion proposals are refused with an explanation.
147
+ - **Metadata limits.**
148
+ - Producer fingerprints, rank and occurrence counts are not rendered.
149
+ - A logical location accompanying a physical location is not rendered; a logical-only location is unsupported.
150
+ - Taxonomy classifications remain in preparation evidence but are not shown in the review.
151
+ - **Result limits.** Results are limited to 100 inline comments, 60,000 characters per body or comment, and about 1 MB per request. These are conservative product limits, not GitHub maxima.
152
+ - **Pull request limits.** Pull requests changing more than 3,000 files, and source files larger than 1,000,000 bytes or not valid UTF-8, are refused rather than read partially. A file whose patch GitHub omits cannot receive inline comments.
153
+
154
+ ### Approval hold
155
+
156
+ A run or result may declare `properties.sarifToComment.approval: "awaiting-approval"`. Publication then stops with `blocked`.
157
+
158
+ `options.ignoreApprovalHold` (`--ignore-approval-hold`) overrides **only** that hold, never any other check. The override is not part of the publication's identity, so a retry doesn't need to repeat it.
159
+
160
+ ### Reviewed commit and historical reviews
161
+
162
+ The review is always tied to `reviewedCommit`, even when the author's branch has moved on since. If the pull request's head has advanced past the reviewed commit, the tool doesn't retarget the review. Findings that can no longer be anchored inline go in the body as exact links to the reviewed commit.
163
+
164
+ ### Old-side source
165
+
166
+ Findings about deleted or original lines need to know the diff's old side. By default the tool asks GitHub to compare the pull request, and verifies each old file by reversing the pull request's own patch against the exact file contents. Old-side source of renamed files is not supported.
167
+
168
+ If that comparison can't establish the old side, you may pass `oldSourceCommit` (`--old-source-commit`) as a candidate. It is verified the same way, never trusted blindly, and it becomes part of the publication's identity. SARIF provenance naming other revisions is still published as general feedback. It is never chosen automatically as the diff's old side.
169
+
170
+ ## Limitations
171
+
172
+ - **Live verification.** The library and CLI have been verified against live GitHub. The runs covered:
173
+ - complete pending reviews, with exact source and original-anchor readback;
174
+ - rendered inline comments and suggestions, and general feedback;
175
+ - branch advance;
176
+ - read-only recovery after a discarded create response and after a process kill.
177
+
178
+ These bounded fixture runs do not establish every host failure mode or suggestion shape. Native application of one-line-to-three, two-lines-to-one and middle-line deletion suggestions was verified by exact resulting file bytes and Git blob identities. Inline-only and CRLF-source review bodies also read back exactly. The evidence is recorded in the source repository (`docs/milestone-e2e-evidence.md` and `docs/suggestion-application-e2e.md`).
179
+ - Only `https://api.github.com` is supported.
180
+ - The hidden marker only identifies a review; it is not a secret. A human edit to an unconfirmed draft leaves delivery `uncertain` rather than being "fixed".
181
+ - The durability steps (write, flush, then send) are ordered for crash safety, but that has not been tested against power loss.
182
+ - GitHub Enterprise Server and GitHub App installation tokens are not supported.
183
+ - There is no review maintenance, re-review or synchronisation back to SARIF.
184
+ - npm attaches a provenance attestation only when the source repository is public at publish time. A release published while the repository is private has no provenance attestation (see [Releasing](#releasing)).
185
+
186
+ ## Development
187
+
188
+ ```sh
189
+ pnpm install
190
+ pnpm test # node --test test/*.test.cjs
191
+ pnpm run check # lint, types, API report/docs freshness, release plan, tests (read-only)
192
+ pnpm run build # regenerate api-report/ and docs/api/ after changing types/index.d.ts
193
+ pnpm changeset # describe a change for the next release
194
+ ```
195
+
196
+ The public TypeScript declarations are written by hand in `types/index.d.ts` and must match the CommonJS runtime in `src/index.cjs`. API Extractor checks them and writes a reviewable API report (`api-report/`) and a doc model. API Documenter renders the doc model as the Markdown reference in `docs/api/`. `pnpm run check` fails when either is out of date.
197
+
198
+ ## Releasing
199
+
200
+ Releases use [Changesets](https://changesets.dev/) for versioning and [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) for publication. They are published only by the GitHub Actions workflow `.github/workflows/publish.yml`, which authenticates with a short-lived OIDC token. There is no npm token in the repository or its secrets.
201
+
202
+ ### Versions stay below 1.0.0
203
+
204
+ `MAXIMUM_RELEASE_MAJOR` in `scripts/release-guard.cjs` is `0`. Until someone deliberately raises it in a reviewed change, nothing can version or publish `1.0.0` or higher, including prereleases such as `1.0.0-rc.0`:
205
+
206
+ - **`pnpm run check`** (run in CI on every pull request) fails if a pending changeset would reach 1.0.0. A `major` changeset is refused with an explanation. It is never quietly converted to a smaller bump; choose `minor` yourself if the change shouldn't start 1.0.
207
+ - **`pnpm run release:version`** refuses the same plans before `changeset version` changes any file.
208
+ - **What counts as the plan.** The guard doesn't parse changeset files itself. It runs the real `changeset version` in a throwaway copy and judges the version and changelog entry Changesets produces, so any front matter Changesets accepts is judged by its actual effect. That includes quoted values such as `"sarif-to-comment": "major"`. A changeset Changesets can't read is refused, not ignored.
209
+ - **The publish workflow** refuses any `package.json` version of 1.0.0 or higher, and so does the `prepublishOnly` backstop for a manual publish.
210
+
211
+ Changesets pre mode (prereleases) is not part of this release path.
212
+
213
+ **Deliberately releasing 1.0.** In a reviewed change, raise `MAXIMUM_RELEASE_MAJOR` to `1` and update the test in `test/release.test.cjs` that pins its value. That is the only step: a `major` changeset then versions and publishes `1.0.0` through the normal flow above, while `2.0.0` and above stay blocked.
214
+
215
+ ### Making a release
216
+
217
+ 1. With each change, add a changeset (`pnpm changeset`) choosing `patch` or `minor`, and merge it to `main` with the change.
218
+ 2. To release, run `pnpm run release:version` on an up-to-date `main`. It checks the pending plan, then runs `changeset version`, which:
219
+ - bumps `package.json`;
220
+ - writes the `CHANGELOG.md` entry;
221
+ - consumes the changesets.
222
+
223
+ Review and commit the result, then push it to `main`, for example through a pull request.
224
+ 3. When that version bump reaches `main`, `.github/workflows/publish.yml` runs. It runs only for `main` pushes that change `package.json`, and never for pull requests or other branches. Its release guard decides first:
225
+ - **Already on npm:** a version that is already published is a no-op.
226
+ - **Otherwise, refused unless all of these hold:**
227
+ - the version is stable and below 1.0.0;
228
+ - `CHANGELOG.md`'s newest entry is that version, which shows Changesets produced it;
229
+ - pre mode is off;
230
+ - the package metadata is publishable, with the exact repository URL;
231
+ - the commit is on `main`;
232
+ - npm is at least 11.5.1 and Node at least 22.14.0.
233
+ - **When allowed:** it runs `pnpm run check`, packs the tarball, verifies it contains exactly the distribution files, and publishes that tarball.
234
+
235
+ Publishes never overlap, and a publish in progress is never cancelled.
236
+ 4. **If publishing fails**, what to do depends on where the cause is. A version npm has already accepted can never be republished.
237
+ - **Outside the repository:** a transient registry, network or runner failure, or npm trusted-publisher settings that are missing or wrong. Correct it there, then re-run the failed workflow run. A re-run executes the same commit with the same workflow file, so it is only the right tool when nothing in the repository needs to change.
238
+ - **In the repository:** the release guard, the workflow, a test, the package contents or its metadata. A re-run would repeat the same code, and a push to `main` that doesn't change `package.json` does not start a publish run. Merge the fix to `main`, then prepare the next patch release:
239
+ 1. add a `patch` changeset with `pnpm changeset`;
240
+ 2. run `pnpm run release:version`;
241
+ 3. merge the resulting commit.
242
+
243
+ That commit changes `package.json`, so the workflow runs with the fixed code and publishes the new version. The version that failed stays unpublished; its changelog entry remains as history.
244
+
245
+ **Recording the release commit.** The workflow publishes the verified tarball but doesn't create git tags, and npm's registry metadata for 0.1.0 records no `gitHead`. Do not assume that field identifies a tarball release. The commit is identified in two places:
246
+ - by the successful `publish.yml` workflow run for that commit on `main`;
247
+ - when the repository is public at publish time, by the version's npm provenance attestation, which names the source repository, workflow and commit.
248
+
249
+ Maintainers who want tags can run `pnpm changeset git-tag` locally on that commit and push the tags.
250
+
251
+ **First release.** The release history starts from npm's pre-existing `0.0.0`, the bootstrap baseline for this package. The initial `minor` changeset versions that baseline to **0.1.0**, with the first changelog entry, and 0.1.0 is the first version this workflow publishes. Later releases follow the same steps from whatever version `package.json` then holds.
252
+
253
+ ### One-time setup (npmjs.com)
254
+
255
+ On the package's **Settings → Trusted publishing** page, add a GitHub Actions trusted publisher:
256
+
257
+ | Field | Value |
258
+ | --- | --- |
259
+ | Organization or user | `mike-north` |
260
+ | Repository | `sarif-to-comment` |
261
+ | Workflow filename | `publish.yml` |
262
+ | Environment | *(leave empty)* |
263
+
264
+ npm requires `repository.url` in `package.json` to match this repository exactly. It is `git+https://github.com/mike-north/sarif-to-comment.git`. Once a trusted release has succeeded, npm recommends setting **Publishing access** to *Require two-factor authentication and disallow tokens*.
265
+
266
+ **Provenance.** Trusted publishing authenticates with OIDC from a private or public repository alike. npm attaches a provenance attestation automatically only when the source repository is public at publish time; a release from a private repository has no provenance attestation. That limitation comes from npm, not from this workflow, and the workflow never changes repository visibility. Version 0.1.0 was published from the repository while it was public, and its npm provenance attestation verifies, naming commit `3797ca6efe2156d4c952fad7fed10b569f1dcbbb` and `.github/workflows/publish.yml`. The workflow deliberately does not pass `--provenance`, which fails for a private repository; npm adds provenance on its own whenever the repository is public.
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Thin command-line interface over publishSarifReview (src/index.cjs).
6
+ *
7
+ * Reads one SARIF JSON file, calls the same library operation library
8
+ * consumers use, and prints that operation's Markdown. It has no rendering,
9
+ * placement or delivery logic of its own: argument parsing, file reading,
10
+ * credential selection and exit-status mapping are its only concerns.
11
+ *
12
+ * Usage:
13
+ * sarif-to-comment --sarif FILE --repo OWNER/REPO --pull N --commit FULLSHA
14
+ * --state ABSOLUTE_FILE [--source-root ABSOLUTE_FILE_URI]
15
+ * [--old-source-commit FULLSHA] [--ignore-approval-hold]
16
+ * sarif-to-comment --help
17
+ *
18
+ * Flags accept `--flag value` or `--flag=value`. Unknown, missing, valueless
19
+ * or repeated flags and positional arguments are usage errors.
20
+ * `--ignore-approval-hold` takes no value and bypasses only an approval hold.
21
+ * There is no token flag and no reset/force-resend flag: `--state` is the
22
+ * durable identity of one publication; retry with the same path.
23
+ *
24
+ * Credential: GH_TOKEN, else GITHUB_TOKEN (empty counts as unset). Both are
25
+ * treated as user/PAT credentials; automatic Actions tokens are not claimed to
26
+ * be supported. The token never appears in output; any occurrence in an error
27
+ * is redacted. `--help` needs no token and makes no network call.
28
+ *
29
+ * Output and exit status:
30
+ * stdout: the operation's Markdown for any outcome
31
+ * stderr: usage, input-file and operational errors (actionable, redacted)
32
+ * 0 published 2 blocked 3 uncertain
33
+ * 1 usage error, unreadable/unparsable SARIF file, operational failure,
34
+ * local state refusal, or GitHub refusal of the create request
35
+ *
36
+ * ---------------------------------------------------------------------------
37
+ * main({ argv, env, stdout, stderr }, internals?) -> Promise<exitCode>
38
+ * argv: arguments after the executable; env: environment variables;
39
+ * stdout/stderr: writable streams. `internals` is passed through to
40
+ * publishSarifReview (private test seam). Running this file directly calls
41
+ * main with the process and sets process.exitCode.
42
+ */
43
+
44
+ const fs = require('node:fs');
45
+ const path = require('node:path');
46
+
47
+ const { publishSarifReview } = require('../src/index.cjs');
48
+
49
+ /** Exit status per public outcome (and for help); every refusal or failure is 1. */
50
+ const EXIT = Object.freeze({ help: 0, published: 0, failure: 1, blocked: 2, uncertain: 3, rejected: 1 });
51
+
52
+ /** Flags that take exactly one value. */
53
+ const VALUE_FLAGS = new Set(['--sarif', '--repo', '--pull', '--commit', '--state', '--source-root', '--old-source-commit']);
54
+
55
+ /**
56
+ * Decoder for SARIF files, which are UTF-8 JSON (SARIF 2.1.0 §3.1, RFC 8259
57
+ * §8.1). `fatal` refuses bytes that are not valid UTF-8 instead of silently
58
+ * substituting U+FFFD, which would change what is fingerprinted and
59
+ * published. A leading UTF-8 byte-order mark is an encoding signature, not
60
+ * JSON content, so it is removed (the decoder's default, `ignoreBOM: false`);
61
+ * the library then sees exactly the document a caller would pass in memory.
62
+ */
63
+ const SARIF_DECODER = new TextDecoder('utf-8', { fatal: true, ignoreBOM: false });
64
+
65
+ /** Flags that take no value. */
66
+ const BOOLEAN_FLAGS = new Set(['--ignore-approval-hold']);
67
+
68
+ /** Flags every publication needs. */
69
+ const REQUIRED_FLAGS = ['--sarif', '--repo', '--pull', '--commit', '--state'];
70
+
71
+ const md = String.raw;
72
+
73
+ const USAGE = md`sarif-to-comment — publish a ready SARIF file as one GitHub draft review
74
+
75
+ Usage:
76
+ sarif-to-comment --sarif FILE --repo OWNER/REPO --pull N --commit FULLSHA
77
+ --state ABSOLUTE_FILE [--source-root ABSOLUTE_FILE_URI]
78
+ [--old-source-commit FULLSHA] [--ignore-approval-hold]
79
+ sarif-to-comment --help
80
+
81
+ Options:
82
+ --sarif FILE SARIF 2.1.0 JSON file to publish.
83
+ --repo OWNER/REPO Repository of the pull request.
84
+ --pull N Pull request number.
85
+ --commit FULLSHA Full 40-character commit the review is about.
86
+ --state ABSOLUTE_FILE Durable publication state. Retry with the same
87
+ file; never delete it after an uncertain
88
+ result. A new file starts a separate review.
89
+ --source-root ABSOLUTE_FILE_URI
90
+ Repository root in the SARIF producer's file
91
+ system (file:///.../ ending in "/").
92
+ --old-source-commit FULLSHA Candidate commit for the diff's old side, used
93
+ only when GitHub's comparison cannot establish
94
+ it; verified against the pull request's patches.
95
+ --ignore-approval-hold Publish despite an approval hold (bypasses only
96
+ the hold, never validation).
97
+ --help Show this help. Needs no token, makes no request.
98
+
99
+ Credentials:
100
+ GH_TOKEN, or else GITHUB_TOKEN: a GitHub personal access token or user token.
101
+ GitHub App installation tokens (including the automatic Actions token) are
102
+ not supported. There is no token flag.
103
+
104
+ Exit status:
105
+ 0 published (or already published)
106
+ 2 blocked: nothing was published
107
+ 3 uncertain: delivery could not be confirmed; retry with the same --state
108
+ 1 usage error, unreadable SARIF file, refused request, or operational failure
109
+ `;
110
+
111
+ /** A command-line mistake the user can fix by changing the arguments. */
112
+ class UsageError extends Error {}
113
+
114
+ /** Parses argv into { help } or { values, flags } exactly; throws UsageError. */
115
+ function parseArgs(argv) {
116
+ if (argv.includes('--help') || argv.includes('-h')) return { help: true };
117
+ const values = new Map();
118
+ const flags = new Set();
119
+ for (let i = 0; i < argv.length; i += 1) {
120
+ const arg = argv[i];
121
+ if (!arg.startsWith('--')) throw new UsageError(`unexpected argument ${arg}`);
122
+ const eq = arg.indexOf('=');
123
+ const name = eq === -1 ? arg : arg.slice(0, eq);
124
+ if (name === '--token') {
125
+ throw new UsageError('unknown option --token: the token is read only from GH_TOKEN or GITHUB_TOKEN');
126
+ }
127
+ if (BOOLEAN_FLAGS.has(name)) {
128
+ if (eq !== -1) throw new UsageError(`${name} takes no value`);
129
+ if (flags.has(name)) throw new UsageError(`${name} was given more than once`);
130
+ flags.add(name);
131
+ continue;
132
+ }
133
+ if (!VALUE_FLAGS.has(name)) throw new UsageError(`unknown option ${name}`);
134
+ if (values.has(name)) throw new UsageError(`${name} was given more than once`);
135
+ let value;
136
+ if (eq !== -1) {
137
+ value = arg.slice(eq + 1);
138
+ } else {
139
+ value = argv[i + 1];
140
+ if (value === undefined || value.startsWith('--')) throw new UsageError(`${name} requires a value`);
141
+ i += 1;
142
+ }
143
+ if (value === '') throw new UsageError(`${name} requires a non-empty value`);
144
+ values.set(name, value);
145
+ }
146
+ const missing = REQUIRED_FLAGS.filter((flag) => !values.has(flag));
147
+ if (missing.length > 0) throw new UsageError(`missing required option ${missing.join(', ')}`);
148
+ return { help: false, values, flags };
149
+ }
150
+
151
+ /** Converts parsed flags into library input fields; throws UsageError. */
152
+ function interpretArgs({ values, flags }) {
153
+ const repo = /^([^/\s]+)\/([^/\s]+)$/.exec(values.get('--repo'));
154
+ if (!repo) throw new UsageError('--repo must be OWNER/REPO');
155
+ const pull = values.get('--pull');
156
+ if (!/^[1-9][0-9]*$/.test(pull) || !Number.isSafeInteger(Number(pull))) {
157
+ throw new UsageError('--pull must be a positive pull request number');
158
+ }
159
+ const commitFlag = (flag) => {
160
+ const value = values.get(flag);
161
+ if (value !== undefined && !/^[0-9a-f]{40}$/.test(value)) {
162
+ throw new UsageError(`${flag} must be a full 40-character lowercase commit SHA`);
163
+ }
164
+ return value;
165
+ };
166
+ const statePath = values.get('--state');
167
+ if (!path.isAbsolute(statePath)) throw new UsageError('--state must be an absolute file path');
168
+ const sourceRootUri = values.get('--source-root');
169
+ if (sourceRootUri !== undefined && !(sourceRootUri.startsWith('file:') && sourceRootUri.endsWith('/'))) {
170
+ throw new UsageError('--source-root must be an absolute file: URI ending in "/"');
171
+ }
172
+ const input = {
173
+ destination: { owner: repo[1], repo: repo[2], pullNumber: Number(pull) },
174
+ reviewedCommit: commitFlag('--commit'),
175
+ statePath,
176
+ };
177
+ const oldSourceCommit = commitFlag('--old-source-commit');
178
+ if (oldSourceCommit !== undefined) input.oldSourceCommit = oldSourceCommit;
179
+ if (sourceRootUri !== undefined) input.sourceRootUri = sourceRootUri;
180
+ if (flags.has('--ignore-approval-hold')) input.options = { ignoreApprovalHold: true };
181
+ return { sarifPath: values.get('--sarif'), input };
182
+ }
183
+
184
+ /** The credential from the environment: GH_TOKEN, else GITHUB_TOKEN; empty is unset. */
185
+ function tokenFrom(env) {
186
+ if (typeof env.GH_TOKEN === 'string' && env.GH_TOKEN !== '') return env.GH_TOKEN;
187
+ if (typeof env.GITHUB_TOKEN === 'string' && env.GITHUB_TOKEN !== '') return env.GITHUB_TOKEN;
188
+ return undefined;
189
+ }
190
+
191
+ /** An error's message and cause chain, one line each. */
192
+ function describeError(err) {
193
+ const lines = [];
194
+ const seen = new Set();
195
+ for (let current = err; current !== undefined && current !== null && !seen.has(current); current = current.cause) {
196
+ seen.add(current);
197
+ lines.push(lines.length === 0 ? String(current.message ?? current) : ` caused by: ${current.message ?? current}`);
198
+ if (!(current instanceof Error)) break;
199
+ }
200
+ return lines.join('\n');
201
+ }
202
+
203
+ /**
204
+ * Runs the CLI. Returns the exit status; never throws for expected failures.
205
+ */
206
+ async function main({ argv, env, stdout, stderr }, internals) {
207
+ const token = tokenFrom(env);
208
+ const safe = (text) => (token === undefined ? String(text) : String(text).split(token).join('[redacted]'));
209
+ const fail = (message) => {
210
+ stderr.write(`sarif-to-comment: ${safe(message)}\n`);
211
+ return EXIT.failure;
212
+ };
213
+
214
+ let parsed;
215
+ let request;
216
+ try {
217
+ parsed = parseArgs(argv);
218
+ if (parsed.help) {
219
+ stdout.write(USAGE);
220
+ return EXIT.help;
221
+ }
222
+ request = interpretArgs(parsed);
223
+ } catch (err) {
224
+ if (err instanceof UsageError) return fail(`${err.message}\nRun sarif-to-comment --help for usage.`);
225
+ throw err;
226
+ }
227
+ if (token === undefined) {
228
+ return fail('no GitHub token: set GH_TOKEN (or GITHUB_TOKEN) to a personal access token or user token.');
229
+ }
230
+
231
+ let bytes;
232
+ try {
233
+ bytes = fs.readFileSync(request.sarifPath);
234
+ } catch (err) {
235
+ return fail(`cannot read SARIF file ${request.sarifPath}: ${err.message}`);
236
+ }
237
+ let text;
238
+ try {
239
+ text = SARIF_DECODER.decode(bytes);
240
+ } catch {
241
+ return fail(
242
+ `SARIF file ${request.sarifPath} is not valid UTF-8; nothing was published. SARIF files must be UTF-8 encoded JSON.`,
243
+ );
244
+ }
245
+ let sarif;
246
+ try {
247
+ sarif = JSON.parse(text);
248
+ } catch (err) {
249
+ return fail(`SARIF file ${request.sarifPath} is not valid JSON: ${err.message}`);
250
+ }
251
+
252
+ let outcome;
253
+ try {
254
+ outcome = await publishSarifReview({ ...request.input, sarif, token }, internals);
255
+ } catch (err) {
256
+ return fail(describeError(err));
257
+ }
258
+ const markdown = safe(outcome.markdown);
259
+ stdout.write(markdown.endsWith('\n') ? markdown : `${markdown}\n`);
260
+ return EXIT[outcome.status] ?? EXIT.failure;
261
+ }
262
+
263
+ if (require.main === module) {
264
+ main({ argv: process.argv.slice(2), env: process.env, stdout: process.stdout, stderr: process.stderr }).then(
265
+ (code) => {
266
+ process.exitCode = code;
267
+ },
268
+ (err) => {
269
+ process.stderr.write(`sarif-to-comment: unexpected failure: ${err && err.message}\n`);
270
+ process.exitCode = EXIT.failure;
271
+ },
272
+ );
273
+ }
274
+
275
+ module.exports = { main };
@@ -0,0 +1,32 @@
1
+ <!-- Do not edit this file. It is automatically generated by API Documenter. -->
2
+
3
+ [Home](./index.md)
4
+
5
+ ## API Reference
6
+
7
+ ## Packages
8
+
9
+ <table><thead><tr><th>
10
+
11
+ Package
12
+
13
+
14
+ </th><th>
15
+
16
+ Description
17
+
18
+
19
+ </th></tr></thead>
20
+ <tbody><tr><td>
21
+
22
+ [sarif-to-comment](./sarif-to-comment.md)
23
+
24
+
25
+ </td><td>
26
+
27
+ Publish a ready SARIF 2.1.0 document as one GitHub draft pull request review.
28
+
29
+
30
+ </td></tr>
31
+ </tbody></table>
32
+
@@ -0,0 +1,13 @@
1
+ <!-- Do not edit this file. It is automatically generated by API Documenter. -->
2
+
3
+ [Home](./index.md) &gt; [sarif-to-comment](./sarif-to-comment.md) &gt; [IBlockedOutcome](./sarif-to-comment.iblockedoutcome.md) &gt; [markdown](./sarif-to-comment.iblockedoutcome.markdown.md)
4
+
5
+ ## IBlockedOutcome.markdown property
6
+
7
+ Every problem, with a pointer into the SARIF document.
8
+
9
+ **Signature:**
10
+
11
+ ```typescript
12
+ readonly markdown: string;
13
+ ```