Skip to content

release

release #13

Workflow file for this run

name: release
on:
workflow_dispatch:
inputs:
bump:
description: "Semver bump (ignored if `version` is set)"
type: choice
options: [patch, minor, major]
default: patch
version:
description: "Explicit version override (e.g., 1.2.3)"
required: false
type: string
permissions:
contents: write
id-token: write # required for PyPI Trusted Publishing (OIDC); npm uses NPM_TOKEN
env:
NODE_VERSION: "24"
PYTHON_VERSION: "3.12"
jobs:
# Resolve the release version once and share it with every downstream job.
# If `version` is set, use it verbatim; otherwise bump the current version in
# framework/core/pyproject.toml by the chosen level.
resolve:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.compute.outputs.version }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- id: compute
shell: bash
run: |
if [ -n "${{ inputs.version }}" ]; then
ver="${{ inputs.version }}"
else
ver=$(BUMP="${{ inputs.bump }}" python3 <<'PY'
import os, pathlib, re, tomllib
cur = tomllib.loads(pathlib.Path("framework/core/pyproject.toml").read_text())["project"]["version"]
m = re.match(r"^(\d+)\.(\d+)\.(\d+)", cur)
if not m:
raise SystemExit(f"cannot parse current version: {cur!r}")
major, minor, patch = map(int, m.groups())
bump = os.environ["BUMP"]
if bump == "major":
major, minor, patch = major + 1, 0, 0
elif bump == "minor":
minor, patch = minor + 1, 0
else:
patch += 1
print(f"{major}.{minor}.{patch}")
PY
)
fi
echo "${ver}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([.-]?(a|b|rc|alpha|beta)[0-9]*)?$' \
|| { echo "::error::'${ver}' is not a valid version"; exit 1; }
echo "version=${ver}" >> "$GITHUB_OUTPUT"
echo "Resolved version: ${ver}"
# Bump versions in the working tree (no commit yet), build artifacts, upload.
# The commit + tag only hit origin in `finalize`, after publishes succeed,
# so a failed build never leaves an orphan tag to clean up.
build:
needs: resolve
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v8.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Bump all package versions (working tree only)
run: uv run python scripts/bump_version.py "${{ needs.resolve.outputs.version }}"
- name: Regenerate npm lockfile
run: npm install --package-lock-only
- name: Build Python wheels + sdists
run: uv build --all-packages --out-dir dist-py
- name: Pack npm tarballs
run: |
mkdir -p dist-npm
for pkg in packages/*/; do
npm pack "$pkg" --pack-destination dist-npm
done
- uses: actions/upload-artifact@v4
with:
name: dist-py
path: dist-py/
retention-days: 14
- uses: actions/upload-artifact@v4
with:
name: dist-npm
path: dist-npm/
retention-days: 14
publish-pypi:
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
package:
- simple_module_cli
- simple_module_core
- simple_module_db
- simple_module_hosting
- simple_module_auth
- simple_module_background_tasks
- simple_module_dashboard
- simple_module_feature_flags
- simple_module_file_storage
- simple_module_permissions
- simple_module_settings
- simple_module_users
environment:
name: pypi
url: https://pypi.org/project/${{ matrix.package }}/
steps:
- uses: actions/download-artifact@v4
with:
name: dist-py
path: dist-py/
- name: Filter artifacts for this package
shell: bash
run: |
shopt -s nullglob
files=( dist-py/${{ matrix.package }}-* )
if [ ${#files[@]} -eq 0 ]; then
echo "::error::no artifacts matched dist-py/${{ matrix.package }}-*"
ls -la dist-py
exit 1
fi
mkdir -p to-publish
mv "${files[@]}" to-publish/
ls to-publish
- uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: to-publish
publish-npm:
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
package: [ui, i18n, tsconfig]
environment: npm
steps:
- uses: actions/download-artifact@v4
with:
name: dist-npm
path: dist-npm/
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Configure npm auth
# Write the registry line ourselves so _authToken reads from NPM_TOKEN
# (setup-node's registry-url would hard-code NODE_AUTH_TOKEN instead).
# The backslash escapes the $ so the literal ${NPM_TOKEN} lands in
# .npmrc and is expanded by npm at publish time.
run: echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > ~/.npmrc
- name: Publish tarball
shell: bash
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
shopt -s nullglob
# `npm pack` on a scoped package writes <scope>-<name>-<version>.tgz with
# the leading @ stripped and '/' replaced by '-'.
files=( dist-npm/simple-module-py-${{ matrix.package }}-*.tgz )
if [ ${#files[@]} -eq 0 ]; then
echo "::error::no tarball for @simple-module-py/${{ matrix.package }} in dist-npm/"
ls -la dist-npm
exit 1
fi
# Guard: the stripped-@ filename is ambiguous between scoped and
# unscoped packages (both would yield simple-module-py-ui-*.tgz),
# so verify the tarball's package.json actually names an
# @simple-module-py/* package before publishing.
name=$(tar -xzOf "${files[0]}" package/package.json | python3 -c 'import json,sys; print(json.load(sys.stdin)["name"])')
expected="@simple-module-py/${{ matrix.package }}"
if [ "$name" != "$expected" ]; then
echo "::error::refusing to publish ${files[0]}: package name is '$name', expected '$expected'"
exit 1
fi
# Prefix with ./ so npm treats the arg as a file path, not a
# GitHub `user/repo` shorthand (which it does for any bare
# single-slash arg, even when the tarball exists on disk).
npm publish --access public "./${files[0]}"
# Only after every publish succeeded do we commit the version bump + tag.
finalize:
needs: [resolve, publish-pypi, publish-npm]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_PUSH_TOKEN || github.token }}
- uses: astral-sh/setup-uv@v8.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Re-apply version bump
run: uv run python scripts/bump_version.py "${{ needs.resolve.outputs.version }}"
- name: Regenerate npm lockfile
run: npm install --package-lock-only
- name: Commit, tag, push
env:
VERSION: ${{ needs.resolve.outputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
if ! git diff --cached --quiet; then
git commit -m "release: v${VERSION}"
git push origin HEAD:main
else
echo "working tree clean — packages already published at this version, tagging HEAD"
fi
git tag "v${VERSION}"
git push origin "v${VERSION}"