Skip to content

Latest commit

 

History

History
411 lines (302 loc) · 18.4 KB

run-book.adoc

File metadata and controls

411 lines (302 loc) · 18.4 KB

Run-book: operating the API-Only Publisher and Subscriber

Note
Where the links in this document go

Every link ends with a marker saying where it leads, so you can tell before you follow it:

↗

An external site, outside this repository.

→

Another file in this repository.

§

A section of this same file.

This run-book says how to invoke both tools in normal operation, once a setup from one of the use cases exists, and what to do when something fails. It describes the Publisher and the Subscriber as they are on this repository’s main branch; the badges at the top of the Publisher’s README → and the Subscriber’s README → show each one’s latest release. How the Publisher and the Subscriber work together → explains the mechanism behind every step.

Who does what

Role Does

Contract author

Changes fragments, raises the contract’s version, and runs the Publisher’s build and lint locally.

Implementer or consumer

Declares the contract version a project builds against, runs ./gradlew check, and commits apionly.lock.

The build

In provider and client use cases 1 and 2, and the combined use case, runs the Publisher and the Subscriber in one Gradle build.

The release job

In provider use case 3, publishes every contract whose version is new, and tags it.

The commands you run

Publisher

Run in the directory that holds apionly.yaml and the Publisher’s package.json, always through the npm script.

Command What it does When

npm ci

Installs the Publisher at the version package-lock.json pins.

After cloning, and whenever package-lock.json changes

npm run apionly — build [--target <t>]

Builds, stamps and lints every contract, or one, and logs each operation that has no example (a warning unless lint.examples.<kind> says error).

After changing fragments or a version file

npm run apionly — lint

Lints every built contract, and fails on unreferenced fragments.

After build; in CI

npm run apionly — changed --since <ref>

Lists the contracts a change reaches.

Before raising versions; in release checks

npm run apionly — publish [--target <t>] [--channel <c>]

Packs and ships built contracts.

From the build (provider and client use cases 1 and 2) or the release job (provider use case 3)

npm run apionly — build --target <t> --pre-release <ids>
npm run apionly — publish --target <t> --pre-release <ids>

Builds and publishes a pre-release of the version in the contract’s version file.

When a consumer needs an unfinished contract

npm run apionly — targets

Lists the contracts and what each builds.

When in doubt

npm run apionly — closure

Shows each contract’s files and hash.

When investigating what a fragment affects

Never run npx api-only-publisher: that unscoped name is not this package.

Gradle

Task What it does

./gradlew check

In provider and client use cases 1 and 2, and the combined use case: builds, publishes and lints the contracts, fetches and verifies them, and runs the tests. In provider use case 4, and client use cases 3 and 4: fetches, verifies and tests.

./gradlew fetchApiSpec

Fetches every subscribed contract, and updates apionly.lock when a version changed.

./gradlew verifyApiSpec

Checks the fetched documents against apionly.lock, without fetching.

./gradlew updateApiOnlySubscriberDSL

After upgrading the Subscriber: adds new defaulted properties to the apiOnlySubscriber { } block, keeping build.gradle.bak.

Routine procedures

Changing a contract

  1. Edit the fragments.

  2. Raise version in the contract’s <target>.bundle.properties: major for a breaking change, minor for an additive one, patch for anything else.

  3. In provider use cases 1 and 2, raise apiContractVersion in the implementing project’s gradle.properties to the same version, and run ./gradlew check; it prints Updated <target> from <old> to <new>.

  4. Commit the fragments, the version changes and the updated apionly.lock together.

In provider use case 3, the pull request changes the fragments and the version file; consumers adopt the release afterwards, as in Taking a new version §.

Changing a shared fragment

A shared fragment changes every contract that reaches it.

  1. Edit the fragment.

  2. List the contracts it reaches: npm run apionly — changed --since HEAD --quiet.

  3. Raise the version of each of those contracts, and, in provider use cases 1 and 2, the apiContractVersion of each implementing project.

  4. Run ./gradlew check, or the release check in provider use case 3, and commit everything together.

Adding a contract

  1. Add a bundle root and its <target>.bundle.properties to the library.

  2. Add the target to targets: in apionly.yaml.

  3. In provider use case 2, add a project named after the target, with apiContractVersion; in provider use cases 3 and 4, and client use cases 3 and 4, subscribe from the projects that implement or call it after the first release.

  4. Run npm run apionly — build and npm run apionly — lint.

Removing a fragment

Remove every $ref to it, then delete the file. A fragment that stays behind unreferenced fails lint.

Taking a new version

  1. Change apiContractVersion in the project’s gradle.properties, or on the subscription; for an API the project calls, the apiContractVersion of its subscribeAsClient block. To build against another version once, without changing anything, pass -PapiContractVersion=<version>; it overrides every other setting, but not a called API’s.

  2. Run ./gradlew check; it prints Updated <target> from <old> to <new>.

  3. Commit the version change and apionly.lock together.

