@arc-e-tect/api-only-publisher 0.15.0 → 0.16.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/README.adoc +77 -42
- package/package.json +1 -1
- package/src/channel-types.js +13 -0
- package/src/channels.js +7 -2
- package/src/cli.js +35 -9
- package/src/config.js +85 -2
- package/src/handoff.js +2 -2
- package/src/remote.js +5 -1
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.
|
|
9
|
+
:api-only-publisher-version: 0.16.1
|
|
10
10
|
|
|
11
11
|
[NOTE]
|
|
12
12
|
.About this documentation
|
|
@@ -182,7 +182,7 @@ At a terminal it asks the same shape of question `init` would, offering what is
|
|
|
182
182
|
Without `--target`, it also fails on a fragment no target reaches, as <<unreferenced,A fragment no target reaches fails the lint §>> describes.
|
|
183
183
|
|
|
184
184
|
|`targets`
|
|
185
|
-
|List the declared targets, what each one builds,
|
|
185
|
+
|List the declared targets, what each one builds, whether it is published, and to which channels.
|
|
186
186
|
|
|
187
187
|
|`closure [--target <t>]...`
|
|
188
188
|
|Show each target's dependency closure: how many files it reaches, and the hash of their content.
|
|
@@ -195,7 +195,7 @@ A target whose closure is untouched needs no new version, however much else in t
|
|
|
195
195
|
|Archive each built, published target with a `manifest.json`, at the version its version file declares.
|
|
196
196
|
|
|
197
197
|
|`publish [--pre-release <ids>] [--target <t>]... [--channel <c>]... [--out <dir>]`
|
|
198
|
-
|Pack once, then ship those same bytes to
|
|
198
|
+
|Pack each target once, then ship those same bytes to each of its channels, or only to those of them each `--channel` names (<<channel-routing,Which channels a target goes to §>>).
|
|
199
199
|
|
|
200
200
|
|`split [--out <dir>] [--by kind\|target]`
|
|
201
201
|
|Make the library safe to break into separate repositories.
|
|
@@ -660,6 +660,43 @@ In a GitLab CI job it needs no settings at all. See <<gitlab-package,The `gitlab
|
|
|
660
660
|
Native for .NET consumers: a `PackageReference` at a version, restored from a feed. See <<nuget,The `nuget` channel §>>.
|
|
661
661
|
|===
|
|
662
662
|
|
|
663
|
+
[#channel-routing]
|
|
664
|
+
=== Which channels a target goes to
|
|
665
|
+
|
|
666
|
+
A channel has a *name*, the key under `channels`, and a *type*, one of those above.
|
|
667
|
+
A channel named after a type is that type; any other name -- lowercase words joined by single hyphens, such as `partner-a` -- says its type with `type`.
|
|
668
|
+
So one `apionly.yaml` can hold several channels of one type, each with its own destination, settings and token:
|
|
669
|
+
|
|
670
|
+
[source,yaml]
|
|
671
|
+
----
|
|
672
|
+
channels:
|
|
673
|
+
maven: # a Maven channel, named after its type
|
|
674
|
+
groupId: com.example.api
|
|
675
|
+
repository: https://maven.example.com/internal
|
|
676
|
+
partner-a: # another one, for one partner
|
|
677
|
+
type: maven
|
|
678
|
+
groupId: com.example.partner
|
|
679
|
+
repository: https://maven.partner-a.example
|
|
680
|
+
tokenEnv: PARTNER_A_TOKEN
|
|
681
|
+
|
|
682
|
+
targets:
|
|
683
|
+
orders:
|
|
684
|
+
channels: [maven, partner-a] # published to these two
|
|
685
|
+
openapi:
|
|
686
|
+
bundle: bundles/orders_openapi_structure.yaml
|
|
687
|
+
billing: # no list: every configured channel
|
|
688
|
+
openapi:
|
|
689
|
+
bundle: bundles/billing_openapi_structure.yaml
|
|
690
|
+
----
|
|
691
|
+
|
|
692
|
+
* A target with a `channels` list is published to those channels; a target without one, to every configured channel.
|
|
693
|
+
* Everything is checked when `apionly.yaml` is read: a channel name that is not kebab-case, a name that is not a type without a `type`, a `type` that is not one, a type name given another `type`, and a target that lists a channel `apionly.yaml` does not configure, lists one twice, lists none (`channels: []`, where `publish: false` is meant), or is never published -- `publish: false`, or an aggregate.
|
|
694
|
+
* `--channel` narrows each target to the named channels it is routed to.
|
|
695
|
+
A target that is not routed to one is skipped, and `publish` says so: `payments: not published to npm (not in targets.payments.channels)`.
|
|
696
|
+
A run that would publish nothing at all fails instead of succeeding quietly.
|
|
697
|
+
* A `--channel` that `apionly.yaml` does not configure is refused, unless it is a type's name: `--channel github-release` is that channel with no settings, as it has always been.
|
|
698
|
+
* `targets` lists each published target with its channels.
|
|
699
|
+
|
|
663
700
|
[#idempotent]
|
|
664
701
|
=== Publishing the same version again
|
|
665
702
|
|
|
@@ -712,7 +749,7 @@ Error: API_ONLY_PUBLISHER_REMOTE=never refuses the remote channels maven, github
|
|
|
712
749
|
----
|
|
713
750
|
|
|
714
751
|
Any other value fails `publish`, whatever its channels: a typo in a switch that keeps the Publisher sealed is not ignored.
|
|
715
|
-
|
|
752
|
+
Only the channels a run would publish to are checked, by type: a channel named `partner-a` with `type: maven` and a URL is remote; `publish --channel file`, or a target routed only to local channels, succeeds under `never`, whatever else `apionly.yaml` configures.
|
|
716
753
|
The Publisher used as a library keeps the same rule.
|
|
717
754
|
|
|
718
755
|
The switch is a promise the Publisher keeps, not an enforcement: whoever runs it can change the variable.
|
|
@@ -728,7 +765,7 @@ Every `publish` run lists each file its channels wrote locally, with its SHA-256
|
|
|
728
765
|
| File | Content
|
|
729
766
|
|
|
730
767
|
| `packages.json`
|
|
731
|
-
| `schemaVersion`, and `files`: for each file, its `path`, `sha256`, `target`, `version` and `channel
|
|
768
|
+
| `schemaVersion`, and `files`: for each file, its `path`, `sha256`, `target`, `version`, `channel` -- the channel's name -- and `type`, the channel's type.
|
|
732
769
|
|
|
733
770
|
| `packages.sha256`
|
|
734
771
|
| The same files in the format `sha256sum -c` checks.
|
|
@@ -990,6 +1027,7 @@ channels:
|
|
|
990
1027
|
|
|
991
1028
|
targets:
|
|
992
1029
|
orders:
|
|
1030
|
+
# channels: [file] # the channels it is published to; default: every one
|
|
993
1031
|
# versionFile: versions/orders.properties
|
|
994
1032
|
# default: orders.bundle.properties beside the first bundle root
|
|
995
1033
|
openapi:
|
|
@@ -1161,20 +1199,30 @@ They are starting points, not boundaries: the situations describe where each sha
|
|
|
1161
1199
|
[#recipes-two-facts]
|
|
1162
1200
|
=== Two facts every recipe rests on
|
|
1163
1201
|
|
|
1164
|
-
*A channel
|
|
1165
|
-
`channels.maven` is
|
|
1166
|
-
|
|
1202
|
+
*A channel has a name and a type.*
|
|
1203
|
+
A channel named after a type is that type -- `channels.maven` is a Maven channel -- and any other name says its type with `type`, so one `apionly.yaml` can hold two channels of one type, each with a destination of its own:
|
|
1204
|
+
|
|
1205
|
+
[source,yaml]
|
|
1206
|
+
----
|
|
1207
|
+
channels:
|
|
1208
|
+
partner-a: { type: maven, groupId: com.example.partner, repository: https://maven.partner-a.example }
|
|
1209
|
+
partner-b: { type: maven, groupId: com.example.partner, repository: https://maven.partner-b.example }
|
|
1210
|
+
----
|
|
1167
1211
|
|
|
1168
|
-
*`apionly.yaml` says which
|
|
1169
|
-
|
|
1170
|
-
|
|
1212
|
+
*`apionly.yaml` says which target goes where.*
|
|
1213
|
+
A target with a `channels` list is published to those channels; a target without one is published to every configured channel.
|
|
1214
|
+
`publish` packs each target once, and sends the same bytes to each of its channels:
|
|
1171
1215
|
|
|
1172
|
-
[source,
|
|
1216
|
+
[source,yaml]
|
|
1173
1217
|
----
|
|
1174
|
-
|
|
1218
|
+
targets:
|
|
1219
|
+
payments:
|
|
1220
|
+
channels: [partner-a]
|
|
1221
|
+
openapi:
|
|
1222
|
+
bundle: bundles/payments_openapi_structure.yaml
|
|
1175
1223
|
----
|
|
1176
1224
|
|
|
1177
|
-
|
|
1225
|
+
`--target` and `--channel` narrow a run, and both may be repeated; `--channel` publishes each target only to the named channels it is routed to, and says which targets it skipped (<<channel-routing,Which channels a target goes to §>>).
|
|
1178
1226
|
|
|
1179
1227
|
The excerpts below show only `channels` and `targets`; the rest of `apionly.yaml` is as <<configuration,Configuration §>> describes, and as `init` writes it.
|
|
1180
1228
|
Every published target also needs its version file, as <<versions,Versions §>> describes, and a `build` before its `publish`.
|
|
@@ -1311,8 +1359,8 @@ The other channels already have the version, and would leave it alone anyway.
|
|
|
1311
1359
|
* A pre-release, `publish --pre-release rc.1`, is how a partner gets an unfinished contract to start building against, without any partner's production build picking it up by accident (<<pre-releases,Pre-releases §>>).
|
|
1312
1360
|
* Each channel takes its credential from the environment, never from `apionly.yaml`: `MAVEN_TOKEN`, `NUGET_API_KEY` and `GITLAB_TOKEN` by default, npm's own configuration for `npm`, and the GitHub CLI's for `github-release` (<<channel-text,What a package says about itself §>>).
|
|
1313
1361
|
* A partner without a package manager gets a download: `github-release` where the library is on GitHub, `gitlab-package` where it is on GitLab.
|
|
1314
|
-
* Partners that each need a repository of *the same type* -- two Maven repositories, one per partner --
|
|
1315
|
-
|
|
1362
|
+
* Partners that each need a repository of *the same type* -- two Maven repositories, one per partner -- are two channels with names of their own and `type: maven`, as <<recipes-two-facts,the two facts §>> show.
|
|
1363
|
+
* A partner that may see only some contracts gets only those: list its channel on those targets (<<recipe-ecosystem,Eco-system §>>).
|
|
1316
1364
|
|
|
1317
1365
|
[#recipe-platform]
|
|
1318
1366
|
=== Platform: several targets, several channels, every target on every channel
|
|
@@ -1391,7 +1439,7 @@ Start from a routing table: which target goes to which channel.
|
|
|
1391
1439
|
|`payments` |Yes | |Yes
|
|
1392
1440
|
|===
|
|
1393
1441
|
|
|
1394
|
-
`apionly.yaml`
|
|
1442
|
+
The routing table goes into `apionly.yaml` as it stands: each target lists the channels it is published to.
|
|
1395
1443
|
|
|
1396
1444
|
.apionly.yaml (excerpt)
|
|
1397
1445
|
[source,yaml]
|
|
@@ -1409,52 +1457,39 @@ channels:
|
|
|
1409
1457
|
|
|
1410
1458
|
targets:
|
|
1411
1459
|
catalog:
|
|
1460
|
+
channels: [maven, npm]
|
|
1412
1461
|
openapi:
|
|
1413
1462
|
bundle: bundles/catalog_openapi_structure.yaml
|
|
1414
1463
|
customers:
|
|
1464
|
+
channels: [maven]
|
|
1415
1465
|
openapi:
|
|
1416
1466
|
bundle: bundles/customers_openapi_structure.yaml
|
|
1417
1467
|
orders:
|
|
1468
|
+
channels: [maven, npm]
|
|
1418
1469
|
openapi:
|
|
1419
1470
|
bundle: bundles/orders_openapi_structure.yaml
|
|
1420
1471
|
payments:
|
|
1472
|
+
channels: [maven, nuget]
|
|
1421
1473
|
openapi:
|
|
1422
1474
|
bundle: bundles/payments_openapi_structure.yaml
|
|
1423
1475
|
----
|
|
1424
1476
|
|
|
1425
|
-
The routing table becomes one `publish` per channel, each naming its targets.
|
|
1426
|
-
Keeping those commands as scripts in the library's `package.json` keeps the routing in one place, reviewed with the library, rather than spread across pipeline files:
|
|
1427
|
-
|
|
1428
|
-
.package.json (excerpt)
|
|
1429
|
-
[source,json]
|
|
1430
|
-
----
|
|
1431
|
-
{
|
|
1432
|
-
"scripts": {
|
|
1433
|
-
"apionly": "api-only-publisher",
|
|
1434
|
-
"publish:maven": "api-only-publisher publish --channel maven",
|
|
1435
|
-
"publish:npm": "api-only-publisher publish --channel npm --target catalog --target orders",
|
|
1436
|
-
"publish:nuget": "api-only-publisher publish --channel nuget --target payments",
|
|
1437
|
-
"publish:all": "npm run publish:maven && npm run publish:npm && npm run publish:nuget"
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
----
|
|
1441
|
-
|
|
1442
1477
|
[source,console]
|
|
1443
1478
|
----
|
|
1444
1479
|
npm run apionly -- build
|
|
1445
|
-
npm run publish
|
|
1480
|
+
npm run apionly -- publish
|
|
1446
1481
|
----
|
|
1447
1482
|
|
|
1448
|
-
`publish
|
|
1483
|
+
One `publish` sends each target to its own channels, packing it once.
|
|
1484
|
+
`npm run apionly -- targets` lists each target with its channels, so the routing can be read back.
|
|
1449
1485
|
|
|
1450
1486
|
*Worth knowing.*
|
|
1451
1487
|
|
|
1452
|
-
* A
|
|
1453
|
-
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1456
|
-
*
|
|
1457
|
-
A pipeline that publishes the files itself gives each run its own `--out`, such as `--out build/packages/npm`, and keeps every channel's list.
|
|
1488
|
+
* A target without a `channels` list goes to every configured channel.
|
|
1489
|
+
In an eco-system, give every target a list: a new target then goes nowhere until someone decides where, which is the deliberate step an eco-system wants.
|
|
1490
|
+
* `--channel` publishes to one channel alone, say after its registry was unreachable: `npm run apionly -- publish --channel npm` sends `catalog` and `orders`, says that it skipped `customers` and `payments`, and fails if it would publish nothing at all.
|
|
1491
|
+
* The routing is checked when `apionly.yaml` is read: a target that lists a channel `apionly.yaml` does not configure is refused, naming both, before anything is built.
|
|
1492
|
+
* A third party that needs a registry of a type another party already uses -- a second Maven repository -- gets a channel of its own, with `type: maven` (<<recipes-two-facts,the two facts §>>).
|
|
1458
1493
|
|
|
1459
1494
|
[#behaviour]
|
|
1460
1495
|
== Behaviour worth knowing about
|
package/package.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// The channel types, in a module of their own so that the configuration can check a
|
|
4
|
+
// channel's type without requiring the channels themselves.
|
|
5
|
+
|
|
6
|
+
const CHANNEL_TYPES = Object.freeze(["file", "maven", "npm", "github-release", "nuget", "gitlab-package"]);
|
|
7
|
+
|
|
8
|
+
/** The types as a sentence names them: "file, maven, ... or gitlab-package". */
|
|
9
|
+
function describeTypes() {
|
|
10
|
+
return `${CHANNEL_TYPES.slice(0, -1).join(", ")} or ${CHANNEL_TYPES[CHANNEL_TYPES.length - 1]}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { CHANNEL_TYPES, describeTypes };
|
package/src/channels.js
CHANGED
|
@@ -456,8 +456,13 @@ const CHANNELS = {
|
|
|
456
456
|
"gitlab-package": publishGitlabPackage,
|
|
457
457
|
};
|
|
458
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Publishes to the channel named `channel`, of the type its settings' `type` names, or
|
|
461
|
+
* of the type it is named after -- `maven` is a Maven channel, as it has always been.
|
|
462
|
+
*/
|
|
459
463
|
function publish(archive, manifest, channel, options, log = () => {}) {
|
|
460
|
-
const
|
|
464
|
+
const { type = channel, ...settings } = options || {};
|
|
465
|
+
const handler = CHANNELS[type];
|
|
461
466
|
if (!handler) {
|
|
462
467
|
throw new ChannelError(
|
|
463
468
|
`unknown channel '${channel}'; available channels are ${Object.keys(CHANNELS).join(", ")}`
|
|
@@ -465,7 +470,7 @@ function publish(archive, manifest, channel, options, log = () => {}) {
|
|
|
465
470
|
}
|
|
466
471
|
// Here as well as in the CLI, so the Publisher used as a library keeps it too.
|
|
467
472
|
refuseRemote([channel], { [channel]: options || {} });
|
|
468
|
-
return handler(archive, manifest,
|
|
473
|
+
return handler(archive, manifest, settings, log);
|
|
469
474
|
}
|
|
470
475
|
|
|
471
476
|
module.exports = { publish, CHANNELS, ChannelError, pom, format, DEFAULT_DESCRIPTION };
|
package/src/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ const { changedSince, ChangedError } = require("./changed");
|
|
|
18
18
|
const { split, SplitError } = require("./split");
|
|
19
19
|
const { publish, ChannelError } = require("./channels");
|
|
20
20
|
const { refuseRemote } = require("./remote");
|
|
21
|
+
const { CHANNEL_TYPES } = require("./channel-types");
|
|
21
22
|
const { VersionError: PolicyError, describe } = require("./version-policy");
|
|
22
23
|
const { versionOf, BundleVersionError } = require("./bundle-version");
|
|
23
24
|
const { unreferenced } = require("./unreferenced");
|
|
@@ -324,7 +325,9 @@ async function main(argv, io = {}) {
|
|
|
324
325
|
for (const name of Object.keys(config.targets)) {
|
|
325
326
|
const kinds = ["openapi", "asyncapi"].filter((k) => config.targets[name][k]);
|
|
326
327
|
const published = config.isPublished(name) ? "" : " (publish: false)";
|
|
327
|
-
|
|
328
|
+
const routed = config.isPublished(name) && Object.keys(config.channels || {}).length > 0
|
|
329
|
+
? ` channels: ${config.channelsOf(name).join(", ")}` : "";
|
|
330
|
+
console.log(`${name} [${kinds.join(", ")}]${published}${routed}`);
|
|
328
331
|
}
|
|
329
332
|
return 0;
|
|
330
333
|
}
|
|
@@ -448,20 +451,43 @@ async function main(argv, io = {}) {
|
|
|
448
451
|
case "publish": {
|
|
449
452
|
const outDir = options.out ? path.resolve(options.dir, options.out) : config.packagesDir();
|
|
450
453
|
const configured = config.channels || {};
|
|
451
|
-
const
|
|
452
|
-
|
|
454
|
+
for (const name of options.channels || []) {
|
|
455
|
+
// An unconfigured channel named after a type is that channel with no settings.
|
|
456
|
+
if (!configured[name] && !(name in configured) && !CHANNEL_TYPES.includes(name)) {
|
|
457
|
+
const names = Object.keys(configured);
|
|
458
|
+
throw new ConfigError(`--channel '${name}' is not a configured channel; ` +
|
|
459
|
+
`apionly.yaml configures ${names.length ? names.join(", ") : "no channels"}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (!options.channels && Object.keys(configured).length === 0) {
|
|
453
463
|
throw new ConfigError("no channels configured; add a `channels:` block or pass --channel");
|
|
454
464
|
}
|
|
465
|
+
// Each target's channels: its own list, or every configured one, narrowed by --channel.
|
|
466
|
+
const shippable = shippedTargets(config, targets);
|
|
467
|
+
const routes = new Map(shippable.map((target) => {
|
|
468
|
+
const own = config.targets[target].channels;
|
|
469
|
+
return [target, options.channels
|
|
470
|
+
? options.channels.filter((name) => !own || own.includes(name))
|
|
471
|
+
: config.channelsOf(target)];
|
|
472
|
+
}));
|
|
473
|
+
for (const target of shippable) {
|
|
474
|
+
const own = config.targets[target].channels;
|
|
475
|
+
for (const name of (own && options.channels) || []) {
|
|
476
|
+
if (!own.includes(name)) log(`${target}: not published to ${name} (not in targets.${target}.channels)`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const shipped = shippable.filter((target) => routes.get(target).length > 0);
|
|
480
|
+
if (options.channels && shippable.length > 0 && shipped.length === 0) {
|
|
481
|
+
throw new ConfigError(`nothing was published: no target in this run is routed to ${options.channels.join(", ")}`);
|
|
482
|
+
}
|
|
455
483
|
// Every channel this run uses, before anything is read, packed or sent.
|
|
456
|
-
refuseRemote(
|
|
457
|
-
const shipped = shippedTargets(config, targets);
|
|
484
|
+
refuseRemote([...new Set(shipped.flatMap((target) => routes.get(target)))], configured);
|
|
458
485
|
const versions = versionsOf(config, shipped, options.preRelease);
|
|
459
486
|
const closures = forTargets(config, undefined, shipped);
|
|
460
487
|
let published = 0;
|
|
461
488
|
const handoff = [];
|
|
462
489
|
for (const target of Object.keys(config.targets)) {
|
|
463
|
-
if (
|
|
464
|
-
if (!config.isPublished(target)) continue;
|
|
490
|
+
if (!shipped.includes(target)) continue;
|
|
465
491
|
const closure = closures.get(target);
|
|
466
492
|
// Packed once, then shipped unchanged to every channel.
|
|
467
493
|
const { archive, manifest } = pack(config, target, {
|
|
@@ -469,11 +495,11 @@ async function main(argv, io = {}) {
|
|
|
469
495
|
closureSha256: closure ? closure.sha256 : null,
|
|
470
496
|
outDir, log,
|
|
471
497
|
});
|
|
472
|
-
for (const name of
|
|
498
|
+
for (const name of routes.get(target)) {
|
|
473
499
|
// A remote channel returns a promise; a local one does not.
|
|
474
500
|
const result = await publish(archive, manifest, name, { ...(configured[name] || {}), baseDir: config.root }, log);
|
|
475
501
|
for (const file of result.files || []) {
|
|
476
|
-
handoff.push({ file, target, version: manifest.version, channel: name });
|
|
502
|
+
handoff.push({ file, target, version: manifest.version, channel: name, type: config.channelType(name) });
|
|
477
503
|
}
|
|
478
504
|
published += 1;
|
|
479
505
|
}
|
package/src/config.js
CHANGED
|
@@ -9,6 +9,8 @@ const fs = require("fs");
|
|
|
9
9
|
const path = require("path");
|
|
10
10
|
const YAML = require("yaml");
|
|
11
11
|
|
|
12
|
+
const { CHANNEL_TYPES, describeTypes } = require("./channel-types");
|
|
13
|
+
|
|
12
14
|
const CONFIG_NAME = "apionly.yaml";
|
|
13
15
|
const SUPPORTED_SCHEMA_VERSION = 1;
|
|
14
16
|
|
|
@@ -30,7 +32,7 @@ const REPORTS_KEYS = ["lint"];
|
|
|
30
32
|
const LINT_KEYS = ["unreferenced", "examples"];
|
|
31
33
|
const LINT_EXAMPLES_KEYS = ["openapi", "asyncapi"];
|
|
32
34
|
const DISTRIBUTION_KEYS = ["root", "layout"];
|
|
33
|
-
const TARGET_KEYS = ["openapi", "asyncapi", "publish", "versionFile"];
|
|
35
|
+
const TARGET_KEYS = ["openapi", "asyncapi", "publish", "versionFile", "channels"];
|
|
34
36
|
const TARGET_KIND_KEYS = ["bundle", "aggregate", "info"];
|
|
35
37
|
const PORTFOLIO_KEYS = ["openapi", "security", "location"];
|
|
36
38
|
const PORTFOLIO_OPENAPI_KEYS = ["paths", "operationIds", "tags"];
|
|
@@ -49,6 +51,73 @@ const PATH_STRATEGIES = ["target-prefix", "none"];
|
|
|
49
51
|
|
|
50
52
|
class ConfigError extends Error {}
|
|
51
53
|
|
|
54
|
+
// A channel's name, as init asks of a target's: lowercase words joined by single hyphens.
|
|
55
|
+
const CHANNEL_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Each configured channel's type, by name. A channel named after a type is that type;
|
|
59
|
+
* any other name says its type with `type`, which is how one apionly.yaml has two
|
|
60
|
+
* channels of one type -- a Maven repository per partner.
|
|
61
|
+
*/
|
|
62
|
+
function channelTypes(channels) {
|
|
63
|
+
if (typeof channels !== "object" || Array.isArray(channels)) {
|
|
64
|
+
throw new ConfigError("channels must be a mapping of channel names to their settings");
|
|
65
|
+
}
|
|
66
|
+
const types = {};
|
|
67
|
+
for (const [name, settings] of Object.entries(channels)) {
|
|
68
|
+
if (!CHANNEL_NAME.test(name)) {
|
|
69
|
+
throw new ConfigError(`channel name '${name}': use lowercase letters and digits, in words joined by single ` +
|
|
70
|
+
"hyphens, starting with a letter, such as partner-a");
|
|
71
|
+
}
|
|
72
|
+
if (settings !== null && (typeof settings !== "object" || Array.isArray(settings))) {
|
|
73
|
+
throw new ConfigError(`channels.${name} must be a mapping of settings`);
|
|
74
|
+
}
|
|
75
|
+
const declared = settings ? settings.type : undefined;
|
|
76
|
+
if (CHANNEL_TYPES.includes(name)) {
|
|
77
|
+
if (declared !== undefined && declared !== name) {
|
|
78
|
+
throw new ConfigError(`channel '${name}' is named after a channel type, so its type is ${name}, ` +
|
|
79
|
+
`not ${declared}; give it another name`);
|
|
80
|
+
}
|
|
81
|
+
types[name] = name;
|
|
82
|
+
} else if (declared === undefined) {
|
|
83
|
+
throw new ConfigError(`channel '${name}' is not a channel type; say which one it is with type: ${describeTypes()}`);
|
|
84
|
+
} else if (!CHANNEL_TYPES.includes(declared)) {
|
|
85
|
+
throw new ConfigError(`channels.${name}.type '${declared}' is not a channel type: ${describeTypes()}`);
|
|
86
|
+
} else {
|
|
87
|
+
types[name] = declared;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return types;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Refuses a target's `channels` that cannot be what it means. */
|
|
94
|
+
function checkTargetChannels(name, target, channels, isAggregate) {
|
|
95
|
+
const list = target.channels;
|
|
96
|
+
if (list === undefined) return;
|
|
97
|
+
if (!Array.isArray(list) || list.some((c) => typeof c !== "string")) {
|
|
98
|
+
throw new ConfigError(`targets.${name}.channels must be a list of channel names`);
|
|
99
|
+
}
|
|
100
|
+
if (list.length === 0) {
|
|
101
|
+
throw new ConfigError(`targets.${name}.channels is empty; a target that is published nowhere says so with publish: false`);
|
|
102
|
+
}
|
|
103
|
+
if (isAggregate) {
|
|
104
|
+
throw new ConfigError(`target '${name}' is an aggregate, which is never published, so it has no channels`);
|
|
105
|
+
}
|
|
106
|
+
if (target.publish === false) {
|
|
107
|
+
throw new ConfigError(`target '${name}' is not published (publish: false), so it has no channels`);
|
|
108
|
+
}
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
for (const channel of list) {
|
|
111
|
+
if (seen.has(channel)) throw new ConfigError(`target '${name}' lists channel '${channel}' twice`);
|
|
112
|
+
seen.add(channel);
|
|
113
|
+
if (!Object.prototype.hasOwnProperty.call(channels, channel)) {
|
|
114
|
+
const configured = Object.keys(channels);
|
|
115
|
+
throw new ConfigError(`target '${name}' lists channel '${channel}', which apionly.yaml does not configure; ` +
|
|
116
|
+
`it configures ${configured.length ? configured.join(", ") : "no channels"}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
52
121
|
/** Refuses a key `obj` has that is not in `allowed`, naming the key and where it is. */
|
|
53
122
|
function checkKeys(obj, allowed, where) {
|
|
54
123
|
if (!obj || typeof obj !== "object") return;
|
|
@@ -135,6 +204,9 @@ function load(configPath) {
|
|
|
135
204
|
);
|
|
136
205
|
}
|
|
137
206
|
|
|
207
|
+
const channels = parsed.channels || {};
|
|
208
|
+
const types = channelTypes(channels);
|
|
209
|
+
|
|
138
210
|
const targets = parsed.targets;
|
|
139
211
|
if (!targets || typeof targets !== "object" || Object.keys(targets).length === 0) {
|
|
140
212
|
throw new ConfigError(`${configPath} declares no targets`);
|
|
@@ -176,6 +248,8 @@ function load(configPath) {
|
|
|
176
248
|
);
|
|
177
249
|
}
|
|
178
250
|
|
|
251
|
+
checkTargetChannels(name, target, channels, aggregateKinds.length > 0);
|
|
252
|
+
|
|
179
253
|
for (const kind of kinds) {
|
|
180
254
|
checkKeys(target[kind], TARGET_KIND_KEYS, `targets.${name}.${kind}`);
|
|
181
255
|
if (target[kind].aggregate !== undefined) {
|
|
@@ -238,7 +312,7 @@ function load(configPath) {
|
|
|
238
312
|
reports,
|
|
239
313
|
lint,
|
|
240
314
|
distribution: parsed.distribution || null,
|
|
241
|
-
channels
|
|
315
|
+
channels,
|
|
242
316
|
portfolio,
|
|
243
317
|
targets,
|
|
244
318
|
|
|
@@ -326,6 +400,15 @@ function load(configPath) {
|
|
|
326
400
|
// An aggregate is never published either, whether or not it says so --
|
|
327
401
|
// load() already refuses the one case that would contradict this, `publish:
|
|
328
402
|
// true` alongside an aggregate, so nothing here needs to re-check that.
|
|
403
|
+
// A configured channel's type; a name that is not configured is taken for a type,
|
|
404
|
+
// as `publish --channel github-release` has always meant that channel unconfigured.
|
|
405
|
+
channelType(name) {
|
|
406
|
+
return types[name] || name;
|
|
407
|
+
},
|
|
408
|
+
// The channels a target is published to: its own list, or every configured one.
|
|
409
|
+
channelsOf(target) {
|
|
410
|
+
return this.targets[target].channels || Object.keys(this.channels);
|
|
411
|
+
},
|
|
329
412
|
isPublished(target) {
|
|
330
413
|
return this.targets[target].publish !== false && !this.isAggregate(target);
|
|
331
414
|
},
|
package/src/handoff.js
CHANGED
|
@@ -25,10 +25,10 @@ const HANDOFF_SCHEMA_VERSION = 1;
|
|
|
25
25
|
*/
|
|
26
26
|
function writeHandoff(root, outDir, entries) {
|
|
27
27
|
const files = entries
|
|
28
|
-
.map(({ file, target, version, channel }) => ({
|
|
28
|
+
.map(({ file, target, version, channel, type = channel }) => ({
|
|
29
29
|
path: path.relative(root, file).split(path.sep).join("/"),
|
|
30
30
|
sha256: crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"),
|
|
31
|
-
target, version, channel,
|
|
31
|
+
target, version, channel, type,
|
|
32
32
|
}))
|
|
33
33
|
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
34
34
|
|
package/src/remote.js
CHANGED
|
@@ -50,7 +50,11 @@ function isRemote(channel, settings = {}) {
|
|
|
50
50
|
*/
|
|
51
51
|
function refuseRemote(names, configured = {}, env = process.env) {
|
|
52
52
|
if (remoteMode(env) === "allow") return;
|
|
53
|
-
|
|
53
|
+
// Judged by type: a channel named partner-a may be a Maven repository at a URL.
|
|
54
|
+
const remote = names.filter((name) => {
|
|
55
|
+
const settings = configured[name] || {};
|
|
56
|
+
return isRemote(settings.type || name, settings);
|
|
57
|
+
});
|
|
54
58
|
if (remote.length === 0) return;
|
|
55
59
|
throw new ChannelError(
|
|
56
60
|
`${REMOTE_ENV}=never refuses the remote channels ${remote.join(", ")}, so nothing was built, packed or sent. ` +
|