Skip to content

Commit 20d54f4

Browse files
committed
Merge remote-tracking branch 'origin/main' into ladvoc/livekit-capture
2 parents 975f8d8 + 0b5462a commit 20d54f4

46 files changed

Lines changed: 941 additions & 324 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/fix-publisher-renegotiation-deadlock.md

Lines changed: 0 additions & 6 deletions
This file was deleted.

.changeset/fix_nvenc_dynamic_bitrate_updates.md

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
libwebrtc: patch
3+
livekit: patch
4+
livekit-ffi: patch
5+
webrtc-sys: patch
6+
---
7+
8+
Only advertise internal H264 decode formats if the decoder works - #1313 (@MaxHeimbrock)

.github/actions/uniffi-deps/action.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,3 @@ runs:
55
- name: Install cargo-make
66
uses: taiki-e/install-action@682e7d9e49c5e653d371fc6adbda67653461378a # v2.82.4
77
with: { tool: cargo-make@0.37 }
8-
- name: Install tera-cli
9-
uses: taiki-e/install-action@682e7d9e49c5e653d371fc6adbda67653461378a # v2.82.4
10-
with: { tool: tera-cli@0.5.0 }
11-
# ^ This would be installed by the cargo-make file automatically, but this ensures
12-
# it is already available and downloads binary distribution (no need to build from source).

