@zairakai/js-dev-tools 3.3.2 → 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.
@@ -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"
@@ -449,7 +452,7 @@ js:release:
449
452
  deps:update:
450
453
  <<: *base_job
451
454
  stage: install
452
- image: ${NODE_IMAGE}
455
+ image: ${NODE_UPDATE_IMAGE}
453
456
  needs: []
454
457
  script:
455
458
  - node --version
@@ -474,7 +477,7 @@ deps:release:
474
477
  deps:tag:
475
478
  <<: *base_job
476
479
  stage: release
477
- image: ${NODE_IMAGE}
480
+ image: ${NODE_UPDATE_IMAGE}
478
481
  variables:
479
482
  GIT_DEPTH: 0
480
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.2",
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 RUNTIME_SECTIONS.some((section) => sorted(before?.[section]) !== sorted(after?.[section]))
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 ALL_SECTIONS) {
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
 
@@ -305,18 +316,22 @@ async function update(config) {
305
316
  }
306
317
 
307
318
  configureSigning()
308
- const before = JSON.parse(readFileSync('package.json', 'utf8'))
319
+ const ecosystem = ECOSYSTEMS[detectEcosystem()]
320
+ const before = JSON.parse(run('git', ['show', `HEAD:${ecosystem.file}`]))
309
321
  const branch = `${BRANCH_PREFIX}-${new Date().toISOString().slice(0, 10).replaceAll('-', '')}`
310
322
  run('git', ['switch', '-c', branch])
311
323
 
312
- // The new versions wait for the cooling period. The packages of the group are trusted: no waiting for them.
313
- const reject = 0 < config.reject.length ? ['--reject', config.reject.join(',')] : []
314
- ncu(['--cooldown', config.cooldown, ...reject])
315
- ncu(['--filter', '@zairakai/*', ...reject])
316
- run('npm', ['install', '--no-audit', '--no-fund'])
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
+ }
317
332
 
318
- const after = JSON.parse(readFileSync('package.json', 'utf8'))
319
- const rows = diffDependencies(before, after)
333
+ const after = JSON.parse(readFileSync(ecosystem.file, 'utf8'))
334
+ const rows = diffDependencies(before, after, ecosystem.all)
320
335
 
321
336
  if ('' === run('git', ['status', '--porcelain'])) {
322
337
  log('Everything is up to date.')
@@ -383,10 +398,11 @@ async function tag(config) {
383
398
  .split('\n')
384
399
  .filter(Boolean)
385
400
  : []
386
- const before = last ? JSON.parse(run('git', ['show', `${last}:package.json`])) : null
387
- const after = JSON.parse(readFileSync('package.json', 'utf8'))
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'))
388
404
 
389
- const plan = planTag({ config, last, commits, before, after })
405
+ const plan = planTag({ config, last, commits, before, after, sections: ecosystem.runtime })
390
406
 
391
407
  if (!plan.version) {
392
408
  log(`No tag: ${plan.reason}.`)
@@ -400,6 +416,21 @@ async function tag(config) {
400
416
  log(`Tag ${plan.version} signed and pushed (last tag ${last}): ${plan.reason}. Its pipeline publishes the package.`)
401
417
  }
402
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
+
403
434
  async function cascade(config) {
404
435
  assertEnvironment()
405
436
 
@@ -408,7 +439,8 @@ async function cascade(config) {
408
439
  return
409
440
  }
410
441
 
411
- const name = JSON.parse(readFileSync('package.json', 'utf8')).name
442
+ const ecosystem = detectEcosystem()
443
+ const name = JSON.parse(readFileSync(ECOSYSTEMS[ecosystem].file, 'utf8')).name
412
444
  const version = process.env.CI_COMMIT_TAG
413
445
 
414
446
  if (!version) throw new Error('The cascade runs on the pipeline of a tag.')
@@ -418,7 +450,7 @@ async function cascade(config) {
418
450
 
419
451
  for (let attempt = 0; 60 > attempt && !available; attempt++) {
420
452
  try {
421
- available = run('npm', ['view', `${name}@${version}`, 'version', '--prefer-online']) === version
453
+ available = await isAvailable(ecosystem, name, version)
422
454
  } catch {
423
455
  available = false
424
456
  }