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
3 changes: 2 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
* @seokju-na
* @seokju-na @marshallku

31 changes: 31 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ just typecheck
just docs
```

### Generated Files

`index.js` and `index.d.ts` are written by the build, but they are committed to the repository:
`index.js` is the package entry point, and the [reference documentation](https://es-git.slash.page)
is generated from `index.d.ts`.

If you change the public API on the Rust side, run `just build` and commit the result together with
your change. CI builds the project and fails when what you committed differs from a fresh build.

## Pull Requests

Please open a Pull Request to merge changes.
Expand All @@ -79,6 +88,28 @@ docs: fix link to website page
chore: upgrade vitest to v3
```

## Releasing

Maintainers release from `main`.

1. Run the [Prepare release](https://github.com/toss/es-git/actions/workflows/prepare-release.yml)
workflow and choose whether to raise the major, minor or patch version. It raises the version,
writes the changelog section from the commits since the last release, thanks the outside
contributors among them, rebuilds the binding and pushes a `release/vX.Y.Z` branch.
2. Read the pull request it opens, and merge it. The pull request is opened with a maintainer's
token rather than the workflow's own, because CI does not run on one the workflow opens itself.
If that token is missing or has expired the branch is still pushed, so open the pull request by
hand and nothing else changes.

Merging is what releases. A commit landing on `main` whose version has no tag yet is tagged, gets a
GitHub release built from its changelog section, and is published to npm. Every other push to `main`
publishes a prerelease under the `next` dist-tag instead, so there is nothing to do between releases.

A release is refused if the version is not newer than the one npm currently serves as `latest`,
which is what stops a release from moving the tag backwards. The binding is rebuilt during the
release and the freshly built one is what gets published, so what ships always matches the commit it
was built from.

## Documentation

This project aims to maintain high documentation quality.
Expand Down
16 changes: 14 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,14 @@ jobs:
- host: macos-latest
target: aarch64-apple-darwin
build: yarn build --target aarch64-apple-darwin
- host: windows-latest
# Pinned: windows-latest now means Windows Server 2025 with Visual Studio 2026, and the
# cmake crate cannot work out a generator for it, so libz-sys fails to build. Support
# exists on cmake-rs master but is not in a published version yet. Move back to
# windows-latest once it is released.
- host: windows-2022
target: x86_64-pc-windows-msvc
build: yarn build --target x86_64-pc-windows-msvc
- host: windows-latest
- host: windows-2022
target: aarch64-pc-windows-msvc
build: yarn build --target aarch64-pc-windows-msvc
- host: ubuntu-22.04
Expand Down Expand Up @@ -146,6 +150,14 @@ jobs:
- name: Build
run: ${{ matrix.settings.build }}
shell: bash
- name: Check generated bindings are up to date
if: ${{ matrix.settings.target == 'aarch64-apple-darwin' }}
run: |
if ! git diff --exit-code -- index.js index.d.ts; then
echo "::error::index.js / index.d.ts are out of date. Run \`yarn build\` and commit the result."
exit 1
fi
shell: bash
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
Expand Down
205 changes: 205 additions & 0 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
name: Prepare release
on:
workflow_dispatch:
inputs:
bump:
description: Which part of the version to raise
required: true
type: choice
options:
- patch
- minor
- major
env:
DEBUG: napi:*
concurrency:
group: prepare-release
jobs:
prepare:
# Releases are cut from main. A clean checkout of it is also why this needs none of the
# "is the working tree dirty / is main behind origin" checks a local script would.
if: github.ref_name == 'main'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# git-cliff reads the history and the tags to work out what is unreleased.
fetch-depth: 0
- name: Setup node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: ".node-version"
check-latest: true
cache: yarn
- name: Install rust
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
with:
toolchain: "1.91.0"
- name: Cache cargo
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
.cargo-cache
target/
key: prepare-release-cargo
- name: Install dependencies
run: yarn install --immutable
- name: Raise the version
id: version
env:
BUMP: ${{ inputs.bump }}
run: |
set -eu
# npm version resolves a prerelease down to its release rather than raising it, so
# 0.8.0-next.5 would quietly become 0.8.0. main should never carry one.
current=$(node -p "require('./package.json').version")
case "$current" in
*-*)
echo "::error::package.json is at the prerelease version $current. Releases are cut from plain versions."
exit 1
;;
esac
tag=$(npm version --no-git-tag-version "$BUMP")
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "branch=release/$tag" >> "$GITHUB_OUTPUT"
- name: Refuse a release that already exists
env:
TAG: ${{ steps.version.outputs.tag }}
BRANCH: ${{ steps.version.outputs.branch }}
run: |
set -eu
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "::error::Tag $TAG already exists."
exit 1
fi
if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
echo "::error::Branch $BRANCH already exists."
exit 1
fi
- name: Write the changelog
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
set -eu
# git-cliff prepends a section whether or not anything landed, so ask it what it would
# write before letting it write.
if ! yarn dlx git-cliff@2.13.1 --unreleased --tag "$TAG" | grep -q '^- '; then
echo "::error::Nothing to release: no commits since the last tag."
exit 1
fi
yarn dlx git-cliff@2.13.1 --unreleased --tag "$TAG" --prepend CHANGELOG.md
- name: Thank the outside contributors
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
previous=$(git describe --tags --abbrev=0 HEAD 2>/dev/null || true)
range=${previous:+$previous..}HEAD
# author_association is the field behind the role badge GitHub shows on a pull request.
# The line thanks people from outside the org -- @racgoo appears in two releases running,
# so it is not a first-timers list. Name the insiders and keep everyone else, because the
# outsider side of the enum has more values than CONTRIBUTOR: someone's first pull request
# is FIRST_TIME_CONTRIBUTOR, and dropping those would miss exactly who this line is for.
# Bots open pull requests too, and dependabot's association is CONTRIBUTOR.
# A failed lookup exits rather than quietly thanking a shorter list.
handles=$(
for sha in $(git log --no-merges --format=%H "$range"); do
gh api "repos/$GITHUB_REPOSITORY/commits/$sha/pulls" --jq '
.[]?
| select(.user.login != null and .user.type != "Bot")
| select(.author_association != "OWNER" and .author_association != "MEMBER" and .author_association != "COLLABORATOR")
| .user.login
' || exit 1
done | sort -u
)
if [ -z "$handles" ]; then
echo "No outside contributors in $range."
exit 0
fi
list=$(echo "$handles" | awk '{ printf "%s@%s", sep, $0; sep = ", " }')
line="We sincerely thank $list for their contributions. We appreciate your great efforts!"
echo "$line"
# The section just written is the first one, so it ends where the previous release starts.
awk -v line="$line" '
/^## Version / { seen++ }
seen == 2 && !done { print line; print ""; done = 1 }
{ print }
END { if (!done) { print ""; print line } }
' CHANGELOG.md > CHANGELOG.next
mv CHANGELOG.next CHANGELOG.md
# The generated binding is committed, and the release job publishes what a build produces.
# Build here so the release branch carries a binding that matches the crate at this commit.
- name: Build
run: yarn build
- name: Push the release branch
env:
TAG: ${{ steps.version.outputs.tag }}
BRANCH: ${{ steps.version.outputs.branch }}
run: |
set -eu
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
git add CHANGELOG.md package.json index.js index.d.ts
git commit -m "$TAG"
leftover=$(git status --porcelain)
if [ -n "$leftover" ]; then
echo "::error::The build touched files the release commit did not stage:"
echo "$leftover"
exit 1
fi
# The build takes minutes, and the changelog was rendered from the commit this run checked
# out. If main moved meanwhile, the eventual tag would cover commits no section mentions.
git fetch origin main
if [ "$(git rev-parse FETCH_HEAD)" != "$GITHUB_SHA" ]; then
echo "::error::main moved while this ran, so the changelog would miss what landed. Re-run."
exit 1
fi
# Create-only: an empty expected value makes git refuse if the branch appeared since the
# guard above, even when it could be fast-forwarded.
if ! git push --force-with-lease="refs/heads/$BRANCH:" origin "$BRANCH"; then
echo "::error::Could not create $BRANCH. It may have appeared while this ran."
exit 1
fi
# Opened with a personal token rather than GITHUB_TOKEN, because CI does not run on a pull
# request GITHUB_TOKEN opened. If the secret is missing or has expired this step is skipped and
# the branch is still there to open by hand, which is what the summary below falls back to.
- name: Open the pull request
id: pr
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.RELEASE_PR_TOKEN }}
TAG: ${{ steps.version.outputs.tag }}
BRANCH: ${{ steps.version.outputs.branch }}
run: |
set -eu
if [ -z "${GH_TOKEN:-}" ]; then
# Not configured is not a failure. Only a token that is set and does not work is.
echo "No RELEASE_PR_TOKEN. Leaving the pull request to be opened by hand."
exit 0
fi
body="Prepared by the Prepare release workflow. Merging this releases $TAG: the tag, the GitHub release and the npm publish all follow from it. Read the changelog section it carries first."
url=$(gh pr create --base main --head "$BRANCH" --title "$TAG" --body "$body")
echo "url=$url" >> "$GITHUB_OUTPUT"
- name: Summarise
env:
TAG: ${{ steps.version.outputs.tag }}
BRANCH: ${{ steps.version.outputs.branch }}
PR_URL: ${{ steps.pr.outputs.url }}
run: |
{
echo "### $TAG is ready on \`$BRANCH\`"
echo
if [ -n "${PR_URL:-}" ]; then
echo "1. Read [the pull request]($PR_URL), and the changelog section it carries."
else
echo "1. [Open the pull request](${{ github.server_url }}/${{ github.repository }}/compare/main...$BRANCH?expand=1) yourself, which is what gets CI to run on it. Read the changelog while you are there."
fi
echo "2. Merge it. Landing a version no tag exists for is what releases it: the tag, the GitHub release and the npm publish all follow from the merge."
} >> "$GITHUB_STEP_SUMMARY"
Loading
Loading