Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .agents/skills/writing-changesets/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Do not add one for documentation-only or test-only changes, changes to the separ

## Package and version bump

The bundle is versioned as one unit. The Changeset frontmatter must target only `@hypercerts-org/happyview-modules`:
The bundle is versioned as one unit. The repository's other released package, the `@hypercerts-org/api` client, is not released yet and is in the Changesets `ignore` list (see `packages/api/README.md`), so until that changes the Changeset frontmatter must target only `@hypercerts-org/happyview-modules`:

```md
---
Expand Down
18 changes: 13 additions & 5 deletions .changeset/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
# API bundle changesets
# Changesets

Add a Changeset when a pull request changes the installable API bundle or its operator-visible behavior. Run:
This repository releases two packages, each with its own version:

- `@hypercerts-org/happyview-modules`: the installable API bundle (Lua modules, Lexicons and installer). Released as a Git tag and GitHub Release, not published to npm; operators retrieve the tagged source from this public repository and install it into their own HappyView instance.
- `@hypercerts-org/api`: the TypeScript query definitions and types for clients. Published to npm, with a Git tag and GitHub Release. Not released yet; see [packages/api/README.md](../packages/api/README.md#versioning-and-releases).

Add a Changeset when a pull request changes what either package ships. Run:

```sh
pnpm changeset
```

Target `@hypercerts-org/happyview-modules` and write release notes for endpoint or schema additions, compatible behavior changes, fixes, breaking changes, and installer or configuration changes that operators need to know about. Use `patch` for compatible fixes, `minor` for additive API capabilities, and `major` for incompatible API changes.
and select each package the pull request changes. Use `patch` for compatible fixes, `minor` for additive capabilities, and `major` for incompatible changes.

- For `@hypercerts-org/happyview-modules`, write release notes for endpoint or schema additions, compatible behavior changes, fixes, breaking changes, and installer or configuration changes that operators need to know about.
- For `@hypercerts-org/api`, write release notes for client-visible changes: new or changed queries, parameters and types, and the `@hypercerts-org/lexicon` version it builds on. An API change that touches both the modules and the generated client gets an entry for each package.

The API bundle is versioned as one unit. Do not add Changesets for the separate endpoint-explorer package, documentation-only changes, tests, or release/CI maintenance that does not change the installable bundle. If the release impact is unclear, decide with the reviewer rather than guessing a version bump.
Do not add Changesets for the separate endpoint-explorer package, documentation-only changes, tests, or release/CI maintenance that does not change what a package ships. If the release impact is unclear, decide with the reviewer rather than guessing a version bump.

A Changesets release PR updates `happyview-modules/package.json` and `happyview-modules/CHANGELOG.md`. After it is merged to `main`, release CI validates that exact commit and creates an `@hypercerts-org/happyview-modules@X.Y.Z` Git tag and public GitHub Release. The bundle is not published to npm; operators retrieve the tagged source from this public repository and install it into their own HappyView instance.
A Changesets release PR updates each bumped package's `package.json` and `CHANGELOG.md`. After it is merged to `main`, release CI validates that exact commit, publishes the bumped public packages to npm, and creates a `<package>@X.Y.Z` Git tag and public GitHub Release for each released version.
136 changes: 90 additions & 46 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,26 @@ jobs:
needs: select-mode
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
contents: read
steps:
# The Release Bot app, as in the lexicon repository: a Release PR opened
# with GITHUB_TOKEN would not trigger CI.
- name: Generate Release Bot app token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write

- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
token: ${{ steps.generate-token.outputs.token }}
fetch-depth: 0
persist-credentials: false
persist-credentials: true

- name: Set up pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
Expand All @@ -80,26 +91,39 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Version bundle and changelog
- name: Version packages and changelogs
uses: changesets/action/version@8488615a623b1b9c987934bb89eae8af6a946ac1 # v2.1.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ steps.generate-token.outputs.token }}
script: pnpm version:packages
commit-message: 'release: version API bundle'
pr-title: Release API bundle
commit-message: 'release: version packages'
pr-title: Release packages
pr-base-branch: main
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}

publish:
name: Validate and publish GitHub Release
name: Validate and publish releases
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.head.ref == 'changeset-release/main'
runs-on: ubuntu-latest
permissions:
contents: write
contents: read
# npm Trusted Publishing: npm exchanges the workflow's OIDC token for a
# short-lived publish token, so the repository holds no npm token.
id-token: write
steps:
- name: Generate Release Bot app token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_APP_PRIVATE_KEY }}
permission-contents: write

- name: Check out exact merged commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down Expand Up @@ -149,31 +173,37 @@ jobs:
node-version-file: .nvmrc
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
registry-url: https://registry.npmjs.org

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Check API bundle
- name: Check packages
run: pnpm check

- name: Build API bundle
- name: Build packages
run: pnpm build

- name: Check existing tag and GitHub Release
- name: Check existing tags and GitHub Releases
id: preflight
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_API_URL: ${{ github.api_url }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
package_name="$(node --print "require('./happyview-modules/package.json').name")"
version="$(node --print "require('./happyview-modules/package.json').version")"
if [ -z "$package_name" ] || [ -z "$version" ]; then
echo "::error::Could not derive the API bundle name and version from happyview-modules/package.json"
exit 1
fi
tag="${package_name}@${version}"
# Every released package, as name@version: the workspace packages
# that Changesets does not ignore.
tags="$(node --input-type=module --eval '
import { readFileSync } from "node:fs";
const read = (file) => JSON.parse(readFileSync(file, "utf8"));
const ignored = new Set(read(".changeset/config.json").ignore);
for (const manifest of ["happyview-modules/package.json", "packages/api/package.json"]) {
const { name, version } = read(manifest);
if (!name || !version) throw new Error(`No name or version in ${manifest}`);
if (!ignored.has(name)) console.log(`${name}@${version}`);
}
')"
api_url="${GITHUB_API_URL%/}"

get_status() {
Expand All @@ -184,41 +214,55 @@ jobs:
--header "Authorization: Bearer ${GH_TOKEN}" \
--header 'X-GitHub-Api-Version: 2022-11-28' \
"${api_url}/${endpoint}")"; then
echo "::error::Unable to query GitHub while checking ${tag}" >&2
echo "::error::Unable to query GitHub while checking ${endpoint}" >&2
exit 1
fi
printf '%s' "$status"
}

tag_status="$(get_status "repos/${GITHUB_REPOSITORY}/git/ref/tags/${tag}")"
release_status="$(get_status "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}")"
case "${tag_status}:${release_status}" in
200:200)
echo "::notice::${tag} already has both a remote tag and GitHub Release; nothing to publish."
echo 'publish=false' >> "$GITHUB_OUTPUT"
;;
404:404)
echo "No existing tag or GitHub Release found for ${tag}; publishing is required."
echo 'publish=true' >> "$GITHUB_OUTPUT"
;;
200:404)
echo "::error::Release state is inconsistent for ${tag}: the remote tag exists but the GitHub Release is missing. Reconcile the tag and Release manually, then rerun this workflow."
exit 1
;;
404:200)
echo "::error::Release state is inconsistent for ${tag}: the GitHub Release exists but the remote tag is missing. Reconcile the tag and Release manually, then rerun this workflow."
exit 1
;;
*)
echo "::error::Unexpected GitHub API response while checking ${tag}: tag=${tag_status}, release=${release_status}"
exit 1
;;
esac
# Publishing runs when at least one package version has no tag and
# GitHub Release yet; Changesets skips the versions already released.
publish=false
for tag in $tags; do
tag_status="$(get_status "repos/${GITHUB_REPOSITORY}/git/ref/tags/${tag}")"
release_status="$(get_status "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}")"
case "${tag_status}:${release_status}" in
200:200)
echo "${tag} already has both a remote tag and GitHub Release."
;;
404:404)
echo "No existing tag or GitHub Release found for ${tag}; publishing is required."
publish=true
;;
200:404)
echo "::error::Release state is inconsistent for ${tag}: the remote tag exists but the GitHub Release is missing. Reconcile the tag and Release manually, then rerun this workflow."
exit 1
;;
404:200)
echo "::error::Release state is inconsistent for ${tag}: the GitHub Release exists but the remote tag is missing. Reconcile the tag and Release manually, then rerun this workflow."
exit 1
;;
*)
echo "::error::Unexpected GitHub API response while checking ${tag}: tag=${tag_status}, release=${release_status}"
exit 1
;;
esac
done
if [ "$publish" = false ]; then
echo "::notice::Every package version already has a tag and GitHub Release; nothing to publish."
fi
echo "publish=${publish}" >> "$GITHUB_OUTPUT"

- name: Create tag and GitHub Release
# Changesets publishes public packages to npm (through `pnpm publish`,
# which hands the tarball to the npm CLI bundled with Node 24, new enough
# for Trusted Publishing), tags private packages, and creates a tag and
# GitHub Release for each released version.
- name: Publish to npm, create tags and GitHub Releases
if: steps.preflight.outputs.publish == 'true'
uses: changesets/action/publish@8488615a623b1b9c987934bb89eae8af6a946ac1 # v2.1.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ steps.generate-token.outputs.token }}
create-github-releases: true
push-git-tags: true
env:
NPM_CONFIG_PROVENANCE: true
11 changes: 8 additions & 3 deletions packages/api/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @hypercerts-org/api

TypeScript query definitions and types for the Hypercerts API, for use with [`@atproto/lex`](https://www.npmjs.com/package/@atproto/lex). Not published to npm yet; use it from this workspace, or from a tarball made with `pnpm pack` (which builds first). A git dependency does not work: `dist/` is not committed.
TypeScript query definitions and types for the Hypercerts API, for use with [`@atproto/lex`](https://www.npmjs.com/package/@atproto/lex). Not published to npm yet (see [Versioning and releases](#versioning-and-releases)); use it from this workspace, or from a tarball made with `pnpm pack` (which builds first). A git dependency does not work: `dist/` is not committed.

```ts
import { Client } from "@atproto/lex";
Expand Down Expand Up @@ -61,9 +61,14 @@ Run from `packages/api/`:

Tooling follows the SDK repository (TECH-748): Biome, TypeScript 7, ESM, Node 24 or later. `exactOptionalPropertyTypes` is off because code generated by `@atproto/lex` 0.4.2 does not compile with it.

## Versioning
## Versioning and releases

The package is not versioned or released yet: it is listed in the Changesets `ignore` list, because the release workflow only tags the API module bundle (TECH-818). Releases start when the workflow supports more than one package.
The package is versioned with Changesets, independently of the API modules. The release workflow publishes it to npm with [Trusted Publishing](https://docs.npmjs.com/trusted-publishers) and provenance, and creates an `@hypercerts-org/api@X.Y.Z` tag and GitHub Release, when a Release PR that bumps it is merged.

It is not released yet: it is `private` and listed in the Changesets `ignore` list until it builds against a published lexicon package. To start releasing it:

1. Configure a Trusted Publisher for `@hypercerts-org/api` on npmjs.com: repository `hypercerts-org/api`, workflow `release.yml`. If npm requires the package to exist first, an owner of the `@hypercerts-org` npm scope publishes the first version by hand.
2. In one pull request: remove `"private": true`, remove the package from `ignore` in `.changeset/config.json`, and add a changeset for it.

## License

Expand Down
Loading