Skip to content

Commit 6f1ff16

Browse files
authored
Support bun for optional package management on cicd-changesets (#1638)
1 parent 17626c7 commit 6f1ff16

6 files changed

Lines changed: 425 additions & 7 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
"cicd-changesets": minor
3+
---
4+
5+
Let a bun repo run its own releases.
6+
7+
The action installed Node and pnpm unconditionally and ran `pnpm install`, so a
8+
repo whose dependencies are managed by bun could not use it: there was no pnpm
9+
lockfile to install from, and `node-version-file` had no `engines.node` to read
10+
in a package.json that pins `engines.bun` instead. It failed at the Node setup
11+
step, before any changesets work.
12+
13+
A new `package-manager` input selects the toolchain. It defaults to `pnpm`, so
14+
every existing caller is unaffected. Set it to `bun` and Node and pnpm are not
15+
installed at all: bun is set up and `bun install --frozen-lockfile` runs
16+
instead.
17+
18+
The bun version is read from the repo's own pin, so the workflow does not have
19+
to restate it (and let it drift). `bun-version` still wins when set, but it
20+
defaults to empty, and the action then resolves the version from
21+
`bun-version-file` or auto-detection. setup-bun already reads `.tool-versions`,
22+
`.bun-version`, and `package.json` natively, so those are forwarded to it
23+
unchanged; the action reads `mise.toml`, `.mise.toml`, and
24+
`.config/mise/config.toml` itself (setup-bun does not understand mise) and
25+
passes the version to setup-bun. Auto-detection tries a mise file first, then
26+
forwards `.tool-versions` or `.bun-version` if present, and setup-bun finally
27+
reads `package.json` or installs `latest`.
28+
29+
`changesets-publish-cmd` and `changesets-version-cmd` now default to
30+
`<package-manager> run ci:changeset:{publish,version}` rather than hard-coding
31+
`pnpm`, so a bun caller that overrides neither does not shell out to a pnpm that
32+
was never installed. A pnpm caller resolves to the same two commands as before.
33+
34+
The `signed-commits` action needed no change. It runs whichever commands it is
35+
given, and bun installs a normal `node_modules` tree, so it resolves
36+
`@changesets/cli/bin.js` exactly as it does under pnpm.

‎.changeset/flat-worms-escape.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"cicd-changesets": minor
3+
---
4+
5+
Support bun for package management

‎actions/cicd-changesets/README.md‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,48 @@ with:
4747
aws-role-arn: ${{ secrets.AWS_ROLE_ARN_GATI_CHANGESETS }}
4848
aws-lambda-url: ${{ secrets.AWS_LAMBDA_URL_GATI }}
4949
```
50+
51+
## Package manager
52+
53+
`package-manager` selects the toolchain; defaults to `pnpm`.
54+
55+
### pnpm
56+
57+
Installs Node and pnpm, runs `pnpm install`, and defaults the changesets
58+
commands to `pnpm run ci:changeset:{version,publish}`. The repo's `package.json`
59+
must resolve a Node version (via `engines.node` or `node-version-file`).
60+
61+
### bun
62+
63+
```yaml
64+
with:
65+
package-manager: bun
66+
```
67+
68+
Installs bun (not Node/pnpm), runs `bun install --frozen-lockfile`, and defaults
69+
the changesets commands to `bun run ci:changeset:{version,publish}`. Define
70+
those scripts in `package.json`:
71+
72+
```json
73+
{
74+
"scripts": {
75+
"ci:changeset:version": "bun run changeset version",
76+
"ci:changeset:publish": "bun run changeset publish"
77+
},
78+
"devDependencies": { "@changesets/cli": "~2.31.0" }
79+
}
80+
```
81+
82+
#### Bun version
83+
84+
Resolved in order: `bun-version` (explicit) → `bun-version-file` →
85+
auto-detection (the default).
86+
87+
setup-bun reads `.tool-versions`, `.bun-version`, and `package.json` natively,
88+
so those are forwarded to it. This action reads `mise.toml` itself (setup-bun
89+
does not understand mise) and passes the version to setup-bun. With both inputs
90+
empty it auto-detects: mise file first, then `.tool-versions`/`.bun-version`,
91+
then `package.json`, then `latest`.
92+
93+
A repo that pins bun in `mise.toml` (as the `ts-service` blueprint does) needs
94+
no `bun-version` input.

‎actions/cicd-changesets/action.yml‎

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,45 @@ inputs:
4444
aws-lambda-url:
4545
description: ""
4646
required: false
47-
# nodejs inputs
47+
# toolchain inputs
48+
package-manager:
49+
description: |
50+
Which package manager installs dependencies and runs the changesets
51+
commands: `pnpm` (default) or `bun`.
52+
53+
With `pnpm`, Node and pnpm are installed and `pnpm install` runs. With
54+
`bun`, neither is installed: bun is set up instead and
55+
`bun install --frozen-lockfile` runs, so a bun repo needs no
56+
`engines.node` in its package.json and no pnpm lockfile.
57+
required: false
58+
default: "pnpm"
59+
bun-version:
60+
description: |
61+
The bun version to install, e.g. `1.3.14`. Only used when
62+
`package-manager` is `bun`. Takes precedence over `bun-version-file` and
63+
auto-detection. Leave it empty (the default) to read the version from
64+
the repo's own pin instead of restating it here, where it can drift.
65+
required: false
66+
default: ""
67+
bun-version-file:
68+
description: |
69+
Path to a file the bun version is read from when `bun-version` is empty.
70+
Only used when `package-manager` is `bun`.
71+
72+
setup-bun reads `.tool-versions`, `.bun-version`, and `package.json`
73+
(`packageManager`, else `engines.bun`) natively, so this action forwards
74+
those straight to setup-bun. It does not understand `mise.toml`, which is
75+
where our TypeScript repos pin an exact bun, so this action reads
76+
`mise.toml`, `.mise.toml`, and `.config/mise/config.toml` itself and
77+
passes the version to setup-bun as `bun-version`.
78+
79+
Leave it empty (the default) to auto-detect: the action checks for a mise
80+
file first, then forwards `.tool-versions` or `.bun-version` to setup-bun
81+
if present, and setup-bun finally reads `package.json` or installs
82+
`latest`.
83+
required: false
84+
default: ""
85+
# nodejs inputs (package-manager: pnpm only)
4886
node-version-file:
4987
description: ""
5088
required: false
@@ -59,13 +97,17 @@ inputs:
5997
default: "true"
6098
# changesets inputs
6199
changesets-publish-cmd:
62-
description: ""
100+
description: |
101+
Defaults to `<package-manager> run ci:changeset:publish`, i.e.
102+
`pnpm run ci:changeset:publish` unless `package-manager` is `bun`.
63103
required: false
64-
default: pnpm run ci:changeset:publish
104+
default: ""
65105
changesets-version-cmd:
66-
description: ""
106+
description: |
107+
Defaults to `<package-manager> run ci:changeset:version`, i.e.
108+
`pnpm run ci:changeset:version` unless `package-manager` is `bun`.
67109
required: false
68-
default: pnpm run ci:changeset:version
110+
default: ""
69111
changesets-create-gh-release:
70112
description: ""
71113
required: false
@@ -121,6 +163,20 @@ outputs:
121163
runs:
122164
using: composite
123165
steps:
166+
- name: Validate package-manager
167+
shell: bash
168+
env:
169+
PACKAGE_MANAGER: ${{ inputs.package-manager }}
170+
run: |
171+
set -euo pipefail
172+
case "${PACKAGE_MANAGER}" in
173+
pnpm | bun) ;;
174+
*)
175+
echo "::error::package-manager must be 'pnpm' or 'bun', got '${PACKAGE_MANAGER}'"
176+
exit 1
177+
;;
178+
esac
179+
124180
# GATI is optional. This step only runs when `aws-role-arn` is provided.
125181
# When GATI is not configured, the action falls back to `github-token`
126182
# (the automatic GITHUB_TOKEN by default). Using GATI requires the calling
@@ -167,22 +223,74 @@ runs:
167223
git config user.email "${{ inputs.git-email }}"
168224
git config user.name "${{ inputs.git-user }}"
169225
226+
# One of the two toolchain paths below runs, never both. `bun` skips Node and
227+
# pnpm entirely: a bun repo has no pnpm lockfile to install from, and its
228+
# package.json carries `engines.bun` rather than the `engines.node` that
229+
# node-version-file needs.
170230
- name: Setup nodejs
231+
if: inputs.package-manager == 'pnpm'
171232
uses: smartcontractkit/.github/actions/setup-nodejs@setup-nodejs/v1
172233
with:
173234
node-version-file: ${{ inputs.node-version-file }}
174235
pnpm-version: ${{ inputs.pnpm-version }}
175236
use-cache: ${{ inputs.pnpm-use-cache }}
176237
run-install: "true"
177238

239+
# setup-bun reads .tool-versions, .bun-version, and package.json natively
240+
# via its bun-version-file input, so those are forwarded to it unchanged.
241+
# It does not read mise.toml, which is where our TypeScript repos pin an
242+
# exact bun, so the script resolves that case to a plain version instead.
243+
# `bun-version` still wins when set, the same way setup-nodejs lets
244+
# pnpm-version override .tool-versions.
245+
- name: Resolve bun version
246+
id: bun-version
247+
if: inputs.package-manager == 'bun'
248+
shell: bash
249+
env:
250+
BUN_VERSION: ${{ inputs.bun-version }}
251+
BUN_VERSION_FILE: ${{ inputs.bun-version-file }}
252+
run: "${GITHUB_ACTION_PATH}/scripts/resolve-bun-version.sh"
253+
254+
- name: Setup bun
255+
if: inputs.package-manager == 'bun'
256+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
257+
with:
258+
bun-version: ${{ steps.bun-version.outputs.version }}
259+
bun-version-file: ${{ steps.bun-version.outputs.file }}
260+
261+
# bun.lock records the root package's name and its dependencies, not the
262+
# root's own version, so a `changeset version` bump does not invalidate it
263+
# and --frozen-lockfile keeps holding across releases.
264+
- name: Run package manager install (bun)
265+
if: inputs.package-manager == 'bun'
266+
shell: bash
267+
run: bun install --frozen-lockfile
268+
269+
# Both commands default to the selected package manager, so a bun caller
270+
# that overrides neither does not fall back to a pnpm that was never
271+
# installed. For a pnpm caller the resolved values are unchanged.
272+
- name: Resolve changesets commands
273+
id: cmds
274+
shell: bash
275+
env:
276+
PACKAGE_MANAGER: ${{ inputs.package-manager }}
277+
PUBLISH_CMD: ${{ inputs.changesets-publish-cmd }}
278+
VERSION_CMD: ${{ inputs.changesets-version-cmd }}
279+
run: |
280+
set -euo pipefail
281+
{
282+
echo "publish=${PUBLISH_CMD:-${PACKAGE_MANAGER} run ci:changeset:publish}"
283+
echo "version=${VERSION_CMD:-${PACKAGE_MANAGER} run ci:changeset:version}"
284+
} | tee -a "${GITHUB_OUTPUT}"
285+
178286
- name: Run changesets
179287
id: changesets
180288
uses: smartcontractkit/.github/actions/signed-commits@changesets-signed-commits/v1
181289
env:
182290
GITHUB_TOKEN: ${{ steps.resolve-gh-token.outputs.token }}
183291
with:
184-
publish: ${{ inputs.changesets-publish-cmd }}
185-
version: ${{ inputs.changesets-version-cmd }}
292+
publish: ${{ steps.cmds.outputs.publish }}
293+
version: ${{ steps.cmds.outputs.version }}
186294
createGithubReleases: ${{ inputs.changesets-create-gh-release }}
187295
rootVersionPackagePath:
188296
${{ inputs.changesets-root-version-package-path }}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Decide which bun version setup-bun should install, and how to tell it.
4+
#
5+
# setup-bun reads .tool-versions, .bun-version, and package.json natively via
6+
# its own bun-version-file input, so this script forwards those to it unchanged.
7+
# It does not understand mise.toml, which is where our TypeScript repos pin an
8+
# exact bun, so this script reads mise files itself and emits a plain version.
9+
#
10+
# Inputs (env):
11+
# BUN_VERSION explicit version; wins over everything, no file is read
12+
# BUN_VERSION_FILE path to a version file; auto-detect when empty
13+
#
14+
# Outputs (GITHUB_OUTPUT):
15+
# version pass to setup-bun's bun-version (empty when a file is forwarded)
16+
# file pass to setup-bun's bun-version-file (empty when version is set)
17+
#
18+
# Exactly one of the two outputs is non-empty, or both are empty to let
19+
# setup-bun read package.json itself and fall back to latest.
20+
21+
set -euo pipefail
22+
23+
BUN_VERSION="${BUN_VERSION:-}"
24+
BUN_VERSION_FILE="${BUN_VERSION_FILE:-}"
25+
26+
# mise config file names, in the order mise itself prefers them.
27+
MISE_PATHS=(mise.toml .mise.toml .config/mise/config.toml)
28+
# Files setup-bun can read on its own. package.json is omitted deliberately:
29+
# setup-bun already reads it when given no file at all.
30+
FORWARDABLE_PATHS=(.tool-versions .bun-version)
31+
32+
# Print the bun entry of a mise [tools] table, in any of the forms mise
33+
# accepts: a bare string, an array of versions, or a table with a version key.
34+
mise_bun_line() {
35+
awk '
36+
/^[[:space:]]*\[/ { tools = ($0 ~ /^[[:space:]]*\[tools\][[:space:]]*$/); next }
37+
tools && /^[[:space:]]*("bun"|bun)[[:space:]]*=/ { print; exit }
38+
' "$1"
39+
}
40+
41+
# Reduce a mise bun entry to a bare version: drop the key, a trailing comment,
42+
# the punctuation of all three forms, an inner `version =`, and any extra
43+
# versions of the array form.
44+
mise_bun_version() {
45+
local raw
46+
raw=$(mise_bun_line "$1")
47+
raw="${raw#*=}"
48+
raw="${raw%%#*}"
49+
raw=$(printf '%s' "${raw}" | tr -d "\"'[]{} ")
50+
raw="${raw##*=}"
51+
printf '%s' "${raw%%,*}"
52+
}
53+
54+
is_mise_file() {
55+
case "$(basename "$1")" in
56+
mise.toml | .mise.toml | config.toml) return 0 ;;
57+
*) return 1 ;;
58+
esac
59+
}
60+
61+
first_existing() {
62+
local candidate
63+
for candidate in "$@"; do
64+
if [[ -f "${candidate}" ]]; then
65+
printf '%s' "${candidate}"
66+
return
67+
fi
68+
done
69+
}
70+
71+
out_version=""
72+
out_file=""
73+
74+
if [[ -n "${BUN_VERSION}" ]]; then
75+
out_version="${BUN_VERSION}"
76+
echo "Using bun version from the bun-version input: ${out_version}"
77+
else
78+
# Read a mise file if one is named or found; setup-bun cannot do it for us.
79+
if [[ -n "${BUN_VERSION_FILE}" ]]; then
80+
is_mise_file "${BUN_VERSION_FILE}" && mise_file="${BUN_VERSION_FILE}" || mise_file=""
81+
else
82+
mise_file=$(first_existing "${MISE_PATHS[@]}")
83+
fi
84+
85+
if [[ -n "${mise_file}" ]]; then
86+
if [[ ! -f "${mise_file}" ]]; then
87+
echo "::error::bun-version-file '${mise_file}' does not exist"
88+
exit 1
89+
fi
90+
out_version=$(mise_bun_version "${mise_file}")
91+
if [[ -n "${out_version}" ]]; then
92+
echo "Using bun version from ${mise_file}: ${out_version}"
93+
elif [[ -n "${BUN_VERSION_FILE}" ]]; then
94+
# An explicitly named mise file must pin bun. An auto-detected one that
95+
# does not is skipped, so the other files still get a chance.
96+
echo "::error::bun-version-file '${mise_file}' does not pin a bun version"
97+
exit 1
98+
fi
99+
fi
100+
101+
# No mise pin: hand a file setup-bun understands to setup-bun.
102+
if [[ -z "${out_version}" ]]; then
103+
if [[ -n "${BUN_VERSION_FILE}" ]]; then
104+
out_file="${BUN_VERSION_FILE}"
105+
else
106+
out_file=$(first_existing "${FORWARDABLE_PATHS[@]}")
107+
fi
108+
if [[ -n "${out_file}" ]]; then
109+
echo "Forwarding ${out_file} to setup-bun"
110+
else
111+
echo "No bun pin found; setup-bun will read package.json or install latest"
112+
fi
113+
fi
114+
fi
115+
116+
{
117+
echo "version=${out_version}"
118+
echo "file=${out_file}"
119+
} | tee -a "${GITHUB_OUTPUT}"

0 commit comments

Comments
 (0)