diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000..43c4e92a --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,66 @@ +# Changesets + +Add a changeset to any PR that should be released. Create a markdown file in +this directory (any name, e.g. `fix-token-parsing.md`): + +```md +--- +"livekit-api": patch +--- + +Short description of the change, used in the release notes. +``` + +List one line per package the change should release: + +- **Packages:** `livekit-rtc`, `livekit-api`, `livekit-protocol` +- **Types:** + + | Type | Example | + |---|---| + | `patch` | 1.5.1 → 1.5.2 | + | `minor` | 1.5.1 → 1.6.0 | + | `major` | 1.5.1 → 2.0.0 | + | `patch-rc` | 1.5.1 → 1.5.2.rc1 | + | `minor-rc` | 1.5.1 → 1.6.0.rc1 | + | `major-rc` | 1.5.1 → 2.0.0.rc1 | + | `next-rc` | 1.6.0.rc1 → 1.6.0.rc2 | + | `promote` | 1.6.0.rc2 → 1.6.0 | + +When several pending changesets touch the same package, the largest bump wins. +Mixing stable and rc types for one package, or combining `promote` with +anything else, is rejected. Only rc types are allowed on branches other than +`main`. + +While a package is at an rc (e.g. `1.6.0.rc1`), only `next-rc` and `promote` +are accepted for it, so fixes land in the next rc and the planned `1.6.0` still +ships. Any other type would skip it (`patch` would release `1.6.1`). To move +to a different version line, use the Manual Version Bump workflow below. + +## How releases happen + +1. Every PR is checked by the **Changeset** workflow; invalid changesets fail + the check. +2. On each push to `main`, **Package Release** applies all pending changesets + and opens (or updates) the `Release: ...` PR with the version bumps and + release notes, then deletes the changesets it used. Changesets used by an + rc release are kept instead (and recorded in `.changeset/pre.json`) until + that package's next stable release, normally `promote`, whose release notes + then list every change since the last stable version. +3. Merging that release PR tags each bumped package, publishes it to PyPI and + creates its GitHub Release. + +Changes that don't need a release (docs, CI, tests) don't need a changeset. + +## Manual version bump + +As a fallback, run the **Manual Version Bump** workflow with a package and +bump type. It opens a `release/-v` PR without reading any +changeset; merging it publishes the same way as a changeset release PR. +It doesn't touch `.changeset/`, so changesets kept from rc releases stay until +that package's next changeset stable release. + +The two flows don't know about each other: a manual bump doesn't close an open +`Release: ...` changeset PR for the same package (or the other way round), and +both change that package's version, so once one merges the other conflicts. +Close the stale one before merging. diff --git a/.github/changesets.py b/.github/changesets.py new file mode 100644 index 00000000..a4722fdd --- /dev/null +++ b/.github/changesets.py @@ -0,0 +1,294 @@ +"""Consume pending .changeset/*.md files and bump package versions. + +A changeset looks like: + + --- + "livekit-rtc": patch + "livekit-api": minor-rc + --- + + Summary of the change. + +Supported bump types: patch, minor, major, patch-rc, minor-rc, major-rc, +next-rc, promote. When several changesets touch the same package, the +largest bump wins (an explicit X-rc wins over next-rc). Mixing stable and +rc bumps, or combining promote with anything else, is an error. While a +package is at an rc, only next-rc and promote are accepted for it. + +Changesets used by an rc release are kept and recorded in .changeset/pre.json +(package -> file names) instead of being deleted, so later runs skip them. The +package's next stable release (normally promote) lists them in its release +notes and deletes them. +""" + +from __future__ import annotations + +import json +import os +import pathlib +import re +import subprocess + +import click +from packaging.version import Version +from update_versions import PACKAGES, do_bump, do_prerelease, read_version + +CHANGESET_DIR = pathlib.Path(".changeset") +# Changesets already released in an rc, kept until the package's stable release +PRE_STATE = CHANGESET_DIR / "pre.json" + +# Names accepted in changeset frontmatter -> key in PACKAGES +PACKAGE_ALIASES = { + "livekit": "livekit", + "livekit-rtc": "livekit", + "livekit-api": "livekit-api", + "livekit-protocol": "livekit-protocol", +} + +STABLE = {"patch": 1, "minor": 2, "major": 3} +RC = {"patch-rc": 1, "minor-rc": 2, "major-rc": 3} +BUMP_TYPES = set(STABLE) | set(RC) | {"next-rc", "promote"} + + +def parse_changeset(path: pathlib.Path) -> tuple[dict[str, str], str]: + m = re.match(r"^---\s*\n(.*?)\n?---\s*\n?(.*)$", path.read_text(), re.DOTALL) + if not m: + raise click.ClickException(f"{path}: missing '---' frontmatter") + + bumps: dict[str, str] = {} + for line in m.group(1).splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + lm = re.fullmatch(r'[\'"]?([\w.\-]+)[\'"]?\s*:\s*[\'"]?([\w\-]+)[\'"]?', line) + if not lm: + raise click.ClickException(f"{path}: cannot parse line {line!r}") + name, kind = lm.groups() + if name not in PACKAGE_ALIASES: + raise click.ClickException( + f"{path}: unknown package {name!r} (expected one of {sorted(PACKAGE_ALIASES)})" + ) + if kind not in BUMP_TYPES: + raise click.ClickException( + f"{path}: unknown bump type {kind!r} (expected one of {sorted(BUMP_TYPES)})" + ) + bumps[PACKAGE_ALIASES[name]] = kind + return bumps, m.group(2).strip() + + +def resolve(package: str, kinds: set[str]) -> str: + if "promote" in kinds: + if len(kinds) > 1: + raise click.ClickException( + f"{package}: 'promote' cannot be combined with {sorted(kinds - {'promote'})}" + ) + return "promote" + + stable = kinds & STABLE.keys() + if stable and kinds - stable: + raise click.ClickException(f"{package}: cannot mix stable and rc bumps ({sorted(kinds)})") + if stable: + return max(stable, key=STABLE.__getitem__) + + explicit_rc = kinds & RC.keys() + if explicit_rc: + return max(explicit_rc, key=RC.__getitem__) + return "next-rc" + + +def validate(package: str, kind: str) -> None: + cur = Version(read_version(pathlib.Path(PACKAGES[package]))) + if kind == "next-rc" and not (cur.pre and cur.pre[0] == "rc"): + raise click.ClickException(f"{package}: next-rc requires an rc version (got {cur})") + if kind == "promote" and not cur.is_prerelease: + raise click.ClickException(f"{package}: promote requires a pre-release (got {cur})") + # Any other bump would move past the rc's planned stable version, e.g. + # patch on 1.4.0.rc1 releases 1.4.1 and 1.4.0 never ships. + if cur.is_prerelease and kind not in ("next-rc", "promote"): + raise click.ClickException( + f"{package}: {kind} is not allowed while at pre-release {cur}; " + "use next-rc or promote (or the Manual Version Bump workflow)" + ) + + +def apply(package: str, kind: str) -> None: + if kind in STABLE: + do_bump(package, kind) + elif kind in RC: + do_bump(package, kind.removesuffix("-rc")) + do_prerelease(package, "rc") + elif kind == "next-rc": + do_prerelease(package, "rc") + elif kind == "promote": + do_bump(package, "release") + + +def changeset_origin(path: pathlib.Path, repo: str | None) -> str: + """Return " (#123 by @user)" for the PR that added this changeset, or "".""" + if not repo: + return "" + try: + sha = subprocess.run( + ["git", "log", "--diff-filter=A", "--format=%H", "-1", "--", str(path)], + capture_output=True, + text=True, + check=True, + ).stdout.strip() + if not sha: + return "" + pr = subprocess.run( + [ + "gh", + "api", + f"repos/{repo}/commits/{sha}/pulls", + "--jq", + '.[0] // empty | "\\(.number) \\(.user.login)"', + ], + capture_output=True, + text=True, + check=True, + ).stdout.split() + except (OSError, subprocess.CalledProcessError) as e: + click.echo(f"warning: cannot resolve PR for {path}: {e}", err=True) + return "" + if len(pr) == 2: + return f" (#{pr[0]} by @{pr[1]})" + return f" ({sha[:7]})" + + +def kept_entries(names: list[str], package: str, repo: str | None) -> list[str]: + """Release-note entries for this package from changesets kept since its rc releases.""" + entries = [] + for name in names: + path = CHANGESET_DIR / name + bumps, summary = parse_changeset(path) + if summary and package in bumps: + entries.append(f"`{bumps[package]}` {summary}{changeset_origin(path, repo)}") + return entries + + +@click.command() +@click.option( + "--rc-only", + is_flag=True, + default=False, + help="Reject stable bumps (used for non-main branches).", +) +@click.option( + "--notes", + type=click.Path(dir_okay=False, path_type=pathlib.Path), + default=None, + help="Write release notes (markdown) to this file.", +) +@click.option( + "--repo", + envvar="GITHUB_REPOSITORY", + default=None, + help="owner/name used to look up the PR and author of each changeset (needs gh).", +) +@click.option( + "--check", + is_flag=True, + default=False, + help="Only validate the pending changesets; don't change any file.", +) +def main(rc_only: bool, notes: pathlib.Path | None, repo: str | None, check: bool) -> None: + pre: dict[str, list[str]] = json.loads(PRE_STATE.read_text()) if PRE_STATE.exists() else {} + kept = {name for names in pre.values() for name in names} + # A kept changeset still owes its entry to the stable release notes. + missing = sorted(name for name in kept if not (CHANGESET_DIR / name).exists()) + if missing: + raise click.ClickException( + f"changesets kept from rc releases are missing: {missing} " + f"(restore them, or remove them from {PRE_STATE} on purpose)" + ) + # Parse them now too, so a broken kept changeset fails the PR check + # instead of the stable release. + for name in sorted(kept): + parse_changeset(CHANGESET_DIR / name) + files = sorted( + p + for p in CHANGESET_DIR.glob("*.md") + if p.name.lower() != "readme.md" and p.name not in kept + ) + + kinds: dict[str, set[str]] = {} + summaries: dict[str, list[str]] = {} + packages_of: dict[str, set[str]] = {} + for path in files: + bumps, summary = parse_changeset(path) + packages_of[path.name] = set(bumps) + if summary and not check: + summary += changeset_origin(path, repo) + for package, kind in bumps.items(): + kinds.setdefault(package, set()).add(kind) + if summary: + # Each entry shows the bump type its changeset asked for. + summaries.setdefault(package, []).append(f"`{kind}` {summary}") + + resolved = {package: resolve(package, k) for package, k in kinds.items()} + if rc_only: + stable = {p: k for p, k in resolved.items() if k in STABLE or k == "promote"} + if stable: + raise click.ClickException( + f"only rc releases are allowed from non-main branches (got {stable})" + ) + # Validate everything before touching any version file. + for package, kind in resolved.items(): + validate(package, kind) + + if check: + click.echo(f"{len(files)} changeset(s) valid ({len(kept)} kept from rc releases).") + for package in PACKAGES: + if package in resolved: + cur = read_version(pathlib.Path(PACKAGES[package])) + click.echo(f" {package}: {cur} ({resolved[package]})") + return + + lines: list[str] = [] + released: list[str] = [] + for package in PACKAGES: # stable order + if package not in resolved: + continue + kind = resolved[package] + apply(package, kind) + version = read_version(pathlib.Path(PACKAGES[package])) + released.append(f"{package} v{version}") + lines.append(f"## {package} v{version} ({kind})\n") + entries = summaries.get(package, []) + if kind in RC or kind == "next-rc": + # Keep this rc's changesets for the stable release notes. + pre.setdefault(package, []).extend( + p.name for p in files if package in packages_of[p.name] + ) + else: + # A stable release (normally promote) also lists the changesets + # kept since this package's rc releases. + entries = kept_entries(pre.pop(package, []), package, repo) + entries + for summary in entries: + lines.append("- " + summary.replace("\n", "\n ")) + lines.append("") + + # Changesets are consumed once applied (including ones listing no package), + # except those still kept for a package's stable release. + still_kept = {name for names in pre.values() for name in names} + for name in {p.name for p in files} | kept: + if name not in still_kept: + (CHANGESET_DIR / name).unlink(missing_ok=True) + if pre: + PRE_STATE.write_text(json.dumps(pre, indent=2, sort_keys=True) + "\n") + else: + PRE_STATE.unlink(missing_ok=True) + + if notes is not None: + notes.write_text("\n".join(lines)) + + title = ", ".join(released) + print(f"Release: {title or '(nothing to release)'}") + if gh_output := os.environ.get("GITHUB_OUTPUT"): + with open(gh_output, "a") as f: + f.write(f"title={title}\n") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/changeset-reminder.yml b/.github/workflows/changeset-reminder.yml new file mode 100644 index 00000000..508fea72 --- /dev/null +++ b/.github/workflows/changeset-reminder.yml @@ -0,0 +1,81 @@ +name: Changeset Reminder + +# Comment once with a short format guide when a PR has no .changeset file, and +# delete that comment once one is added. +on: + # Needed for a write token on fork PRs. This workflow never checks out or + # runs PR code; it only reads the PR's file list through the API. + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +jobs: + remind: + name: Changeset reminder + if: | + !startsWith(github.event.pull_request.head.ref, 'release/changesets-') + && !startsWith(github.event.pull_request.head.ref, 'renovate/') + && !endsWith(github.event.pull_request.user.login, '[bot]') + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Comment when the PR has no changeset + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + marker="" + + files=$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/files" \ + --jq '.[] | "\(.status) \(.filename)"') + + # Dependency-only updates (lockfiles / package.json, and the root + # pyproject.toml, which is the unpublished workspace config) don't need + # a changeset. Package pyproject.toml files declare published runtime + # dependencies, so changing them still gets the reminder. + if [ -z "$(grep -vE '^[a-z]+ ((.*/)?([^/]+\.lock|package\.json|pnpm-lock\.yaml)|pyproject\.toml)$' <<< "$files")" ]; then + echo "Only dependency files changed, skipping" + exit 0 + fi + + changesets=$(grep -v '^removed ' <<< "$files" | cut -d' ' -f2- \ + | grep -E '^\.changeset/[^/]+\.md$' | grep -viE '/readme\.md$' || true) + existing=$(gh api --paginate "repos/$REPO/issues/$PR_NUMBER/comments" \ + --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | contains(\"$marker\"))) | .id") + + if [ -n "$changesets" ]; then + echo "Changeset found:"; echo "$changesets" + # Drop the reminder once a changeset has been added + for id in $existing; do + gh api -X DELETE "repos/$REPO/issues/comments/$id" + done + exit 0 + fi + + if [ -n "$existing" ]; then + echo "Reminder already posted" + exit 0 + fi + + cat > "$RUNNER_TEMP/reminder.md" <<'EOF' + + 👋 This PR has no changeset. If it should be released, add a markdown file under `.changeset/` (any name, e.g. `.changeset/fix-token-parsing.md`): + + ```md + --- + "livekit-rtc": patch + --- + + Short description of the change, used in the release notes. + ``` + + - **Packages:** `livekit-rtc`, `livekit-api`, `livekit-protocol` (one line per package) + - **Types:** `patch`, `minor`, `major`, `patch-rc`, `minor-rc`, `major-rc`, `next-rc`, `promote` + + If this change doesn't need a release (docs, CI, tests), you can ignore this comment. + EOF + gh pr comment "$PR_NUMBER" --repo "$REPO" --body-file "$RUNNER_TEMP/reminder.md" diff --git a/.github/workflows/changeset.yml b/.github/workflows/changeset.yml new file mode 100644 index 00000000..b7eb99fa --- /dev/null +++ b/.github/workflows/changeset.yml @@ -0,0 +1,49 @@ +name: Changeset + +# Validate .changeset files on every commit of every PR, with the same rules +# the Package Release workflow applies when it bumps versions. The reminder for +# PRs without a changeset lives in changeset-reminder.yml; keeping it separate +# means this workflow never reports a skipped "Validate changesets" check that +# could satisfy a required status check. +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +jobs: + validate: + name: Validate changesets + # renovate/* PRs are dependency/security updates and never carry changesets + if: "!startsWith(github.event.pull_request.head.ref, 'renovate/')" + runs-on: ubuntu-latest + steps: + # Default ref is the PR merge commit, i.e. the base branch's versions plus + # every changeset that would be pending once this PR merges. + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 + with: + python-version: "3.10" + + - name: Install dependencies + run: pip install click packaging + + - name: Validate changesets + env: + BASE_BRANCH: ${{ github.base_ref }} + run: | + # Only RC releases are allowed from non-main branches + args=() + if [ "$BASE_BRANCH" != "main" ]; then + args+=(--rc-only) + fi + + if ! out=$(python .github/changesets.py --check "${args[@]}" 2>&1); then + echo "$out" + echo "::error title=Invalid changeset::${out#Error: }" + exit 1 + fi + echo "$out" diff --git a/.github/workflows/manual-version-bump.yml b/.github/workflows/manual-version-bump.yml new file mode 100644 index 00000000..a68ed540 --- /dev/null +++ b/.github/workflows/manual-version-bump.yml @@ -0,0 +1,152 @@ +name: Manual Version Bump + +# Fallback for the changeset flow: bump one package by hand and open a release +# PR. Merging that PR publishes through the Package Release workflow. +on: + workflow_dispatch: + inputs: + package: + description: "Package to publish" + type: choice + required: true + options: + - livekit + - livekit-api + - livekit-protocol + version: + description: "What to publish" + type: choice + required: true + options: + - "patch (1.5.1 → 1.5.2)" + - "minor (1.5.1 → 1.6.0)" + - "major (1.5.1 → 2.0.0)" + - "patch-rc (1.5.1 → 1.5.2.rc1)" + - "minor-rc (1.5.1 → 1.6.0.rc1)" + - "major-rc (1.5.1 → 2.0.0.rc1)" + - "next-rc (.rc1 → .rc2)" + - "promote (1.6.0.rc2 → 1.6.0)" + branch: + description: "Branch to publish from (default: main)" + type: string + required: false + default: "main" + +permissions: + contents: write + pull-requests: write + +jobs: + bump: + name: Create release PR + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + ref: ${{ inputs.branch }} + submodules: true + + - name: Guard non-main branches + env: + INPUT_VERSION: ${{ inputs.version }} + INPUT_BRANCH: ${{ inputs.branch }} + run: | + key=$(echo "$INPUT_VERSION" | awk '{print $1}') + if [ "$INPUT_BRANCH" != "main" ]; then + case "$key" in + *-rc|next-rc) ;; # allowed + *) echo "::error::Only RC releases are allowed from non-main branches (got '$key' on '$INPUT_BRANCH')"; exit 1 ;; + esac + fi + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 + with: + python-version: "3.10" + + - name: Install dependencies + run: pip install click packaging + + - name: Bump version + env: + INPUT_VERSION: ${{ inputs.version }} + INPUT_PACKAGE: ${{ inputs.package }} + run: | + key=$(echo "$INPUT_VERSION" | awk '{print $1}') + pkg="$INPUT_PACKAGE" + + case "$key" in + patch|minor|major) + python .github/update_versions.py --package "$pkg" --bump-type "$key" + ;; + patch-rc|minor-rc|major-rc) + bump="${key%-rc}" + python .github/update_versions.py --package "$pkg" --bump-type "$bump" + python .github/update_versions.py --package "$pkg" --pre rc + ;; + next-rc) + python .github/update_versions.py --package "$pkg" --pre rc + ;; + promote) + python .github/update_versions.py --package "$pkg" --bump-type release + ;; + esac + + - name: Read new version + id: version + env: + INPUT_PACKAGE: ${{ inputs.package }} + run: | + eval "$(python .github/update_versions.py --package "$INPUT_PACKAGE" --print-version)" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "tag_prefix=$tag_prefix" >> "$GITHUB_OUTPUT" + echo "Package: $INPUT_PACKAGE, New version: $version, Tag: ${tag_prefix}-v${version}" + + - name: Close existing release PRs for this package + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_PREFIX: ${{ steps.version.outputs.tag_prefix }} + run: | + prefix="release/${TAG_PREFIX}-v" + gh pr list --state open --json number,headRefName \ + --jq ".[] | select(.headRefName | startswith(\"$prefix\")) | .number" | while read -r pr; do + echo "Superseding release PR #$pr" + gh pr comment "$pr" --body "Superseded by a new release." + gh pr close "$pr" --delete-branch || true + done + + - name: Create release PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} + TAG_PREFIX: ${{ steps.version.outputs.tag_prefix }} + INPUT_BRANCH: ${{ inputs.branch }} + INPUT_PACKAGE: ${{ inputs.package }} + INPUT_VERSION: ${{ inputs.version }} + ACTOR: ${{ github.actor }} + run: | + branch="release/${TAG_PREFIX}-v${VERSION}" + key=$(echo "$INPUT_VERSION" | awk '{print $1}') + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$branch" + git add -A + git commit -m "${TAG_PREFIX}-v${VERSION}" + git push --force origin "$branch" + + # Same section format as the changeset release PR, so Package Release + # can take the GitHub release notes from it. + { + echo "Merging this PR will publish the following packages to PyPI:" + echo + echo "## ${INPUT_PACKAGE} v${VERSION} (${key})" + echo + echo "- Manual version bump by @${ACTOR}." + } > "$RUNNER_TEMP/pr-body.md" + + gh pr create \ + --base "$INPUT_BRANCH" \ + --head "$branch" \ + --title "${INPUT_PACKAGE} v${VERSION}" \ + --body-file "$RUNNER_TEMP/pr-body.md" diff --git a/.github/workflows/package-release.yml b/.github/workflows/package-release.yml new file mode 100644 index 00000000..cd09c4fa --- /dev/null +++ b/.github/workflows/package-release.yml @@ -0,0 +1,495 @@ +name: Package Release + +on: + # Step 1: A push to main (e.g. a merged PR, including from forks) or a manual + # run consumes pending .changeset files and opens/updates the release PR with + # the version bumps. This runs on `push` rather than `pull_request` so it gets + # a write-capable token for fork contributions too. + push: + branches: [main] + + workflow_dispatch: + inputs: + branch: + description: "Branch to release from (default: main)" + type: string + required: false + default: "main" + + # Step 2: Merging the release PR triggers build + publish + pull_request: + types: [closed] + +# Read-only by default; jobs that write ask for it themselves, so build jobs +# (which run third-party build code) never get a write or OIDC token. +permissions: + contents: read + +jobs: + # ── Step 1: Create / update the release PR from changesets ──── + bump: + name: Create release PR + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + concurrency: + group: package-release-${{ inputs.branch || github.ref_name }} + cancel-in-progress: true + env: + BASE_BRANCH: ${{ inputs.branch || github.ref_name }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + ref: ${{ env.BASE_BRANCH }} + # Full history to find the commit (and PR) that added each changeset + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 + with: + python-version: "3.10" + + - name: Install dependencies + run: pip install click packaging + + - name: Apply changesets + id: changesets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Only RC releases are allowed from non-main branches + args=() + if [ "$BASE_BRANCH" != "main" ]; then + args+=(--rc-only) + fi + python .github/changesets.py --notes "$RUNNER_TEMP/release-notes.md" "${args[@]}" + + - name: Create or update release PR + if: steps.changesets.outputs.title != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TITLE: ${{ steps.changesets.outputs.title }} + run: | + branch="release/changesets-${BASE_BRANCH//\//-}" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$branch" + git add -A + git commit -m "Release: ${TITLE}" + git push --force origin "$branch" + + { + echo "Merging this PR will publish the following packages to PyPI:" + echo + cat "$RUNNER_TEMP/release-notes.md" + } > "$RUNNER_TEMP/pr-body.md" + + existing=$(gh pr list --state open --head "$branch" --base "$BASE_BRANCH" --json number --jq '.[0].number // empty') + if [ -n "$existing" ]; then + gh pr edit "$existing" --title "Release: ${TITLE}" --body-file "$RUNNER_TEMP/pr-body.md" + else + gh pr create \ + --base "$BASE_BRANCH" \ + --head "$branch" \ + --title "Release: ${TITLE}" \ + --body-file "$RUNNER_TEMP/pr-body.md" + fi + + # Pending changesets can disappear after the release PR was opened (e.g. + # reverted); merging that stale PR would still publish. + - name: Close stale release PR + if: steps.changesets.outputs.title == '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + branch="release/changesets-${BASE_BRANCH//\//-}" + existing=$(gh pr list --state open --head "$branch" --base "$BASE_BRANCH" --json number --jq '.[0].number // empty') + if [ -n "$existing" ]; then + gh pr comment "$existing" --body "Closing: no pending changesets left to release." + gh pr close "$existing" --delete-branch + fi + + # ── Step 2: Publish on merge ────────────────────────────────── + detect: + name: Detect packages + # Only release PRs opened by a bump job (same repo, github-actions[bot], + # release/* branch: release/changesets-* from this workflow or + # release/-v from Manual Version Bump) may trigger + # publishing; the branch name alone is not trusted. + if: | + github.event_name == 'pull_request' + && github.event.pull_request.merged == true + && startsWith(github.event.pull_request.head.ref, 'release/') + && github.event.pull_request.head.repo.full_name == github.repository + && github.event.pull_request.user.login == 'github-actions[bot]' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + rtc: ${{ steps.detect.outputs.rtc }} + api: ${{ steps.detect.outputs.api }} + protocol: ${{ steps.detect.outputs.protocol }} + tags: ${{ steps.detect.outputs.tags }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + ref: ${{ github.event.pull_request.merge_commit_sha }} + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 + with: + python-version: "3.10" + + - name: Install dependencies + run: pip install click packaging + + - name: Detect bumped packages + id: detect + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + # A package is released when the release PR changed its version.py + files=$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/files" --jq '.[].filename') + + tags=() + for entry in "rtc:livekit:livekit-rtc/livekit/rtc/version.py" \ + "api:livekit-api:livekit-api/livekit/api/version.py" \ + "protocol:livekit-protocol:livekit-protocol/livekit/protocol/version.py"; do + IFS=: read -r key package path <<< "$entry" + if grep -qxF "$path" <<< "$files"; then + eval "$(python .github/update_versions.py --package "$package" --print-version)" + tags+=("${tag_prefix}-v${version}") + echo "$key=true" >> "$GITHUB_OUTPUT" + echo "Releasing $package v$version" + else + echo "$key=false" >> "$GITHUB_OUTPUT" + fi + done + + echo "tags=${tags[*]}" >> "$GITHUB_OUTPUT" + + tag: + name: Tag & create GitHub releases + needs: detect + if: needs.detect.outputs.tags != '' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + ref: ${{ github.event.pull_request.merge_commit_sha }} + + - name: Create git tags + env: + TAGS: ${{ needs.detect.outputs.tags }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + for tag in $TAGS; do + git tag "$tag" + git push origin "$tag" + done + + - name: Create GitHub releases + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAGS: ${{ needs.detect.outputs.tags }} + PR_BODY: ${{ github.event.pull_request.body }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + printf '%s\n' "$PR_BODY" > "$RUNNER_TEMP/pr-body.md" + + for tag in $TAGS; do + prefix="${tag%%-v*}" + version="${tag#*-v}" + case "$prefix" in + rtc) package="livekit" ;; + api) package="livekit-api" ;; + protocol) package="livekit-protocol" ;; + esac + + # Take this package's "## v (...)" section from + # the release PR body (written from the consumed changesets) + notes="$RUNNER_TEMP/notes-$tag.md" + awk -v heading="## $package v$version " ' + index($0, heading) == 1 { found = 1; next } + found && /^## / { exit } + # Buffer blank lines so leading/trailing ones are dropped + found && /^[[:space:]]*$/ { if (started) blank = blank "\n"; next } + found { printf "%s%s\n", blank, $0; blank = ""; started = 1 } + ' "$RUNNER_TEMP/pr-body.md" > "$notes" + if ! grep -q '[^[:space:]]' "$notes"; then + echo "Released from #$PR_NUMBER." > "$notes" + fi + + args=() + if [[ "$version" == *rc* ]]; then + args+=(--prerelease) + fi + # Created as a draft; each publish job makes its release public + # only after the PyPI upload succeeds. + gh release create "$tag" \ + --draft \ + --verify-tag \ + --title "$package v$version" \ + --notes-file "$notes" \ + "${args[@]}" + done + + # ── RTC builds (multi-platform) ────────────────────────────── + build-rtc: + name: Build RTC wheels (${{ matrix.archs }}) + needs: detect + if: needs.detect.outputs.rtc == 'true' + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + archs: x86_64 + - os: namespace-profile-default-arm64 + archs: aarch64 + - os: windows-latest + archs: AMD64 + - os: macos-latest + archs: x86_64 arm64 + defaults: + run: + working-directory: ./livekit-rtc + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + submodules: true + + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 + id: setup-python + with: + python-version: "3.11" + + - name: Build wheels + run: pipx run --python '${{ steps.setup-python.outputs.python-path }}' cibuildwheel==3.3.1 --output-dir dist + env: + CIBW_ARCHS: ${{ matrix.archs }} + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: dist-rtc-${{ matrix.os }} + path: livekit-rtc/dist/*.whl + + build-rtc-sdist: + name: Build RTC sdist + needs: detect + if: needs.detect.outputs.rtc == 'true' + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./livekit-rtc + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + submodules: true + + - name: Build sdist + run: | + pip3 install build + python3 -m build --sdist + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: dist-rtc-sdist + path: livekit-rtc/dist/*.tar.gz + + publish-rtc: + name: Publish livekit (RTC) + needs: [detect, build-rtc, build-rtc-sdist, tag] + if: needs.detect.outputs.rtc == 'true' + runs-on: ubuntu-latest + environment: pypi + permissions: + contents: write + id-token: write + steps: + - name: Download build artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + pattern: dist-rtc-* + path: dist + merge-multiple: true + + - name: List distributions + run: ls -la dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + + - name: Publish GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAGS: ${{ needs.detect.outputs.tags }} + run: | + for tag in $TAGS; do + if [[ "$tag" == rtc-v* ]]; then + gh release edit "$tag" --repo "$GITHUB_REPOSITORY" --draft=false + fi + done + + # ── API build ──────────────────────────────────────────────── + build-api: + name: Build API + needs: detect + if: needs.detect.outputs.api == 'true' + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./livekit-api + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + submodules: true + + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 + + - name: Build wheel & sdist + run: | + pip3 install build wheel + python3 -m build --wheel --sdist + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: dist-api + path: | + livekit-api/dist/*.whl + livekit-api/dist/*.tar.gz + + publish-api: + name: Publish livekit-api + needs: [detect, build-api, tag] + if: needs.detect.outputs.api == 'true' + runs-on: ubuntu-latest + environment: pypi + permissions: + contents: write + id-token: write + steps: + - name: Download build artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: dist-api + path: dist/ + + - name: List distributions + run: ls -la dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + + - name: Publish GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAGS: ${{ needs.detect.outputs.tags }} + run: | + for tag in $TAGS; do + if [[ "$tag" == api-v* ]]; then + gh release edit "$tag" --repo "$GITHUB_REPOSITORY" --draft=false + fi + done + + # ── Protocol build ─────────────────────────────────────────── + build-protocol: + name: Build Protocol + needs: detect + if: needs.detect.outputs.protocol == 'true' + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./livekit-protocol + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + submodules: true + + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 + + - name: Build wheel & sdist + run: | + pip3 install build wheel + python3 -m build --wheel --sdist + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: dist-protocol + path: | + livekit-protocol/dist/*.whl + livekit-protocol/dist/*.tar.gz + + publish-protocol: + name: Publish livekit-protocol + needs: [detect, build-protocol, tag] + if: needs.detect.outputs.protocol == 'true' + runs-on: ubuntu-latest + environment: pypi + permissions: + contents: write + id-token: write + steps: + - name: Download build artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: dist-protocol + path: dist/ + + - name: List distributions + run: ls -la dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + + - name: Publish GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAGS: ${{ needs.detect.outputs.tags }} + run: | + for tag in $TAGS; do + if [[ "$tag" == protocol-v* ]]; then + gh release edit "$tag" --repo "$GITHUB_REPOSITORY" --draft=false + fi + done + + # ── Docs ───────────────────────────────────────────────────── + docs-rtc: + name: Build RTC docs + needs: [detect, publish-rtc] + if: needs.detect.outputs.rtc == 'true' + uses: ./.github/workflows/build-docs.yml + with: + package_dir: livekit-rtc + package_name: livekit.rtc + secrets: inherit + + docs-api: + name: Build API docs + needs: [detect, publish-api] + if: needs.detect.outputs.api == 'true' + uses: ./.github/workflows/build-docs.yml + with: + package_dir: livekit-api + package_name: livekit.api + secrets: inherit + + docs-protocol: + name: Build Protocol docs + needs: [detect, publish-protocol] + if: needs.detect.outputs.protocol == 'true' + uses: ./.github/workflows/build-docs.yml + with: + package_dir: livekit-protocol + package_name: livekit.protocol + secrets: inherit diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 872fc06e..00000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,411 +0,0 @@ -name: Publish to PyPI - -on: - # Step 1: Dispatch creates a version bump PR - workflow_dispatch: - inputs: - package: - description: "Package to publish" - type: choice - required: true - options: - - livekit - - livekit-api - - livekit-protocol - version: - description: "What to publish" - type: choice - required: true - options: - - "patch (1.5.1 → 1.5.2)" - - "minor (1.5.1 → 1.6.0)" - - "major (1.5.1 → 2.0.0)" - - "patch-rc (1.5.1 → 1.5.2.rc1)" - - "minor-rc (1.5.1 → 1.6.0.rc1)" - - "major-rc (1.5.1 → 2.0.0.rc1)" - - "next-rc (.rc1 → .rc2)" - - "promote (1.6.0.rc2 → 1.6.0)" - branch: - description: "Branch to publish from (default: main)" - type: string - required: false - default: "main" - - # Step 2: Merging the release PR triggers build + publish - pull_request: - types: [closed] - -permissions: - contents: write - pull-requests: write - id-token: write - -jobs: - # ── Step 1: Create a version bump PR ────────────────────────── - bump: - name: Create release PR - if: github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - with: - ref: ${{ inputs.branch }} - submodules: true - - - name: Guard non-main branches - env: - INPUT_VERSION: ${{ inputs.version }} - INPUT_BRANCH: ${{ inputs.branch }} - run: | - key=$(echo "$INPUT_VERSION" | awk '{print $1}') - if [ "$INPUT_BRANCH" != "main" ]; then - case "$key" in - *-rc|next-rc) ;; # allowed - *) echo "::error::Only RC releases are allowed from non-main branches (got '$key' on '$INPUT_BRANCH')"; exit 1 ;; - esac - fi - - - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 - with: - python-version: "3.10" - - - name: Install dependencies - run: pip install click packaging - - - name: Bump version - env: - INPUT_VERSION: ${{ inputs.version }} - INPUT_PACKAGE: ${{ inputs.package }} - run: | - key=$(echo "$INPUT_VERSION" | awk '{print $1}') - pkg="$INPUT_PACKAGE" - - case "$key" in - patch|minor|major) - python .github/update_versions.py --package "$pkg" --bump-type "$key" - ;; - patch-rc|minor-rc|major-rc) - bump="${key%-rc}" - python .github/update_versions.py --package "$pkg" --bump-type "$bump" - python .github/update_versions.py --package "$pkg" --pre rc - ;; - next-rc) - python .github/update_versions.py --package "$pkg" --pre rc - ;; - promote) - python .github/update_versions.py --package "$pkg" --bump-type release - ;; - esac - - - name: Read new version - id: version - env: - INPUT_PACKAGE: ${{ inputs.package }} - run: | - eval "$(python .github/update_versions.py --package "$INPUT_PACKAGE" --print-version)" - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "tag_prefix=$tag_prefix" >> "$GITHUB_OUTPUT" - echo "Package: $INPUT_PACKAGE, New version: $version, Tag: ${tag_prefix}-v${version}" - - - name: Close existing release PRs for this package - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG_PREFIX: ${{ steps.version.outputs.tag_prefix }} - run: | - prefix="release/${TAG_PREFIX}-v" - gh pr list --state open --json number,headRefName \ - --jq ".[] | select(.headRefName | startswith(\"$prefix\")) | .number" | while read -r pr; do - echo "Superseding release PR #$pr" - gh pr comment "$pr" --body "Superseded by a new release." - gh pr close "$pr" --delete-branch || true - done - - - name: Create release PR - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ steps.version.outputs.version }} - TAG_PREFIX: ${{ steps.version.outputs.tag_prefix }} - INPUT_BRANCH: ${{ inputs.branch }} - INPUT_PACKAGE: ${{ inputs.package }} - run: | - branch="release/${TAG_PREFIX}-v${VERSION}" - - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b "$branch" - git add -A - git commit -m "${TAG_PREFIX}-v${VERSION}" - git push --force origin "$branch" - - gh pr create \ - --base "$INPUT_BRANCH" \ - --head "$branch" \ - --title "${INPUT_PACKAGE} v${VERSION}" \ - --body "Merging this PR will publish **${INPUT_PACKAGE}** v${VERSION} to PyPI." \ - - - # ── Step 2: Publish on merge ────────────────────────────────── - detect: - name: Detect package - if: | - github.event_name == 'pull_request' - && github.event.pull_request.merged == true - && startsWith(github.event.pull_request.head.ref, 'release/') - runs-on: ubuntu-latest - outputs: - package: ${{ steps.detect.outputs.package }} - tag: ${{ steps.detect.outputs.tag }} - steps: - - name: Parse release branch - id: detect - env: - HEAD_REF: ${{ github.event.pull_request.head.ref }} - run: | - # branch is like release/rtc-v1.2.0, release/api-v1.1.1, release/protocol-v1.1.5 - ref="${HEAD_REF#release/}" - prefix="${ref%%-v*}" - - case "$prefix" in - rtc) package="livekit" ;; - api) package="livekit-api" ;; - protocol) package="livekit-protocol" ;; - *) echo "::error::Unknown release prefix: $prefix"; exit 1 ;; - esac - - echo "package=$package" >> "$GITHUB_OUTPUT" - echo "tag=$ref" >> "$GITHUB_OUTPUT" - echo "Releasing $package (tag: $ref)" - - tag: - name: Tag release - needs: detect - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - with: - ref: ${{ github.event.pull_request.merge_commit_sha }} - - - name: Create git tag - env: - TAG: ${{ needs.detect.outputs.tag }} - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag "$TAG" - git push origin "$TAG" - - # ── RTC builds (multi-platform) ────────────────────────────── - build-rtc: - name: Build RTC wheels (${{ matrix.archs }}) - needs: detect - if: needs.detect.outputs.package == 'livekit' - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - archs: x86_64 - - os: namespace-profile-default-arm64 - archs: aarch64 - - os: windows-latest - archs: AMD64 - - os: macos-latest - archs: x86_64 arm64 - defaults: - run: - working-directory: ./livekit-rtc - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - with: - submodules: true - - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 - id: setup-python - with: - python-version: "3.11" - - - name: Build wheels - run: pipx run --python '${{ steps.setup-python.outputs.python-path }}' cibuildwheel==3.3.1 --output-dir dist - env: - CIBW_ARCHS: ${{ matrix.archs }} - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: dist-rtc-${{ matrix.os }} - path: livekit-rtc/dist/*.whl - - build-rtc-sdist: - name: Build RTC sdist - needs: detect - if: needs.detect.outputs.package == 'livekit' - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./livekit-rtc - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - with: - submodules: true - - - name: Build sdist - run: | - pip3 install build - python3 -m build --sdist - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: dist-rtc-sdist - path: livekit-rtc/dist/*.tar.gz - - publish-rtc: - name: Publish livekit (RTC) - needs: [detect, build-rtc, build-rtc-sdist] - if: needs.detect.outputs.package == 'livekit' - runs-on: ubuntu-latest - environment: pypi - permissions: - id-token: write - steps: - - name: Download build artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 - with: - pattern: dist-rtc-* - path: dist - merge-multiple: true - - - name: List distributions - run: ls -la dist/ - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 - - # ── API build ──────────────────────────────────────────────── - build-api: - name: Build API - needs: detect - if: needs.detect.outputs.package == 'livekit-api' - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./livekit-api - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - with: - submodules: true - - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 - - - name: Build wheel & sdist - run: | - pip3 install build wheel - python3 -m build --wheel --sdist - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: dist-api - path: | - livekit-api/dist/*.whl - livekit-api/dist/*.tar.gz - - publish-api: - name: Publish livekit-api - needs: [detect, build-api] - if: needs.detect.outputs.package == 'livekit-api' - runs-on: ubuntu-latest - environment: pypi - permissions: - id-token: write - steps: - - name: Download build artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 - with: - name: dist-api - path: dist/ - - - name: List distributions - run: ls -la dist/ - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 - - # ── Protocol build ─────────────────────────────────────────── - build-protocol: - name: Build Protocol - needs: detect - if: needs.detect.outputs.package == 'livekit-protocol' - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./livekit-protocol - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - with: - submodules: true - - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 - - - name: Build wheel & sdist - run: | - pip3 install build wheel - python3 -m build --wheel --sdist - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: dist-protocol - path: | - livekit-protocol/dist/*.whl - livekit-protocol/dist/*.tar.gz - - publish-protocol: - name: Publish livekit-protocol - needs: [detect, build-protocol] - if: needs.detect.outputs.package == 'livekit-protocol' - runs-on: ubuntu-latest - environment: pypi - permissions: - id-token: write - steps: - - name: Download build artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 - with: - name: dist-protocol - path: dist/ - - - name: List distributions - run: ls -la dist/ - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 - - # ── Docs ───────────────────────────────────────────────────── - docs-rtc: - name: Build RTC docs - needs: [detect, publish-rtc] - if: needs.detect.outputs.package == 'livekit' - uses: ./.github/workflows/build-docs.yml - with: - package_dir: livekit-rtc - package_name: livekit.rtc - secrets: inherit - - docs-api: - name: Build API docs - needs: [detect, publish-api] - if: needs.detect.outputs.package == 'livekit-api' - uses: ./.github/workflows/build-docs.yml - with: - package_dir: livekit-api - package_name: livekit.api - secrets: inherit - - docs-protocol: - name: Build Protocol docs - needs: [detect, publish-protocol] - if: needs.detect.outputs.package == 'livekit-protocol' - uses: ./.github/workflows/build-docs.yml - with: - package_dir: livekit-protocol - package_name: livekit.protocol - secrets: inherit