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
36 changes: 36 additions & 0 deletions .changeset/cicd-changesets-bun-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
"cicd-changesets": minor
---

Let a bun repo run its own releases.

The action installed Node and pnpm unconditionally and ran `pnpm install`, so a
repo whose dependencies are managed by bun could not use it: there was no pnpm
lockfile to install from, and `node-version-file` had no `engines.node` to read
in a package.json that pins `engines.bun` instead. It failed at the Node setup
step, before any changesets work.

A new `package-manager` input selects the toolchain. It defaults to `pnpm`, so
every existing caller is unaffected. Set it to `bun` and Node and pnpm are not
installed at all: bun is set up and `bun install --frozen-lockfile` runs
instead.

The bun version is read from the repo's own pin, so the workflow does not have
to restate it (and let it drift). `bun-version` still wins when set, but it
defaults to empty, and the action then resolves the version from
`bun-version-file` or auto-detection. setup-bun already reads `.tool-versions`,
`.bun-version`, and `package.json` natively, so those are forwarded to it
unchanged; the action reads `mise.toml`, `.mise.toml`, and
`.config/mise/config.toml` itself (setup-bun does not understand mise) and
passes the version to setup-bun. Auto-detection tries a mise file first, then
forwards `.tool-versions` or `.bun-version` if present, and setup-bun finally
reads `package.json` or installs `latest`.

`changesets-publish-cmd` and `changesets-version-cmd` now default to
`<package-manager> run ci:changeset:{publish,version}` rather than hard-coding
`pnpm`, so a bun caller that overrides neither does not shell out to a pnpm that
was never installed. A pnpm caller resolves to the same two commands as before.

The `signed-commits` action needed no change. It runs whichever commands it is
given, and bun installs a normal `node_modules` tree, so it resolves
`@changesets/cli/bin.js` exactly as it does under pnpm.
5 changes: 5 additions & 0 deletions .changeset/flat-worms-escape.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"cicd-changesets": minor
---