.github/scripts/changeset_detect.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
"present": [[pkg, bump], ...], # bumps already in the changeset
3636
"missing": [pkg, ...], # required packages not yet covered
3737
"invalid": [{...}, ...], # bump lines in a format knope would ignore
38+
"unmanaged": [pkg, ...], # publishable crates missing from knope.toml
39+
"stale": [pkg, ...], # knope.toml entries with no workspace crate
3840
"changeset_content": str, # a changeset prefilled with `missing`
3941
}
4042
"""
@@ -204,6 +206,38 @@ def detect(meta, knope_packages, changed_files, present):
204206
}
205207

206208

209+
def is_publishable(pkg):
210+
"""True unless the crate opts out of publishing with `publish = false`.
211+
212+
`cargo metadata` encodes the manifest's `publish` field as: `null` (default,
213+
publishable to any registry), `[]` (`publish = false`), or a list of allowed
214+
registry names. Only the empty list means "never published".
215+
"""
216+
return pkg.get("publish") != []
217+
218+
219+
def reconcile_knope_config(meta, knope_packages):
220+
"""Find drift between the Cargo workspace and knope.toml.
221+
222+
Returns (unmanaged, stale):
223+
unmanaged publishable workspace crates absent from knope.toml. These
224+
silently escape changeset enforcement and are never released.
225+
Fix by adding a `[packages.<name>]` block, or by setting
226+
`publish = false` if the crate isn't meant to ship.
227+
stale knope.toml package names with no matching workspace crate
228+
(e.g. a renamed or removed crate leaving a dangling entry).
229+
230+
Note the rule is one-directional: publishable implies knope-managed, but a
231+
knope-managed crate may set `publish = false` (e.g. livekit-ffi / livekit-
232+
uniffi, which CI publishes via wrapper packages rather than `cargo publish`).
233+
"""
234+
workspace_names = {pkg["name"] for pkg in meta["packages"]}
235+
publishable = {pkg["name"] for pkg in meta["packages"] if is_publishable(pkg)}
236+
unmanaged = sorted(publishable - knope_packages)
237+
stale = sorted(knope_packages - workspace_names)
238+
return unmanaged, stale
239+
240+
207241
def load_cargo_metadata():
208242
"""Fetch workspace metadata (--no-deps avoids network access)."""
209243
return json.loads(subprocess.check_output(
@@ -227,12 +261,14 @@ def main():
227261
"direct": [], "downstream": [], "required": [],
228262
"present": sorted(present.items()), "missing": [],
229263
"invalid": invalid,
264+
"unmanaged": [], "stale": [],
230265
"changeset_content": "",
231266
})
232267
return # emit calls sys.exit, but guard against falling through
233268

234269
result = detect(meta, knope_packages, changed_files, present)
235270
result["invalid"] = invalid
271+
result["unmanaged"], result["stale"] = reconcile_knope_config(meta, knope_packages)
236272
emit(result)
237273

238274

.github/scripts/test_changeset_detect.py

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,28 @@
2929
import changeset_detect as cd
3030

3131

32-
def make_meta(packages, deps, workspace_root="/ws"):
32+
def make_meta(packages, deps, workspace_root="/ws", publish=None):
3333
"""Build a fake `cargo metadata` document.
3434
3535
packages: {name -> relative dir}
3636
deps: {name -> [dependency names]}
37+
publish: {name -> publish value}, mirroring cargo metadata's `publish`
38+
field (`[]` for `publish = false`, a list for restricted
39+
registries). Names omitted here get no `publish` key, i.e. the
40+
default publishable-to-any-registry state.
3741
"""
38-
return {
39-
"workspace_root": workspace_root,
40-
"packages": [
41-
{
42-
"name": name,
43-
"manifest_path": f"{workspace_root}/{rel}/Cargo.toml",
44-
"dependencies": [{"name": d} for d in deps.get(name, [])],
45-
}
46-
for name, rel in packages.items()
47-
],
48-
}
42+
publish = publish or {}
43+
pkgs = []
44+
for name, rel in packages.items():
45+
entry = {
46+
"name": name,
47+
"manifest_path": f"{workspace_root}/{rel}/Cargo.toml",
48+
"dependencies": [{"name": d} for d in deps.get(name, [])],
49+
}
50+
if name in publish:
51+
entry["publish"] = publish[name]
52+
pkgs.append(entry)
53+
return {"workspace_root": workspace_root, "packages": pkgs}
4954

5055

5156
# A small synthetic workspace:
@@ -220,6 +225,48 @@ def test_changeset_content_prefills_missing(self):
220225
self.assertNotIn("a-sys", r["changeset_content"])
221226

222227

228+
class TestReconcile(unittest.TestCase):
229+
def test_publishable_crate_missing_from_knope_is_unmanaged(self):
230+
# Both crates are publishable (no `publish` key); only `a` is in knope.
231+
meta = make_meta({"a": "a", "b": "b"}, {})
232+
unmanaged, stale = cd.reconcile_knope_config(meta, {"a"})
233+
self.assertEqual(unmanaged, ["b"])
234+
self.assertEqual(stale, [])
235+
236+
def test_publish_false_crate_not_required_in_knope(self):
237+
# An example crate (publish = false) need not be knope-managed.
238+
meta = make_meta({"a": "a", "ex": "examples/ex"}, {}, publish={"ex": []})
239+
unmanaged, stale = cd.reconcile_knope_config(meta, {"a"})
240+
self.assertEqual(unmanaged, [])
241+
self.assertEqual(stale, [])
242+
243+
def test_publish_false_crate_may_still_be_knope_managed(self):
244+
# e.g. livekit-ffi: publish = false but released via CI, so it's in
245+
# knope. This must not be flagged as stale.
246+
meta = make_meta({"ffi": "ffi"}, {}, publish={"ffi": []})
247+
unmanaged, stale = cd.reconcile_knope_config(meta, {"ffi"})
248+
self.assertEqual(unmanaged, [])
249+
self.assertEqual(stale, [])
250+
251+
def test_restricted_registry_is_still_publishable(self):
252+
meta = make_meta({"a": "a"}, {}, publish={"a": ["crates-io"]})
253+
unmanaged, stale = cd.reconcile_knope_config(meta, set())
254+
self.assertEqual(unmanaged, ["a"])
255+
self.assertEqual(stale, [])
256+
257+
def test_stale_knope_entry_with_no_matching_crate(self):
258+
meta = make_meta({"a": "a"}, {})
259+
unmanaged, stale = cd.reconcile_knope_config(meta, {"a", "ghost"})
260+
self.assertEqual(unmanaged, [])
261+
self.assertEqual(stale, ["ghost"])
262+
263+
def test_both_directions_at_once(self):
264+
meta = make_meta({"a": "a", "b": "b"}, {})
265+
unmanaged, stale = cd.reconcile_knope_config(meta, {"a", "ghost"})
266+
self.assertEqual(unmanaged, ["b"])
267+
self.assertEqual(stale, ["ghost"])
268+
269+
223270
class TestBuildChangesetContent(unittest.TestCase):
224271
def test_deterministic_with_explicit_metadata(self):
225272
content = cd.build_changeset_content(

.github/workflows/changeset-check.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,46 @@ jobs:
126126
exit 1
127127
fi
128128
129+
# --- Gate: knope.toml must stay in sync with the workspace ---
130+
# A publishable crate missing from knope.toml silently escapes changeset
131+
# enforcement and is never released; a knope entry with no matching crate
132+
# is dangling config. Either is a release-pipeline bug, so fail hard until
133+
# the config is reconciled. This runs before the "no versioned packages"
134+
# early-out below because a brand-new unmanaged crate maps to no knope
135+
# package and would otherwise slip through as "nothing affected".
136+
NUM_UNMANAGED=$(echo "$DETECTION" | jq '.unmanaged | length')
137+
NUM_STALE=$(echo "$DETECTION" | jq '.stale | length')
138+
if [ "$NUM_UNMANAGED" != "0" ] || [ "$NUM_STALE" != "0" ]; then
139+
COMMENT_BODY=$(
140+
echo "$COMMENT_MARKER"
141+
echo "### knope.toml is out of sync with the workspace"
142+
echo ""
143+
if [ "$NUM_UNMANAGED" != "0" ]; then
144+
UNMANAGED_LIST=$(echo "$DETECTION" | jq -r '.unmanaged[] | "- `\(.)`"')
145+
echo "These publishable crates are **not** managed by knope, so they would never get a changeset requirement or a release:"
146+
echo ""
147+
echo "$UNMANAGED_LIST"
148+
echo ""
149+
echo "Fix each one by **either**:"
150+
echo "- adding a \`[packages.<name>]\` block to \`knope.toml\` (if it should be released), **or**"
151+
echo "- setting \`publish = false\` in its \`Cargo.toml\` (if it should not)."
152+
echo ""
153+
fi
154+
if [ "$NUM_STALE" != "0" ]; then
155+
STALE_LIST=$(echo "$DETECTION" | jq -r '.stale[] | "- `\(.)`"')
156+
echo "These \`knope.toml\` packages have no matching workspace crate (renamed or removed?):"
157+
echo ""
158+
echo "$STALE_LIST"
159+
echo ""
160+
echo "Remove the stale \`[packages.<name>]\` block(s) from \`knope.toml\`."
161+
echo ""
162+
fi
163+
)
164+
upsert_comment "$COMMENT_BODY" || echo "::warning::Could not post PR comment (this can happen for fork PRs with limited permissions)"
165+
echo "::error::knope.toml is out of sync with the workspace (unmanaged: ${NUM_UNMANAGED}, stale: ${NUM_STALE}). See the PR comment for details."
166+
exit 1
167+
fi
168+
129169
# --- If no versioned packages are affected, no changeset is required ---
130170
NUM_REQUIRED=$(echo "$DETECTION" | jq '.required | length')
131171
if [ "$NUM_REQUIRED" = "0" ]; then

.github/workflows/uniffi-android.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ jobs:
3333
steps:
3434
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
3535
with:
36+
# Build the tagged source, not the dispatch ref (which defaults to main).
37+
ref: ${{ inputs.tag_name }}
3638
submodules: recursive
3739

3840
- name: Setup Rust toolchain
Lines changed: 21 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,53 @@
11
name: UniFFI packages
22

3-
# Orchestrates per-language wrapper-package publishing on knope draft releases
4-
# for the livekit-uniffi crate. Resolves the source tag once, then delegates
5-
# to per-language reusable workflows (uniffi-swift.yml today; uniffi-kotlin.yml
6-
# / uniffi-python.yml would slot in as additional jobs).
3+
# Publishes the per-language livekit-uniffi wrapper packages (Swift xcframework →
4+
# livekit/livekit-uniffi-xcframework; Android AAR → Maven Central) when a
5+
# livekit-uniffi release is published.
76
#
8-
# Trigger pattern mirrors ffi-builds.yml: every push to main checks for a
9-
# draft release with prefix `livekit-uniffi/v`; workflow_dispatch allows
10-
# manual runs with an explicit tag.
7+
# knope publishes the `livekit-uniffi/v*` GitHub release (and its git tag)
8+
# directly on the release merge — no draft — so this reacts to the published
9+
# release event, which fires only for genuine livekit-uniffi releases (the gate
10+
# is free). workflow_dispatch allows a manual re-run with an explicit tag.
11+
#
12+
# The Dart/Flutter cdylib target is experimental and not published for now; add a
13+
# `cdylib` job (uniffi-cdylib.yml) back when it ships.
1114

1215
on:
13-
push:
14-
branches: ["main"]
16+
release:
17+
types: [published]
1518
workflow_dispatch:
1619
inputs:
1720
tag_name:
18-
description: "Release tag (e.g., livekit-uniffi/v0.0.7). Required if no draft release exists."
19-
required: false
21+
description: "Release tag (e.g. livekit-uniffi/v0.1.6)"
22+
required: true
2023
type: string
2124
dry_run:
22-
description: "Build artifacts but skip release upload/PR."
25+
description: "Build artifacts but skip publishing."
2326
type: boolean
2427
default: false
2528

2629
permissions:
2730
contents: read
2831

29-
env:
30-
TAG_PREFIX: livekit-uniffi/v
31-
3232
jobs:
3333
resolve-tag:
3434
runs-on: ubuntu-latest
35+
# The release event fires for every package's release, so proceed only for
36+
# livekit-uniffi. workflow_dispatch always proceeds (explicit tag).
37+
if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.event.release.tag_name, 'livekit-uniffi/v') }}
3538
outputs:
3639
tag_name: ${{ steps.get-tag.outputs.tag_name }}
3740
version: ${{ steps.get-tag.outputs.version }}
3841
steps:
39-
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
4042
- id: get-tag
41-
env:
42-
GH_TOKEN: ${{ github.token }}
4343
run: |
44-
MANUAL_TAG="${{ inputs.tag_name }}"
45-
if [ -n "$MANUAL_TAG" ]; then
46-
TAG="$MANUAL_TAG"
47-
echo "Using manually provided tag: '$TAG'"
48-
else
49-
TAG=$(gh release list --repo "${{ github.repository }}" \
50-
--json 'isDraft,tagName' \
51-
--jq "[.[] | select(.isDraft and (.tagName | startswith(\"${TAG_PREFIX}\")))] | first | .tagName // empty")
52-
echo "Resolved livekit-uniffi draft release tag: '${TAG:-<none>}'"
53-
fi
44+
TAG="${{ github.event.release.tag_name || inputs.tag_name }}"
5445
echo "tag_name=${TAG}" >> "$GITHUB_OUTPUT"
55-
# Strip the "livekit-uniffi/v" prefix to get the hosting-repo tag (e.g., 0.0.7)
56-
VERSION="${TAG#${TAG_PREFIX}}"
57-
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
46+
# Strip the "livekit-uniffi/v" prefix to get the package version (e.g. 0.1.6).
47+
echo "version=${TAG#livekit-uniffi/v}" >> "$GITHUB_OUTPUT"
5848
5949
swift:
6050
needs: resolve-tag
61-
if: needs.resolve-tag.outputs.tag_name != ''
6251
uses: ./.github/workflows/uniffi-swift.yml
6352
with:
6453
version: ${{ needs.resolve-tag.outputs.version }}
@@ -69,26 +58,9 @@ jobs:
6958

7059
android:
7160
needs: resolve-tag
72-
if: needs.resolve-tag.outputs.tag_name != ''
7361
uses: ./.github/workflows/uniffi-android.yml
7462
with:
7563
version: ${{ needs.resolve-tag.outputs.version }}
7664
tag_name: ${{ needs.resolve-tag.outputs.tag_name }}
7765
dry_run: ${{ inputs.dry_run || false }}
7866
secrets: inherit
79-
80-
# Raw cdylibs for the Dart package's build-hook download (build-<triple>.zip).
81-
cdylib:
82-
needs: resolve-tag
83-
if: needs.resolve-tag.outputs.tag_name != ''
84-
# The reusable workflow uploads to this repo's release via github.token; a
85-
# called workflow can't elevate the caller's token, so grant write here (the
86-
# top-level default is read). Swift uses a PAT and Android uses Maven, so
87-
# neither needs this.
88-
permissions:
89-
contents: write
90-
uses: ./.github/workflows/uniffi-cdylib.yml
91-
with:
92-
version: ${{ needs.resolve-tag.outputs.version }}
93-
tag_name: ${{ needs.resolve-tag.outputs.tag_name }}
94-
dry_run: ${{ inputs.dry_run || false }}

.github/workflows/uniffi-swift.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ jobs:
4242
steps:
4343
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
4444
with:
45+
# Build the tagged source, not the dispatch ref (which defaults to main).
46+
ref: ${{ inputs.tag_name }}
4547
submodules: recursive
4648

4749
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
@@ -95,6 +97,7 @@ jobs:
9597
${{ env.OUTPUT_DIR }}/LICENSE:LICENSE
9698
${{ env.OUTPUT_DIR }}/PrivacyInfo.xcprivacy:PrivacyInfo.xcprivacy
9799
${{ env.OUTPUT_DIR }}/Sources/${{ env.SPM_NAME }}/livekit_uniffi.swift:Sources/${{ env.SPM_NAME }}/livekit_uniffi.swift
100+
${{ env.OUTPUT_DIR }}/Sources/${{ env.SPM_NAME }}/livekit_datatrack.swift:Sources/${{ env.SPM_NAME }}/livekit_datatrack.swift
98101
token: ${{ secrets.UNIFFI_XCFRAMEWORK_PAT }}
99102
pr-body: |
100103
Source tag: `${{ inputs.tag_name }}`

0 commit comments

Comments
 (0)