# 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