Skip to main content

Releasing

A push to master that changes a package's shipped files publishes that package to npm in the same run, from the tarball CI built and tested. Nobody writes a changeset or a release note for it, and no version or catalog pull request follows it: a maintainer releases by merging a pull request, and nobody publishes by hand.

What a push publishes​

scripts/release-plan.mjs decides, for each publishable package and from npm and the release tags alone, whether the push publishes it and at which version. The publishable packages are the directories scripts/package-roles.json gives the role pack, library, template or root, less those whose package.json is private: true.

  • The newest published version is the highest of every version npm holds and every tag <name>@<version> in the repository.
  • Its commit is the one its tag names; without a tag, the gitHead of npm's version document; without either, the commit its npm provenance attestation names. A newest version with none of the three fails the run, naming the package.
  • A patch publishes when a file the package ships changed since that commit. A package ships its directory less its tests (__tests__, test and tests directories, *.test.* and *.spec.* files), its fixtures directories and its CHANGELOG.md, at any depth. A package.json change confined to devDependencies ships nothing, and neither does a change to pnpm-lock.yaml alone. The commit's type does not matter: a fix, a feat or a chore that changes a shipped file publishes a patch.
  • A minor publishes in place of the patch, on a package below 1.0.0, when one of the commits since then that changed a shipped file carries a breaking mark (a commit whose only change to the package is in package.json's devDependencies changed none): ! before the colon of its subject (feat!: ..., fix(github)!: ...), or a line of its message beginning with BREAKING CHANGE: or BREAKING-CHANGE:. A squash merge's message is the pull request's title followed by its description, so the mark goes in the title, or on a line of the description of its own.
  • A breaking mark on a package at 1.0.0 or above refuses the run until a pull request writes the next major into the package's package.json (2.0.0 for a package at 1.4.0), which then publishes as written. A minor would sit inside the caret range consumers hold (^1.4.0 admits 1.5.0) and reach them on their next install. The refusal names the package, its version, the commit carrying the mark and the major to write; in a pull request's dry run it turns CI's build red, so the pull request cannot merge without the major. A version written by hand above the newest one but below the next major is refused the same way. A refused package outside --only fails no run: it stays unplanned, with the refusal as its reason. A mark already on master (a squash message edited in the merge dialog, which no check reads) fails no other pull request's dry run: the package stays unplanned there, with the refusal as a warning, and master's own run fails naming it until a pull request writes the major.
  • Any other number, 1.0.0 included, publishes when a pull request raises the package's package.json version above every published one: the merge publishes that number, whatever else it changed. A version at or below the newest published one names nothing: the package publishes by the rules above when its files changed, and the plan warns when a pull request set the version below the newest published one.
  • Only master's head publishes. A run of a commit master has moved past publishes nothing and says so, since the newer commit's run publishes its changes; a run of a commit master does not hold fails.

A dependent follows its siblings. Its range on a sibling (in dependencies, peerDependencies or optionalDependencies) is the range the same field of its newest published manifest names, which is what consumers install, unless a pull request changed that entry in its package.json since its newest version's commit, in which case master's text is the range. In the minutes after a publish, npm can answer 404 for the version document of a newest version the plan knows from its tag alone: the plan reads that document again every 10 seconds for up to 30 minutes, the time the release job's waits for a version before its dependents share, and fails naming the version when npm still serves none, since master's text in its place would raise again a range an earlier run raised. When the range does not admit the sibling's version in the run (the version the run publishes, else the sibling's newest published one), the run raises the range in its checkout to that version, in the range's own form (^, ~, >= or the exact version), and publishes the dependent as a patch, after the sibling. That patch is looked for on npm first, as every planned version is: when npm holds it already (a run that published it stopped before its tag, or a hand published it), the plan counts from it, and publishes nothing more when it carries the raised range and the next patch when it does not. A workspace: range stays as it is in the checkout, and pnpm converts it when it packs (pnpm's workspace protocol): workspace: and workspace:* become the sibling's version, workspace:~ and workspace:^ that version after a tilde or a caret, and a range written after workspace: (workspace:^1.2.0) that range as it is written, which does not follow the sibling. devDependencies never publish a dependent.

