@arc-e-tect/api-only-publisher 0.10.0 → 0.11.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/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.10.0
9
+ :api-only-publisher-version: 0.11.0
10
10
 
11
11
  image:https://github.com/Arc-E-Tect/SoftwareEngineeringDoneRight-API/actions/workflows/nvd-cache-refresh.yml/badge.svg[Vulnerability Scan,link=https://github.com/Arc-E-Tect/SoftwareEngineeringDoneRight-API/actions/workflows/nvd-cache-refresh.yml]
12
12
  image:https://img.shields.io/npm/v/@arc-e-tect/api-only-publisher[npm,link=https://www.npmjs.com/package/@arc-e-tect/api-only-publisher]
@@ -169,7 +169,7 @@ A target whose closure is untouched needs no new version, however much else in t
169
169
  |`publish [--pre-release <ids>] [--target <t>]... [--channel <c>]... [--out <dir>]`
170
170
  |Pack once, then ship those same bytes to every configured channel, or to each `--channel`.
171
171
 
172
- |`split --out <dir> [--by kind\|target]`
172
+ |`split [--out <dir>] [--by kind\|target]`
173
173
  |Make the library safe to break into separate repositories.
174
174
  |===
175
175
 
@@ -651,6 +651,16 @@ What happens when the destination already has the version:
651
651
  A NuGet feed that cannot return the package it has, so there is nothing to compare, fails as a different one would.
652
652
  |===
653
653
 
