scripts/release-after-suite.mjs runs the release-checks suite and then the release action (npm publish, or the Python sdist and wheel build that the PyPI step uploads) from one process, and releases only if the suite passed. No step condition can release what the suite did not pass, so the pin that read if: conditions (status functions, multi-line if:, the leading-conjunct rule) is deleted with its tests and docs. What remains compares each job's release steps with their expected text exactly, less their if: line, and refuses continue-on-error, shell and defaults in the workflow. Ruling asura:ruling:suite-gate-structural-not-parsed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
191 lines
7.5 KiB
YAML
191 lines
7.5 KiB
YAML
name: Publish packages
|
|
|
|
# Non-Rust consumers cannot take a package from a subdirectory of a git repo the
|
|
# way cargo can, which is why they ended up vendoring forks instead. This gives
|
|
# them a versioned artifact to depend on so CultLib stays the only source.
|
|
#
|
|
# Rust needs nothing here: cargo git dependencies pin a CultLib revision
|
|
# directly, which is already how Ghostlight, Epiphany, CodexConnector and Odin
|
|
# consume the core stack.
|
|
#
|
|
# Tag a release to publish, for example `cultcache-ts-v0.13.6` or
|
|
# `cultnet-py-v0.1.0`. The tag's version must match the version in the package
|
|
# manifest; the workflow refuses the publish otherwise rather than shipping a
|
|
# mislabelled artifact.
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "cultcache-ts-v*"
|
|
- "cultcache-py-v*"
|
|
- "cultnet-py-v*"
|
|
- "cultmesh-py-v*"
|
|
workflow_dispatch:
|
|
inputs:
|
|
package:
|
|
description: Package to build and test (publishes nothing on manual runs)
|
|
required: true
|
|
type: choice
|
|
options:
|
|
- cultcache-ts
|
|
- cultcache-py
|
|
- cultnet-py
|
|
- cultmesh-py
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
cultcache-ts:
|
|
name: "@gamecult/cultcache-ts to npm"
|
|
if: startsWith(github.ref, 'refs/tags/cultcache-ts-v') || inputs.package == 'cultcache-ts'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
steps:
|
|
# fetch-depth: 0 so the semver check below can see every previously
|
|
# published tag, not just the one that triggered this run.
|
|
- name: Check out CultLib
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Set up Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 24.x
|
|
registry-url: https://registry.npmjs.org
|
|
|
|
# The release-checks suite, which Publish runs, needs .NET for the API gate.
|
|
- name: Set up .NET
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: 10.0.x
|
|
|
|
- name: Install workspace dependencies
|
|
run: npm ci
|
|
|
|
- name: Verify the tag matches the manifest version
|
|
if: startsWith(github.ref, 'refs/tags/cultcache-ts-v')
|
|
run: |
|
|
manifest="$(node -p "require('./packages/cultcache-ts/package.json').version")"
|
|
tagged="${GITHUB_REF_NAME#cultcache-ts-v}"
|
|
if [ "$manifest" != "$tagged" ]; then
|
|
echo "tag $GITHUB_REF_NAME does not match manifest version $manifest" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# docs/semver-policy.md: a breaking change under too small a bump, or a
|
|
# version that skips or reverses the previous release, fails here
|
|
# instead of shipping with a number that lies.
|
|
- name: Verify semver policy
|
|
if: startsWith(github.ref, 'refs/tags/cultcache-ts-v')
|
|
run: |
|
|
tagged="${GITHUB_REF_NAME#cultcache-ts-v}"
|
|
node scripts/check-changelog-semver.mjs --package "@gamecult/cultcache-ts" --version "$tagged"
|
|
|
|
- name: Build and test
|
|
working-directory: packages/cultcache-ts
|
|
run: npm test
|
|
|
|
# Runs the release-checks suite, then npm publish --access public, in one
|
|
# process that publishes only if the suite passed (docs/semver-policy.md).
|
|
- name: Publish
|
|
if: startsWith(github.ref, 'refs/tags/cultcache-ts-v')
|
|
run: node scripts/release-after-suite.mjs npm-publish packages/cultcache-ts
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
|
|
# The three Python packages layer cultcache-py < cultnet-py < cultmesh-py, the
|
|
# same way the Rust crates and npm packages do. One job shape serves all
|
|
# three; the matrix entry names the package and the tag prefix that releases it.
|
|
python:
|
|
name: ${{ matrix.package }} to PyPI
|
|
if: startsWith(github.ref, 'refs/tags/cultcache-py-v') || startsWith(github.ref, 'refs/tags/cultnet-py-v') || startsWith(github.ref, 'refs/tags/cultmesh-py-v') || endsWith(inputs.package, '-py')
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
package: [cultcache-py, cultnet-py, cultmesh-py]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
environment: pypi
|
|
permissions:
|
|
contents: read
|
|
# PyPI trusted publishing exchanges this for a short-lived token, so no
|
|
# long-lived API token is stored in the repository.
|
|
id-token: write
|
|
steps:
|
|
- name: Skip packages this run does not name
|
|
id: selected
|
|
run: |
|
|
if [ "${GITHUB_REF_NAME#${{ matrix.package }}-v}" != "$GITHUB_REF_NAME" ] || [ "${{ inputs.package }}" = "${{ matrix.package }}" ]; then
|
|
echo "run=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "run=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# fetch-depth: 0 so the semver check below can see every previously
|
|
# published tag, not just the one that triggered this run.
|
|
- name: Check out CultLib
|
|
if: steps.selected.outputs.run == 'true'
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Set up Python
|
|
if: steps.selected.outputs.run == 'true'
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Set up Node
|
|
if: steps.selected.outputs.run == 'true'
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 24.x
|
|
|
|
- name: Set up .NET
|
|
if: steps.selected.outputs.run == 'true'
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: 10.0.x
|
|
|
|
- name: Verify the tag matches the manifest version
|
|
if: steps.selected.outputs.run == 'true' && startsWith(github.ref, 'refs/tags/')
|
|
run: |
|
|
manifest="$(python -c "import tomllib,pathlib;print(tomllib.loads(pathlib.Path('packages/${{ matrix.package }}/pyproject.toml').read_text())['project']['version'])")"
|
|
tagged="${GITHUB_REF_NAME#${{ matrix.package }}-v}"
|
|
if [ "$manifest" != "$tagged" ]; then
|
|
echo "tag $GITHUB_REF_NAME does not match manifest version $manifest" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# docs/semver-policy.md: a breaking change under too small a bump, or a
|
|
# version that skips or reverses the previous release, fails here
|
|
# instead of shipping with a number that lies.
|
|
- name: Verify semver policy
|
|
if: steps.selected.outputs.run == 'true' && startsWith(github.ref, 'refs/tags/')
|
|
run: |
|
|
tagged="${GITHUB_REF_NAME#${{ matrix.package }}-v}"
|
|
node scripts/check-changelog-semver.mjs --package "${{ matrix.package }}" --version "$tagged"
|
|
|
|
# Lower layers install from source so a release of one package is tested
|
|
# against the sibling revision it was cut with, not whatever PyPI has.
|
|
- name: Install and test
|
|
if: steps.selected.outputs.run == 'true'
|
|
run: |
|
|
python -m pip install --upgrade build -e packages/cultcache-py -e packages/cultnet-py -e packages/cultmesh-py
|
|
python -m unittest discover -s packages/${{ matrix.package }}/tests
|
|
|
|
# Runs the release-checks suite, then builds the sdist and wheel, in one
|
|
# process that builds only if the suite passed (docs/semver-policy.md).
|
|
# Publish uploads only what this step built, so it has nothing to upload
|
|
# unless the suite passed.
|
|
- name: Build sdist and wheel
|
|
if: steps.selected.outputs.run == 'true'
|
|
run: node scripts/release-after-suite.mjs python-build packages/${{ matrix.package }}
|
|
|
|
- name: Publish
|
|
if: steps.selected.outputs.run == 'true' && startsWith(github.ref, 'refs/tags/')
|
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
with:
|
|
packages-dir: packages/${{ matrix.package }}/dist
|