A pack that starts to use something a newer release of another pack of this workspace adds, such as a new reason in the account sources' SourceGone, raises its own peer floor on that pack in the same pull request. A release raises a dependent's range only when the new version falls outside it, so a release of one pack never raises the floor of a pack that does not need it.

A pack that depends on another pack of this workspace keeps a peer floor no lower than that pack's own: the Google Calendar pack depends on the Google Drive pack, so when Drive's floor on the account sources rises, Calendar's rises with it in the same pull request.

The root package @framers/agentos-extensions is decided after the packs, as the catalog and the root package says.

What happens after a push to master​

  1. The release workflow (release.yml) starts. Its first job runs CI (ci.yml), which checks the repository out with its history and its tags. CI's step "The packages this push would publish" runs node scripts/release-plan.mjs plan, then node scripts/release-plan.mjs apply, which writes the decision into the job's checkout: each planned package's version, every other package's newest published version, the raised ranges, each planned package's manifest.json version and a gitHead naming the commit. Nothing of it is committed. The install, the builds and the tests run on that checkout, and the pack guard packs each planned package, checks the tarball, installs it and keeps it. The job uploads the plan and the tarballs, and its summary lists each planned package with its version, its bump, the reason and its base commit.
  2. The release job runs on master once CI passes. It installs and builds nothing: it downloads the plan and the tarballs and publishes each tarball with npm publish <tarball> --access public --provenance, one at a time and in the plan's order, a dependency before its dependents and the root last. Before each publish it reads master's head again, and when master has moved on it stops there and ends green, since the newer commit's run plans the rest; and it reads the package's versions on npm, where a version npm gained during the run fails it. The planned version itself, when npm holds it with this commit as its gitHead and the tarball's integrity, was published by an earlier attempt of the run: the step records it as published, already on npm, publishes it nowhere again and goes on. Each accepted publish is written to a journal before the next starts, and a dependent waits until npm serves the version of the sibling it names, read every 10 seconds, since npm's document for a new version has answered 404 for over twenty-five minutes after its publish (13 October 2026). The run's waits share 30 minutes in all, however many dependents it publishes, and a wait that finds them spent fails the step naming the version; the packages after it publish in a later run. The job's time limit, two hours, holds those 30 minutes and the images step's 45 (step 4) beside the publishes.
  3. It tags each published version <name>@<version> at the commit it was published from, pushes the tags in one atomic push, and creates a GitHub release for each tag, whose notes list the subjects of the commits since the package's previous version with their pull request numbers, then each range the run raised on a sibling, - <sibling> <from> to <to>, in <field>, so a dependent published for a raised range alone says why. When the create fails, the step reads the tag's release again, every 10 seconds while that read fails, within one budget of 60 seconds for the whole step. A release there then exists whoever made it: another run in the meantime, such as a second publish workflow carrying the same release repair in a concurrency group of its own, or an earlier run whose release the step's first read missed on a failed answer. The step logs a line saying so, lists it in the summary apart from the releases it made, and goes on; any other failed create fails the step naming the tag. A tag that another run pushed at the same commit between the step's read of the remote and its push makes the atomic push fail, since git refuses to update a tag without force (git push), and the read after it finds that tag in place, so the next attempt pushes the others. A version npm holds with no tag, or a tag with no release, gets the missing one at its own commit in every run of master's head, whether or not that run publishes a package: the newest version, and every version above the newest one a tag records (for a missing release, above the newest one a release records), so a version whose tag push failed is still tagged after a later run has published past it. A gap at or below those is history, left as it is.
  4. It writes the images' version tags, as the version tags of the images says.

What is published is what CI tested, byte for byte. The release commits nothing to master and opens no pull request.