654
+ [#channel-text]
655
+ === What a package says about itself
656
+
657
+ The Maven POM, the npm `package.json`, the NuGet `.nuspec` and a GitHub release all describe the contract.
658
+ By default each says `API description documents for <target>.`, and a GitHub release is titled `<target> <version>`.
659
+ `channels.<channel>.description` replaces the description, and `channels.github-release.title` and `notes` the release's title and notes (notes default to the description); `{target}` and `{version}` are filled in, as in `tagFormat`.
660
+
661
+ npm publishes a release under the dist-tag `latest` and a pre-release under `next`.
662
+ `channels.npm.releaseTag` and `prereleaseTag` name others: publishing a patch to an older line under `releaseTag: v1` keeps `latest` on the newest line, which publishing it under `latest` would move back.
663
+
654
664
  Credentials are read from the environment, never from configuration.
655
665
  `channels.maven.tokenEnv` names the variable, `MAVEN_TOKEN` by default, and `channels.nuget.tokenEnv` the NuGet API key's, `NUGET_API_KEY` by default; a configuration file gets committed, and a credential in a committed file is a credential that has leaked.
656
666
 
@@ -671,6 +681,7 @@ channels:
671
681
  # tokenEnv: NUGET_API_KEY # the variable holding the API key, for a URL
672
682
  # authors: Contoso API team # default: idPrefix
673
683
  # license: MIT # an SPDX expression; default: UNLICENSED
684
+ # description: "{target} contract" # default: API description documents for {target}.
674
685
  ----
675
686
 
676
687
  *The package id* is `<idPrefix>.<target in PascalCase>`: target `user-account` with `idPrefix: Contoso.Contracts` is `Contoso.Contracts.UserAccount`, the dotted PascalCase .NET uses for namespaces, from a kebab-case target.
@@ -775,6 +786,8 @@ defaults:
775
786
  build:
776
787
  staging: build/staging
777
788
  dist: build/dist
789
+ # packages: build/packages # where pack and publish write archives; --out overrides
790
+ # split: build/split # where split writes its parts; --out overrides
778
791
  reports:
779
792
  lint: build/reports/lint
780
793
  lint:
@@ -791,6 +804,14 @@ channels:
791
804
  file:
792
805
  directory: build/publish
793
806
  clean: true # keep only the version being published
807
+ # Every channel that describes a package or a release takes `description`, with
808
+ # {target} and {version} filled in; default: API description documents for {target}.
809
+ # npm:
810
+ # releaseTag: latest # the dist-tag of a release; another keeps latest where it is
811
+ # prereleaseTag: next # the dist-tag of a pre-release
812
+ # github-release:
813
+ # title: "{target} {version}"
814
+ # notes: "..." # default: description
794
815
 
795
816
  targets:
796
817
  orders:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arc-e-tect/api-only-publisher",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Builds, packs and publishes API description documents from a library of reusable fragments.",
5
5
  "license": "MIT",
6
6
  "author": "Arc-E-Tect",
package/src/channels.js CHANGED
@@ -16,6 +16,14 @@ const { isPrerelease, npmDistTag } = require("./version-policy");
16
16
  const { ChannelError } = require("./channel-error");
17
17
  const { publishNuget } = require("./nuget");
18
18
 
19
+ /** What every package and release says it is, unless `description` says otherwise. */
20
+ const DEFAULT_DESCRIPTION = "API description documents for {target}.";
21
+
22
+ /** `template` with {target} and {version} filled in, as tagFormat has always been. */
23
+ function format(template, manifest) {
24
+ return String(template).replace(/\{target\}/g, manifest.target).replace(/\{version\}/g, manifest.version);
25
+ }
26
+
19
27
 
20
28
  /**
21
29
  * `file` -- publish to a local directory.
@@ -44,7 +52,7 @@ function publishFile(archive, manifest, options, log) {
44
52
  return { location: archiveDest };
45
53
  }
46
54
 
47
- function pom(groupId, artifactId, version, packaging) {
55
+ function pom(groupId, artifactId, version, packaging, description = `API description documents for ${artifactId}.`) {
48
56
  return `<?xml version="1.0" encoding="UTF-8"?>
49
57
  <project xmlns="http://maven.apache.org/POM/4.0.0"
50
58
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
@@ -54,11 +62,16 @@ function pom(groupId, artifactId, version, packaging) {
54
62
  <artifactId>${artifactId}</artifactId>
55
63
  <version>${version}</version>
56
64
  <packaging>${packaging}</packaging>
57
- <description>API description documents for ${artifactId}.</description>
65
+ <description>${description.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}</description>
58
66
  </project>
59
67
  `;
60
68
  }
61
69
 
70
+ /** The POM's description: configured, or the POM's own default. */
71
+ function pomDescription(options, manifest) {
72
+ return options.description ? format(options.description, manifest) : undefined;
73
+ }
74
+
62
75
  /**
63
76
  * `maven` -- publish into a Maven repository layout.
64
77
  *
@@ -91,7 +104,7 @@ function publishMaven(archive, manifest, options, log) {
91
104
  const base = `${artifactId}-${version}`;
92
105
  const artifactDest = path.join(dir, `${base}.${extension}`);
93
106
  fs.copyFileSync(archive, artifactDest);
94
- fs.writeFileSync(path.join(dir, `${base}.pom`), pom(groupId, artifactId, version, extension));
107
+ fs.writeFileSync(path.join(dir, `${base}.pom`), pom(groupId, artifactId, version, extension, pomDescription(options, manifest)));
95
108
 
96
109
  // The manifest travels beside the artifact as well as inside it, so a
97
110
  // consumer can read provenance without unpacking anything.
@@ -115,7 +128,7 @@ function publishNpm(archive, manifest, options, log) {
115
128
  const scope = options.scope;
116
129
  const name = scope ? `${scope}/${manifest.target}` : (options.namePrefix || "") + manifest.target;
117
130
  const version = manifest.version;
118
- const distTag = npmDistTag(version);
131
+ const distTag = npmDistTag(version, options);
119
132
 
120
133
  const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "api-only-npm-"));
121
134
  try {
@@ -126,7 +139,7 @@ function publishNpm(archive, manifest, options, log) {
126
139
  fs.writeFileSync(path.join(workDir, "package.json"), JSON.stringify({
127
140
  name,
128
141
  version,
129
- description: `API description documents for ${manifest.target}.`,
142
+ description: format(options.description || DEFAULT_DESCRIPTION, manifest),
130
143
  license: options.license || "UNLICENSED",
131
144
  files: fs.readdirSync(workDir).filter((f) => f !== "package.json").sort(),
132
145
  // npm defaults a scoped package to restricted. A contract exists to be
@@ -210,9 +223,7 @@ function publishGithubRelease(archive, manifest, options, log) {
210
223
  const repository = options.repository;
211
224
  if (!repository) throw new ChannelError("the github-release channel requires a repository");
212
225
 
213
- const tag = (options.tagFormat || "{target}-v{version}")
214
- .replace(/\{target\}/g, manifest.target)
215
- .replace(/\{version\}/g, manifest.version);
226
+ const tag = format(options.tagFormat || "{target}-v{version}", manifest);
216
227
 
217
228
  const gh = (args) => execFileSync("gh", args, { encoding: "utf8", stdio: "pipe" });
218
229
 
@@ -225,8 +236,8 @@ function publishGithubRelease(archive, manifest, options, log) {
225
236
 
226
237
  if (!exists) {
227
238
  const args = ["release", "create", tag, "--repo", repository,
228
- "--title", `${manifest.target} ${manifest.version}`,
229
- "--notes", `API description documents for ${manifest.target}.`];
239
+ "--title", format(options.title || "{target} {version}", manifest),
240
+ "--notes", format(options.notes || options.description || DEFAULT_DESCRIPTION, manifest)];
230
241
  // A pre-release is marked as one, so that "latest release" never resolves
231
242
  // to a contract that is not finished.
232
243
  if (isPrerelease(manifest.version)) args.push("--prerelease");
@@ -333,7 +344,7 @@ function publishMavenRemote(archive, manifest, options, log) {
333
344
  }
334
345
  await putFile(`${base}.${extension}`, bytes, token, "application/octet-stream");
335
346
  await putFile(`${base}.pom`,
336
- Buffer.from(pom(options.groupId, artifactId, version, extension), "utf8"),
347
+ Buffer.from(pom(options.groupId, artifactId, version, extension, pomDescription(options, manifest)), "utf8"),
337
348
  token, "application/xml");
338
349
  await putFile(`${base}-manifest.json`,
339
350
  Buffer.from(JSON.stringify(manifest, null, 2) + "\n", "utf8"),
@@ -368,4 +379,4 @@ function publish(archive, manifest, channel, options, log = () => {}) {
368
379
  return handler(archive, manifest, options || {}, log);
369
380
  }
370
381
 
371
- module.exports = { publish, CHANNELS, ChannelError, pom };
382
+ module.exports = { publish, CHANNELS, ChannelError, pom, format, DEFAULT_DESCRIPTION };
package/src/cli.js CHANGED
@@ -38,7 +38,7 @@ Usage:
38
38
  api-only-publisher changed --since <ref>
39
39
  api-only-publisher pack [--pre-release <ids>] [--target <name>]... [--out <dir>]
40
40
  api-only-publisher publish [--pre-release <ids>] [--target <name>]... [--channel <name>]... [--out <dir>]
41
- api-only-publisher split --out <dir> [--by kind|target]
41
+ api-only-publisher split [--out <dir>] [--by kind|target]
42
42
 
43
43
  Options:
44
44
  --target <name> Restrict to one target; repeat for several. Default: all.
@@ -60,7 +60,8 @@ Options:
60
60
  --portfolio-paths <target-prefix|none>, --portfolio-location <dir>
61
61
  config only: the value of that question; a flag always wins.
62
62
  --since <ref> changed only: the git ref to compare the working tree against.
63
- --out <dir> pack/publish/split: where to write.
63
+ --out <dir> pack/publish/split: where to write, instead of build.packages
64
+ or build.split in apionly.yaml.
64
65
  --channel <name> publish only: repeat for several. Default: every configured channel.
65
66
  --by <kind|target> split only: the boundary to split along. Default: kind.
66
67
  -C <dir> Run as if started in <dir>.
@@ -407,7 +408,7 @@ async function main(argv, io = {}) {
407
408
  return 0;
408
409
  }
409
410
  case "pack": {
410
- const outDir = path.resolve(options.dir, options.out || "build/packages");
411
+ const outDir = options.out ? path.resolve(options.dir, options.out) : config.packagesDir();
411
412
  const shipped = shippedTargets(config, targets);
412
413
  const versions = versionsOf(config, shipped, options.preRelease);
413
414
  const closures = forTargets(config, undefined, shipped);
@@ -430,7 +431,7 @@ async function main(argv, io = {}) {
430
431
  return 0;
431
432
  }
432
433
  case "publish": {
433
- const outDir = path.resolve(options.dir, options.out || "build/packages");
434
+ const outDir = options.out ? path.resolve(options.dir, options.out) : config.packagesDir();
434
435
  const configured = config.channels || {};
435
436
  const names = options.channels || Object.keys(configured);
436
437
  if (names.length === 0) {
@@ -460,7 +461,7 @@ async function main(argv, io = {}) {
460
461
  return 0;
461
462
  }
462
463
  case "split": {
463
- const outDir = path.resolve(options.dir, options.out || "build/split");
464
+ const outDir = options.out ? path.resolve(options.dir, options.out) : config.splitDir();
464
465
  prepare(config);
465
466
  const parts = split(config, { by: options.by, outDir, log });
466
467
  const duplicated = parts.reduce((n, p) => n + p.imported.length, 0);
package/src/config.js CHANGED
@@ -25,7 +25,7 @@ const SOURCES_KEYS = ["root", "openapi", "asyncapi"];
25
25
  const DEFAULTS_KEYS = ["openapi", "asyncapi", "placeholders"];
26
26
  const DEFAULTS_KIND_KEYS = { openapi: ["lint", "outputName", "fragmentPaths"], asyncapi: ["lint", "outputName", "fragmentPaths"] };
27
27
  const PLACEHOLDERS_KEYS = ["strict"];
28
- const BUILD_KEYS = ["staging", "dist"];
28
+ const BUILD_KEYS = ["staging", "dist", "packages", "split"];
29
29
  const REPORTS_KEYS = ["lint"];
30
30
  const LINT_KEYS = ["unreferenced", "examples"];
31
31
  const LINT_EXAMPLES_KEYS = ["openapi", "asyncapi"];
@@ -255,6 +255,14 @@ function load(configPath) {
255
255
  stagingDir(kind) {
256
256
  return path.join(this.stagingRoot(kind), requireString(this.sources[kind], `sources.${kind}`));
257
257
  },
258
+ // Where pack and publish write archives, and split its parts, unless --out says
259
+ // otherwise; relative to this configuration, as every other path here is.
260
+ packagesDir() {
261
+ return path.resolve(this.root, this.build.packages || "build/packages");
262
+ },
263
+ splitDir() {
264
+ return path.resolve(this.root, this.build.split || "build/split");
265
+ },
258
266
  distDir(target) {
259
267
  const dist = this.build.dist || "dist";
260
268
  return path.resolve(this.root, dist, target);
package/src/nuget.js CHANGED
@@ -88,7 +88,8 @@ function nuspec(id, manifest, options) {
88
88
  ` <id>${escape(id)}</id>\n` +
89
89
  ` <version>${escape(manifest.version)}</version>\n` +
90
90
  ` <authors>${escape(options.authors || options.idPrefix || id)}</authors>\n` +
91
- ` <description>API description documents for ${escape(manifest.target)}.</description>\n` +
91
+ ` <description>${escape((options.description || "API description documents for {target}.")
92
+ .replace(/\{target\}/g, manifest.target).replace(/\{version\}/g, manifest.version))}</description>\n` +
92
93
  ` <license type="expression">${escape(options.license || "UNLICENSED")}</license>\n` +
93
94
  repository +
94
95
  " </metadata>\n" +
@@ -44,10 +44,11 @@ function isPrerelease(version) {
44
44
  * The npm dist-tag a version should be published under.
45
45
  *
46
46
  * A pre-release goes to `next`, never `latest`, so that `npm install <pkg>`
47
- * cannot pick one up by accident.
47
+ * cannot pick one up by accident. Both can be named otherwise -- `releaseTag` for a
48
+ * patch to an older line that must not become `latest`, say.
48
49
  */
49
- function npmDistTag(version) {
50
- return isPrerelease(version) ? "next" : "latest";
50
+ function npmDistTag(version, { releaseTag = "latest", prereleaseTag = "next" } = {}) {
51
+ return isPrerelease(version) ? prereleaseTag : releaseTag;
51
52
  }
52
53
 
53
54
  /**