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
128 changes: 128 additions & 0 deletions .github/beta-release/bump.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import argparse
import json
import pathlib
import re
import subprocess
import sys

ROOT = pathlib.Path(__file__).resolve().parents[2]

CORE = {
"core/macros": "zen-macros",
"core/types": "zen-types",
"core/expression": "zen-expression",
"core/template": "zen-tmpl",
"core/engine": "zen-engine",
}

BINDINGS = {
"nodejs": "bindings/nodejs",
"python": "bindings/python",
}

TAG_PREFIX = {
"rust": "zen-engine",
"nodejs": "nodejs",
"python": "python",
}

VERSION = re.compile(r"^\d+\.\d+\.\d+-beta\.\d+$")
PACKAGE_VERSION = re.compile(r'^version\s*=\s*"[^"]*"', re.MULTILINE)


def fail(message):
print(f"error: {message}", file=sys.stderr)
sys.exit(1)


def tags(pattern):
out = subprocess.run(
["git", "tag", "--list", pattern], cwd=ROOT, check=True, capture_output=True, text=True
).stdout
return [line.strip() for line in out.splitlines() if line.strip()]


def next_version(branch):
match = re.fullmatch(r"release/(\d+)\.(\d+)", branch)
if not match:
fail(f"cannot derive a version from branch '{branch}' (expected release/<major>.<minor>)")
base = f"{match.group(1)}.{match.group(2)}.0"
numbers = []
for prefix in TAG_PREFIX.values():
for tag in tags(f"{prefix}-v{base}-beta.*"):
found = re.fullmatch(rf"{re.escape(prefix)}-v{re.escape(base)}-beta\.(\d+)", tag)
if found:
numbers.append(int(found.group(1)))
return f"{base}-beta.{max(numbers) + 1 if numbers else 0}"


def set_package_version(path, version):
manifest = ROOT / path / "Cargo.toml"
text = manifest.read_text()
updated, count = PACKAGE_VERSION.subn(f'version = "{version}"', text, count=1)
if count != 1:
fail(f"no package version in {manifest}")
manifest.write_text(updated)


def set_requirements(version):
names = "|".join(re.escape(name) for name in CORE.values())
requirement = re.compile(
rf'^((?:{names})\s*=\s*\{{[^}}\n]*\bversion\s*=\s*")[^"]*(")', re.MULTILINE
)
for manifest in ROOT.glob("**/Cargo.toml"):
if "target" in manifest.relative_to(ROOT).parts:
continue
text = manifest.read_text()
updated = requirement.sub(rf"\g<1>{version}\g<2>", text)
if updated != text:
manifest.write_text(updated)


def set_package_json(path, version):
package = ROOT / path / "package.json"
text = package.read_text()
updated, count = re.subn(
r'^ "version": "[^"]*"', f' "version": "{version}"', text, count=1, flags=re.MULTILINE
)
if count != 1 or json.loads(updated)["version"] != version:
fail(f"no top-level version in {package}")
package.write_text(updated)


def main():
parser = argparse.ArgumentParser()
parser.add_argument("--branch", required=True)
parser.add_argument("--version", default="")
parser.add_argument("--targets", required=True)
args = parser.parse_args()

targets = [t for t in args.targets.split(",") if t]
unknown = [t for t in targets if t not in TAG_PREFIX]
if not targets or unknown:
fail(f"targets must be a non-empty subset of {sorted(TAG_PREFIX)}")

version = args.version.strip() or next_version(args.branch)
if not VERSION.fullmatch(version):
fail(f"'{version}' is not a beta version like 2.2.0-beta.0")

release_tags = [f"{TAG_PREFIX[t]}-v{version}" for t in targets]
existing = [tag for tag in release_tags if tags(tag)]
if existing:
fail(f"tags already exist: {' '.join(existing)}")

for path in CORE:
set_package_version(path, version)
set_requirements(version)
for target in targets:
if target in BINDINGS:
set_package_version(BINDINGS[target], version)
if "nodejs" in targets:
set_package_json(BINDINGS["nodejs"], version)

print(f"version={version}")
print(f"tags={' '.join(release_tags)}")


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions .github/workflows/benchmark.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
tags:
- 'zen-engine-v*'
- '!zen-engine-v*-beta*'
workflow_dispatch:

permissions:
Expand Down
92 changes: 92 additions & 0 deletions .github/workflows/beta-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Beta release

on:
workflow_dispatch:
inputs:
rust:
description: 'Publish the core crates to crates.io'
type: boolean
default: true
nodejs:
description: 'Publish @gorules/zen-engine to npm (dist-tag beta)'
type: boolean
default: true
python:
description: 'Publish zen-engine to PyPI'
type: boolean
default: false
version:
description: 'Exact version, e.g. 2.2.0-beta.3 (empty: next beta for this branch)'
type: string
required: false
default: ''
dry-run:
description: 'Show the version and file changes without tagging'
type: boolean
default: false

permissions:
contents: read

concurrency:
group: beta-release-${{ github.ref }}
cancel-in-progress: false

jobs:
beta:
name: Tag beta
if: startsWith(github.ref, 'refs/heads/release/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
token: ${{ secrets.PAT }}

- name: Bump versions
id: bump
env:
RUST: ${{ inputs.rust }}
NODEJS: ${{ inputs.nodejs }}
PYTHON: ${{ inputs.python }}
VERSION: ${{ inputs.version }}
run: |
targets=()
[ "$RUST" = "true" ] && targets+=(rust)
[ "$NODEJS" = "true" ] && targets+=(nodejs)
[ "$PYTHON" = "true" ] && targets+=(python)
python3 .github/beta-release/bump.py \
--branch "$GITHUB_REF_NAME" \
--version "$VERSION" \
--targets "$(IFS=,; echo "${targets[*]}")" | tee -a "$GITHUB_OUTPUT"

- name: Update Cargo.lock
run: cargo update --workspace

- name: Show changes
run: git diff

- name: Commit and push tags
if: ${{ !inputs.dry-run }}
env:
VERSION: ${{ steps.bump.outputs.version }}
TAGS: ${{ steps.bump.outputs.tags }}
run: |
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git commit -am "chore(release): $VERSION"
for tag in $TAGS; do
git tag -a "$tag" -m "$tag"
done
git push origin $TAGS

- name: Create pre-releases
if: ${{ !inputs.dry-run }}
env:
GH_TOKEN: ${{ secrets.PAT }}
TAGS: ${{ steps.bump.outputs.tags }}
run: |
for tag in $TAGS; do
gh release create "$tag" --prerelease --verify-tag --generate-notes --title "$tag"
done
Loading