Support bun for package management
45 changes: 45 additions & 0 deletions actions/cicd-changesets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,48 @@ with:
aws-role-arn: ${{ secrets.AWS_ROLE_ARN_GATI_CHANGESETS }}
aws-lambda-url: ${{ secrets.AWS_LAMBDA_URL_GATI }}
```

## Package manager

`package-manager` selects the toolchain; defaults to `pnpm`.

### pnpm

Installs Node and pnpm, runs `pnpm install`, and defaults the changesets
commands to `pnpm run ci:changeset:{version,publish}`. The repo's `package.json`
must resolve a Node version (via `engines.node` or `node-version-file`).

### bun

```yaml
with:
package-manager: bun
```

Installs bun (not Node/pnpm), runs `bun install --frozen-lockfile`, and defaults
the changesets commands to `bun run ci:changeset:{version,publish}`. Define
those scripts in `package.json`:

```json
{
"scripts": {
"ci:changeset:version": "bun run changeset version",
"ci:changeset:publish": "bun run changeset publish"
},
"devDependencies": { "@changesets/cli": "~2.31.0" }
}
```

#### Bun version

Resolved in order: `bun-version` (explicit) → `bun-version-file` →
auto-detection (the default).

setup-bun reads `.tool-versions`, `.bun-version`, and `package.json` natively,
so those are forwarded to it. This action reads `mise.toml` itself (setup-bun
does not understand mise) and passes the version to setup-bun. With both inputs
empty it auto-detects: mise file first, then `.tool-versions`/`.bun-version`,
then `package.json`, then `latest`.

A repo that pins bun in `mise.toml` (as the `ts-service` blueprint does) needs
no `bun-version` input.
122 changes: 115 additions & 7 deletions actions/cicd-changesets/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,45 @@ inputs:
aws-lambda-url:
description: ""
required: false
# nodejs inputs
# toolchain inputs
package-manager:
description: |
Which package manager installs dependencies and runs the changesets
commands: `pnpm` (default) or `bun`.

With `pnpm`, Node and pnpm are installed and `pnpm install` runs. With
`bun`, neither is installed: bun is set up instead and
`bun install --frozen-lockfile` runs, so a bun repo needs no
`engines.node` in its package.json and no pnpm lockfile.
required: false
default: "pnpm"
bun-version:
description: |
The bun version to install, e.g. `1.3.14`. Only used when
`package-manager` is `bun`. Takes precedence over `bun-version-file` and
auto-detection. Leave it empty (the default) to read the version from
the repo's own pin instead of restating it here, where it can drift.
required: false
default: ""
bun-version-file:
description: |
Path to a file the bun version is read from when `bun-version` is empty.
Only used when `package-manager` is `bun`.

setup-bun reads `.tool-versions`, `.bun-version`, and `package.json`
(`packageManager`, else `engines.bun`) natively, so this action forwards
those straight to setup-bun. It does not understand `mise.toml`, which is
where our TypeScript repos pin an exact bun, so this action reads
`mise.toml`, `.mise.toml`, and `.config/mise/config.toml` itself and
passes the version to setup-bun as `bun-version`.

Leave it empty (the default) to auto-detect: the action checks for a mise
file first, then forwards `.tool-versions` or `.bun-version` to setup-bun
if present, and setup-bun finally reads `package.json` or installs
`latest`.
required: false
default: ""
# nodejs inputs (package-manager: pnpm only)
node-version-file:
description: ""
required: false
Expand All @@ -59,13 +97,17 @@ inputs:
default: "true"
# changesets inputs
changesets-publish-cmd:
description: ""
description: |
Defaults to `<package-manager> run ci:changeset:publish`, i.e.
`pnpm run ci:changeset:publish` unless `package-manager` is `bun`.
required: false
default: pnpm run ci:changeset:publish
default: ""
changesets-version-cmd:
description: ""
description: |
Defaults to `<package-manager> run ci:changeset:version`, i.e.
`pnpm run ci:changeset:version` unless `package-manager` is `bun`.
required: false
default: pnpm run ci:changeset:version
default: ""
changesets-create-gh-release:
description: ""
required: false
Expand Down Expand Up @@ -121,6 +163,20 @@ outputs:
runs:
using: composite
steps:
- name: Validate package-manager
shell: bash
env:
PACKAGE_MANAGER: ${{ inputs.package-manager }}
run: |
set -euo pipefail
case "${PACKAGE_MANAGER}" in
pnpm | bun) ;;
*)
echo "::error::package-manager must be 'pnpm' or 'bun', got '${PACKAGE_MANAGER}'"
exit 1
;;
esac

# GATI is optional. This step only runs when `aws-role-arn` is provided.
# When GATI is not configured, the action falls back to `github-token`
# (the automatic GITHUB_TOKEN by default). Using GATI requires the calling
Expand Down Expand Up @@ -167,22 +223,74 @@ runs:
git config user.email "${{ inputs.git-email }}"
git config user.name "${{ inputs.git-user }}"

# One of the two toolchain paths below runs, never both. `bun` skips Node and
# pnpm entirely: a bun repo has no pnpm lockfile to install from, and its
# package.json carries `engines.bun` rather than the `engines.node` that
# node-version-file needs.
- name: Setup nodejs
if: inputs.package-manager == 'pnpm'
uses: smartcontractkit/.github/actions/setup-nodejs@setup-nodejs/v1
with:
node-version-file: ${{ inputs.node-version-file }}
pnpm-version: ${{ inputs.pnpm-version }}
use-cache: ${{ inputs.pnpm-use-cache }}
run-install: "true"

# setup-bun reads .tool-versions, .bun-version, and package.json natively
# via its bun-version-file input, so those are forwarded to it unchanged.
# It does not read mise.toml, which is where our TypeScript repos pin an
# exact bun, so the script resolves that case to a plain version instead.
# `bun-version` still wins when set, the same way setup-nodejs lets
# pnpm-version override .tool-versions.
- name: Resolve bun version
id: bun-version
if: inputs.package-manager == 'bun'
shell: bash
env:
BUN_VERSION: ${{ inputs.bun-version }}
BUN_VERSION_FILE: ${{ inputs.bun-version-file }}
run: "${GITHUB_ACTION_PATH}/scripts/resolve-bun-version.sh"

- name: Setup bun
if: inputs.package-manager == 'bun'
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: ${{ steps.bun-version.outputs.version }}
bun-version-file: ${{ steps.bun-version.outputs.file }}

# bun.lock records the root package's name and its dependencies, not the
# root's own version, so a `changeset version` bump does not invalidate it
# and --frozen-lockfile keeps holding across releases.
- name: Run package manager install (bun)
if: inputs.package-manager == 'bun'
shell: bash
run: bun install --frozen-lockfile

# Both commands default to the selected package manager, so a bun caller
# that overrides neither does not fall back to a pnpm that was never
# installed. For a pnpm caller the resolved values are unchanged.
- name: Resolve changesets commands
id: cmds
shell: bash
env:
PACKAGE_MANAGER: ${{ inputs.package-manager }}
PUBLISH_CMD: ${{ inputs.changesets-publish-cmd }}
VERSION_CMD: ${{ inputs.changesets-version-cmd }}
run: |
set -euo pipefail
{
echo "publish=${PUBLISH_CMD:-${PACKAGE_MANAGER} run ci:changeset:publish}"
echo "version=${VERSION_CMD:-${PACKAGE_MANAGER} run ci:changeset:version}"
} | tee -a "${GITHUB_OUTPUT}"

- name: Run changesets
id: changesets
uses: smartcontractkit/.github/actions/signed-commits@changesets-signed-commits/v1
env:
GITHUB_TOKEN: ${{ steps.resolve-gh-token.outputs.token }}
with:
publish: ${{ inputs.changesets-publish-cmd }}
version: ${{ inputs.changesets-version-cmd }}
publish: ${{ steps.cmds.outputs.publish }}
version: ${{ steps.cmds.outputs.version }}
createGithubReleases: ${{ inputs.changesets-create-gh-release }}
rootVersionPackagePath:
${{ inputs.changesets-root-version-package-path }}
Expand Down
119 changes: 119 additions & 0 deletions actions/cicd-changesets/scripts/resolve-bun-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
#
# Decide which bun version setup-bun should install, and how to tell it.
#
# setup-bun reads .tool-versions, .bun-version, and package.json natively via
# its own bun-version-file input, so this script forwards those to it unchanged.
# It does not understand mise.toml, which is where our TypeScript repos pin an
# exact bun, so this script reads mise files itself and emits a plain version.
#
# Inputs (env):
# BUN_VERSION explicit version; wins over everything, no file is read
# BUN_VERSION_FILE path to a version file; auto-detect when empty
#
# Outputs (GITHUB_OUTPUT):
# version pass to setup-bun's bun-version (empty when a file is forwarded)
# file pass to setup-bun's bun-version-file (empty when version is set)
#
# Exactly one of the two outputs is non-empty, or both are empty to let
# setup-bun read package.json itself and fall back to latest.

set -euo pipefail

BUN_VERSION="${BUN_VERSION:-}"
BUN_VERSION_FILE="${BUN_VERSION_FILE:-}"

# mise config file names, in the order mise itself prefers them.
MISE_PATHS=(mise.toml .mise.toml .config/mise/config.toml)
# Files setup-bun can read on its own. package.json is omitted deliberately:
# setup-bun already reads it when given no file at all.
FORWARDABLE_PATHS=(.tool-versions .bun-version)

# Print the bun entry of a mise [tools] table, in any of the forms mise
# accepts: a bare string, an array of versions, or a table with a version key.
mise_bun_line() {
awk '
/^[[:space:]]*\[/ { tools = ($0 ~ /^[[:space:]]*\[tools\][[:space:]]*$/); next }
tools && /^[[:space:]]*("bun"|bun)[[:space:]]*=/ { print; exit }
' "$1"
}

# Reduce a mise bun entry to a bare version: drop the key, a trailing comment,
# the punctuation of all three forms, an inner `version =`, and any extra
# versions of the array form.
mise_bun_version() {
local raw
raw=$(mise_bun_line "$1")
raw="${raw#*=}"
raw="${raw%%#*}"
raw=$(printf '%s' "${raw}" | tr -d "\"'[]{} ")
raw="${raw##*=}"
printf '%s' "${raw%%,*}"
}

is_mise_file() {
case "$(basename "$1")" in
mise.toml | .mise.toml | config.toml) return 0 ;;
*) return 1 ;;
esac
}

first_existing() {
local candidate
for candidate in "$@"; do
if [[ -f "${candidate}" ]]; then
printf '%s' "${candidate}"
return
fi
done
}

out_version=""
out_file=""

if [[ -n "${BUN_VERSION}" ]]; then
out_version="${BUN_VERSION}"
echo "Using bun version from the bun-version input: ${out_version}"
else
# Read a mise file if one is named or found; setup-bun cannot do it for us.
if [[ -n "${BUN_VERSION_FILE}" ]]; then
is_mise_file "${BUN_VERSION_FILE}" && mise_file="${BUN_VERSION_FILE}" || mise_file=""
else
mise_file=$(first_existing "${MISE_PATHS[@]}")
fi

if [[ -n "${mise_file}" ]]; then
if [[ ! -f "${mise_file}" ]]; then
echo "::error::bun-version-file '${mise_file}' does not exist"
exit 1
fi
out_version=$(mise_bun_version "${mise_file}")
if [[ -n "${out_version}" ]]; then
echo "Using bun version from ${mise_file}: ${out_version}"
elif [[ -n "${BUN_VERSION_FILE}" ]]; then
# An explicitly named mise file must pin bun. An auto-detected one that
# does not is skipped, so the other files still get a chance.
echo "::error::bun-version-file '${mise_file}' does not pin a bun version"
exit 1
fi
fi

# No mise pin: hand a file setup-bun understands to setup-bun.
if [[ -z "${out_version}" ]]; then
if [[ -n "${BUN_VERSION_FILE}" ]]; then
out_file="${BUN_VERSION_FILE}"
else
out_file=$(first_existing "${FORWARDABLE_PATHS[@]}")
fi
if [[ -n "${out_file}" ]]; then
echo "Forwarding ${out_file} to setup-bun"
else
echo "No bun pin found; setup-bun will read package.json or install latest"
fi
fi
fi

{
echo "version=${out_version}"
echo "file=${out_file}"
} | tee -a "${GITHUB_OUTPUT}"
Loading
Loading