A maintainer can also start the workflow by hand from the Actions tab. On master it runs the same jobs and publishes what is owed; on another branch it runs CI with the plan as a dry run and publishes nothing.

The released state​

master's version fields, dependency ranges and CHANGELOG.md files are not the released state: they hold what a maintainer last wrote, and each run's checkout carries the published versions. The released state is read from:

  • the tags <name>@<version>, each at the commit its version was published from;
  • the GitHub releases, one per tag, which are the release notes;
  • npm's version documents, whose gitHead and provenance attestation name the same commit;
  • the catalog registry.json the root package ships.

A version raised in a package.json to catch up with npm is published as that number, so leave the versions as they are.

No tarball ships a CHANGELOG.md: every package's files list leaves it out, and npm adds only package.json, the README, the LICENSE and the files of main and bin, whatever the list says (npm's files field). Each CHANGELOG.md stops at the last version it recorded, under a line pointing at the GitHub releases.

The peer floor @framers/agentos-ext-account-sources publishes on @framers/agentos is the core release CI tested it against: scripts/resolve-core-release.mjs writes that floor into CI's checkout before the install (see Configuration), and the release publishes the tarball CI packed.

A pull request's dry run​

On a pull request, CI's step "The packages this push would publish" plans the merge as a dry run: it reads the pull request's title and description as the message its squash merge will carry, writes the plan into the checkout as on master, and its job summary lists what the merge would publish, each package with its version, its bump and the reason. The build, the tests and the pack guard run on that checkout. Nothing is uploaded and nothing publishes. Read the summary before merging.

