docs: restyle README header to match Channels SDK and OpenTag (#375) #401
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/ |