@arc-e-tect/api-only-publisher 0.18.4 → 0.18.5
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/README.adoc +125 -9
- package/package.json +1 -1
- package/src/config.js +7 -4
- package/src/version-guard.js +2 -2
package/README.adoc
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
:source-highlighter: rouge
|
|
7
7
|
// The released version of this component: its snippets use it. The release
|
|
8
8
|
// workflow updates it; do not change it by hand.
|
|
9
|
-
:api-only-publisher-version: 0.18.
|
|
9
|
+
:api-only-publisher-version: 0.18.5
|
|
10
10
|
|
|
11
11
|
[NOTE]
|
|
12
12
|
.About this documentation
|
|
@@ -23,6 +23,8 @@ image:https://img.shields.io/github/v/release/Arc-E-Tect/SoftwareEngineeringDone
|
|
|
23
23
|
image:https://img.shields.io/badge/Node-22.14%2B-339933?logo=node.js[Node 22.14+,link=https://nodejs.org]
|
|
24
24
|
image:https://img.shields.io/badge/License-MIT-blue[MIT,link=LICENSE]
|
|
25
25
|
|
|
26
|
+
Current release: *{api-only-publisher-version}*, as link:CHANGELOG.md[the changelog →] records it.
|
|
27
|
+
|
|
26
28
|
[NOTE]
|
|
27
29
|
.Where the links in this document go
|
|
28
30
|
====
|
|
@@ -385,6 +387,19 @@ Only `build` does this; `lint` does not, and no report file is written.
|
|
|
385
387
|
[#versions]
|
|
386
388
|
== Versions
|
|
387
389
|
|
|
390
|
+
[WARNING]
|
|
391
|
+
.You maintain the version files yourself
|
|
392
|
+
====
|
|
393
|
+
The Publisher never writes a version file.
|
|
394
|
+
It does not raise a version when it builds, packs or publishes, and it does not work one out from commit messages the way semantic-release does.
|
|
395
|
+
The version it publishes is whatever the target's version file says.
|
|
396
|
+
It only writes that version into the built document's `info.version`.
|
|
397
|
+
|
|
398
|
+
So when you change a target's fragments, raise the version in that target's version file yourself, in the same commit.
|
|
399
|
+
The Publisher checks that you did: `check` and `publish` refuse a target that changed without a new version (<<version-guard,A changed target needs a new version §>>).
|
|
400
|
+
A `pre-push` git hook runs the same check on your own machine, before anything leaves it (<<version-hook,Checking versions before every push §>>).
|
|
401
|
+
====
|
|
402
|
+
|
|
388
403
|
Every published target has a version of its own, and it is kept in the library, beside the fragments it describes.
|
|
389
404
|
|
|
390
405
|
.specs/openapi/bundles/orders.bundle.properties
|
|
@@ -434,34 +449,131 @@ Both read only git: the target's release tags, its history, and the working tree
|
|
|
434
449
|
|A version above the latest release; one below it is refused.
|
|
435
450
|
|
|
436
451
|
|No release tags at all
|
|
437
|
-
|Compared with the merge-base with the
|
|
452
|
+
|Compared with the merge-base with the base branch, or with the ref `--since` names: a target changed since then needs a higher version than its version file declared there, and a version lower than it was there is refused.
|
|
438
453
|
A target whose version file was not there yet is new, and passes.
|
|
439
454
|
|===
|
|
440
455
|
|
|
441
456
|
Release tags are named by `versioning.tagFormat`, `{target}-v{version}` by default: `orders-v2.1.0`.
|
|
442
457
|
The Publisher never creates them: the pipeline that publishes does, after it has published, because a tool that pushes to git needs write access to the repository it builds from.
|
|
443
458
|
The reference pipelines show the step, and the `gitlab-package` channel's `release: true` creates the tag with the release.
|
|
444
|
-
The
|
|
459
|
+
The base branch is `versioning.baseBranch`, `main` by default; its remote-tracking branch, `origin/<base branch>`, is preferred, as that is what a CI checkout has.
|
|
460
|
+
Set it to the branch that holds the release you validate against, which need not be `main`.
|
|
461
|
+
A library whose default branch is `pre-prod` cuts a release there and deploys it to staging, and later merges `pre-prod` into `main` only to record that the production deployment was approved.
|
|
462
|
+
For that library the last reviewed versions live on `pre-prod`, so that is the branch to compare with:
|
|
463
|
+
|
|
464
|
+
[source,yaml]
|
|
465
|
+
----
|
|
466
|
+
versioning:
|
|
467
|
+
baseBranch: pre-prod # compared with origin/pre-prod, or pre-prod when there is no remote copy
|
|
468
|
+
----
|
|
469
|
+
|
|
445
470
|
|
|
446
471
|
A pre-release follows the same rules, by its full version:
|
|
447
472
|
|
|
448
|
-
* Its version file need not change: `--pre-release rc.2` already makes a new version, so a target changed since the
|
|
473
|
+
* Its version file need not change: `--pre-release rc.2` already makes a new version, so a target changed since the base branch passes with an unchanged file.
|
|
449
474
|
* A pre-release that was released before -- a tag `orders-v2.1.0-rc.1` -- is refused when the target changed since, as a release is.
|
|
450
475
|
* A pre-release of a version already released is refused: `2.1.0-rc.1` sorts before `2.1.0`, so it can never be the next version.
|
|
451
476
|
|
|
452
477
|
Where the guard cannot decide, it says so, and only `check` fails on it: `check` is the gate, while `publish` fails on a proven violation alone.
|
|
453
478
|
Outside a git repository, `check` fails and `publish` warns that the versions were not checked.
|
|
454
|
-
A target with no release tag, in a checkout that has no merge-base with the
|
|
479
|
+
A target with no release tag, in a checkout that has no merge-base with the base branch -- a shallow clone, or no such branch at all -- is `undecided`: `check` fails naming it, and `publish` warns and publishes it; a target with a release tag is decided all the same.
|
|
455
480
|
In CI, fetch the full history and the tags -- `actions/checkout` with `fetch-depth: 0`, or `GIT_DEPTH: 0` on GitLab -- so that nothing is left undecided.
|
|
456
|
-
Without release tags, the check on the
|
|
481
|
+
Without release tags, the check on the base branch itself compares the base branch with itself, so it is in the pull request that it catches a missing bump; the tags are what make it exact everywhere.
|
|
482
|
+
|
|
483
|
+
[#version-hook]
|
|
484
|
+
=== Checking versions before every push
|
|
457
485
|
|
|
458
|
-
|
|
486
|
+
The pipeline catches a missing version bump, but only after you have pushed.
|
|
487
|
+
A git hook catches it before then.
|
|
488
|
+
A hook is a script that git runs on your machine at a fixed moment.
|
|
489
|
+
A `pre-push` hook runs when you type `git push`, and if it fails, git sends nothing.
|
|
490
|
+
The steps below set one up so that it runs `check`.
|
|
491
|
+
They need no hook manager and nothing outside git and the Publisher.
|
|
459
492
|
|
|
493
|
+
. *Create the hook* as `.githooks/pre-push` in the repository, the one that holds `apionly.yaml`.
|
|
494
|
+
The file has no extension.
|
|
495
|
+
Pick the variant for the way the library runs the Publisher (<<installation,Installation §>>):
|
|
496
|
+
+
|
|
497
|
+
.`.githooks/pre-push`, with the Publisher in `package.json`
|
|
460
498
|
[source,sh]
|
|
461
499
|
----
|
|
462
500
|
#!/bin/sh
|
|
501
|
+
# Refuse the push when a target changed without a new version in its version file.
|
|
502
|
+
# The branch versioning.baseBranch names in apionly.yaml: main unless it says otherwise.
|
|
503
|
+
BASE_BRANCH=main
|
|
504
|
+
# The check compares with origin/$BASE_BRANCH and the release tags, so bring them up to date first.
|
|
505
|
+
git fetch --quiet --tags origin "$BASE_BRANCH" || echo "pre-push: could not fetch origin/$BASE_BRANCH; checking against what is here" >&2
|
|
463
506
|
exec npx --no-install api-only-publisher check
|
|
464
507
|
----
|
|
508
|
+
+
|
|
509
|
+
.`.githooks/pre-push`, with the Docker image
|
|
510
|
+
[source,sh,subs="attributes+"]
|
|
511
|
+
----
|
|
512
|
+
#!/bin/sh
|
|
513
|
+
# Refuse the push when a target changed without a new version in its version file.
|
|
514
|
+
# The branch versioning.baseBranch names in apionly.yaml: main unless it says otherwise.
|
|
515
|
+
BASE_BRANCH=main
|
|
516
|
+
# The check compares with origin/$BASE_BRANCH and the release tags, so bring them up to date first.
|
|
517
|
+
git fetch --quiet --tags origin "$BASE_BRANCH" || echo "pre-push: could not fetch origin/$BASE_BRANCH; checking against what is here" >&2
|
|
518
|
+
exec docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" \
|
|
519
|
+
arcetect/api-only-publisher:{api-only-publisher-version} check
|
|
520
|
+
----
|
|
521
|
+
+
|
|
522
|
+
Set `BASE_BRANCH` to the branch `versioning.baseBranch` names, `pre-prod` for the library above.
|
|
523
|
+
The hook only fetches that branch; `check` reads which branch to compare with from `apionly.yaml`, so the two must name the same one.
|
|
524
|
+
+
|
|
525
|
+
Git runs a hook from the top of the repository.
|
|
526
|
+
When `apionly.yaml` is in a subdirectory, add `-C <that directory>` after `check`.
|
|
527
|
+
|
|
528
|
+
. *Make it executable*, and commit it, so that everyone who clones the repository gets it:
|
|
529
|
+
+
|
|
530
|
+
[source,sh]
|
|
531
|
+
----
|
|
532
|
+
chmod +x .githooks/pre-push
|
|
533
|
+
git add .githooks/pre-push
|
|
534
|
+
git commit -m "chore: check API versions before every push"
|
|
535
|
+
----
|
|
536
|
+
+
|
|
537
|
+
On Windows, `chmod` has no effect.
|
|
538
|
+
Use `git update-index --chmod=+x .githooks/pre-push` before the commit instead.
|
|
539
|
+
Git for Windows runs the hook with its own `sh`, so the script works there as it is.
|
|
540
|
+
|
|
541
|
+
. *Tell git to use the directory.*
|
|
542
|
+
Git only looks in `.git/hooks` by default, and that directory is never committed.
|
|
543
|
+
Every clone has to run this once:
|
|
544
|
+
+
|
|
545
|
+
[source,sh]
|
|
546
|
+
----
|
|
547
|
+
git config core.hooksPath .githooks
|
|
548
|
+
----
|
|
549
|
+
+
|
|
550
|
+
Put this line in the library's own README, or in its setup script, so nobody has to remember it.
|
|
551
|
+
A hook manager such as husky or lefthook does this step for you when its dependencies are installed.
|
|
552
|
+
If you already use one, call the same `check` command from its `pre-push` entry instead.
|
|
553
|
+
|
|
554
|
+
. *Try it.*
|
|
555
|
+
Change a fragment of a published target and commit it without changing its version file.
|
|
556
|
+
`git push` should now stop, and the message names the target and its version file:
|
|
557
|
+
+
|
|
558
|
+
[source,console]
|
|
559
|
+
----
|
|
560
|
+
$ git push
|
|
561
|
+
orders 2.1.0 VIOLATION orders changed since the merge-base with main (3f2a91c0d4e1), but its version is still 2.1.0: raise it in specs/openapi/bundles/orders.bundle.properties
|
|
562
|
+
Error: 1 target(s) changed without a new version:
|
|
563
|
+
orders 2.1.0: orders changed since the merge-base with main (3f2a91c0d4e1), but its version is still 2.1.0: raise it in specs/openapi/bundles/orders.bundle.properties
|
|
564
|
+
A published version is never replaced: a changed specification needs a changed version.
|
|
565
|
+
error: failed to push some refs to 'origin'
|
|
566
|
+
----
|
|
567
|
+
+
|
|
568
|
+
Raise the version, commit, and push again.
|
|
569
|
+
|
|
570
|
+
Some things to know about the hook:
|
|
571
|
+
|
|
572
|
+
* `check` looks at the working tree, and that includes changes you have not committed yet.
|
|
573
|
+
Commit or stash them before you push, so it checks what you are actually pushing.
|
|
574
|
+
* It needs neither credentials nor a network beyond the `git fetch`, and it builds nothing, so it takes seconds.
|
|
575
|
+
* `git push --no-verify` skips the hook.
|
|
576
|
+
The pipeline still runs the same check, so skipping it only means you find out later.
|
|
465
577
|
|
|
466
578
|
[#closure]
|
|
467
579
|
== Independent versioning, and the dependency closure
|
|
@@ -952,7 +1064,7 @@ It publishes each target to a GitLab project's Generic Package Registry, on gitl
|
|
|
952
1064
|
[source,yaml]
|
|
953
1065
|
----
|
|
954
1066
|
channels:
|
|
955
|
-
gitlab-package: # in a GitLab CI job, nothing below is needed
|
|
1067
|
+
gitlab-package: # in a GitLab CI job, nothing below is needed, unless it inherits a GITLAB_TOKEN
|
|
956
1068
|
# url: https://gitlab.example.com/api/v4 # default: $CI_API_V4_URL, else https://gitlab.com/api/v4
|
|
957
1069
|
# project: group/subgroup/api-library # an ID or a path; default: $CI_PROJECT_ID
|
|
958
1070
|
# tokenEnv: GITLAB_TOKEN # default; when unset, $CI_JOB_TOKEN is used
|
|
@@ -985,6 +1097,10 @@ link:pipelines/gitlab-ci.yml[The reference pipeline →] publishes this way, fro
|
|
|
985
1097
|
|===
|
|
986
1098
|
|
|
987
1099
|
Without either, nothing is sent to GitLab, and `publish` warns, naming both (<<channel-problems,When a channel cannot take a version §>>).
|
|
1100
|
+
|
|
1101
|
+
*A `GITLAB_TOKEN` the job inherits wins over `CI_JOB_TOKEN`*, whether it comes from the project, a parent group or the instance, and is sent as a personal or project access token.
|
|
1102
|
+
When it is expired or lacks the `api` scope, the read-back fails with `401 Unauthorized` or `403 insufficient_scope`, although the job token itself would have worked.
|
|
1103
|
+
Set `tokenEnv: CI_JOB_TOKEN` and `tokenType: job` on the channel to use the job token regardless (link:../docs/guides/publishing-to-gitlab/overview.adoc#token-shadowed[When the job fails with 401 or 403 →]).
|
|
988
1104
|
A deploy token is the least privilege for packages alone: it reaches one project's package registry and nothing else.
|
|
989
1105
|
|
|
990
1106
|
*The same version again.*
|
|
@@ -1135,7 +1251,7 @@ portfolio: # absent is fine; an aggregate builds on documented de
|
|
|
1135
1251
|
|
|
1136
1252
|
versioning: # absent is fine; see A changed target needs a new version §
|
|
1137
1253
|
tagFormat: "{target}-v{version}" # the release tags the pipeline creates; the default
|
|
1138
|
-
|
|
1254
|
+
baseBranch: main # compared with when a target has no release tag; the default
|
|
1139
1255
|
----
|
|
1140
1256
|
|
|
1141
1257
|
`targets` is the single source of truth for what gets built.
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -36,7 +36,7 @@ const TARGET_KEYS = ["openapi", "asyncapi", "publish", "versionFile", "channels"
|
|
|
36
36
|
const TARGET_KIND_KEYS = ["bundle", "aggregate", "info"];
|
|
37
37
|
const PORTFOLIO_KEYS = ["openapi", "security", "location"];
|
|
38
38
|
const PORTFOLIO_OPENAPI_KEYS = ["paths", "operationIds", "tags"];
|
|
39
|
-
const VERSIONING_KEYS = ["tagFormat", "
|
|
39
|
+
const VERSIONING_KEYS = ["tagFormat", "baseBranch"];
|
|
40
40
|
|
|
41
41
|
// What every portfolio setting is when apionly.yaml says nothing: safe for the
|
|
42
42
|
// ordinary case (a gateway routing /<target>/** to each service), and named rather
|
|
@@ -206,6 +206,9 @@ function load(configPath) {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
const versioning = parsed.versioning || {};
|
|
209
|
+
if (versioning && typeof versioning === "object" && "mainBranch" in versioning) {
|
|
210
|
+
throw new ConfigError("versioning.mainBranch was renamed to versioning.baseBranch: rename the key, its value stays the same");
|
|
211
|
+
}
|
|
209
212
|
checkKeys(versioning, VERSIONING_KEYS, "versioning");
|
|
210
213
|
if (versioning.tagFormat !== undefined) {
|
|
211
214
|
const format = requireString(versioning.tagFormat, "versioning.tagFormat");
|
|
@@ -213,7 +216,7 @@ function load(configPath) {
|
|
|
213
216
|
throw new ConfigError(`versioning.tagFormat must contain {target} and {version}, not ${JSON.stringify(format)}`);
|
|
214
217
|
}
|
|
215
218
|
}
|
|
216
|
-
if (versioning.
|
|
219
|
+
if (versioning.baseBranch !== undefined) requireString(versioning.baseBranch, "versioning.baseBranch");
|
|
217
220
|
|
|
218
221
|
const channels = parsed.channels || {};
|
|
219
222
|
const types = channelTypes(channels);
|
|
@@ -347,8 +350,8 @@ function load(configPath) {
|
|
|
347
350
|
tagFormat() {
|
|
348
351
|
return this.versioning.tagFormat || "{target}-v{version}";
|
|
349
352
|
},
|
|
350
|
-
|
|
351
|
-
return this.versioning.
|
|
353
|
+
baseBranch() {
|
|
354
|
+
return this.versioning.baseBranch || "main";
|
|
352
355
|
},
|
|
353
356
|
// Where pack and publish write archives, and split its parts, unless --out says
|
|
354
357
|
// otherwise; relative to this configuration, as every other path here is.
|
package/src/version-guard.js
CHANGED
|
@@ -62,13 +62,13 @@ function fallbackBaseline(config, repoRoot, since) {
|
|
|
62
62
|
if (!commit) throw new GuardError(`--since ${since} is not a commit in this repository`);
|
|
63
63
|
return { ref: commit, label: `${since} (${commit.slice(0, 12)})` };
|
|
64
64
|
}
|
|
65
|
-
const branch = config.
|
|
65
|
+
const branch = config.baseBranch();
|
|
66
66
|
for (const candidate of [`origin/${branch}`, branch]) {
|
|
67
67
|
if (!tryGit(["rev-parse", "--verify", "--quiet", `${candidate}^{commit}`], repoRoot)) continue;
|
|
68
68
|
const base = tryGit(["merge-base", "HEAD", candidate], repoRoot);
|
|
69
69
|
if (base) return { ref: base, label: `the merge-base with ${branch} (${base.slice(0, 12)})` };
|
|
70
70
|
}
|
|
71
|
-
return { missing: `no merge-base with ${branch}, the branch versioning.
|
|
71
|
+
return { missing: `no merge-base with ${branch}, the branch versioning.baseBranch names, to compare a target without a release tag with. ` +
|
|
72
72
|
"In CI, fetch the full history and the branch (actions/checkout with fetch-depth: 0, or GIT_DEPTH: 0 on GitLab), " +
|
|
73
73
|
"or name the ref to compare with: --since <ref>." };
|
|
74
74
|
}
|