CultLib/.github/workflows/publish-packages.yml
Meta Vens bc4af4fa58 Release gate: publish-packages.yml releases through one entrypoint that runs the suite first; the if: pin is gone
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>
2026-10-03 13:43:47 +02:00

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