The summary is made when CI runs, and CI runs when the pull request is opened, reopened or pushed to, never when its title or description is edited (the pull_request event's default activity types, events that trigger workflows). A ! added to the title after the last push changes what the merge does while the old summary still shows a patch: it publishes a minor of a package below 1.0.0, and refuses a package at 1.0.0 or above. The required Pull request title check plans the merge again on every edit and fails on that refusal; the summary stays CI's. After editing the title or the description, run CI again and read the new summary before merging. Push a commit to the pull request, or close and reopen it, which starts a new run on the title and description as they are then; a re-run of an earlier run runs on the original event that started it (re-running workflows and jobs).

The pull request's own change is what its merge commit changed since its merge base with master's head, so the message, and a breaking mark in its title, lands on the packages the pull request touched alone (a package it changed only in package.json's devDependencies is not touched, as on master), also once master has moved on from the commit GitHub made the merge commit on. When master has released a package after the pull request's base, the package's tag names a commit the merge commit's history does not hold: the dry run reads that package from the merge base of the two commits, with a warning naming both, since the merge lands on top of that release. A run on another branch, and a re-run of an old pull request's run, read the same way. master's own run of such a commit fails instead, which is where a tag moved by hand is caught.

The catalog and the root package​

registry.json is a build output: pnpm run build writes it from the packages on disk, and git does not track it. The published catalog ships in the root package @framers/agentos-extensions, and https://unpkg.com/@framers/agentos-extensions/registry.json serves the newest one. A registry.json built from a checkout lists master's versions, which are not the published ones.

Every run decides the root after the packs. plan downloads the registry.json of the root's newest published tarball; apply places it at the checkout's registry.json, regenerates the catalog with scripts/update-registry.js --root from the versions it wrote (the published copy's timestamps kept for each entry that did not change), and plans the root when the regenerated catalog differs from the published one, or when one of the root's own shipped files changed since its newest version's commit: the files of its files list (index.mjs, types.d.ts, README.md), its package.json, and the files npm packs whatever that list says, a top-level README, LICENSE, LICENCE or COPYING in any case and with any extension (npm's files field). A catalog change is a patch, a breaking mark in the root's own commits is read as on a package (a minor below 1.0.0, a refusal at 1.0.0 or above), and a hand-raised number publishes as written. The root's GitHub release lists, after the subjects of its own commits, each catalog entry whose version moved from the published copy, - <name> <from> to <to>. The root publishes last, so a run that publishes packs and fails before the root is repaired by the next run's plan, which finds the catalog still differing, and whose root release lists the versions that moved since the published copy, those of the earlier run included.

Foreign names and private packages​

Two directories hold copies of packages npm receives from another repository: registry/curated/safety/topicality (@framers/agentos-ext-topicality) and registry/curated/safety/ml-classifiers (@framers/agentos-ext-ml-classifiers), whose newest versions on npm name manicinc/voice-chat-assistant as their repository. A package whose npm latest version has no tag here and whose version document names another repository is foreign: the plan never publishes it, writes npm's version into the checkout, so registry.json lists the version npm serves, and its summary says foreign: <name> is published from <repository>; its copy here is not planned.

A package marked private: true never publishes: registry/curated/memory/postgres-memory, registry/curated/memory/relationship-memory, registry/curated/productivity/email-intelligence, templates/guardrail and templates/multi-tool.

The pack guard​

CI runs scripts/pack-guard.mjs on the packages the plan names, node scripts/pack-guard.mjs --only <planned directories> --tarballs "$RUNNER_TEMP/release-tarballs", and when the plan names none it checks nothing and says so. The tarballs it leaves in that directory are the ones the release job publishes. It checks that each tarball contains the package's entry point, the file of every other entry its exports map gives import and, for a pattern such as ./features/*, at least one file it serves (one its target matches that the subpath spelling its name loads through it), installs it into an empty project, and imports there the package and each entry the role map lists for it (entryExports). A code entry the role map neither lists nor exempts (entryExemptions), a pattern serving code that it neither exempts nor serves by a listed subpath, a key Node resolves no subpath through, a key whose target Node refuses for import or reads as an address rather than as the path it spells, such as ./worker.js?raw (the root's in any form exports gives it), and an exports map that mixes subpath and condition keys fail the guard. A red guard fails CI, so nothing publishes. The contributing guide describes what it checks.

Releasing a new package​

A new pack publishes with the merge that adds it, at its package.json version, since npm holds no version of it. It needs:

  • a name under the @framers scope, "publishConfig": { "access": "public" } and a repository.url naming this repository (https://github.com/framerslab/agentos-extensions.git), in its package.json
  • the role pack in scripts/package-roles.json, and each code entry its exports map has besides the root listed under entryExports or exempted under entryExemptions there (a pattern exempted by its key, or a subpath it serves listed)
  • once its first version is on npm, a trusted publisher, as trusted publishing and the npm token says
  • a CHANGELOG.md of its own, which holds its title and one line pointing at the GitHub releases, where each of its versions is recorded

A pack copied from templates/basic-tool replaces the copy's CHANGELOG.md, which names the template, its versions and its tag, with this file, its name filled in (the release scripts' tests fail a file whose line names another package's tag):

# @framers/agentos-ext-<name>

Releases are recorded at https://github.com/framerslab/agentos-extensions/releases, one per tag `@framers/agentos-ext-<name>@<version>`.

The workspace globs in pnpm-workspace.yaml cover registry/curated/* and registry/curated/*/*, so the pack needs no workspace entry.

Trusted publishing and the npm token​

npm's trusted publishers for these packages name this repository and the workflow file release.yml, so the file keeps that name: npm compares the workflow file's name and the package's repository.url with the run that publishes (npm's trusted publishing). The release job holds id-token: write, and npm authenticates a publish through OIDC first and falls back to a token (the same page): the NODE_AUTH_TOKEN of the publish step, the NPM_TOKEN secret, is that fallback, for a package with no trusted publisher.

npm configures a trusted publisher only for a package it already holds (npm trust), so a new package's first version publishes through the token. Then an owner of the @framers packages configures its publisher, with npm 11.15.0 or later and the account's second factor:

npm trust github <name> --file release.yml --repo framerslab/agentos-extensions --allow-publish -y

A new configuration expires unless a publish goes through it within two days (npm's trusted publishing page). When the package has no change pending, a one-line change to its README, which ships, makes the next merge publish through it.

The publish step says when a version came through the token. When the step ends it reads each version it published on npm: npm sets _npmUser.trustedPublisher on a version's document for a publish through a trusted publisher and leaves it off for a publish through a token. Since npm falls back to the token without a word, this is the only sign a run gives.

  • A warning, "published through the npm token", on a package with an earlier version: the package's trusted publisher is missing or lapsed, and each later version goes through the token until it is fixed. Run the command the warning prints, with the second factor, then make a publish go through it within two days. npm's page says an expired configuration can no longer be used or edited, and the registry holds one configuration per package, so the command answers an error while a lapsed one is there: read its id with npm trust list <name> and delete it with npm trust revoke --id <id> <name> first (npm trust). A warning that names an earlier attempt of the run is about a version that attempt published: the publisher may have been set since, and then there is nothing to do.
  • A notice on a package's first version: that is the path above working as designed, and the command still has to be run.
  • A warning, "could not read the publisher": the read failed, npm does not serve the version's document yet after fifteen minutes, or the document names no publisher. It says nothing either way about the package; read _npmUser on the version's document by hand, or wait for the package's next release.

The step summary lists the same lines and published through the token: <n> of <m>. None of them fails the run, since the package is on npm by then.

The token stays until first versions have another way. GitHub's changelog of 31 July 2026 says bypass-2FA granular tokens "will also lose direct publish", reducing them to reading private packages and staging a publish a maintainer approves with 2FA, with January 2027 as its target (restricting bypass-2FA tokens). After that, a new package's first version cannot publish the way the path above says, and this workflow's answer is open: its publish step runs npm publish, not npm stage publish. npm's route for it is that a token creates the package with npm stage publish, npm serves a placeholder version 0.0.0-stage meanwhile, the first version waits in the staged queue until a maintainer approves it with 2FA, and trusted publishing is configured once the package exists (staged publishing for new packages, staged publishing).

The browser meeting bot's image​

@framers/agentos-ext-meeting-browser ships a container image beside its npm package. A push to master that changes registry/curated/meetings/browser/ runs meeting-bot-image.yml, which builds the image, runs its lifecycle test and pushes it to GHCR as ghcr.io/framerslab/agentos-meeting-bot:sha-<commit>, then prints the pushed digest. The build and the test run with a read-only token; the push is a job of its own, the only one holding packages: write, and pushes the image the test ran against. The workflow reads no version: the released version's tag is the release workflow's, as the version tags of the images says.

The document parser's image​

@framers/agentos-ext-document-parser ships a container image beside its npm package. document-parser-image.yml builds it from the pack's Dockerfile and runs its lifecycle test (image/lifecycle.sh) on a pull request that touches registry/curated/tools/document-parser/, pnpm-lock.yaml, from which the image takes every package it installs, or the root package.json, whose packageManager names the pnpm release that installs them, once it is ready for review, and on a push to master that touches the pack. On master a second job, the only one holding packages: write, pushes the image the test ran against to GHCR as ghcr.io/framerslab/agentos-document-parser:sha-<commit>, and prints the pushed digest in its summary; a manual run on another branch builds and tests and pushes nothing. The workflow reads no version: the released version's tag is the release workflow's, as the version tags of the images says.

The version tags of the images​

A released version's image is tagged on GHCR by the release workflow's images step, node scripts/release-plan.mjs images, on every release run, and by nothing else. For each pack named in the images key of scripts/package-roles.json, which gives the pack's folder, its image and its image workflow, the step reads every release tag <name>@<version> of the pack made since the role map named its image, that is every tag whose commit's scripts/package-roles.json lists the pack's folder under images (the tags made before pull request 423 merged are left to the image workflow that wrote their version tags then), and reads the image's tags on GHCR once, as one list, with skopeo list-tags. For each of those versions whose <image>:<version> GHCR lacks, oldest first, it takes the image commit: the newest commit on master's first-parent line, at or before the tag's commit, that changed the pack's folder, whose push ran the image workflow and pushed :sha-<its first seven characters>. It reads the image workflow's runs on the image commit: a successful one, and it copies that commit's :sha- manifest to :<version> with docker buildx imagetools create, with no rebuild, and reads both digests back as equal; a queued or running one, and it waits for it, every 30 seconds, within one wait of up to 45 minutes for the whole step, so a second run still going when the time is up is read once more and not waited for again; a red one, none, or one still going after the wait, and the step fails naming the version and the run, while the npm publish stands and every later release run reconciles that version again until a re-run of the image workflow on that commit is green. An older version's missing tag is never lost to a newer release: the step goes on naming it after the newer version's tag is written. The pack's folder at the release equals its folder at the image commit, since no later first-parent commit changed it, so the version's image is the one that workflow built and tested. A version's tag already on GHCR is never moved, so a host that pins a release keeps the image that release built.

Re-run only image runs made after pull request 423 merged, which gave the version tags to the release workflow. A re-run repeats its run on the same commit, and a run stays re-runnable for 30 days (re-running workflows and jobs); a meeting bot image run made before that merge pushed :<version> from the pack's package.json with no check for a tag already on GHCR, so a re-run of one can move a released version's tag onto the image it rebuilds. When the image commit's run cannot be re-run, because it has none (a [skip ci] in its message skips the push workflows, skipping workflow runs) or its red run is older than 30 days, build the image from a checkout of the image commit as the image workflow builds it and push it as <image>:<version> by hand: the step then finds the version's tag on GHCR and leaves it as it is.

The release script in voice-chat-assistant​

manicinc/voice-chat-assistant releases its packages with a copy of scripts/release-plan.mjs in its own scripts/. Copy a change made here into that file the same day, byte for byte, so the two repositories release by one script.

A private repository's copy needs GH_TOKEN (${{ github.token }}) on its publish step, which this repository's publish step does not set. Before each package the step reads master's head with git ls-remote, through the repository's address with that token when GH_TOKEN and GITHUB_REPOSITORY are set, and through origin otherwise; the release job's checkout keeps no credential (persist-credentials: false), so origin answers that read here only because this repository is public.

Configuration​

  • scripts/package-roles.json lists every workspace package with its role, the package list the release plan and the pack guard read, and the image packs under images.
  • pnpm-workspace.yaml lists the workspace packages and overrides @framers/agentos to one range for every pack.
  • CI sets that override before its install with scripts/resolve-core-release.mjs, to the newest release tag (v<x.y.z>, no prerelease part) reachable from the head of the core checkout in packages/agentos, which ci.yml checks out with its whole history and its tags, and sets account-sources' peer floor on the core to the same release. The core is released by semantic-release, which pushes the tag and publishes and commits nothing back to its master, so the version in the core's package.json is not its release. The resolver reads that version only when the checkout has no release tag in its history, and says so in a notice; while npm does not serve the tag's version yet, it pins the newest published release that the account-sources peer floor admits, with a notice.

Secrets​

The release job publishes through npm's trusted publishing, with the id-token: write permission, and keeps the NPM_TOKEN repository secret (an npm granular access token with read and write access to the @framers packages) as NODE_AUTH_TOKEN on its publish step alone, npm's fallback for a package with no trusted publisher. The tags, the GitHub releases and the images' version tags are written with the GITHUB_TOKEN that GitHub Actions provides. The meeting bot's and the document parser's images are pushed with the GITHUB_TOKEN and the packages: write permission: each :sha-<commit> by the push job of its image's workflow, the only job in that workflow that holds it, and each :<version> by the release workflow's images step.

Never​

  • Publish from a workstation with npm publish or pnpm publish. It skips CI, the pack guard and provenance.
  • Rename release.yml, or publish from another workflow: npm's trusted publishers name the file.
  • Add a workflow step that pushes a commit to master. Its rules refuse the push (GH006), so the step fails after everything before it has run. A step that changes the repository pushes a branch of its own and opens a pull request. The build job runs .github/scripts/check-workflow-pushes.mjs, which fails the pull request that adds such a push: a git push to master (named, or sent there by the remote.<name>.push setting of the remote it pushes to, else by push.default; a push that names no remote, as an operand or with --repo, may reach origin or any remote with refspecs, and one held in a variable any remote), or to the branch the run is on from a workflow that runs on master (a merged pull request's closed run is on the branch it went into), or one of the push actions it knows (peaceiris/actions-gh-pages, JamesIves/github-pages-deploy-action, ad-m/github-push-action, stefanzweifel/git-auto-commit-action, EndBug/add-and-commit) pushing there (a deploy action whose repository input names another repository is left to that repository's rules). It reads a command where the shell runs it, in a substitution, an sh -c string, an eval or text piped or fed to a shell; follows the checked-out branch of each checkout (the root's, and one actions/checkout makes at a path) for each git command in the checkout its directory is in: the step's working-directory, then the script's cd and pushd (a subshell, a substitution, a command of a pipeline and a background job, the whole and-or list an & ends, change directory for themselves alone, and a function's body where the function is called; past a pipeline whose last command changes directory, which shopt -s lastpipe runs in the script's own shell, the directory is one it cannot tell), env -C (the last one an env is given; a script piped or fed to a shell runs where that shell runs, in the directory its env -C names, with its GIT_DIR, after the substitutions in an unquoted heredoc's body, which run where the heredoc's command runs, before it), git -C, --git-dir and GIT_DIR (set before the command, or in the env: of its step, job or workflow; env -i and env -u remove it, and through sudo without -E the checkout is one it cannot tell), or a push action's directory input; a command whose directory it cannot tell is read in every checkout. It follows the branch through &&, ||, if, case, loops and steps with an if: condition (any of which may not run) and into the local composite actions a job calls (uses: ./<path>, with the inputs its with: gives, else their defaults; an input in a step's if: is a string, so an input of false there still runs the step); reads git push options as git does (--dry-run --no-dry-run pushes, --mir is --mirror); and reads git config as git does: in the checkout it runs in, with --global and --system beneath every checkout's own, and --unset removing only the value its pattern matches in the key it names (remote.origin.push and remote.backup.push are two keys). A script a shell reads from a file, a variable or a download, a git setting made through GIT_CONFIG_* variables or a config file, a GIT_DIR a script sets, exports or unsets, a checkout git clone or git worktree add makes, and a push made by another action, are not read. It reads the YAML with mikefarah's yq v4, which GitHub's Ubuntu runners carry; run it elsewhere with yq on the path, or YQ naming it: node .github/scripts/check-workflow-pushes.mjs. The release job's one push is its tags, which name no branch.
  • Commit registry.json. It is a build output, and .gitignore names it.

Limits​

  • A range written after workspace: (workspace:^1.2.0) is packed as it is written, so no release raises it. When a sibling is released past it, the plan names it in a warning and publishes the dependent as a patch, as it does for every range that does not admit its sibling; the patch carries the same range, so each later run of master publishes the dependent again, until a pull request names the sibling at workspace:^ or at a range that admits its version. Every workspace: range in this repository is workspace:^.

Troubleshooting​

  • Nothing was published: the summary of CI's step "The packages this push would publish" gives each package's reason: nothing it ships changed since its newest version, or master had moved past the commit, whose newer run publishes its changes.
  • The pack guard fails: the named tarball lacks its entry point or the file of an exported entry, the pack or a listed entry does not import, the pack does not construct, or the role map neither lists nor exempts a code entry the pack exports. Fix the pack or its role map entries; nothing publishes while CI is red.
  • The plan fails: nothing is written, and the message names the cause.
    • master's tip <sha> does not hold this commit <sha>: only master publishes: the run is of a commit master no longer holds, and nothing publishes.
    • <name>@<version> names <commit> ..., which this commit's history does not hold, in a run of master (a dry run reads that package from the merge base and warns, as a pull request's dry run says): a tag moved by hand, or a version published from another history. Point the tag back at the commit the version was published from, git tag -f -a <tag> -m <tag> <commit> && git push --force origin refs/tags/<tag>, and re-run the workflow; or raise the package's package.json version above <version> to publish from this history.
    • npm holds <name>@<version>, but no tag, gitHead or provenance attestation names a commit of this repository for it: a version published from outside this workflow. Tag the commit it was published from, as above.
    • npm serves no document for <name>@<version>, its newest version, after 1800 seconds of reading again: the version's tag is in the repository, and npm served no version document for it within the plan's thirty minutes of reading. Re-run the workflow once npm view <name>@<version> answers; a tag made by hand for a version npm never held is removed, git push origin :refs/tags/<tag>.
    • A registry or GitHub answer other than a success or npm's 404 for a version it does not hold: re-run the workflow.
  • The publish step fails: the packages already published stay published, and the journal lines in the step's log name them. The tags step still tags and releases those, then fails the run naming the packages left unpublished. The next push's run plans them again at the same versions. To publish them without waiting, re-run the run's failed jobs (Re-run failed jobs): the release job runs again on the first attempt's plan and tarballs, records each version that attempt published, which npm holds with the plan's commit as its gitHead and the tarball's integrity, as published, already on npm, and publishes the rest; its tags step leaves out of the packages left unpublished each one whose tag names that commit already. Or start the release workflow on master by hand (Actions, Release & Publish, Run workflow), a new run that plans again from npm and the tags. Re-run all jobs runs CI again as well, which uploads a second plan and a second set of tarballs under the same artifact names, and each download takes the artifact of its name with the highest id, which need not be the newer attempt's (actions/toolkit's getArtifact): when the plan and the tarballs come from different attempts, the publish step fails before it publishes anything, naming the tarballs it found and the plan's order.
  • npm's document for <name>@<version> did not answer within <n> seconds of its publish: npm served no document for a version a later package names before the run's 30 minutes of waiting were spent. The packages before it are published and tagged, and the tags step names the rest as left unpublished; re-run the failed jobs once npm view <name>@<version> answers, or let a later run publish them.
  • npm gained <version> of <name> during the run: a version appeared on npm after the plan was made that this run's tarballs did not make: one published from outside this workflow, or a copy of the planned version whose gitHead names another commit or whose integrity is not the tarball's. A version an earlier attempt of the run published never stops a re-run here: the step records it as published, as the publish step's item above says. Nothing is published twice. The run publishes nothing more, and the next run's plan counts what npm holds and plans the rest: the next push's run, or one started on master by hand.
  • The tag push fails: the publish stands, and the run fails naming the tags. The next run's plan finds each published version with no tag and makes its tag and release at the commit npm's gitHead names. To make one at once: git tag -f -a <tag> -m <tag> <commit> && git push --force origin refs/tags/<tag>, with <commit> the gitHead of that version on npm.
  • The images step fails: the npm publish stands, and the step names each version whose tag is missing, (<image>:<version>), with the image workflow's run on its image commit, or says there is none. Re-run that run once its cause is fixed, never a run made before pull request 423 merged, and the next release run writes the version's tag; a version whose run cannot be re-run is tagged by hand, as the version tags of the images says. A failure that names no version, could not read the tags of <image> on GHCR, is the step's read of the image's tag list (skopeo list-tags, with the login the step makes with the job's token), and the next release run reads the list again.
  • A foreign name in the summary: foreign: <name> is published from <repository>; its copy here is not planned is expected for @framers/agentos-ext-topicality and @framers/agentos-ext-ml-classifiers. Any other package named there is published from another repository, and its source is settled with that repository's owners before it publishes here.