sync-header-metadata 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +263 -0
- package/bin/sync-header-metadata.js +111 -0
- package/default.gitattributes +17 -0
- package/package.json +73 -0
- package/src/action_log.js +89 -0
- package/src/sync.js +265 -0
package/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
<!-- vim:set expandtab shiftwidth=4 filetype=markdown foldlevel=3: -->
|
|
2
|
+
<!-- SPDX-License-Identifier: GPL-3.0-only -->
|
|
3
|
+
|
|
4
|
+
<!--
|
|
5
|
+
-
|
|
6
|
+
- ~chewygumxx/sync-header-metadata.git
|
|
7
|
+
- ::: :/README.md
|
|
8
|
+
-
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
<!--
|
|
12
|
+
- Checks/rewrites the "~owner/repo.git" and
|
|
13
|
+
- "::: :/<path>" lines in tracked files' header banners against each
|
|
14
|
+
- file's actual repository and path.
|
|
15
|
+
-->
|
|
16
|
+
|
|
17
|
+
# sync-header-metadata
|
|
18
|
+
|
|
19
|
+
A GitHub Action (and local CLI) that keeps a file-header convention honest.
|
|
20
|
+
|
|
21
|
+
Checks/rewrites the `~owner/repo.git` and `::: :/<path>` lines in tracked files'
|
|
22
|
+
header banners against each file's actual repository and path.
|
|
23
|
+
|
|
24
|
+
## Purpose
|
|
25
|
+
|
|
26
|
+
Consider the following textfile header, compliant in accordance with the
|
|
27
|
+
formalised format standard of a given repository:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
#!/usr/bin/env node
|
|
31
|
+
// vim:set expandtab shiftwidth=4 filetype=javascript:
|
|
32
|
+
|
|
33
|
+
//
|
|
34
|
+
//
|
|
35
|
+
// ~owner/repo.git
|
|
36
|
+
// ::: :/path/to/this/file
|
|
37
|
+
//
|
|
38
|
+
//
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Were a file with this header to be renamed, moved, forked, or otherwise
|
|
42
|
+
displaced, those two lines quietly go stale. This action finds every tracked
|
|
43
|
+
file whose banner has drifted from its current repository and/or path and either
|
|
44
|
+
loudly fails (`verify` mode), or rewrites it (`update` mode). The logic is
|
|
45
|
+
commentstring invariant and will perform irrespective of surrounding language
|
|
46
|
+
syntax eg. `-- %s`, `// %s`, `# %s`, `; %s`, et cetera.
|
|
47
|
+
|
|
48
|
+
Both markers must be the last non-whitespace content on their line, so this
|
|
49
|
+
matches the multi-line comment-block style shown above (marker on its own
|
|
50
|
+
line, closer on a separate line), but not a single-line closed comment like
|
|
51
|
+
`<!-- ~owner/repo.git -->` or `/* ~owner/repo.git */`.
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
To verify without rewrite, failing on drift.
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
- uses: actions/checkout@v7
|
|
59
|
+
|
|
60
|
+
- name: Verify header repository and path
|
|
61
|
+
uses: chewygumxx/sync-header-metadata@v2
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
To rewrite and update files instead of failing on desync:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
- uses: actions/checkout@v7
|
|
68
|
+
with:
|
|
69
|
+
ref: ${{ github.head_ref || github.ref_name }}
|
|
70
|
+
persist-credentials: true
|
|
71
|
+
|
|
72
|
+
- name: Update tracked files
|
|
73
|
+
uses: chewygumxx/sync-header-metadata@v2
|
|
74
|
+
with:
|
|
75
|
+
mode: update
|
|
76
|
+
|
|
77
|
+
- name: Commit changes
|
|
78
|
+
uses: stefanzweifel/git-auto-commit-action@v7
|
|
79
|
+
with:
|
|
80
|
+
commit_message: "chore: Sync header metadata"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For a complete, worked example, see the reusable
|
|
84
|
+
[`sync-header-metadata.yaml`](https://github.com/chewygumxx/.github/blob/v1/.github/workflows/sync-header-metadata.yaml)
|
|
85
|
+
workflow in `chewygumxx/.github`, which this repository's CI calls.
|
|
86
|
+
|
|
87
|
+
### Inputs
|
|
88
|
+
|
|
89
|
+
| Input | Required | Default | Description |
|
|
90
|
+
| ------------ | -------- | --------- | ----------------------------------------------------------------- |
|
|
91
|
+
| `mode` | No | `verify` | `verify` exits non-zero on drift; `update` rewrites in place. |
|
|
92
|
+
| `verbose` | No | `false` | Enable INFO-level logging. |
|
|
93
|
+
| `annotation` | No | `false` | Emit `::notice::`/`::warning::`/`::error::` workflow annotations. |
|
|
94
|
+
|
|
95
|
+
### Local usage
|
|
96
|
+
|
|
97
|
+
The same checks run from any local checkout via the
|
|
98
|
+
[`sync-header-metadata`](https://www.npmjs.com/package/sync-header-metadata)
|
|
99
|
+
npm package. Requires Node.js 24+ and `git` on `PATH`.
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
npx sync-header-metadata # verify
|
|
103
|
+
npx sync-header-metadata --update # rewrite in place
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Pin a major (`sync-header-metadata@2`) or an exact version
|
|
107
|
+
(`sync-header-metadata@2.3.0`) for reproducibility; npm versions match the
|
|
108
|
+
action's `vX.Y.Z` release tags.
|
|
109
|
+
|
|
110
|
+
| Option | Default | Description |
|
|
111
|
+
| ---------------------------- | --------- | ------------------------------------------------------------- |
|
|
112
|
+
| `-m`, `--mode <mode>` | `verify` | `verify` exits non-zero on drift; `update` rewrites in place. |
|
|
113
|
+
| `-u`, `--update` | Off | Shorthand for `--mode update`. |
|
|
114
|
+
| `-r`, `--repo <owner/repo>` | See below | Repository the `~owner/repo.git` line is checked against. |
|
|
115
|
+
| `-v`, `--verbose` | Off | Enable INFO-level logging. |
|
|
116
|
+
| `-h`, `--help` | | Show usage. |
|
|
117
|
+
|
|
118
|
+
Without `--repo`, the repository is taken from `$GITHUB_REPOSITORY` if set,
|
|
119
|
+
otherwise from the `origin` remote's URL. Workflow annotations are never
|
|
120
|
+
emitted locally.
|
|
121
|
+
|
|
122
|
+
Only files in git's index are checked (`git ls-files`), exactly as in CI. A
|
|
123
|
+
new file isn't checked until it's been `git add`ed; untracked files are
|
|
124
|
+
skipped without warning.
|
|
125
|
+
|
|
126
|
+
To catch drift before it reaches CI, e.g. as a husky `pre-commit` hook
|
|
127
|
+
(staged files are in the index, so they're covered):
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
npx --yes sync-header-metadata@2
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Exit codes
|
|
134
|
+
|
|
135
|
+
| Code | Meaning |
|
|
136
|
+
| ----- | -------------------------------------------------------------------------- |
|
|
137
|
+
| `0` | `verify` passed, or `update` completed. |
|
|
138
|
+
| `1` | `verify` found drift, or a fatal error (e.g. invalid mode, no repository). |
|
|
139
|
+
| `2` | Invalid command-line arguments, e.g. `--update` with `--mode verify`. |
|
|
140
|
+
| `127` | `git` not found on `PATH`. |
|
|
141
|
+
|
|
142
|
+
### Ignoring files
|
|
143
|
+
|
|
144
|
+
To exclude a path, explicitly unset the `sync-header-metadata` boolean
|
|
145
|
+
attribute for it in `.gitattributes`:
|
|
146
|
+
|
|
147
|
+
```gitattributes
|
|
148
|
+
vendor/** -sync-header-metadata
|
|
149
|
+
*.min.js -sync-header-metadata
|
|
150
|
+
/config.js -sync-header-metadata
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Standard `.gitattributes` matching applies:
|
|
154
|
+
|
|
155
|
+
- A bare pattern with no leading `/` matches at any depth.
|
|
156
|
+
- A leading `/` anchors it to that `.gitattributes` file's own directory.
|
|
157
|
+
- Nested `.gitattributes` files can re-enable syncing for a subtree per greater
|
|
158
|
+
specificity by setting the attribute back, e.g.
|
|
159
|
+
`important/** sync-header-metadata`.
|
|
160
|
+
|
|
161
|
+
#### Built-in defaults
|
|
162
|
+
|
|
163
|
+
The action ships a baseline exclusion list for files that structurally can't
|
|
164
|
+
carry a header comment, or are generated/lockfiles that shouldn't be
|
|
165
|
+
hand-edited:
|
|
166
|
+
|
|
167
|
+
```gitattributes
|
|
168
|
+
/LICENSE* -sync-header-metadata
|
|
169
|
+
.keep -sync-header-metadata
|
|
170
|
+
*.json -sync-header-metadata
|
|
171
|
+
*.lock -sync-header-metadata
|
|
172
|
+
pnpm-lock.yaml -sync-header-metadata
|
|
173
|
+
go.sum -sync-header-metadata
|
|
174
|
+
*.min.js -sync-header-metadata
|
|
175
|
+
*.min.css -sync-header-metadata
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
These are loaded at the lowest precedence, so they never need to be declared
|
|
179
|
+
in your own `.gitattributes`. Any matching line in your repo (set or
|
|
180
|
+
unset) always overrides a default, e.g. to re-enable syncing for one JSON
|
|
181
|
+
file despite the blanket `*.json` default:
|
|
182
|
+
|
|
183
|
+
```gitattributes
|
|
184
|
+
config/version.json sync-header-metadata
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
## Limitations
|
|
189
|
+
|
|
190
|
+
### Workflow files
|
|
191
|
+
|
|
192
|
+
`update` mode rewrites `.github/workflows/*.yaml` headers the same as any
|
|
193
|
+
other tracked file. By default, `GITHUB_TOKEN` cannot push a commit that
|
|
194
|
+
touches `.github/workflows/` without the `workflows: write` permission
|
|
195
|
+
explicitly granted in the calling workflow. Without it, the commit/push step
|
|
196
|
+
following this action (`git-auto-commit-action` or otherwise) fails the
|
|
197
|
+
entire commit, and all update writes per this action are lost.
|
|
198
|
+
|
|
199
|
+
If you haven't granted `workflows: write`, elide workflow files in the same
|
|
200
|
+
manner as any other ignored path or learn this security restriction at push.
|
|
201
|
+
|
|
202
|
+
```gitattributes
|
|
203
|
+
.github/workflows/** -sync-header-metadata
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
*(It's a very inconsequential failure. Handling involves either providing the
|
|
207
|
+
permission, excluding as shown, or manually updating the out-of-sync workflow
|
|
208
|
+
header.)*
|
|
209
|
+
|
|
210
|
+
### Annotation limits
|
|
211
|
+
|
|
212
|
+
GitHub caps workflow annotations at 10 errors, 10 warnings, and 10 notices
|
|
213
|
+
per step, regardless of the `annotation` input. This action runs as a single
|
|
214
|
+
step and can emit up to two `error` annotations per drifted file (one for
|
|
215
|
+
the repo line, one for the path line), so a repo with more than a handful of
|
|
216
|
+
drifted files will exceed the cap: only the first 10 of each level render in
|
|
217
|
+
the PR's Checks/Files-changed UI, the rest are silently dropped by GitHub.
|
|
218
|
+
|
|
219
|
+
This doesn't affect correctness, the exit code and the plain `[ERROR]` log
|
|
220
|
+
lines printed to the job's raw log aren't subject to the cap, only the
|
|
221
|
+
`::error::`/`::warning::`/`::notice::` UI annotations are. Treat annotations
|
|
222
|
+
as a convenience for small drifts and rely on the job log or `mode: update`'s
|
|
223
|
+
diff for anything larger.
|
|
224
|
+
|
|
225
|
+
A GitHub check run is bound to a single commit (`head_sha`) for its whole
|
|
226
|
+
lifetime, and an annotation only renders as an inline bubble on that commit's
|
|
227
|
+
own "Files changed" page if the annotated file is part of *that specific
|
|
228
|
+
commit's* diff. This action scans every tracked file on each run, not just
|
|
229
|
+
what the triggering commit touched, so the use case matters:
|
|
230
|
+
|
|
231
|
+
- **`verify` mode gating a PR** that renamed or moved a file: the drifted
|
|
232
|
+
file is, by definition, part of that PR's own diff, so the annotation
|
|
233
|
+
lands inline exactly where it's useful. This is the primary intended use
|
|
234
|
+
case and where annotations work well.
|
|
235
|
+
- **`update` mode as a scheduled or manually dispatched sweep** across a
|
|
236
|
+
repo's whole tracked-file set (e.g. a periodic cleanup job): most flagged
|
|
237
|
+
files have nothing to do with whatever commit triggered that run, so most
|
|
238
|
+
annotations can't attach to a diff line at all. They still show up in the
|
|
239
|
+
workflow run's own Annotations summary panel, just without a working deep
|
|
240
|
+
link. Rely on the job log and exit code for this shape of run instead.
|
|
241
|
+
|
|
242
|
+
## Development
|
|
243
|
+
|
|
244
|
+
A native `node24` action with no install step and no runtime dependencies.
|
|
245
|
+
Both entry points are thin wrappers around the same logic:
|
|
246
|
+
|
|
247
|
+
| File | Role |
|
|
248
|
+
| ----------------------------- | --------------------------------------------------------------- |
|
|
249
|
+
| `src/sync.js` | Core: resolves tracked files, checks/rewrites headers. |
|
|
250
|
+
| `run.js` | Action entry: reads `INPUT_*` and `GITHUB_REPOSITORY`. |
|
|
251
|
+
| `bin/sync-header-metadata.js` | CLI entry: reads flags, falls back to the `origin` remote. |
|
|
252
|
+
| `src/action_log.js` | Logging and `::error::`-style workflow-command annotations. |
|
|
253
|
+
|
|
254
|
+
Sanity-check changes locally, from inside a git checkout:
|
|
255
|
+
|
|
256
|
+
```sh
|
|
257
|
+
node bin/sync-header-metadata.js --verbose # run the CLI against this repo
|
|
258
|
+
npm test # both entry points, in throwaway repos
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
[GNU General Public License v3.0 only](LICENSE)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// vim:set expandtab shiftwidth=4 filetype=javascript:
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
|
|
5
|
+
//
|
|
6
|
+
//
|
|
7
|
+
// ~chewygumxx/sync-header-metadata.git
|
|
8
|
+
// ::: :/bin/sync-header-metadata.js
|
|
9
|
+
//
|
|
10
|
+
//
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
const { parseArgs } = require("node:util");
|
|
15
|
+
const { execFileSync } = require("node:child_process");
|
|
16
|
+
|
|
17
|
+
const ActionLog = require("../src/action_log.js");
|
|
18
|
+
const { sync } = require("../src/sync.js");
|
|
19
|
+
|
|
20
|
+
const USAGE = `\
|
|
21
|
+
Usage: sync-header-metadata [options]
|
|
22
|
+
|
|
23
|
+
Checks and updates the "~owner/repo.git" and "::: :/<path>" header lines
|
|
24
|
+
in the tracked files of the current git repository.
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
-m, --mode <verify|update> verify: fail if any header line is out-of-sync (default)
|
|
28
|
+
update: rewrite out-of-sync header lines
|
|
29
|
+
-u, --update Shorthand for --mode update
|
|
30
|
+
-r, --repo <owner/repo> Repository to sync against
|
|
31
|
+
(default: $GITHUB_REPOSITORY, then the origin remote)
|
|
32
|
+
-v, --verbose Enable INFO-level logging
|
|
33
|
+
-h, --help Show this help`;
|
|
34
|
+
|
|
35
|
+
// ----------------
|
|
36
|
+
// Parse Arguments
|
|
37
|
+
// ----------------
|
|
38
|
+
|
|
39
|
+
let args;
|
|
40
|
+
try {
|
|
41
|
+
({ values: args } = parseArgs({
|
|
42
|
+
options: {
|
|
43
|
+
// No default: --update must be able to tell an explicit
|
|
44
|
+
// `--mode verify` (a contradiction) from an omitted --mode.
|
|
45
|
+
mode: { type: "string", short: "m" },
|
|
46
|
+
update: { type: "boolean", short: "u", default: false },
|
|
47
|
+
repo: { type: "string", short: "r" },
|
|
48
|
+
verbose: { type: "boolean", short: "v", default: false },
|
|
49
|
+
help: { type: "boolean", short: "h", default: false },
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error(`${err.message}\n\n${USAGE}`);
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (args.update && args.mode && args.mode.toLowerCase() !== "update") {
|
|
58
|
+
console.error(
|
|
59
|
+
`Option '--update' conflicts with '--mode ${args.mode}'\n\n${USAGE}`,
|
|
60
|
+
);
|
|
61
|
+
process.exit(2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (args.help) {
|
|
65
|
+
console.log(USAGE);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Annotations are GitHub workflow commands; they are only noise in a terminal.
|
|
70
|
+
const log = new ActionLog(args.verbose, false);
|
|
71
|
+
|
|
72
|
+
const mode = args.update ? "update" : (args.mode ?? "verify").toLowerCase();
|
|
73
|
+
if (mode !== "verify" && mode !== "update")
|
|
74
|
+
log.fatal(
|
|
75
|
+
`Invalid mode: Must be 'verify' or 'update', received: ${args.mode}`,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// -------------------
|
|
79
|
+
// Resolve Repository
|
|
80
|
+
// -------------------
|
|
81
|
+
|
|
82
|
+
// Accepts the scp-like (git@host:owner/repo.git) and URL
|
|
83
|
+
// (https://host/owner/repo, ssh://git@host/owner/repo.git) remote forms.
|
|
84
|
+
const REMOTE_RE =
|
|
85
|
+
/^(?:[a-z+]+:\/\/[^/]+\/|[^@/]+@[^:]+:)(\S+?\/[^/\s]+?)(?:\.git)?\/?$/;
|
|
86
|
+
function repoFromOrigin() {
|
|
87
|
+
let url;
|
|
88
|
+
try {
|
|
89
|
+
url = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
90
|
+
encoding: "utf8",
|
|
91
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
92
|
+
}).trim();
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const m = REMOTE_RE.exec(url);
|
|
97
|
+
return m ? m[1] : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const repository =
|
|
101
|
+
args.repo || process.env.GITHUB_REPOSITORY || repoFromOrigin();
|
|
102
|
+
if (!repository)
|
|
103
|
+
log.fatal(
|
|
104
|
+
"Could not resolve repository from the origin remote: Pass --repo <owner/repo>",
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// ----
|
|
108
|
+
// Run
|
|
109
|
+
// ----
|
|
110
|
+
|
|
111
|
+
process.exit(sync({ mode, repository, log }));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Bundled default exclusions for sync-header-metadata.
|
|
2
|
+
#
|
|
3
|
+
# Loaded as the lowest-precedence attribute source (via
|
|
4
|
+
# `core.attributesFile`), so any matching line in a consumer repo's own
|
|
5
|
+
# .gitattributes/info/attributes always wins over these defaults.
|
|
6
|
+
#
|
|
7
|
+
# These cover files that structurally can't carry a header comment, or are
|
|
8
|
+
# generated/lockfiles that shouldn't be hand-edited.
|
|
9
|
+
|
|
10
|
+
/LICENSE* -sync-header-metadata
|
|
11
|
+
.keep -sync-header-metadata
|
|
12
|
+
*.json -sync-header-metadata
|
|
13
|
+
*.lock -sync-header-metadata
|
|
14
|
+
pnpm-lock.yaml -sync-header-metadata
|
|
15
|
+
go.sum -sync-header-metadata
|
|
16
|
+
*.min.js -sync-header-metadata
|
|
17
|
+
*.min.css -sync-header-metadata
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sync-header-metadata",
|
|
3
|
+
"description": "Checks and updates the \"~owner/repo.git\" and \"::: :/<path>\" header lines in tracked files.",
|
|
4
|
+
"version": "2.3.0",
|
|
5
|
+
"license": "GPL-3.0-only",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"cli",
|
|
8
|
+
"github-actions",
|
|
9
|
+
"header",
|
|
10
|
+
"metadata",
|
|
11
|
+
"repository"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/chewygumxx/sync-header-metadata#readme",
|
|
14
|
+
"bugs": "https://github.com/chewygumxx/sync-header-metadata/issues",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/chewygumxx/sync-header-metadata.git"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"sync-header-metadata": "bin/sync-header-metadata.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin/",
|
|
24
|
+
"src/",
|
|
25
|
+
"default.gitattributes"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=24"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"provenance": true
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@biomejs/biome": "2.5.14",
|
|
35
|
+
"@chewygumxx/biome-config": "^1.0.0",
|
|
36
|
+
"@chewygumxx/commitlint-config": "^1.0.0",
|
|
37
|
+
"@chewygumxx/cz-commitlint": "^1.0.2",
|
|
38
|
+
"@chewygumxx/remark-preset": "^1.0.3",
|
|
39
|
+
"@chewygumxx/yamllint-config": "^1.0.0",
|
|
40
|
+
"@commitlint/cli": "^21.2.2",
|
|
41
|
+
"@types/node": "^24.19.0",
|
|
42
|
+
"commitizen": "^4.3.2",
|
|
43
|
+
"husky": "^9.1.7",
|
|
44
|
+
"prettier": "^3.9.9",
|
|
45
|
+
"remark-cli": "^12.0.1",
|
|
46
|
+
"typescript": "^7.0.2"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"check": "npm run typecheck && npm run lint:biome && npm run lint:md && npm run lint:yaml && npm run lint:emdash && npm test",
|
|
50
|
+
"commit": "cz",
|
|
51
|
+
"format": "biome format --write . && npm run format:yaml",
|
|
52
|
+
"format:check": "biome format .",
|
|
53
|
+
"format:yaml": "git ls-files -z '*.yaml' '*.yml' | xargs -0 -r prettier --write --log-level warn",
|
|
54
|
+
"lint": "biome lint .",
|
|
55
|
+
"lint:biome": "biome ci .",
|
|
56
|
+
"lint:emdash": "git grep -nIP --untracked '\\x{2014}'; test $? -eq 1",
|
|
57
|
+
"lint:md": "git ls-files -z '*.md' | xargs -0 -r remark --frail --quiet --no-stdout",
|
|
58
|
+
"lint:yaml": "git ls-files -z '*.yaml' '*.yml' | xargs -0 -r prettier --check && git ls-files -z '*.yaml' '*.yml' | xargs -0 -r yamllint --strict",
|
|
59
|
+
"prepare": "test -d node_modules/husky && husky || true",
|
|
60
|
+
"test": "node --test",
|
|
61
|
+
"typecheck": "tsc"
|
|
62
|
+
},
|
|
63
|
+
"config": {
|
|
64
|
+
"commitizen": {
|
|
65
|
+
"path": "@chewygumxx/cz-commitlint"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"remarkConfig": {
|
|
69
|
+
"plugins": [
|
|
70
|
+
"@chewygumxx/remark-preset"
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// vim:set expandtab shiftwidth=4 filetype=javascript:
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
3
|
+
|
|
4
|
+
//
|
|
5
|
+
//
|
|
6
|
+
// ~chewygumxx/sync-header-metadata.git
|
|
7
|
+
// ::: :/src/action_log.js
|
|
8
|
+
//
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
function escapeData(value) {
|
|
14
|
+
return String(value)
|
|
15
|
+
.replace(/%/g, "%25")
|
|
16
|
+
.replace(/\r/g, "%0D")
|
|
17
|
+
.replace(/\n/g, "%0A");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function escapeProperty(value) {
|
|
21
|
+
return escapeData(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function output(level, message) {
|
|
25
|
+
const output = typeof message === "string" ? message : message.join("\n");
|
|
26
|
+
console.log(`[${level.toUpperCase()}] ${output}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// GitHub's workflow-command annotation properties are named file/line/endLine/
|
|
30
|
+
// col/endColumn/title; startLine/endLine here mirror @actions/core's
|
|
31
|
+
// AnnotationProperties naming, translated to the wire names GitHub expects.
|
|
32
|
+
function annotate(command, opts) {
|
|
33
|
+
const commandProps = {
|
|
34
|
+
title: opts.title,
|
|
35
|
+
file: opts.file,
|
|
36
|
+
line: opts.startLine,
|
|
37
|
+
endLine: opts.endLine || opts.startLine,
|
|
38
|
+
};
|
|
39
|
+
const props = Object.entries(commandProps)
|
|
40
|
+
.filter(
|
|
41
|
+
([, value]) =>
|
|
42
|
+
value !== undefined && value !== null && value !== "",
|
|
43
|
+
)
|
|
44
|
+
.map(([key, value]) => `${key}=${escapeProperty(value)}`)
|
|
45
|
+
.join(",");
|
|
46
|
+
console.log(
|
|
47
|
+
`::${command === "warn" ? "warning" : command}${props ? " " + props : ""}::${escapeData(opts.message)}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function wrap(command, opts, annotation_enabled) {
|
|
52
|
+
output(
|
|
53
|
+
command,
|
|
54
|
+
`${opts.title}: ${opts.message}${opts.file ? ` ${opts.file}` : ""}`,
|
|
55
|
+
);
|
|
56
|
+
if (annotation_enabled) annotate(command, opts);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class ActionLog {
|
|
60
|
+
constructor(verbose, annotation) {
|
|
61
|
+
this.verbose = verbose === true;
|
|
62
|
+
this.annotation = annotation === true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
info(message) {
|
|
66
|
+
if (!this.verbose) return;
|
|
67
|
+
output("info", message);
|
|
68
|
+
}
|
|
69
|
+
notice(opts) {
|
|
70
|
+
wrap("notice", opts, this.annotation);
|
|
71
|
+
}
|
|
72
|
+
warn(opts) {
|
|
73
|
+
wrap("warn", opts, this.annotation);
|
|
74
|
+
}
|
|
75
|
+
error(opts) {
|
|
76
|
+
wrap("error", opts, this.annotation);
|
|
77
|
+
}
|
|
78
|
+
fatal(message, code = 1) {
|
|
79
|
+
output("fatal", message);
|
|
80
|
+
if (this.annotation)
|
|
81
|
+
annotate("error", {
|
|
82
|
+
title: `[FATAL] ${message}`,
|
|
83
|
+
message: `[FATAL] ${message}`,
|
|
84
|
+
});
|
|
85
|
+
process.exit(typeof code === "number" ? code : 1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = ActionLog;
|