diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 40ef7d4..8ad9dea 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -130,6 +130,59 @@ jobs: blocks[i] = re.sub(rf'^version = "{re.escape(old)}"$', f'version = "{new}"', block, count=1, flags=re.M) path.write_text("\n[[package]]\n".join(blocks), encoding="utf-8") + # bun.lock is JSON with trailing commas: parsed only for the positions of its values, so the + # version is replaced in place and the rest of the file stays as Bun wrote it. + # Objects become {key: (value, start, end)}, other values their text span. + def jsonc(text, i=0): + ws = re.compile(r"\s*") + i = ws.match(text, i).end() + if text[i] == '"': + end = re.compile(r'"(?:[^"\\]|\\.)*"').match(text, i).end() + return json.loads(text[i:end]), end + if text[i] in "{[": + close, members = "}" if text[i] == "{" else "]", {} + i += 1 + while True: + i = ws.match(text, i).end() + if text[i] == ",": + i += 1 + continue + if text[i] == close: + return members, i + 1 + if close == "}": + key, i = jsonc(text, i) + i = ws.match(text, i).end() + 1 # the colon + start = ws.match(text, i).end() + value, i = jsonc(text, start) + if close == "}": + members[key] = (value, start, i) + end = re.compile(r"[^,}\]\s]+").match(text, i).end() + return text[i:end], end + + # A Bun workspace keeps each member's version in the bun.lock at the workspace root, under + # "workspaces" → the member's path relative to it ("" for the root package, which gets none) + def bun_lock(path): + repo = Path.cwd().resolve() + directory = path.resolve().parent + for d in [directory, *directory.parents]: + if (d / "bun.lock").is_file(): + lock = d / "bun.lock" + member = directory.relative_to(d).as_posix() + return (lock.relative_to(repo) if lock.is_relative_to(repo) else lock), ("" if member == "." else member) + if d == repo: + return None, None + return None, None + + def replace_bun_lock(path, member, new): + text = path.read_text(encoding="utf-8") + entry = jsonc(text)[0].get("workspaces", ({},))[0].get(member) + version = entry and isinstance(entry[0], dict) and entry[0].get("version") + if not version: + return False + _, start, end = version + path.write_text(text[:start] + json.dumps(new) + text[end:], encoding="utf-8") + return True + prefix = os.environ["TAG_PREFIX"] main = Path(os.environ["VERSION_FILE"]) if os.environ["VERSION_FILE"] else None extras = [Path(line.strip()) for line in os.environ["EXTRA_FILES"].splitlines() if line.strip()] @@ -194,6 +247,10 @@ jobs: # The root entry and packages[""] replace(lock, old, new, count=2) changed.append(lock) + lock, member = bun_lock(path) if path.name == "package.json" else (None, None) + # Also when the lock is still at an older version (an earlier bump without it) + if lock and replace_bun_lock(lock, member, new) and lock not in changed: + changed.append(lock) lock = path.with_name("Cargo.lock") if path.name == "Cargo.toml" else None if lock and lock.is_file(): replace_cargo_lock(lock, old, new) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 523f186..7722ba3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,15 +1,18 @@ name: Lint -# Only the workflows are linted (and the apt-install action tried), so only a change to them runs it. +# Only the workflows are linted (and the apt-install action and the bump script tried), so only a +# change to them runs it. on: push: paths: - ".github/workflows/**" - "actions/**" + - "tests/**" pull_request: paths: - ".github/workflows/**" - "actions/**" + - "tests/**" jobs: actionlint: @@ -23,6 +26,17 @@ jobs: bash <(curl -fsSL "https://raw.githubusercontent.com/rhysd/actionlint/v${ACTIONLINT_VERSION}/scripts/download-actionlint.bash") "$ACTIONLINT_VERSION" ./actionlint -color + # The Python of bump-version.yml on fixtures: which version files and lock files it changes + bump-version: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.4.4 + - run: tests/bump-version/test.sh + # The apt-install action itself: a first install fills the cache, a second job takes it from there apt-install: runs-on: ubuntu-latest diff --git a/README.md b/README.md index a241fcf..202fcd0 100644 --- a/README.md +++ b/README.md @@ -415,7 +415,13 @@ jobs: | `release-workflow` | `release.yml` | started on the new tag; `""` starts nothing | A `package-lock.json` next to a `package.json` and a `Cargo.lock` next to a `Cargo.toml` (the -workspace's own crates) are updated along with it. Outputs: `version`, `tag`. +workspace's own crates) are updated along with it. In a Bun workspace the `bun.lock` in the +`package.json`'s folder or the nearest one above it (up to the repository root) carries the version +of a member, e.g. `apps/web/package.json` under `"workspaces"` → `"apps/web"`: that entry is updated +too, so the next `bun install` changes nothing. The root package gets no version in `bun.lock`, so +there the lock stays as it is. Outputs: `version`, `tag`. + +`tests/bump-version/test.sh` runs the step's Python on fixtures (in the Lint workflow). **Release candidates** on the default branch, no extra branch: `preminor` tags `v1.3.0-rc.1` and the [Docker release](#docker-release) pushes `:next`, while `:latest` stays on `1.2.x`; fixes go diff --git a/tests/bump-version/test.sh b/tests/bump-version/test.sh new file mode 100755 index 0000000..7d46458 --- /dev/null +++ b/tests/bump-version/test.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# Runs the Python of bump-version.yml's "Raise the version" step on small fixtures and checks which +# files it changes: the version file, a package-lock.json or Cargo.lock beside it, and the member's +# entry in a Bun workspace's bun.lock. Needs python3 with PyYAML and bun. +# Usage: tests/bump-version/test.sh +set -euo pipefail +here=$(cd "$(dirname "$0")" && pwd) +workflow=$here/../../.github/workflows/bump-version.yml +work=$(mktemp -d); trap 'rm -rf "$work"' EXIT + +# The step's script, exactly as the workflow runs it (shell: python) +python3 - "$workflow" "$work/bump.py" <<'PY' +import sys, yaml +steps = yaml.safe_load(open(sys.argv[1]))["jobs"]["bump"]["steps"] +open(sys.argv[2], "w").write(next(s for s in steps if s.get("id") == "bump")["run"]) +PY + +failures=0 +fail() { echo " FAIL: $*"; failures=$((failures + 1)); } +check() { if eval "$2"; then echo " ok: $1"; else fail "$1"; fi; } +version_of() { python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$1"; } +# The version in bun.lock's workspaces entry $2 (the file is JSON with trailing commas) +lock_version() { + python3 -c ' +import json, re, sys +text = re.sub(r",(\s*[}\]])", r"\1", open(sys.argv[1]).read()) +print(json.loads(text)["workspaces"][sys.argv[2]].get("version", "-"))' "$1" "$2" +} +# bump