sarif-to-comment 0.2.0 → 0.2.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.
- package/CHANGELOG.md +9 -0
- package/README.md +23 -8
- package/dist/artifact-files.cjs +273 -0
- package/dist/cli.cjs +1062 -0
- package/dist/github.cjs +1181 -0
- package/dist/index.cjs +39 -0
- package/dist/placement.cjs +649 -0
- package/dist/prepare-review.cjs +1623 -0
- package/dist/publication.cjs +1055 -0
- package/dist/publish-sarif-review.cjs +560 -0
- package/dist/replacements.cjs +459 -0
- package/dist/sarif-authoring.cjs +313 -0
- package/dist/sarif-common.cjs +657 -0
- package/dist/sarif-inspection.cjs +834 -0
- package/dist/sarif-to-comment.cjs +37 -0
- package/dist/sarif-to-comment.d.ts +1018 -0
- package/dist/staged-changes.cjs +1140 -0
- package/dist/staged-git.cjs +391 -0
- package/docs/api/sarif-to-comment.iinspectionrun.md +2 -2
- package/docs/api/sarif-to-comment.iinspectionrun.source.md +4 -1
- package/docs/api/sarif-to-comment.iinspectionrun.tool.md +4 -1
- package/docs/api/sarif-to-comment.isarifinspection.log.md +3 -1
- package/docs/api/sarif-to-comment.isarifinspection.md +1 -1
- package/docs/api/sarif-to-comment.isarifinspection.summary.md +7 -7
- package/docs/api/sarif-to-comment.md +1 -1
- package/package.json +30 -17
- package/bin/sarif-to-comment.cjs +0 -29
- package/src/artifact-files.cjs +0 -255
- package/src/cli.cjs +0 -927
- package/src/github.cjs +0 -1092
- package/src/index.cjs +0 -532
- package/src/placement.cjs +0 -586
- package/src/prepare-review.cjs +0 -1547
- package/src/publication.cjs +0 -994
- package/src/replacements.cjs +0 -463
- package/src/sarif-authoring.cjs +0 -230
- package/src/sarif-common.cjs +0 -589
- package/src/sarif-inspection.cjs +0 -677
- package/src/staged-changes.cjs +0 -1026
- package/src/staged-git.cjs +0 -367
- package/types/index.d.ts +0 -1023
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# sarif-to-comment
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6d868ab: The implementation is now strict TypeScript, and the shipped TypeScript declarations are generated from it rather than written by hand.
|
|
8
|
+
|
|
9
|
+
- No change to the public API, the CLI or behavior. The declarations describe the same functions and types as before.
|
|
10
|
+
- The packaged runtime files moved from `src/` and `bin/` to `dist/`. The package entry points are unchanged: `require('sarif-to-comment')`, `import … from 'sarif-to-comment'`, its TypeScript types and the `sarif-to-comment` command resolve through `package.json` as before. Only code that reached into the package's internal file paths is affected.
|
|
11
|
+
|
|
3
12
|
## 0.2.0
|
|
4
13
|
|
|
5
14
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -228,13 +228,28 @@ If that comparison can't establish the old side, you may pass `oldSourceCommit`
|
|
|
228
228
|
|
|
229
229
|
```sh
|
|
230
230
|
pnpm install
|
|
231
|
-
pnpm
|
|
231
|
+
pnpm run build # rebuild dist/ from src/, roll up the declarations, regenerate api-report/ and docs/api/
|
|
232
232
|
pnpm run check # lint, types, API report/docs freshness, release plan, tests (read-only)
|
|
233
|
-
pnpm
|
|
233
|
+
pnpm test # refuse a missing or stale dist/, then node --test "test/**/*.test.mts"
|
|
234
234
|
pnpm changeset # describe a change for the next release
|
|
235
235
|
```
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
Development and release tooling need Node 22.18.0 or later, enforced by `devEngines` in `package.json`: the tests and the build, check and release scripts are TypeScript that Node runs directly by type stripping. The published package still needs only Node 22 or later (`engines`).
|
|
238
|
+
|
|
239
|
+
Run `pnpm run build` before `pnpm run check` or `pnpm test`, and again after changing sources or build configuration. The tests exercise, and the package ships, the built `dist/`. `pnpm test` (and therefore `pnpm run check`) refuses a `dist/` that is missing, incomplete, or was built from different sources or configuration; it compares content hashes of every build input and output, not modification times.
|
|
240
|
+
|
|
241
|
+
The implementation is strict TypeScript, and there are no hand-written declarations. The public TypeScript declarations are generated from the implementation. `src/public-api.cts` declares the public API, and the CommonJS runtime entry `src/index.cts` is checked at compile time to export exactly its functions. `tsc` emits per-module declarations, API Extractor rolls them up into the shipped `dist/sarif-to-comment.d.ts` and writes a reviewable API report (`api-report/`) and a doc model, and API Documenter renders the doc model as the Markdown reference in `docs/api/`. `pnpm run check` fails when either is out of date.
|
|
242
|
+
|
|
243
|
+
`pnpm run build` rewrites `api-report/` and `docs/api/` whenever the public API or its TSDoc changes; commit them with the change. CI and the publish workflow build and then fail if either differs from the committed copy.
|
|
244
|
+
|
|
245
|
+
**Repository layout.**
|
|
246
|
+
- `src/*.cts`: the implementation. `tsc` compiles each module to CommonJS in `dist/*.cjs`; `src/index.cts` is the package entry and `src/sarif-to-comment.cts` the CLI executable.
|
|
247
|
+
- `dist/`: build output. It is not committed; the package ships its runtime modules and the rolled-up declarations.
|
|
248
|
+
- `test/`: `node:test` suites (`*.test.mts`), fixtures and helpers, run against `dist/`.
|
|
249
|
+
- `scripts/*.mts`: build, type-check, API documentation and release-guard tooling.
|
|
250
|
+
- `api-report/` and `docs/api/`: generated by `pnpm run build` and committed, so API changes are reviewed.
|
|
251
|
+
- `vendor/`: the official SARIF 2.1.0 schema used for validation.
|
|
252
|
+
- `.changeset/`: pending release notes.
|
|
238
253
|
|
|
239
254
|
## Releasing
|
|
240
255
|
|
|
@@ -242,16 +257,16 @@ Releases use [Changesets](https://changesets.dev/) for versioning and [npm trust
|
|
|
242
257
|
|
|
243
258
|
### Versions stay below 1.0.0
|
|
244
259
|
|
|
245
|
-
`MAXIMUM_RELEASE_MAJOR` in `scripts/release-guard.
|
|
260
|
+
`MAXIMUM_RELEASE_MAJOR` in `scripts/release-guard.mts` 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`:
|
|
246
261
|
|
|
247
262
|
- **`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.
|
|
248
263
|
- **`pnpm run release:version`** refuses the same plans before `changeset version` changes any file.
|
|
249
264
|
- **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.
|
|
250
|
-
- **The publish workflow** refuses any `package.json` version of 1.0.0 or higher, and so does the `prepublishOnly` backstop for a manual publish
|
|
265
|
+
- **The publish workflow** refuses any `package.json` version of 1.0.0 or higher, and so does the `prepublishOnly` backstop for a manual publish, which also refuses a missing or stale `dist/`.
|
|
251
266
|
|
|
252
267
|
Changesets pre mode (prereleases) is not part of this release path.
|
|
253
268
|
|
|
254
|
-
**Deliberately releasing 1.0.** In a reviewed change, raise `MAXIMUM_RELEASE_MAJOR` to `1` and update the test in `test/release.test.
|
|
269
|
+
**Deliberately releasing 1.0.** In a reviewed change, raise `MAXIMUM_RELEASE_MAJOR` to `1` and update the test in `test/release.test.mts` 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.
|
|
255
270
|
|
|
256
271
|
### Making a release
|
|
257
272
|
|
|
@@ -270,8 +285,8 @@ Changesets pre mode (prereleases) is not part of this release path.
|
|
|
270
285
|
- pre mode is off;
|
|
271
286
|
- the package metadata is publishable, with the exact repository URL;
|
|
272
287
|
- the commit is on `main`;
|
|
273
|
-
- npm is at least 11.5.1 and Node at least 22.
|
|
274
|
-
- **When allowed:** it runs `pnpm run check`, packs the tarball, verifies it contains exactly the distribution files, and publishes that tarball.
|
|
288
|
+
- npm is at least 11.5.1 and Node at least 22.18.0.
|
|
289
|
+
- **When allowed:** it builds `dist/`, runs `pnpm run check`, packs the tarball, verifies it contains exactly the distribution files, and publishes that tarball.
|
|
275
290
|
|
|
276
291
|
Publishes never overlap, and a publish in progress is never cancelled.
|
|
277
292
|
4. **If publishing fails**, what to do depends on where the cause is. A version npm has already accepted can never be republished.
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* File handling for the CLI's local SARIF artifacts. The library never touches
|
|
4
|
+
* files; the command-line interface uses this module so that every receipt it
|
|
5
|
+
* prints describes what actually happened on disk.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities and guarantees:
|
|
8
|
+
* - Reading: SARIF and message files are strict UTF-8 (a leading byte-order
|
|
9
|
+
* mark is an encoding signature and is removed); undecodable bytes are
|
|
10
|
+
* refused rather than replaced, so nothing is silently altered.
|
|
11
|
+
* - Exclusive creation (`createExclusive`): the content is written and
|
|
12
|
+
* flushed to a temporary sibling, then hard-linked to the destination,
|
|
13
|
+
* which fails if the destination exists. A file therefore appears whole or
|
|
14
|
+
* not at all, and an output that reappeared concurrently is never
|
|
15
|
+
* overwritten.
|
|
16
|
+
* - Ownership (`acquireOwnership`): cooperating sarif-to-comment commands
|
|
17
|
+
* editing or producing the same artifact exclude each other with an
|
|
18
|
+
* exclusively created marker file, `.<name>.sarif-to-comment-lock`, beside
|
|
19
|
+
* the artifact. A marker that already exists is reported, never taken over:
|
|
20
|
+
* only a person can know that its owner is gone. The owner removes it when
|
|
21
|
+
* it finishes, whatever the outcome.
|
|
22
|
+
* - In-place replacement (`replaceIfUnchanged`): the new content is written
|
|
23
|
+
* to a temporary sibling with the original mode; immediately before the
|
|
24
|
+
* atomic rename the file is re-read and compared with the bytes the edit
|
|
25
|
+
* was computed from, and an observed external change is refused. This is
|
|
26
|
+
* not a filesystem compare-and-swap: a non-cooperating writer acting
|
|
27
|
+
* between that check and the rename is outside the guarantee.
|
|
28
|
+
* - Output preservation (`archiveExisting`): an existing output is moved
|
|
29
|
+
* aside as `<YYYY-MM-DDTHH-mm-ss.SSSZ>.old.<name>` in its directory, the
|
|
30
|
+
* stamp being the file's birth time in UTC (or its modification time when
|
|
31
|
+
* the platform reports no birth time), with `-2`, `-3`, … appended to the
|
|
32
|
+
* stamp on collision. The archive is an exclusive hard link followed by
|
|
33
|
+
* removal of the original name, so it never overwrites anything.
|
|
34
|
+
*
|
|
35
|
+
* Failures a user must act on are `ArtifactError`s with actionable messages
|
|
36
|
+
* that name the paths involved.
|
|
37
|
+
*/
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.ArtifactError = void 0;
|
|
40
|
+
exports.readTextFile = readTextFile;
|
|
41
|
+
exports.readJsonFile = readJsonFile;
|
|
42
|
+
exports.createExclusive = createExclusive;
|
|
43
|
+
exports.ownershipMarkerFor = ownershipMarkerFor;
|
|
44
|
+
exports.acquireOwnership = acquireOwnership;
|
|
45
|
+
exports.replaceIfUnchanged = replaceIfUnchanged;
|
|
46
|
+
exports.archiveExisting = archiveExisting;
|
|
47
|
+
exports.sameExistingFile = sameExistingFile;
|
|
48
|
+
const crypto = require("node:crypto");
|
|
49
|
+
const fs = require("node:fs");
|
|
50
|
+
const path = require("node:path");
|
|
51
|
+
/** A file problem the user must resolve; the message names the paths. */
|
|
52
|
+
class ArtifactError extends Error {
|
|
53
|
+
}
|
|
54
|
+
exports.ArtifactError = ArtifactError;
|
|
55
|
+
/**
|
|
56
|
+
* A caught failure's `message`, read as a template would read `err.message`.
|
|
57
|
+
* Node's file-system calls throw Errors (SystemError), so this is their
|
|
58
|
+
* message; a value without one reads as "undefined".
|
|
59
|
+
*/
|
|
60
|
+
function messageOf(err) {
|
|
61
|
+
return String(typeof err === 'object' && err !== null && 'message' in err ? err.message : undefined);
|
|
62
|
+
}
|
|
63
|
+
/** A caught failure's Node error `code` (such as `EEXIST`), or undefined when it has none. */
|
|
64
|
+
function codeOf(err) {
|
|
65
|
+
return typeof err === 'object' && err !== null && 'code' in err ? err.code : undefined;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Decoder for UTF-8 text files: `fatal` refuses invalid bytes instead of
|
|
69
|
+
* substituting U+FFFD; the default `ignoreBOM: false` removes a leading BOM.
|
|
70
|
+
*/
|
|
71
|
+
const UTF8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: false });
|
|
72
|
+
/** Reads a UTF-8 text file; `label` names it in errors (e.g. "message file"). */
|
|
73
|
+
function readTextFile(file, label) {
|
|
74
|
+
let bytes;
|
|
75
|
+
try {
|
|
76
|
+
bytes = fs.readFileSync(file);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
throw new ArtifactError(`cannot read ${label} ${file}: ${messageOf(err)}`);
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
return { text: UTF8.decode(bytes), bytes };
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new ArtifactError(`${label} ${file} is not valid UTF-8; it must be UTF-8 encoded.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Reads a UTF-8 JSON file: { value, bytes } (the bytes as read, for change detection). */
|
|
89
|
+
function readJsonFile(file, label) {
|
|
90
|
+
const { text, bytes } = readTextFile(file, label);
|
|
91
|
+
try {
|
|
92
|
+
const value = JSON.parse(text);
|
|
93
|
+
return { value, bytes };
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
throw new ArtifactError(`${label} ${file} is not valid JSON: ${messageOf(err)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Flushes a directory entry change (best effort where directories cannot be opened). */
|
|
100
|
+
function syncDirectory(dir) {
|
|
101
|
+
let fd;
|
|
102
|
+
try {
|
|
103
|
+
fd = fs.openSync(dir, 'r');
|
|
104
|
+
fs.fsyncSync(fd);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Some platforms do not allow fsync on a directory; the rename/link itself is still atomic.
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
if (fd !== undefined)
|
|
111
|
+
fs.closeSync(fd);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Writes and flushes `text` to a new uniquely named temporary sibling of `file`. */
|
|
115
|
+
function writeTemporarySibling(file, text, mode) {
|
|
116
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${String(process.pid)}.${crypto.randomUUID()}.tmp`);
|
|
117
|
+
const fd = fs.openSync(temporary, 'wx', mode ?? 0o666);
|
|
118
|
+
try {
|
|
119
|
+
fs.writeFileSync(fd, text);
|
|
120
|
+
if (mode !== undefined)
|
|
121
|
+
fs.fchmodSync(fd, mode);
|
|
122
|
+
fs.fsyncSync(fd);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
fs.closeSync(fd);
|
|
126
|
+
}
|
|
127
|
+
return temporary;
|
|
128
|
+
}
|
|
129
|
+
/** Removes a temporary file if it is still present. */
|
|
130
|
+
function discard(temporary) {
|
|
131
|
+
try {
|
|
132
|
+
fs.unlinkSync(temporary);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Already gone.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Creates `file` with `text` only if it does not exist. `hooks.beforeLink`
|
|
140
|
+
* (tests only) runs after the temporary file is complete.
|
|
141
|
+
*/
|
|
142
|
+
function createExclusive(file, text, hooks = {}) {
|
|
143
|
+
const temporary = writeTemporarySibling(file, text);
|
|
144
|
+
try {
|
|
145
|
+
if (hooks.beforeLink)
|
|
146
|
+
hooks.beforeLink();
|
|
147
|
+
try {
|
|
148
|
+
fs.linkSync(temporary, file);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
if (codeOf(err) === 'EEXIST') {
|
|
152
|
+
throw new ArtifactError(`${file} already exists (it may have been created by another program); it was left unchanged.`);
|
|
153
|
+
}
|
|
154
|
+
throw new ArtifactError(`cannot create ${file}: ${messageOf(err)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
discard(temporary);
|
|
159
|
+
}
|
|
160
|
+
syncDirectory(path.dirname(file));
|
|
161
|
+
}
|
|
162
|
+
/** The ownership marker for an artifact: `.<name>.sarif-to-comment-lock` in its directory. */
|
|
163
|
+
function ownershipMarkerFor(file) {
|
|
164
|
+
return path.join(path.dirname(file), `.${path.basename(file)}.sarif-to-comment-lock`);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Takes exclusive cooperating-writer ownership of `file` (which need not
|
|
168
|
+
* exist). Returns a release function. Throws ArtifactError if another owner
|
|
169
|
+
* holds it.
|
|
170
|
+
*/
|
|
171
|
+
function acquireOwnership(file) {
|
|
172
|
+
const marker = ownershipMarkerFor(file);
|
|
173
|
+
const token = `${String(process.pid)} ${new Date().toISOString()} ${crypto.randomUUID()}\n`;
|
|
174
|
+
try {
|
|
175
|
+
fs.writeFileSync(marker, token, { flag: 'wx' });
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
if (codeOf(err) === 'EEXIST') {
|
|
179
|
+
throw new ArtifactError(`${file} is being written by another sarif-to-comment command (ownership marker ${marker}). ` +
|
|
180
|
+
`Wait for it to finish. If no such command is running, the marker is stale: delete ${marker} and run again.`);
|
|
181
|
+
}
|
|
182
|
+
throw new ArtifactError(`cannot take ownership of ${file} (marker ${marker}): ${messageOf(err)}`);
|
|
183
|
+
}
|
|
184
|
+
return function release() {
|
|
185
|
+
// Remove only our own marker: never delete one another process created.
|
|
186
|
+
try {
|
|
187
|
+
if (fs.readFileSync(marker, 'utf8') === token)
|
|
188
|
+
fs.unlinkSync(marker);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Already removed.
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Atomically replaces `file` with `text`, provided its content still equals
|
|
197
|
+
* `expectedBytes` immediately before the rename. `hooks.beforeReplace` (tests
|
|
198
|
+
* only) runs after the temporary file is complete.
|
|
199
|
+
*/
|
|
200
|
+
function replaceIfUnchanged(file, expectedBytes, text, hooks = {}) {
|
|
201
|
+
const { mode } = fs.statSync(file);
|
|
202
|
+
const temporary = writeTemporarySibling(file, text, mode & 0o7777);
|
|
203
|
+
try {
|
|
204
|
+
if (hooks.beforeReplace)
|
|
205
|
+
hooks.beforeReplace();
|
|
206
|
+
let current;
|
|
207
|
+
try {
|
|
208
|
+
current = fs.readFileSync(file);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
throw new ArtifactError(`${file} could not be re-read before replacement (${messageOf(err)}); it was not changed.`);
|
|
212
|
+
}
|
|
213
|
+
if (!current.equals(expectedBytes)) {
|
|
214
|
+
throw new ArtifactError(`${file} changed while this command was running; it was not overwritten. Run the command again.`);
|
|
215
|
+
}
|
|
216
|
+
fs.renameSync(temporary, file);
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
discard(temporary);
|
|
220
|
+
}
|
|
221
|
+
syncDirectory(path.dirname(file));
|
|
222
|
+
}
|
|
223
|
+
/** A stat time as the archive stamp, `YYYY-MM-DDTHH-mm-ss.SSSZ` (colons are not portable in names). */
|
|
224
|
+
function archiveStamp(milliseconds) {
|
|
225
|
+
return new Date(milliseconds).toISOString().replace(/:/g, '-');
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Moves an existing `file` aside under its archive name. Returns
|
|
229
|
+
* { path, from, timeSource: 'birth' | 'modified' }, or null if `file` does not
|
|
230
|
+
* exist. `options.stat` (tests only) replaces fs.statSync.
|
|
231
|
+
*/
|
|
232
|
+
function archiveExisting(file, { stat = fs.statSync } = {}) {
|
|
233
|
+
let info;
|
|
234
|
+
try {
|
|
235
|
+
info = stat(file);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
if (codeOf(err) === 'ENOENT')
|
|
239
|
+
return null;
|
|
240
|
+
throw new ArtifactError(`cannot examine existing output ${file}: ${messageOf(err)}`);
|
|
241
|
+
}
|
|
242
|
+
const birth = Number.isFinite(info.birthtimeMs) && info.birthtimeMs > 0;
|
|
243
|
+
const stamp = archiveStamp(birth ? info.birthtimeMs : info.mtimeMs);
|
|
244
|
+
const dir = path.dirname(file);
|
|
245
|
+
const name = path.basename(file);
|
|
246
|
+
for (let n = 1;; n += 1) {
|
|
247
|
+
const archive = path.join(dir, `${stamp}${n === 1 ? '' : `-${String(n)}`}.old.${name}`);
|
|
248
|
+
try {
|
|
249
|
+
fs.linkSync(file, archive);
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
if (codeOf(err) === 'EEXIST')
|
|
253
|
+
continue;
|
|
254
|
+
throw new ArtifactError(`cannot archive existing output ${file} as ${archive}: ${messageOf(err)}`);
|
|
255
|
+
}
|
|
256
|
+
fs.unlinkSync(file);
|
|
257
|
+
syncDirectory(dir);
|
|
258
|
+
return { path: archive, from: file, timeSource: birth ? 'birth' : 'modified' };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** True when two existing paths name the same file (including symbolic and hard links). */
|
|
262
|
+
function sameExistingFile(a, b) {
|
|
263
|
+
let sa;
|
|
264
|
+
let sb;
|
|
265
|
+
try {
|
|
266
|
+
sa = fs.statSync(a);
|
|
267
|
+
sb = fs.statSync(b);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
return sa.dev === sb.dev && sa.ino === sb.ino;
|
|
273
|
+
}
|