Skip to content

Capture: RTSP source #2345

Capture: RTSP source

Capture: RTSP source #2345

# Copyright 2026 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Changeset Check
on:
pull_request:
types: [opened, synchronize, labeled, unlabeled]
branches: [main]
permissions:
contents: read
pull-requests: write
jobs:
# Trusted publishing cannot create a new crate (see release.yml). This check
# runs on the release PR, which is the last gate before publication.
check-crates-published:
name: Check crates exist on crates.io
if: github.head_ref == 'knope/release'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- run: .github/scripts/check_crates_published.sh
check-changeset:
name: Check for changeset
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- name: Test detection script
run: python3 -m unittest discover -s .github/scripts -p 'test_*.py'
- name: Check for changeset
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
REPO: ${{ github.repository }}
run: |
set -eo pipefail
COMMENT_MARKER="<!-- changeset-check -->"
delete_comment() {
local comment_id
comment_id=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --paginate \
--jq ".[] | select(.body | contains(\"${COMMENT_MARKER}\")) | .id" | head -1)
if [ -n "$comment_id" ]; then
gh api -X DELETE "repos/${REPO}/issues/comments/${comment_id}" || true
fi
}
upsert_comment() {
local body="$1"
local comment_id
comment_id=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --paginate \
--jq ".[] | select(.body | contains(\"${COMMENT_MARKER}\")) | .id" | head -1)
if [ -n "$comment_id" ]; then
gh api -X PATCH "repos/${REPO}/issues/comments/${comment_id}" -f body="$body"
else
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$body"
fi
}
# --- Check for internal label ---
HAS_INTERNAL=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels \
--jq '[.labels[].name] | any(. == "internal")')
if [ "$HAS_INTERNAL" = "true" ]; then
echo "PR has 'internal' label - changeset not required"
delete_comment
exit 0
fi
# --- Skip knope's release PR ---
# The release PR consumes changesets (deletes .changeset/*.md and bumps
# versions) rather than adding one, so it would always false-positive.
# It is always opened from the 'knope/release' branch (see release.yml).
if [ "$PR_BRANCH" = "knope/release" ]; then
echo "knope release PR - changeset not required"
delete_comment
exit 0
fi
# --- Get changed files ---
CHANGED_FILES=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate --jq '.[].filename')
# --- Identify changeset files present in the diff ---
CHANGESET_FILES=$(echo "$CHANGED_FILES" | grep '^\.changeset/.*\.md$' || true)
# --- Detect affected packages AND validate existing changeset coverage ---
#
# A changed crate requires a version bump not just for itself, but for
# every crate that (transitively) depends on it. The changeset must
# cover the full closure; a changeset that bumps only a subset is
# incomplete and fails the check. See .github/scripts/changeset_detect.py.
export CHANGED_FILES CHANGESET_FILES
DETECTION=$(python3 .github/scripts/changeset_detect.py)
# Handle cargo metadata errors gracefully
ERROR=$(echo "$DETECTION" | jq -r '.error // empty')
if [ -n "$ERROR" ]; then
echo "::warning::Failed to detect packages via cargo metadata: $ERROR"
fi
# --- Reject bump lines knope can't parse (e.g. quoted package names) ---
# Knope silently ignores entries like `"livekit": patch` (the JS
# changesets convention), so they'd never be released.
NUM_INVALID=$(echo "$DETECTION" | jq '.invalid | length')
if [ "$NUM_INVALID" != "0" ]; then
INVALID_LIST=$(echo "$DETECTION" | jq -r '.invalid[] | "- `\(.file)` line \(.line): `\(.content)` should be `\(.fixed)`"')
COMMENT_BODY=$(
echo "$COMMENT_MARKER"
echo "### Changeset format error"
echo ""
echo "Knope requires unquoted package names in changeset front matter and silently ignores entries in any other format, so the following lines would never be released:"
echo ""
echo "$INVALID_LIST"
)
upsert_comment "$COMMENT_BODY" || true
echo "::error::Changeset front matter contains bump lines knope would ignore (quoted package names). See the PR comment for fixes."
exit 1
fi
# --- Gate: knope.toml must stay in sync with the workspace ---
# A publishable crate missing from knope.toml silently escapes changeset
# enforcement and is never released; a knope entry with no matching crate
# is dangling config. Either is a release-pipeline bug, so fail hard until
# the config is reconciled. This runs before the "no versioned packages"
# early-out below because a brand-new unmanaged crate maps to no knope
# package and would otherwise slip through as "nothing affected".
NUM_UNMANAGED=$(echo "$DETECTION" | jq '.unmanaged | length')
NUM_STALE=$(echo "$DETECTION" | jq '.stale | length')
if [ "$NUM_UNMANAGED" != "0" ] || [ "$NUM_STALE" != "0" ]; then
COMMENT_BODY=$(
echo "$COMMENT_MARKER"
echo "### knope.toml is out of sync with the workspace"
echo ""
if [ "$NUM_UNMANAGED" != "0" ]; then
UNMANAGED_LIST=$(echo "$DETECTION" | jq -r '.unmanaged[] | "- `\(.)`"')
echo "These publishable crates are **not** managed by knope, so they would never get a changeset requirement or a release:"
echo ""
echo "$UNMANAGED_LIST"
echo ""
echo "Fix each one by **either**:"
echo "- adding a \`[packages.<name>]\` block to \`knope.toml\` (if it should be released), **or**"
echo "- setting \`publish = false\` in its \`Cargo.toml\` (if it should not)."
echo ""
fi
if [ "$NUM_STALE" != "0" ]; then
STALE_LIST=$(echo "$DETECTION" | jq -r '.stale[] | "- `\(.)`"')
echo "These \`knope.toml\` packages have no matching workspace crate (renamed or removed?):"
echo ""
echo "$STALE_LIST"
echo ""
echo "Remove the stale \`[packages.<name>]\` block(s) from \`knope.toml\`."
echo ""
fi
)
upsert_comment "$COMMENT_BODY" || echo "::warning::Could not post PR comment (this can happen for fork PRs with limited permissions)"
echo "::error::knope.toml is out of sync with the workspace (unmanaged: ${NUM_UNMANAGED}, stale: ${NUM_STALE}). See the PR comment for details."
exit 1
fi
# --- If no versioned packages are affected, no changeset is required ---
NUM_REQUIRED=$(echo "$DETECTION" | jq '.required | length')
if [ "$NUM_REQUIRED" = "0" ]; then
echo "No versioned packages affected - changeset not required"
delete_comment
exit 0
fi
# --- If every affected package is covered, the changeset is complete ---
NUM_MISSING=$(echo "$DETECTION" | jq '.missing | length')
if [ "$NUM_MISSING" = "0" ]; then
echo "Changeset covers all affected packages"
TABLE_ROWS=$(echo "$DETECTION" | jq -r '.present[] | "| `\(.[0])` | `\(.[1])` |"')
COMMENT_BODY=$(
echo "$COMMENT_MARKER"
echo "### Changeset ✓"
echo ""
echo "This PR includes a changeset covering all affected packages:"
echo ""
echo "| Package | Bump |"
echo "|---------|------|"
echo "$TABLE_ROWS"
)
upsert_comment "$COMMENT_BODY" || true
exit 0
fi
# --- Some affected packages are missing a bump: build comment and fail ---
CHANGESET_CONTENT=$(echo "$DETECTION" | jq -r '.changeset_content')
# Generate a file-name slug from the PR title
SLUG=$(echo "$PR_TITLE" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' '_' | sed 's/^_//; s/_$//')
SLUG="${SLUG:0:60}"
[ -z "$SLUG" ] && SLUG="pr_${PR_NUMBER}"
# Build a GitHub create-file URL that pre-populates the missing bumps
ENCODED_CONTENT=$(printf '%s' "$CHANGESET_CONTENT" | jq -sRr @uri)
CREATE_URL="https://github.com/${REPO}/new/${PR_BRANCH}?filename=.changeset/${SLUG}.md&value=${ENCODED_CONTENT}"
MISSING_LIST=$(echo "$DETECTION" | jq -r '.missing[] | "- `\(.)`"')
NUM_PRESENT=$(echo "$DETECTION" | jq '.present | length')
if [ "$NUM_PRESENT" = "0" ]; then
HEADING="### No changeset found"
INTRO="This PR modifies versioned packages but doesn't include a changeset. The following packages require a version bump:"
else
HEADING="### Changeset incomplete"
PRESENT_LIST=$(echo "$DETECTION" | jq -r '.present[] | "- `\(.[0])` (`\(.[1])`)"')
INTRO="This PR's changeset is missing version bumps for packages that are affected by the change. The following packages still require a bump:"
fi
COMMENT_BODY=$(
echo "$COMMENT_MARKER"
echo "$HEADING"
echo ""
echo "$INTRO"
echo ""
echo "$MISSING_LIST"
echo ""
if [ "$NUM_PRESENT" != "0" ]; then
echo "Already covered:"
echo ""
echo "$PRESENT_LIST"
echo ""
fi
echo "A package must be bumped when its own files change, **and** whenever a package it depends on is bumped (so downstream consumers get a matching release)."
echo ""
echo "[**Click here to create a changeset for the missing packages**](${CREATE_URL})"
echo ""
echo "The link pre-populates a changeset file with \`patch\` bumps for the missing packages. You can also add them to your existing changeset. Edit the bump types as needed before committing."
echo ""
echo "If this change doesn't require a version bump, add the \`internal\` label to this PR."
)
# --- Post or update comment ---
upsert_comment "$COMMENT_BODY" || echo "::warning::Could not post PR comment (this can happen for fork PRs with limited permissions)"
MISSING_CSV=$(echo "$DETECTION" | jq -r '.missing | join(", ")')
echo "::error::Changeset is missing version bumps for: ${MISSING_CSV}. Add them or apply the 'internal' label."
exit 1