Skip to content

docs: restyle README header to match Channels SDK and OpenTag (#375) #401

docs: restyle README header to match Channels SDK and OpenTag (#375)

docs: restyle README header to match Channels SDK and OpenTag (#375) #401

name: Release
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Check if version is already published
id: check
run: |
PKG_NAME=$(node -p "require('./package.json').name")
PKG_VERSION=$(node -p "require('./package.json').version")
echo "version=${PKG_VERSION}" >> "$GITHUB_OUTPUT"
if npm view "${PKG_NAME}@${PKG_VERSION}" version 2>/dev/null; then
echo "published=true" >> "$GITHUB_OUTPUT"
else
echo "published=false" >> "$GITHUB_OUTPUT"
fi
- name: Sync package.json description from README
if: steps.check.outputs.published == 'false'
run: |
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json','utf-8'));
const lines = fs.readFileSync('README.md','utf-8').split('\n');
for (const line of lines) {
const t = line.trim();
if (!t || t.startsWith('#') || t.startsWith('[') || t.startsWith('http') || t.startsWith('![')) continue;
pkg.description = t.replace(/[*_\`]/g,'').replace(/\s+/g,' ').trim();
break;
}
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
- name: Strip video URLs from README for npm
if: steps.check.outputs.published == 'false'
run: sed -i '/^https:\/\/github.com\/user-attachments\//d' README.md
- name: Build
if: steps.check.outputs.published == 'false'
run: pnpm build
- name: Upload workspace
if: steps.check.outputs.published == 'false'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: workspace
path: .
include-hidden-files: true
retention-days: 1
outputs:
version: ${{ steps.check.outputs.version }}
published: ${{ steps.check.outputs.published }}
publish:
needs: build
if: needs.build.outputs.published == 'false'
runs-on: ubuntu-latest
environment: npm
permissions:
contents: write
id-token: write
actions: write
steps:
- name: Download workspace
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: workspace
- name: Configure git credentials
run: |
git config --local --unset-all http.https://github.com/.extraheader || true
git config --local url."https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/"
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
registry-url: "https://registry.npmjs.org"
cache: pnpm
- name: Publish to npm
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Ensure git tag exists
env:
VERSION: ${{ needs.build.outputs.version }}
run: |
TAG="v${VERSION}"
if ! git rev-parse "${TAG}" >/dev/null 2>&1; then
git tag "${TAG}"
git push origin "${TAG}"
fi
- name: Update major version tag for GitHub Action
env:
VERSION: ${{ needs.build.outputs.version }}
run: |
MAJOR="v$(echo "${VERSION}" | cut -d. -f1)"
git tag -f "${MAJOR}"
git push origin "${MAJOR}" --force
- name: Create GitHub Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.build.outputs.version }}
run: |
TAG="v${VERSION}"
gh release create "${TAG}" --generate-notes --title "${TAG}" --verify-tag --latest
- name: Trigger Docker publish workflow
# GitHub's anti-recursion rule: tags pushed by GITHUB_TOKEN do NOT
# trigger downstream workflows. Explicitly dispatch publish-docker.yml
# so the GHCR image actually gets built on every release.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.build.outputs.version }}
run: |
TAG="v${VERSION}"
gh workflow run publish-docker.yml --ref "${TAG}"
- name: Notify Slack
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
VERSION: ${{ needs.build.outputs.version }}
REPO: ${{ github.repository }}
run: |
if [ -z "$SLACK_WEBHOOK" ]; then echo "SLACK_WEBHOOK not set, skipping"; exit 0; fi
NL=$'\n'
TEXT="📦 *@copilotkit/aimock v${VERSION} published*${NL}npm: https://www.npmjs.com/package/@copilotkit/aimock/v/${VERSION}${NL}Release: https://github.com/${REPO}/releases/tag/v${VERSION}"
PAYLOAD=$(jq -n --arg text "$TEXT" '{text: $text}')
curl -s -X POST "$SLACK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
publish-pytest:
needs: [build, publish]
if: >-
always() &&
(needs.publish.result == 'success' || needs.build.outputs.published == 'true')
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: { persist-credentials: false }
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
package-manager-cache: false
- name: Verify pinned npm release is published
run: |
# Read the pin WITHOUT importing the package: aimock_pytest/__init__.py
# imports _server, which imports `requests`, and no Python deps are
# installed at this point in the job. runpy executes _version.py alone.
VERSION=$(python -c "import runpy; print(runpy.run_path('packages/aimock-pytest/src/aimock_pytest/_version.py')['AIMOCK_VERSION'])")
npm view "@copilotkit/aimock@${VERSION}" version
- name: Install build tools
# HASH-PINNED, and hatchling rather than hatch. Both halves were measured
# on 2026-08-05, not reasoned about:
#
# 1. `pip install hatch` resolves a NAME from PyPI at run time. Run
# verbatim with the registry serving substituted bytes, it installed
# them (exit 0) and `hatch build` then EXECUTED them (exit 0). This
# job carries `environment: pypi` and `id-token: write` and is the
# job that publishes aimock-pytest under our own OIDC identity, so
# that is arbitrary code beside a publish token.
#
# 2. Pinning `hatch` would NOT have been sufficient — the same
# pinned-wrapper/unpinned-payload shape as the Ollama installer.
# `hatch build` builds in an ISOLATED environment and fetches this
# project's `build-system.requires` (hatchling) from PyPI at build
# time, unpinned. Against an empty index `hatch build` dies with
# "hatchling was not found in the package registry"; `python -m
# hatchling build` succeeds, because it fetches nothing.
#
# `--require-hashes` is scoped to THIS invocation on purpose. Applying it
# job-wide (PIP_REQUIRE_HASHES) would also hit the unhashed `pip install
# --dry-run` probe in the next step, whose FAILURE means "not published
# yet" — a hash-gated probe would fail for the wrong reason and turn the
# already-published gate into an unconditional publish.
#
# `python -m pip` rather than `pip` so the interpreter that installs the
# backend is the same one that imports it in the build step below.
run: python -m pip install --require-hashes -r .github/requirements/hatchling.txt
- name: Check if pytest version is already published
id: check
run: |
VERSION=$(python -c "import tomllib; print(tomllib.load(open('packages/aimock-pytest/pyproject.toml', 'rb'))['project']['version'])")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if pip install "aimock-pytest==$VERSION" --dry-run --no-deps 2>/dev/null; then
echo "published=true" >> "$GITHUB_OUTPUT"
else
echo "published=false" >> "$GITHUB_OUTPUT"
fi
- name: Build pytest package
if: steps.check.outputs.published == 'false'
# The published artifacts are unchanged: `hatch build` and `python -m
# hatchling build` produced byte-identical sdist and wheel for this
# package at 0.5.2 — sha256 0a54957ba57c52957c863fdb85b535d9715269f07ff9b4d776de98d0f6d68580
# and 29ac7b3a07d9c73955ab08f75736ebcf728134eceb5264b988e8e933f5ee2c35.
# `python -m` rather than the console script so the build does not depend
# on where the pinned install put its entry points on PATH.
run: cd packages/aimock-pytest && python -m hatchling build
- name: Publish pytest package to PyPI
if: steps.check.outputs.published == 'false'
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
packages-dir: packages/aimock-pytest/dist/