@zairakai/js-dev-tools 3.3.1 → 3.4.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/.gitlab/ci/pipeline-js-package.yml +58 -2
- package/README.md +2 -0
- package/package.json +1 -1
- package/scripts/update-dependencies.mjs +81 -25
|
@@ -77,6 +77,9 @@ variables:
|
|
|
77
77
|
NODE_VERSION: "24"
|
|
78
78
|
NODE_IMAGE: "zairakai/node:latest-dev"
|
|
79
79
|
NODE_TEST_IMAGE: "zairakai/node:latest-test"
|
|
80
|
+
# The jobs of the automatic updates sign with gpg: the image has it since 1.3.0. It is an exact version, published on
|
|
81
|
+
# Docker Hub like our other images: the runner pulls it, whatever the old image it keeps locally.
|
|
82
|
+
NODE_UPDATE_IMAGE: "zairakai/node:1.3.0-dev"
|
|
80
83
|
SHELLCHECK_IMAGE: "koalaman/shellcheck-alpine:v0.10.0"
|
|
81
84
|
RELEASE_CLI_IMAGE: "registry.gitlab.com/gitlab-org/release-cli:v0.18.0"
|
|
82
85
|
ALPINE_IMAGE: "alpine:3.18"
|
|
@@ -134,6 +137,8 @@ variables:
|
|
|
134
137
|
secret_detection:
|
|
135
138
|
stage: security
|
|
136
139
|
rules:
|
|
140
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
141
|
+
when: never
|
|
137
142
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event" || $CI_MERGE_REQUEST_ID
|
|
138
143
|
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"
|
|
139
144
|
|
|
@@ -144,6 +149,8 @@ js:tag-format:
|
|
|
144
149
|
stage: security
|
|
145
150
|
image: ${ALPINE_IMAGE}
|
|
146
151
|
rules:
|
|
152
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
153
|
+
when: never
|
|
147
154
|
- if: $CI_COMMIT_TAG
|
|
148
155
|
script:
|
|
149
156
|
- |
|
|
@@ -166,6 +173,10 @@ js:tag-format:
|
|
|
166
173
|
js:install:
|
|
167
174
|
<<: [*base_job, *cache_node_push]
|
|
168
175
|
stage: install
|
|
176
|
+
rules:
|
|
177
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
178
|
+
when: never
|
|
179
|
+
- when: on_success
|
|
169
180
|
image: ${NODE_IMAGE}
|
|
170
181
|
script:
|
|
171
182
|
- npm ci --cache .npm --prefer-offline
|
|
@@ -184,6 +195,10 @@ js:install:
|
|
|
184
195
|
js:validate:markdownlint:
|
|
185
196
|
<<: [*base_job, *cache_node]
|
|
186
197
|
stage: validate
|
|
198
|
+
rules:
|
|
199
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
200
|
+
when: never
|
|
201
|
+
- when: on_success
|
|
187
202
|
image: ${NODE_IMAGE}
|
|
188
203
|
script:
|
|
189
204
|
- bash "$JS_DEV_TOOLS_SCRIPTS/markdownlint.sh"
|
|
@@ -191,6 +206,10 @@ js:validate:markdownlint:
|
|
|
191
206
|
js:validate:shellcheck:
|
|
192
207
|
<<: *base_job
|
|
193
208
|
stage: validate
|
|
209
|
+
rules:
|
|
210
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
211
|
+
when: never
|
|
212
|
+
- when: on_success
|
|
194
213
|
image: ${SHELLCHECK_IMAGE}
|
|
195
214
|
before_script:
|
|
196
215
|
- apk add --no-cache bash
|
|
@@ -208,6 +227,10 @@ js:validate:shellcheck:
|
|
|
208
227
|
js:quality:lint:
|
|
209
228
|
<<: *job_node
|
|
210
229
|
stage: quality
|
|
230
|
+
rules:
|
|
231
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
232
|
+
when: never
|
|
233
|
+
- when: on_success
|
|
211
234
|
script:
|
|
212
235
|
- bash "$JS_DEV_TOOLS_SCRIPTS/eslint.sh"
|
|
213
236
|
artifacts:
|
|
@@ -221,18 +244,30 @@ js:quality:lint:
|
|
|
221
244
|
js:quality:format:
|
|
222
245
|
<<: *job_node
|
|
223
246
|
stage: quality
|
|
247
|
+
rules:
|
|
248
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
249
|
+
when: never
|
|
250
|
+
- when: on_success
|
|
224
251
|
script:
|
|
225
252
|
- bash "$JS_DEV_TOOLS_SCRIPTS/prettier.sh"
|
|
226
253
|
|
|
227
254
|
js:quality:stylelint:
|
|
228
255
|
<<: *job_node
|
|
229
256
|
stage: quality
|
|
257
|
+
rules:
|
|
258
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
259
|
+
when: never
|
|
260
|
+
- when: on_success
|
|
230
261
|
script:
|
|
231
262
|
- bash "$JS_DEV_TOOLS_SCRIPTS/stylelint.sh"
|
|
232
263
|
|
|
233
264
|
js:quality:typecheck:
|
|
234
265
|
<<: *job_node
|
|
235
266
|
stage: quality
|
|
267
|
+
rules:
|
|
268
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
269
|
+
when: never
|
|
270
|
+
- when: on_success
|
|
236
271
|
script:
|
|
237
272
|
- |
|
|
238
273
|
if [ -f tsconfig.json ] && [ -f node_modules/.bin/tsc ]; then
|
|
@@ -244,12 +279,20 @@ js:quality:typecheck:
|
|
|
244
279
|
js:quality:knip:
|
|
245
280
|
<<: *job_node
|
|
246
281
|
stage: quality
|
|
282
|
+
rules:
|
|
283
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
284
|
+
when: never
|
|
285
|
+
- when: on_success
|
|
247
286
|
script:
|
|
248
287
|
- bash "$JS_DEV_TOOLS_SCRIPTS/knip.sh"
|
|
249
288
|
|
|
250
289
|
js:quality:bats:
|
|
251
290
|
<<: [*base_job, *cache_node]
|
|
252
291
|
stage: quality
|
|
292
|
+
rules:
|
|
293
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
294
|
+
when: never
|
|
295
|
+
- when: on_success
|
|
253
296
|
image: ${ALPINE_IMAGE}
|
|
254
297
|
before_script:
|
|
255
298
|
- apk add --no-cache bash bats git
|
|
@@ -265,6 +308,8 @@ js:quality:outdated:
|
|
|
265
308
|
<<: *job_node
|
|
266
309
|
stage: quality
|
|
267
310
|
rules:
|
|
311
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
312
|
+
when: never
|
|
268
313
|
- if: $CI_PIPELINE_SOURCE == "schedule"
|
|
269
314
|
script:
|
|
270
315
|
- bash "$JS_DEV_TOOLS_SCRIPTS/outdated.sh"
|
|
@@ -280,6 +325,10 @@ js:quality:outdated:
|
|
|
280
325
|
js:test:
|
|
281
326
|
<<: [*base_job, *cache_node]
|
|
282
327
|
stage: test
|
|
328
|
+
rules:
|
|
329
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
330
|
+
when: never
|
|
331
|
+
- when: on_success
|
|
283
332
|
image: ${NODE_TEST_IMAGE}
|
|
284
333
|
script:
|
|
285
334
|
- CI=true bash "$JS_DEV_TOOLS_SCRIPTS/test.sh"
|
|
@@ -314,6 +363,8 @@ js:build:
|
|
|
314
363
|
expire_in: 1 week
|
|
315
364
|
when: on_success
|
|
316
365
|
rules:
|
|
366
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
367
|
+
when: never
|
|
317
368
|
- if: $CI_COMMIT_TAG =~ $VERSION_TAG_REGEX
|
|
318
369
|
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"
|
|
319
370
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event" || $CI_MERGE_REQUEST_ID
|
|
@@ -326,6 +377,8 @@ js:publish:
|
|
|
326
377
|
stage: publish
|
|
327
378
|
image: ${NODE_IMAGE}
|
|
328
379
|
rules:
|
|
380
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
381
|
+
when: never
|
|
329
382
|
- if: $CI_COMMIT_TAG =~ $VERSION_TAG_REGEX
|
|
330
383
|
script:
|
|
331
384
|
- |
|
|
@@ -356,6 +409,8 @@ js:release:
|
|
|
356
409
|
stage: release
|
|
357
410
|
image: ${RELEASE_CLI_IMAGE}
|
|
358
411
|
rules:
|
|
412
|
+
- if: $UPDATE_DEPENDENCIES == "true"
|
|
413
|
+
when: never
|
|
359
414
|
- if: $CI_COMMIT_TAG =~ $VERSION_TAG_REGEX
|
|
360
415
|
before_script:
|
|
361
416
|
- apk add --no-cache bash git jq
|
|
@@ -397,11 +452,12 @@ js:release:
|
|
|
397
452
|
deps:update:
|
|
398
453
|
<<: *base_job
|
|
399
454
|
stage: install
|
|
400
|
-
image: ${
|
|
455
|
+
image: ${NODE_UPDATE_IMAGE}
|
|
401
456
|
needs: []
|
|
402
457
|
script:
|
|
403
458
|
- node --version
|
|
404
459
|
- "[ -d node_modules ] || npm ci --no-audit --no-fund"
|
|
460
|
+
- '[ "${JS_DEV_TOOLS_SCRIPTS}" = "scripts" ] || npm install --no-save --no-audit --no-fund "@zairakai/js-dev-tools@latest"'
|
|
405
461
|
- node "${JS_DEV_TOOLS_SCRIPTS}/update-dependencies.mjs" update
|
|
406
462
|
rules:
|
|
407
463
|
- if: $UPDATE_DEPENDENCIES == "true" && $CI_COMMIT_BRANCH == "develop"
|
|
@@ -421,7 +477,7 @@ deps:release:
|
|
|
421
477
|
deps:tag:
|
|
422
478
|
<<: *base_job
|
|
423
479
|
stage: release
|
|
424
|
-
image: ${
|
|
480
|
+
image: ${NODE_UPDATE_IMAGE}
|
|
425
481
|
variables:
|
|
426
482
|
GIT_DEPTH: 0
|
|
427
483
|
script:
|
package/README.md
CHANGED
|
@@ -178,6 +178,8 @@ The pipeline template updates the dependencies of a project by itself, and this
|
|
|
178
178
|
- `deps:release` brings `develop` into `main` after such an update, and `deps:tag` tags `main` when a published range (`dependencies`, `peerDependencies`) changed. A project that changed only its development dependencies is not tagged: its published archive is the same.
|
|
179
179
|
- After the publication of a tag, `deps:cascade` waits for the version to be on the registry, then starts the update of the projects that have a `.dependency-update.json`.
|
|
180
180
|
|
|
181
|
+
The same script serves the PHP projects (`composer.json`): a job in the PHP image runs `composer update`, then this script commits, signs and opens the merge request, and `deps:tag` looks at `require` only. The jobs that sign (`deps:update`, `deps:tag`) run on an exact version of the node image (`NODE_UPDATE_IMAGE`, published on Docker Hub), because they need `gpg` and the image of the runner is not updated by itself.
|
|
182
|
+
|
|
181
183
|
The exceptions of a project live in `.dependency-update.json` at its root (everything is optional):
|
|
182
184
|
|
|
183
185
|
```json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zairakai/js-dev-tools",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "The unified and centralized development toolkit for JavaScript/TypeScript projects - ESLint, Prettier, Stylelint, Knip, Vitest, Git Hooks, and GitLab CI pipeline templates",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dev-tools",
|
|
@@ -39,6 +39,17 @@ const DEFAULTS = {
|
|
|
39
39
|
const RUNTIME_SECTIONS = ['dependencies', 'peerDependencies', 'optionalDependencies']
|
|
40
40
|
const ALL_SECTIONS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
|
|
41
41
|
|
|
42
|
+
/** The manifest of each ecosystem, with the sections that are published and all of them. */
|
|
43
|
+
const ECOSYSTEMS = {
|
|
44
|
+
npm: { file: 'package.json', runtime: RUNTIME_SECTIONS, all: ALL_SECTIONS },
|
|
45
|
+
composer: { file: 'composer.json', runtime: ['require'], all: ['require', 'require-dev'] },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The ecosystem of the project: a package.json at its root, otherwise a composer.json. */
|
|
49
|
+
export function detectEcosystem(exists = existsSync) {
|
|
50
|
+
return exists('package.json') ? 'npm' : 'composer'
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
/** The configuration of the project, completed with the defaults. */
|
|
43
54
|
export function loadConfig(path = CONFIG_FILE) {
|
|
44
55
|
const user = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : {}
|
|
@@ -78,15 +89,15 @@ function sorted(section) {
|
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
/** True when a range that is published (dependencies, peerDependencies, optionalDependencies) is not the same. */
|
|
81
|
-
export function runtimeChanged(before, after) {
|
|
82
|
-
return
|
|
92
|
+
export function runtimeChanged(before, after, sections = RUNTIME_SECTIONS) {
|
|
93
|
+
return sections.some((section) => sorted(before?.[section]) !== sorted(after?.[section]))
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
/** The dependencies whose range changed, with the section, the old range and the new one. */
|
|
86
|
-
export function diffDependencies(before, after) {
|
|
97
|
+
export function diffDependencies(before, after, sections = ALL_SECTIONS) {
|
|
87
98
|
const rows = []
|
|
88
99
|
|
|
89
|
-
for (const section of
|
|
100
|
+
for (const section of sections) {
|
|
90
101
|
const old = before?.[section] ?? {}
|
|
91
102
|
const current = after?.[section] ?? {}
|
|
92
103
|
|
|
@@ -129,12 +140,12 @@ export function signerIdentity(env) {
|
|
|
129
140
|
}
|
|
130
141
|
|
|
131
142
|
/** What `tag` has to do: the version to create, or null (and why). */
|
|
132
|
-
export function planTag({ config, last, commits, before, after }) {
|
|
143
|
+
export function planTag({ config, last, commits, before, after, sections = RUNTIME_SECTIONS }) {
|
|
133
144
|
if ('never' === config.tag.when) return { version: null, reason: 'the project is never tagged by the update' }
|
|
134
145
|
if (!last) return { version: null, reason: 'there is no tag yet' }
|
|
135
146
|
if (0 === commits.length) return { version: null, reason: 'no update since the last tag' }
|
|
136
147
|
|
|
137
|
-
if ('runtime' === config.tag.when && !runtimeChanged(before, after)) {
|
|
148
|
+
if ('runtime' === config.tag.when && !runtimeChanged(before, after, sections)) {
|
|
138
149
|
return { version: null, reason: 'only the development dependencies changed: the published archive is the same' }
|
|
139
150
|
}
|
|
140
151
|
|
|
@@ -196,16 +207,40 @@ function assertEnvironment() {
|
|
|
196
207
|
run('git', ['config', '--global', '--add', 'safe.directory', process.cwd()])
|
|
197
208
|
}
|
|
198
209
|
|
|
210
|
+
/** GPG is not in the image of the CI: add it with the package manager of the image (Alpine or Debian). */
|
|
211
|
+
function ensureGpg() {
|
|
212
|
+
const available = () => {
|
|
213
|
+
try {
|
|
214
|
+
run('gpg', ['--version'])
|
|
215
|
+
return true
|
|
216
|
+
} catch {
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (available()) return
|
|
222
|
+
|
|
223
|
+
for (const [command, args] of [
|
|
224
|
+
['apk', ['add', '--no-cache', 'gnupg']],
|
|
225
|
+
['apt-get', ['install', '-y', '--no-install-recommends', 'gnupg']],
|
|
226
|
+
]) {
|
|
227
|
+
try {
|
|
228
|
+
run(command, args)
|
|
229
|
+
} catch {
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (available()) return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
throw new Error('gpg is not installed and could not be added: add gnupg to the image of the job.')
|
|
237
|
+
}
|
|
238
|
+
|
|
199
239
|
/** Import the signing key and make Git sign the commits and the tags with it. */
|
|
200
240
|
function configureSigning() {
|
|
201
241
|
const signer = signerIdentity(process.env)
|
|
202
242
|
|
|
203
|
-
|
|
204
|
-
run('gpg', ['--version'])
|
|
205
|
-
} catch {
|
|
206
|
-
run('apt-get', ['update'])
|
|
207
|
-
run('apt-get', ['install', '-y', '--no-install-recommends', 'gnupg'])
|
|
208
|
-
}
|
|
243
|
+
ensureGpg()
|
|
209
244
|
|
|
210
245
|
run('gpg', ['--batch', '--import'], { input: signer.key, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
211
246
|
const fingerprint = run('gpg', ['--list-secret-keys', '--with-colons'])
|
|
@@ -281,18 +316,22 @@ async function update(config) {
|
|
|
281
316
|
}
|
|
282
317
|
|
|
283
318
|
configureSigning()
|
|
284
|
-
const
|
|
319
|
+
const ecosystem = ECOSYSTEMS[detectEcosystem()]
|
|
320
|
+
const before = JSON.parse(run('git', ['show', `HEAD:${ecosystem.file}`]))
|
|
285
321
|
const branch = `${BRANCH_PREFIX}-${new Date().toISOString().slice(0, 10).replaceAll('-', '')}`
|
|
286
322
|
run('git', ['switch', '-c', branch])
|
|
287
323
|
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
324
|
+
// A PHP project was prepared by the job before this one, in its own image: only npm is updated here.
|
|
325
|
+
if ('package.json' === ecosystem.file) {
|
|
326
|
+
// The new versions wait for the cooling period. The packages of the group are trusted: no waiting for them.
|
|
327
|
+
const reject = 0 < config.reject.length ? ['--reject', config.reject.join(',')] : []
|
|
328
|
+
ncu(['--cooldown', config.cooldown, ...reject])
|
|
329
|
+
ncu(['--filter', '@zairakai/*', ...reject])
|
|
330
|
+
run('npm', ['install', '--no-audit', '--no-fund'])
|
|
331
|
+
}
|
|
293
332
|
|
|
294
|
-
const after = JSON.parse(readFileSync(
|
|
295
|
-
const rows = diffDependencies(before, after)
|
|
333
|
+
const after = JSON.parse(readFileSync(ecosystem.file, 'utf8'))
|
|
334
|
+
const rows = diffDependencies(before, after, ecosystem.all)
|
|
296
335
|
|
|
297
336
|
if ('' === run('git', ['status', '--porcelain'])) {
|
|
298
337
|
log('Everything is up to date.')
|
|
@@ -359,10 +398,11 @@ async function tag(config) {
|
|
|
359
398
|
.split('\n')
|
|
360
399
|
.filter(Boolean)
|
|
361
400
|
: []
|
|
362
|
-
const
|
|
363
|
-
const
|
|
401
|
+
const ecosystem = ECOSYSTEMS[detectEcosystem()]
|
|
402
|
+
const before = last ? JSON.parse(run('git', ['show', `${last}:${ecosystem.file}`])) : null
|
|
403
|
+
const after = JSON.parse(readFileSync(ecosystem.file, 'utf8'))
|
|
364
404
|
|
|
365
|
-
const plan = planTag({ config, last, commits, before, after })
|
|
405
|
+
const plan = planTag({ config, last, commits, before, after, sections: ecosystem.runtime })
|
|
366
406
|
|
|
367
407
|
if (!plan.version) {
|
|
368
408
|
log(`No tag: ${plan.reason}.`)
|
|
@@ -376,6 +416,21 @@ async function tag(config) {
|
|
|
376
416
|
log(`Tag ${plan.version} signed and pushed (last tag ${last}): ${plan.reason}. Its pipeline publishes the package.`)
|
|
377
417
|
}
|
|
378
418
|
|
|
419
|
+
/** True when the registry of the ecosystem (npm or Packagist) lists the version. */
|
|
420
|
+
async function isAvailable(ecosystem, name, version) {
|
|
421
|
+
if ('npm' === ecosystem) {
|
|
422
|
+
return run('npm', ['view', `${name}@${version}`, 'version', '--prefer-online']) === version
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const response = await fetch(`https://repo.packagist.org/p2/${name}.json`)
|
|
426
|
+
|
|
427
|
+
if (!response.ok) return false
|
|
428
|
+
|
|
429
|
+
const data = await response.json()
|
|
430
|
+
|
|
431
|
+
return (data.packages?.[name] ?? []).some((release) => release.version === version)
|
|
432
|
+
}
|
|
433
|
+
|
|
379
434
|
async function cascade(config) {
|
|
380
435
|
assertEnvironment()
|
|
381
436
|
|
|
@@ -384,7 +439,8 @@ async function cascade(config) {
|
|
|
384
439
|
return
|
|
385
440
|
}
|
|
386
441
|
|
|
387
|
-
const
|
|
442
|
+
const ecosystem = detectEcosystem()
|
|
443
|
+
const name = JSON.parse(readFileSync(ECOSYSTEMS[ecosystem].file, 'utf8')).name
|
|
388
444
|
const version = process.env.CI_COMMIT_TAG
|
|
389
445
|
|
|
390
446
|
if (!version) throw new Error('The cascade runs on the pipeline of a tag.')
|
|
@@ -394,7 +450,7 @@ async function cascade(config) {
|
|
|
394
450
|
|
|
395
451
|
for (let attempt = 0; 60 > attempt && !available; attempt++) {
|
|
396
452
|
try {
|
|
397
|
-
available =
|
|
453
|
+
available = await isAvailable(ecosystem, name, version)
|
|
398
454
|
} catch {
|
|
399
455
|
available = false
|
|
400
456
|
}
|