ReMe/.github/workflows/release-reme-studio.yml
jinliyl 9ad3dafce5
Some checks failed
CI / DSH plugin / Validate DSH plugin (push) Has been cancelled
CI / OpenClaw plugin / Validate OpenClaw plugin (push) Has been cancelled
CI / Python packages / Build and verify distributions (push) Has been cancelled
CI / Python quality / GitHub Actions (push) Has been cancelled
CI / Python quality / Pre-commit (push) Has been cancelled
CI / Python tests / Unit Tests - py3.11 (push) Has been cancelled
CI / Python tests / Unit Tests - py3.12 (push) Has been cancelled
CI / Python tests / Unit Tests - py3.13 (push) Has been cancelled
CI / Python tests / Unit Tests - py3.14 (push) Has been cancelled
CI / ReMe Studio / Studio checks (push) Has been cancelled
CI / Windows / CLI smoke - py3.11 (push) Has been cancelled
Deploy / Documentation / Build documentation (push) Has been cancelled
Security / CodeQL / Analyze javascript-typescript (push) Has been cancelled
Security / CodeQL / Analyze python (push) Has been cancelled
Deploy / Documentation / deploy (push) Has been cancelled
chore(ci): harden and split workflows (#539)
* chore(ci): harden and split workflows

* fix(ci): support token-based npm publishing

* test: make disappearing resource check portable

* fix(ci): make Studio releases recoverable

* fix(ci): stop Studio publishing on cancellation
2026-09-11 18:21:49 +08:00

243 lines
8.8 KiB
YAML

# Release checklist:
# 1. Update reme_studio/pyproject.toml, package.json, and package-lock.json to the same Studio version.
# 2. Configure npm Trusted Publishing with the npm environment and PyPI Trusted Publishing with the pypi environment.
# 3. Run this workflow manually with the exact Studio version.
name: Release / ReMe Studio
run-name: Publish ReMe Studio ${{ inputs.version }} (${{ inputs.npm_tag }})
on:
workflow_dispatch:
inputs:
version:
description: Version from the Studio Python and npm manifests
required: true
type: string
npm_tag:
description: npm distribution tag
required: true
default: latest
type: choice
options:
- next
- latest
use_npm_token:
description: Use the npm environment NPM_TOKEN instead of Trusted Publishing
required: true
default: false
type: boolean
publish_target:
description: Packages to publish; single-package modes are for release recovery
required: true
default: both
type: choice
options:
- both
- pypi
- npm
permissions:
contents: read
concurrency:
group: publish-reme-studio
cancel-in-progress: false
jobs:
build:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 45
env:
RELEASE_VERSION: ${{ inputs.version }}
NPM_TAG: ${{ inputs.npm_tag }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22.22.3"
cache: npm
cache-dependency-path: reme_studio/package-lock.json
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
- name: Validate Studio package names and version
run: |
python - <<'PY'
import json
import os
import tomllib
from pathlib import Path
studio = Path("reme_studio")
python_manifest = tomllib.loads((studio / "pyproject.toml").read_text(encoding="utf-8"))["project"]
npm_manifest = json.loads((studio / "package.json").read_text(encoding="utf-8"))
expected = os.environ["RELEASE_VERSION"].removeprefix("v")
if python_manifest["name"] != "reme_studio":
raise SystemExit(f"Unexpected Python package name: {python_manifest['name']}")
if npm_manifest["name"] != "@agentscope-ai/reme_studio":
raise SystemExit(f"Unexpected npm package name: {npm_manifest['name']}")
if python_manifest["version"] != expected or npm_manifest["version"] != expected:
raise SystemExit(
f"Studio manifests are {python_manifest['version']} and {npm_manifest['version']}; "
f"workflow input is {expected}",
)
prerelease = "-" in expected
if prerelease != (os.environ["NPM_TAG"] == "next"):
raise SystemExit("Prereleases must use next; stable releases must use latest")
PY
- name: Install dependencies and run checks
working-directory: reme_studio
run: |
npm ci
npm run format:check
npm run lint
npm test
- name: Build Studio distributions
run: |
python -m pip install build twine
mkdir -p dist/studio-python dist/studio-npm
npm pack ./reme_studio --pack-destination dist/studio-npm
python scripts/package_studio.py
python -m build reme_studio --outdir dist/studio-python
python -m twine check dist/studio-python/*
- name: Verify Studio distributions and isolated installation
run: |
STUDIO_WHEEL="$(pwd)/$(ls dist/studio-python/reme_studio-*.whl)"
tar -tzf dist/studio-npm/*.tgz | grep '^package/dist-static/index.html$'
python -m venv "${RUNNER_TEMP}/reme-studio-package-smoke"
"${RUNNER_TEMP}/reme-studio-package-smoke/bin/python" -m pip install "${STUDIO_WHEEL}"
cd "${RUNNER_TEMP}"
"${RUNNER_TEMP}/reme-studio-package-smoke/bin/python" - <<'PY'
from reme_studio import static_dir
assert (static_dir() / "index.html").is_file()
PY
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: reme-studio-${{ inputs.version }}
path: |
dist/studio-python/*
dist/studio-npm/*
if-no-files-found: error
publish-python:
if: inputs.publish_target != 'npm'
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
environment: pypi
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: reme-studio-${{ inputs.version }}
path: dist
- name: Publish ReMe Studio to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: dist/studio-python
skip-existing: true
publish-npm:
if: >-
!cancelled() &&
needs.build.result == 'success' &&
(inputs.publish_target == 'npm' ||
(inputs.publish_target == 'both' && needs.publish-python.result == 'success'))
needs: [build, publish-python]
runs-on: ubuntu-latest
timeout-minutes: 10
environment: npm
permissions:
contents: read
id-token: write
steps:
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
registry-url: https://registry.npmjs.org
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: reme-studio-${{ inputs.version }}
path: dist
- name: Verify the matching PyPI release for npm-only recovery
if: inputs.publish_target == 'npm'
env:
PACKAGE_VERSION: ${{ inputs.version }}
run: |
python - <<'PY'
import os
import urllib.error
import urllib.request
version = os.environ["PACKAGE_VERSION"].removeprefix("v")
url = f"https://pypi.org/pypi/reme-studio/{version}/json"
try:
with urllib.request.urlopen(url, timeout=30) as response:
if response.status != 200:
raise SystemExit(f"Unexpected PyPI response for reme-studio {version}: {response.status}")
except urllib.error.HTTPError as exc:
raise SystemExit(f"reme-studio {version} must exist on PyPI before npm-only recovery") from exc
PY
- name: Check for an identical existing npm package
id: npm-version
env:
PACKAGE_VERSION: ${{ inputs.version }}
run: |
package_file=$(find dist/studio-npm -maxdepth 1 -name '*.tgz' -print -quit)
if [[ -z "${package_file}" ]]; then
echo "Studio npm artifact is missing" >&2
exit 1
fi
local_integrity=$(node --input-type=module - "${package_file}" <<'JS'
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
const digest = createHash('sha512').update(readFileSync(process.argv[2])).digest('base64');
console.log(`sha512-${digest}`);
JS
)
if remote_integrity=$(npm view "@agentscope-ai/reme_studio@${PACKAGE_VERSION#v}" dist.integrity 2>/dev/null); then
if [[ "${remote_integrity}" != "${local_integrity}" ]]; then
echo "Existing npm package has different contents" >&2
exit 1
fi
echo "exists=true" >> "${GITHUB_OUTPUT}"
echo "The identical npm package already exists; nothing to publish"
else
echo "exists=false" >> "${GITHUB_OUTPUT}"
fi
- name: Publish ReMe Studio to npm with Trusted Publishing
if: ${{ steps.npm-version.outputs.exists != 'true' && !inputs.use_npm_token }}
env:
NPM_TAG: ${{ inputs.npm_tag }}
run: npm publish dist/studio-npm/*.tgz --access public --tag "${NPM_TAG}" --provenance
- name: Publish ReMe Studio to npm with NPM_TOKEN
if: ${{ steps.npm-version.outputs.exists != 'true' && inputs.use_npm_token }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TAG: ${{ inputs.npm_tag }}
run: |
if [[ -z "${NODE_AUTH_TOKEN}" ]]; then
echo "NPM_TOKEN is required when use_npm_token is enabled" >&2
exit 1
fi
npm publish dist/studio-npm/*.tgz --access public --tag "${NPM_TAG}" --provenance