Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 47 additions & 16 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
name: Publish

# The human gate.
# The step that reaches somebody.
#
# Everything else in this repository is allowed to merge itself: a Dependabot
# bump or an automated compatibility fix that goes green on `main` needs no
# review, because `main` reaches nobody. This workflow is the step that does
# reach somebody, so it is the one a person has to perform — by drafting a
# GitHub release and pressing publish.
# bump or an automated compatibility fix that goes green needs no review,
# because `main` reaches nobody. Publishing does, so it happens only from a
# tag a person decided to createeither by merging the release pull request
# (see release.yml) or by drafting a release by hand.
#
# Deliberately NOT `on: push: branches: [main]`. That trigger would collapse
# the merge gate and the release gate into one, and the whole safety of
# auto-merge rests on them being separate.
on:
release:
types: [published]
workflow_call:
inputs:
tag:
description: "Tag to publish. Required when called; taken from the release otherwise."
required: true
type: string

permissions: {}

Expand All @@ -28,22 +34,49 @@ jobs:
# this workflow from this commit, which is checkable by anyone.
id-token: write
steps:
- name: resolve the tag
id: tag
env:
CALLED: ${{ inputs.tag }}
FROM_RELEASE: ${{ github.event.release.tag_name }}
MARKED_PRERELEASE: ${{ github.event.release.prerelease }}
run: |
set -euo pipefail
TAG="${CALLED:-$FROM_RELEASE}"
if [ -z "$TAG" ]; then
echo "::error::no tag to publish"; exit 1
fi
echo "name=$TAG" >> "$GITHUB_OUTPUT"

# Prerelease if the semver says so or the release was marked as one.
# The union rather than either alone: a hyphenated version published
# as `latest` would be handed to everyone running a plain install,
# and a release somebody deliberately marked prerelease should be
# honoured whatever its tag looks like.
case "$TAG" in
*-*) PRE=true ;;
*) PRE="${MARKED_PRERELEASE:-false}" ;;
esac
[ "$PRE" = "true" ] || PRE=false
echo "prerelease=$PRE" >> "$GITHUB_OUTPUT"
echo "publishing $TAG (prerelease=$PRE)"

- uses: actions/checkout@v6
with:
ref: ${{ github.event.release.tag_name }}
ref: ${{ steps.tag.outputs.name }}
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
registry-url: https://registry.npmjs.org

# A release tag that disagrees with package.json publishes a version
# nobody asked for, under a git ref that does not contain it. Cheaper to
# refuse here than to deprecate afterwards.
# A tag that disagrees with package.json publishes a version nobody
# asked for, under a git ref that does not contain it. Cheaper to refuse
# here than to deprecate afterwards.
- name: check the tag against package.json
env:
TAG: ${{ github.event.release.tag_name }}
TAG: ${{ steps.tag.outputs.name }}
run: |
set -euo pipefail
PKG=$(node -p "require('./package.json').version")
Expand All @@ -53,20 +86,18 @@ jobs:
fi
echo "publishing $PKG from $TAG"

# The same checks `main` had to pass, run once more against the tagged
# tree. A release can be cut from any ref, so being green on `main` is
# not by itself evidence about what is in the tarball.
# The same checks main had to pass, run once more against the tagged
# tree. A release can be cut from any ref, so being green on main is not
# by itself evidence about what is in the tarball.
- run: npm ci
- run: npm run build
- run: npx tsc --noEmit -p tsconfig.test.json
- run: npm test

# A prerelease goes out under the `next` tag, so `npm i @bitrouter/opencode`
# keeps resolving to the last stable one.
- name: publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
PRERELEASE: ${{ github.event.release.prerelease }}
PRERELEASE: ${{ steps.tag.outputs.prerelease }}
run: |
set -euo pipefail
if [ "$PRERELEASE" = "true" ]; then
Expand Down
61 changes: 61 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Release

# Turns merged work into a released version, in two steps with a person
# between them.
#
# release-please keeps one pull request open against `main`, holding the next
# version and the changelog entries it derived from the commit subjects since
# the last release. It rewrites that pull request as more lands. Nothing is
# published while it sits there.
#
# Merging it is the decision to ship — the one step this whole maintenance
# system asks a person to perform. That merge creates the tag and the GitHub
# release, and this workflow then publishes to npm from the tag.
#
# The release pull request is deliberately NOT in plugin-automerge's list of
# automation branches. It is the gate; a gate that opens itself is furniture.
on:
push:
branches: [main]
schedule:
# A merge performed by GitHub's auto-merge is attributed to
# `github-actions[bot]`, and a push from that token does not trigger
# workflows — so a Dependabot bump that merges itself never reaches the
# `push` trigger above. Without this, the release pull request would
# silently stop reflecting anything that merged on its own.
- cron: "0 6 * * *"
workflow_dispatch:

permissions: {}

jobs:
prepare:
name: keep the release pull request current
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
outputs:
released: ${{ steps.release.outputs.release_created }}
tag: ${{ steps.release.outputs.tag_name }}
steps:
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
id: release
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json

# Called rather than left to `publish.yml`'s own `release: published`
# trigger: release-please creates that release with GITHUB_TOKEN, and a
# release created by that token does not trigger workflows. The publish has
# to be invoked, not waited for.
publish:
needs: prepare
if: needs.prepare.outputs.released == 'true'
uses: ./.github/workflows/publish.yml
permissions:
contents: read
id-token: write
secrets: inherit
with:
tag: ${{ needs.prepare.outputs.tag }}
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.1.0"
}
15 changes: 15 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"$comment": "Release PRs are the human gate. Everything else in this repository may merge itself; merging one of these is the decision to ship, and it is the only one a person makes.",
"packages": {
".": {
"release-type": "node",
"changelog-path": "CHANGELOG.md",
"include-v-in-tag": true,
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": false,
"draft": false,
"prerelease": false
}
}
}
Loading