Working against an unfinished contract

  1. Producer: build and publish with --pre-release rc.1, which publishes <version>-rc.1 for the version in the contract’s version file.

  2. Consumer: subscribe to that version, with allowPrerelease = true on the subscription.

  3. At the release: the consumer takes the released version and removes allowPrerelease.

Releasing, in provider use case 3

On the main branch, the release job runs npm ci, build, lint, and a script that publishes every contract whose version has no <target>-v<version> tag, then tags it and pushes the tags. A pull request runs a script that fails when a changed contract’s version is already tagged. Both scripts are in provider use case 3’s tutorial →.

Never delete or move a release tag, and never republish a released version.

Upgrading the tools

Tool Where it is pinned Upgrade

API-Only Publisher

package.json and package-lock.json beside apionly.yaml

npm install --save-dev --save-exact @arc-e-tect/api-only-publisher@<version>

Redocly CLI, AsyncAPI CLI

toolchain.redocly, toolchain.asyncapi in apionly.yaml

Change the pinned version

Node.js

Your CI configuration

Change the version

API-Only Subscriber

The plugins { } block, or the version catalogue

Change the version, then run ./gradlew updateApiOnlySubscriberDSL

A Publisher, bundler or Node.js upgrade can change the published documents without any fragment changing. Find out before committing the upgrade:

  1. Build with the versions in force now, and set build/dist/ (or your build.dist) aside.

  2. Upgrade, build again, and diff -r the two.

  3. No difference: commit the upgrade.

  4. A difference in formatting or ordering: raise the patch version of each contract that differs, adopt them, and commit everything together.

  5. A difference in what a contract says: treat it as a contract change, as in Changing a contract §.

From Publisher 0.3.0 or earlier, every OpenAPI document with components built from fragments gains an x-fragment-path on each such component. That is an addition to the contract: raise the minor version of each of those contracts, or set defaults.openapi.fragmentPaths: false in apionly.yaml until you do. From Publisher 0.4.x or earlier, every AsyncAPI document gains an x-fragment-path on each fragment the bundler inlined, likewise: raise the minor version of each contract with an AsyncAPI document, or set defaults.asyncapi.fragmentPaths: false until you do. What a change to x-fragment-path means for a contract’s version → explains why.

A Subscriber upgrade does not change the contracts, so apionly.lock should not change; if it does, read the Subscriber’s release notes before committing it.

What CI runs

Setup CI

Provider and client use cases 1 and 2, and the combined use case

Node.js and Java; ./gradlew check; fail when an apionly.lock changed during the build; keep the lint reports.

Provider use case 3, contracts repository

Pull requests: npm ci, build, lint, the version check. The main branch: the same, then the release script and git push --tags, with the Maven token.

Provider use cases 3 and 4, and client use cases 3 and 4

Java; ./gradlew check with the repository credentials in ORG_GRADLE_PROJECT_*; fail when apionly.lock changed during the build.

Situations worth knowing

One implemented contract per project

A project implements at most one contract, at the version its apiContractVersion declares. Two implemented contracts in one project would share one version, one classpath root and one release, so neither could change without the other. Put each in a project of its own, in a multi-project build: each project applies the Subscriber, subscribes to its contract, and declares apiContractVersion in its own gradle.properties. The Subscriber’s README → shows the setup.

A second subscribe to a different contract fails the build, and the message suggests subscribeAsClient for a contract the project calls instead.

Calling other APIs

A project may call any number of APIs, next to the contract it implements. It declares each with subscribeAsClient('<target>') { apiContractVersion = '<version>' }. Each gets every check the implemented contract gets: the manifest check, its own entry in apionly.lock, the refusal of changed bytes under a locked version, and verifyApiSpec as part of check. Its documents are on the classpath under contracts/<target>/, and it sets its own version, because apiContractVersion is the implemented contract’s. When its provider publishes somewhere else than the project’s other contracts, it sets a channel { } of its own; every setting it leaves out comes from the project’s channel. The Subscriber’s README → has the details.

A hand-edited fetched document

In a build that fetches before verifying, such as ./gradlew check, the fetch restores the published document and the edit disappears. ./gradlew verifyApiSpec on its own reports the edit. Change the contract in the library, never the fetched document.

The lint configuration inside the fragments' root

The lint skips lint tools' configuration files wherever they sit, redocly.yaml, .redocly.yaml, .redocly.lint-ignore.yaml, .spectral.yaml and .spectral.yml, and the lint configuration apionly.yaml names, whatever it is called.

When something fails

Publisher

Message Meaning What to do

api-only-publisher: command not found

The Publisher is not installed in this directory.

npm ci

npm error 404 Not Found - GET https://registry.npmjs.org/api-only-publisher

npx api-only-publisher, without the scope: that name is not this package.

Run it as npm run apionly — <command>.

