release #13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}" |