Skip to content

Release

Release #11

Workflow file for this run

name: Release
on:
workflow_dispatch:
inputs:
tag:
description: Release tag, or dry-run to build without publishing
required: true
default: dry-run
type: string
concurrency:
group: release-${{ inputs.tag }}
cancel-in-progress: false
permissions: {}
env:
MCP_PUBLISHER_VERSION: "1.8.0"
MCP_PUBLISHER_LINUX_AMD64_SHA256: "1370446bbe74d562608e8005a6ccce02d146a661fbd78674e11cc70b9618d6cf"
jobs:
build:
name: Build and verify release artifacts
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
version: ${{ steps.release.outputs.version }}
publishing: ${{ steps.release.outputs.publishing }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
fetch-tags: true
persist-credentials: false
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
with:
python-version: "3.12"
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24.15.0"
package-manager-cache: false
- name: Validate release identity
id: release
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
python_version=$(uv version --short)
npm_version=$(node -p "require('./npm/package.json').version")
registry_version=$(jq -r '.version' server.json)
registry_package_version=$(jq -r '.packages[0].version' server.json)
if [[ "$python_version" != "$npm_version" ]] ||
[[ "$python_version" != "$registry_version" ]] ||
[[ "$python_version" != "$registry_package_version" ]]; then
echo "Python, npm, and MCP Registry versions must match $python_version" >&2
exit 1
fi
if [[ "$RELEASE_TAG" == "dry-run" ]]; then
publishing=false
elif [[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
if [[ "${RELEASE_TAG#v}" != "$python_version" ]]; then
echo "Tag $RELEASE_TAG does not match package version $python_version" >&2
exit 1
fi
if [[ "$(git cat-file -t "refs/tags/$RELEASE_TAG" 2>/dev/null || true)" != "tag" ]]; then
echo "Release tag $RELEASE_TAG is missing or is not annotated" >&2
exit 1
fi
tag_commit=$(git rev-list -n 1 "$RELEASE_TAG")
if [[ "$tag_commit" != "$GITHUB_SHA" ]]; then
echo "Release tag $RELEASE_TAG points to $tag_commit, not $GITHUB_SHA" >&2
exit 1
fi
publishing=true
else
echo "Release tag must be dry-run or a stable vMAJOR.MINOR.PATCH tag" >&2
exit 1
fi
{
echo "publishing=$publishing"
echo "version=$python_version"
} >> "$GITHUB_OUTPUT"
- name: Install Python dependencies
run: >-
uv sync
--extra dev
--extra python
--extra execution
--extra memory
--extra trace
--extra cpu
- name: Verify Python package
run: |
uv run ruff check src tests tools
uv run ruff format --check src tests tools
uv run mypy src tests tools
uv run lint-imports
uv run python tools/test.py core
uv run python tools/test.py process
uv run python tools/test.py collection
- name: Verify npm bootstrap
working-directory: npm
run: |
npm ci
npm run lint
npm run format:check
npm test
- name: Validate MCP Registry metadata
run: |
set -euo pipefail
archive=mcp-publisher.tar.gz
curl --fail --location --silent --show-error \
--output "$archive" \
"https://github.com/modelcontextprotocol/registry/releases/download/v${MCP_PUBLISHER_VERSION}/mcp-publisher_linux_amd64.tar.gz"
echo "${MCP_PUBLISHER_LINUX_AMD64_SHA256} $archive" | sha256sum --check --strict
tar -xzf "$archive" mcp-publisher
./mcp-publisher validate server.json
- name: Build release artifacts
run: |
mkdir -p dist/python dist/npm
uv build --out-dir dist/python
uvx --from twine twine check dist/python/*
npm pack ./npm --pack-destination "$PWD/dist/npm" --json > dist/npm-pack.json
sha256sum dist/python/* dist/npm/*.tgz > dist/SHA256SUMS
- name: Verify built wheel and npm archive
env:
RELEASE_VERSION: ${{ steps.release.outputs.version }}
run: |
set -euo pipefail
wheel="dist/python/flameox-${RELEASE_VERSION}-py3-none-any.whl"
npm_archive="dist/npm/flameox-${RELEASE_VERSION}.tgz"
test -f "$wheel"
test -f "$npm_archive"
actual_version=$(
uvx \
--no-config \
--no-sources \
--prerelease allow \
--python 3.12 \
--from "$wheel" \
flameox --version
)
if [[ "$actual_version" != "$RELEASE_VERSION" ]]; then
echo "Built CLI reports $actual_version, expected $RELEASE_VERSION" >&2
exit 1
fi
package_json=$(tar -xOf "$npm_archive" package/package.json)
if [[ "$(jq -r '.name' <<< "$package_json")" != "flameox" ]] ||
[[ "$(jq -r '.version' <<< "$package_json")" != "$RELEASE_VERSION" ]]; then
echo "npm archive identity does not match flameox@$RELEASE_VERSION" >&2
exit 1
fi
- name: Upload release artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-artifacts
if-no-files-found: error
retention-days: 14
path: |
dist/python/*
dist/npm/*.tgz
dist/npm-pack.json
dist/SHA256SUMS
publish-pypi:
name: Publish to PyPI
needs: build
if: needs.build.outputs.publishing == 'true'
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write # Mint the short-lived PyPI publishing credential.
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-artifacts
path: dist
- name: Publish Python distributions
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # release/v1
with:
packages-dir: dist/python
publish-npm:
name: Publish to npm
needs:
- build
- publish-pypi
if: needs.build.outputs.publishing == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Mint the npm publishing credential and provenance.
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-artifacts
path: dist
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24.15.0"
registry-url: https://registry.npmjs.org
package-manager-cache: false
- name: Verify npm release archive
id: package
env:
RELEASE_VERSION: ${{ needs.build.outputs.version }}
run: |
set -euo pipefail
npm_version=$(npm --version)
node -e '
const [major, minor, patch] = process.argv[1].split(".").map(Number);
if (major < 11 || (major === 11 && (minor < 5 || (minor === 5 && patch < 1)))) {
throw new Error("npm " + process.argv[1] + " does not support trusted publishing");
}
' "$npm_version"
mapfile -t packages < <(compgen -G 'dist/npm/*.tgz')
if [[ "${#packages[@]}" -ne 1 ]]; then
echo "Expected exactly one npm archive, found ${#packages[@]}" >&2
exit 1
fi
package="${packages[0]}"
package_json=$(tar -xOf "$package" package/package.json)
package_name=$(jq -r '.name' <<< "$package_json")
package_version=$(jq -r '.version' <<< "$package_json")
if [[ "$package_name" != "flameox" || "$package_version" != "$RELEASE_VERSION" ]]; then
echo "Archive identity $package_name@$package_version does not match flameox@$RELEASE_VERSION" >&2
exit 1
fi
npm publish "$package" --access public --dry-run
echo "path=$package" >> "$GITHUB_OUTPUT"
- name: Publish npm package
env:
NPM_PACKAGE: ${{ steps.package.outputs.path }}
run: npm publish "$NPM_PACKAGE" --access public --provenance
publish-mcp-registry:
name: Publish to MCP Registry
needs:
- build
- publish-pypi
if: needs.build.outputs.publishing == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Mint the registry-bound GitHub OIDC credential.
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Verify published PyPI identity
env:
RELEASE_VERSION: ${{ needs.build.outputs.version }}
run: |
set -euo pipefail
for attempt in {1..12}; do
published_version=$(curl \
--fail \
--silent \
--show-error \
"https://pypi.org/pypi/flameox/${RELEASE_VERSION}/json" \
| jq -r '.info.version' 2>/dev/null || true)
if [[ "$published_version" == "$RELEASE_VERSION" ]]; then
exit 0
fi
if [[ "$attempt" -lt 12 ]]; then
sleep 10
fi
done
echo "PyPI did not expose flameox $RELEASE_VERSION within 120 seconds" >&2
exit 1
- name: Install verified MCP publisher
run: |
set -euo pipefail
archive=mcp-publisher.tar.gz
curl --fail --location --silent --show-error \
--output "$archive" \
"https://github.com/modelcontextprotocol/registry/releases/download/v${MCP_PUBLISHER_VERSION}/mcp-publisher_linux_amd64.tar.gz"
echo "${MCP_PUBLISHER_LINUX_AMD64_SHA256} $archive" | sha256sum --check --strict
tar -xzf "$archive" mcp-publisher
- name: Publish MCP server metadata
run: |
./mcp-publisher login github-oidc
./mcp-publisher publish server.json
verify-npm-latest:
name: Verify npm latest bootstrap
needs:
- build
- publish-npm
if: needs.build.outputs.publishing == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24.15.0"
package-manager-cache: false
- name: Wait for npm latest to expose the release
env:
RELEASE_VERSION: ${{ needs.build.outputs.version }}
run: |
set -euo pipefail
latest=""
for attempt in {1..12}; do
latest=$(npm view flameox dist-tags.latest --registry https://registry.npmjs.org 2>/dev/null || true)
if [[ "$latest" == "$RELEASE_VERSION" ]]; then
break
fi
if [[ "$attempt" -lt 12 ]]; then
sleep 10
fi
done
if [[ "$latest" != "$RELEASE_VERSION" ]]; then
echo "npm latest is $latest, expected $RELEASE_VERSION" >&2
exit 1
fi
- name: Verify the explicit latest npx bootstrap
env:
RELEASE_VERSION: ${{ needs.build.outputs.version }}
run: |
set -euo pipefail
resolved=$(npx --yes --prefer-online flameox@latest --version)
if [[ "$resolved" != "$RELEASE_VERSION" ]]; then
echo "npx flameox@latest resolved $resolved, expected $RELEASE_VERSION" >&2
exit 1
fi
github-release:
name: Create GitHub release
needs:
- build
- publish-pypi
- publish-npm
- publish-mcp-registry
- verify-npm-latest
if: >-
always() &&
needs.build.outputs.publishing == 'true' &&
needs.publish-pypi.result == 'success' &&
needs.publish-npm.result == 'success' &&
needs.publish-mcp-registry.result == 'success' &&
needs.verify-npm-latest.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: write # Create the release after both registry publishes succeed.
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-artifacts
path: dist
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_VERSION: ${{ needs.build.outputs.version }}
run: |
gh release create "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
--title "flameox $RELEASE_VERSION" \
--generate-notes \
dist/python/* \
dist/npm/*.tgz \
dist/SHA256SUMS