The captured output starts with > apionly

npm printed the script’s banner, and something is parsing the output.

Run it as npm run --silent apionly — <command>.

no apionly.yaml found in <dir> or any parent directory

The command ran outside the specification library.

Run it from the directory that holds apionly.yaml, or point at it with -C <dir>.

<path> declares schemaVersion <n>; this version of api-only-publisher understands 1

The configuration is newer than the Publisher.

Upgrade the Publisher, as Upgrading the tools § describes.

fatal: not a git repository, or fatal: ambiguous argument 'HEAD', printed during build or publish

Git noise: the Publisher asks git for provenance, and the directory is not a repository, or has no commit yet.

Nothing. The document is built and published either way; commit the repository and it stops.

changed --since <ref> reports nothing, and you know something changed

The change is outside every target’s closure, or it is not committed yet.

Check with closure which files a target reaches, and commit the change first.

target '<t>': no version file at <path>

A published contract has no version file.

Create <target>.bundle.properties with version=1.0.0, or set targets.<t>.versionFile.

<path> declares '<v>', but a version file holds a release version

The version file holds a pre-release.

Put the release version in the file, and use --pre-release.

--version is no longer accepted

An old invocation.

Remove --version; the version comes from the version file.

no Markdown file found for {{<token>}}

A placeholder has no <token>.md under the source root.

Add the file, or fix the token.

target '<t>': bundle root not found at <path>

The bundle path is wrong; it is relative to <sources.root>/<sources.<kind>>.

Fix targets.<t>.<kind>.bundle.

<n> fragment(s) not reachable from any target, so nothing lints them

YAML files under the source root that no contract references.

Reference each from a contract, or delete it.

lint failed for <n> document(s): …

Lint errors, listed above that line.

Fix the fragments.

<n> operation(s) in <file> have no example (lint.examples.<kind> is error):

The library set lint.examples.<kind> to error, and an AsyncAPI message, or an OpenAPI request body or response, has no example. Each is listed below the line.

Add the example to the fragment, or relax the setting (Give every operation an example →).

declares info.version '<a>' but is being packed as '<b>'

The document was built for another version than the one being packed.

Run build again, with the same --pre-release as publish.

no credential in $<VAR>, which channels.maven.tokenEnv names

A remote Maven upload without a token.

Set the variable in the release job’s environment.

PUT <url> failed: 401 or 403

The token was rejected.

Check the token, and that the repository accepts a bearer token.

Contract version not raised (provider use case 3’s check script)

A contract changed, but its version is already released.

Raise the contract’s version in the pull request.

Subscriber

Message Meaning What to do

subscription '<t>' declares no version

No apiContractVersion from the command line, the subscription, apiOnlySubscriber or the project property.

Set apiContractVersion in gradle.properties.

subscription '<t>' resolves the pre-release version <v>

A pre-release, without asking for one.

Take a released version, or set allowPrerelease = true deliberately.

no archive for '<t>' <v> at <path>. Published versions of '<t>': …

Nothing is published at that version in the file channel.

Make the contract’s version and the project’s version agree.

Could not find <group>:<target>:<version>

Gradle could not resolve the archive from the Maven repository.

Check the repository entry, the credentials, the coordinates, and that the version was released.

the published contract for '<t>' <v> is not the one recorded in apionly.lock … was published to the file channel again, with different content

A contract changed under a version that was already locked.

Raise the contract’s version and adopt it; or undo the change.

the published contract for '<t>' <v> is not the one recorded in apionly.lock … A released version was rebuilt, or a tag was moved

A released version in a registry came back with different documents.

Ask the producer, and take a new version when they release one; do not delete the lock entry.

the archive for '<t>' declares version <a> but was resolved as <b>

The archive is not the version it was published as.

Report it to the producer.

… does not match the hash its own manifest declares

The archive is corrupt or was tampered with.

Fetch again; if it persists, report it to the producer.

the contract for '<t>' has drifted from apionly.lock

A fetched document was edited.

./gradlew fetchApiSpec, and change the library instead.

nothing has been fetched for '<t>' yet, so there is nothing to verify

verifyApiSpec ran before any fetch.

./gradlew fetchApiSpec, or ./gradlew check.

apionly.lock has no entry for '<t>'

A subscription that was never fetched and committed.

./gradlew fetchApiSpec, and commit apionly.lock.

apiOnlySubscriber already implements '<a>', so it cannot also implement '<b>'

A second implemented contract in one project.

If the project calls <b>, use subscribeAsClient; otherwise One implemented contract per project §.

apiOnlySubscriber implements '<t>', so it cannot also subscribe to it as a client, or calls '<t>' as a client, so it cannot also implement '<t>'

The same contract declared as implemented and as called.

Keep the one that says what the project does with it.

client subscription '<t>' declares no version

An API the project calls has no version of its own.

Set apiContractVersion in its subscribeAsClient block; Calling other APIs §.