Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/scripts/registry_package_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Order alternate-registry packages by their normal and build dependencies."""

import json
import sys


def package_order(metadata):
packages = {
package["name"]: package
for package in metadata["packages"]
if package.get("publish") == ["phoxal"]
}
dependencies = {
name: {
dependency["name"]
for dependency in package["dependencies"]
if dependency.get("kind") != "dev" and dependency["name"] in packages
}
for name, package in packages.items()
}
pending = set(packages)
result = []
while pending:
ready = sorted(name for name in pending if not dependencies[name] & pending)
if not ready:
raise ValueError("alternate-registry package dependency cycle: " + ", ".join(sorted(pending)))
for name in ready:
result.append((name, bool(dependencies[name])))
pending.remove(name)
if not result:
raise ValueError("no alternate-registry packages found")
return result


if __name__ == "__main__":
for name, dependent in package_order(json.load(sys.stdin)):
print(f"{name}\t{int(dependent)}")
34 changes: 34 additions & 0 deletions .github/scripts/test_registry_package_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import unittest

from registry_package_order import package_order


def package(name, dependencies=(), publish=None):
return {
"name": name,
"publish": ["phoxal"] if publish is None else publish,
"dependencies": [{"name": dependency, "kind": kind} for dependency, kind in dependencies],
}


class PackageOrderTests(unittest.TestCase):
def test_transitive_build_dependencies_and_dev_cycles(self):
metadata = {"packages": [
package("last", [("middle", "build")]),
package("middle", [("root", None)]),
package("root", [("last", "dev"), ("public", None)]),
package("public", publish=["crates-io"]),
]}
self.assertEqual(package_order(metadata), [("root", False), ("middle", True), ("last", True)])

def test_normal_cycle_is_refused(self):
with self.assertRaisesRegex(ValueError, "cycle"):
package_order({"packages": [package("a", [("b", None)]), package("b", [("a", None)])]})

def test_empty_release_is_refused(self):
with self.assertRaisesRegex(ValueError, "no alternate-registry"):
package_order({"packages": []})


if __name__ == "__main__":
unittest.main()
91 changes: 77 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,85 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

# The workspace builds on a bare runner by design. Runnable crates own native
# build requirements in package.metadata.phoxal.build; phoxal's
# `authoring_workspace_build_requirements` test keeps this bootstrap input
# synchronized with their declared union.
# Nothing in the workspace links a native simulator any more: the Webots
# controller lives in phoxal/simulator-webots and carries that requirement with
# it, so this workflow installs no simulator and the checks run on a bare
# runner.
# Runnable crates own apt build requirements in package.metadata.phoxal.build;
# phoxal's `authoring_workspace_build_requirements` test keeps that bootstrap
# input synchronized with their declared union.
# The two Webots controller packages link libController even during
# `cargo check`, so the shared workflow installs the one supported native SDK
# before its clippy and test jobs.
jobs:
ci:
if: >-
github.event_name == 'pull_request' ||
(github.event_name == 'push' &&
!startsWith(github.event.head_commit.message, 'chore(release): release v'))
uses: phoxal/.github/.github/workflows/rust-ci.yml@main
with:
webots-version: R2025a
secrets: inherit

# The ordinary tests prove deterministic source generation. This launches the
# supported native runtime and refuses asset-loader warnings, so IndexedFaceSet
# output cannot regress to a representation R2025a only appears to parse.
webots-native-renderer:
name: Webots native renderer
if: >-
github.event_name == 'pull_request' ||
(github.event_name == 'push' &&
!startsWith(github.event.head_commit.message, 'chore(release): release v'))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions-rust-lang/setup-rust-toolchain@v1

- name: Install native runtime prerequisites
run: |
sudo apt-get update
sudo apt-get install --yes \
ffmpeg \
libavcodec-extra \
libegl1 \
libglu1-mesa \
libxkbcommon-x11-0 \
libxcb-keysyms1 \
libxcb-image0 \
libxcb-icccm4 \
libxcb-randr0 \
libxcb-render-util0 \
libxcb-cursor0 \
libxcb-xinerama0 \
libxcomposite1 \
libxtst6 \
libnss3 \
xvfb

- name: Cache Webots download
id: cache-webots-native-renderer
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/webots.tar.bz2
key: webots-R2025a-${{ runner.os }}-${{ runner.arch }}

- name: Download Webots
if: steps.cache-webots-native-renderer.outputs.cache-hit != 'true'
run: |
curl --fail --location --silent --show-error \
"https://github.com/cyberbotics/webots/releases/download/R2025a/webots-R2025a-x86-64.tar.bz2" \
--output "$RUNNER_TEMP/webots.tar.bz2"

- name: Install Webots
run: |
tar -xjf "$RUNNER_TEMP/webots.tar.bz2" -C "$RUNNER_TEMP"
echo "WEBOTS_HOME=$RUNNER_TEMP/webots" >> "$GITHUB_ENV"

- name: Prove decoded GLB geometry in R2025a
run: >-
xvfb-run --auto-servernum
cargo test -p phoxal-simulator-webots-host --bin phoxal-simulator-webots-host
installed_webots_loads_native_decoded_geometry_without_asset_warnings
-- --ignored --nocapture

# Every consumer profile, built explicitly.
#
# The shared `ci` job builds the workspace, and a workspace build unifies
Expand Down Expand Up @@ -81,10 +143,9 @@ jobs:
done
cargo doc -p phoxal --all-features --no-deps

# The shared `ci` job's `cargo test --workspace` unifies only the features
# the workspace members enable, which never includes `session` or
# `simulator`. The tests that live in those profiles, and the integration
# tests that state `required-features`, run only here.
# The shared workspace test unifies the features its members enable.
# Test all profiles explicitly as well, including integration tests with
# `required-features`, independently of the current member composition.
- name: Test every profile together
run: cargo test -p phoxal --all-features

Expand Down Expand Up @@ -128,8 +189,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
status=0
report="$(cargo semver-checks --package phoxal --all-features 2>&1)" || status=$?
report="$(cargo semver-checks --package phoxal --all-features 2>&1)" || true
echo "$report"
{
echo "## Rust API surface (all profiles)"
Expand Down Expand Up @@ -160,6 +220,9 @@ jobs:

- uses: actions-rust-lang/setup-rust-toolchain@v1

- name: Test registry dependency ordering
run: python3 -m unittest discover -s .github/scripts -p 'test_registry_package_order.py'

- name: Check the workspace policy
shell: bash
run: |
Expand Down
Loading
Loading