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
118 changes: 80 additions & 38 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ env:

jobs:
contract:
# Every PR gets this cheap contract lane, including owner-authored PRs that
# have not opted into the heavier `run-ci` jobs below. Keep it limited to
# deterministic, in-process/static checks so feedback stays fast.
if: github.event_name == 'pull_request'
# Mandatory first lane for every workflow invocation: pull requests and
# pushes to main. Owner-authored PRs do not need `run-ci` for this cheap gate.
# Keep it limited to deterministic, in-process/static checks so feedback stays fast.
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
Expand Down Expand Up @@ -49,6 +48,16 @@ jobs:
bash scripts/workspace_boundary_check.sh
- name: Check formatting
run: cargo fmt --all -- --check
- name: Ensure test inventory dependency
run: |
if ! command -v rg >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install --no-install-recommends -y ripgrep
fi
- name: Check test inventory contract
run: |
bash scripts/test_inventory.sh --self-test
bash scripts/test_inventory.sh
- name: Check schema and metadata contracts
run: |
cargo test --locked -p webcodex --lib -- \
Expand All @@ -60,43 +69,57 @@ jobs:
explicit_resume_mcp_schema_and_metadata_are_exposed \
http_project_connector_lists_and_dispatches_only_canonical_capabilities

test:
# External PRs run automatically (subject to GitHub's normal fork approval
# protections). PRs authored by the repository owner are intentionally
# opt-in: adding the `run-ci` label triggers this workflow again and enables
# the heavy jobs. Removing the label also cancels an in-progress PR run via
# the concurrency group above. Pushes to main always run.
test-linux-rust:
needs: contract
if: >-
github.event_name == 'push' ||
github.event.pull_request.user.login != github.repository_owner ||
contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- shard: server
packages: "-p webcodex"
- shard: runner
packages: "-p webcodex-runner"
- shard: workspace-crates
packages: >-
-p webcodex-admin
-p webcodex-agent-config
-p webcodex-core
-p webcodex-cli
-p webcodex-persistent-shell
-p webcodex-sandbox
-p webcodex-workspace
-p webcodex-process
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- uses: Swatinem/rust-cache@v2
with:
shared-key: linux-ci
- name: Run ${{ matrix.shard }} Rust tests
run: cargo test --locked ${{ matrix.packages }}

test-linux-tooling:
needs: contract
if: >-
github.event_name == 'push' ||
github.event.pull_request.user.login != github.repository_owner ||
contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
# The retained npm release-smoke tooling below invokes node/npm directly.
# Frontend dependency installation and frontend-specific npm caching remain
# contract-lane responsibilities and are intentionally not repeated here.
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
run: npm ci --prefix frontend
- name: Type-check frontend
run: npm --prefix frontend run typecheck
- name: Test frontend
run: npm --prefix frontend test
- name: Verify committed frontend build
run: npm --prefix frontend run check:dist
- name: Verify workspace boundaries
run: |
bash scripts/workspace_boundary_check.sh --self-test
bash scripts/workspace_boundary_check.sh
- name: Test release verification tooling
run: |
python3 -m unittest scripts.tests.test_verify_public_release scripts.tests.test_collect_release_bundle scripts.tests.test_release_readiness scripts.tests.test_release_publication scripts.tests.test_check_markdown_links
Expand All @@ -114,12 +137,38 @@ jobs:
bash -n scripts/npm_package_smoke.sh scripts/tests/test_npm_package_smoke_existing_binaries.sh
bash scripts/npm_package_smoke.sh --help >/dev/null
bash scripts/tests/test_npm_package_smoke_existing_binaries.sh
- name: Check formatting
run: cargo fmt --all -- --check
- name: Run tests
run: cargo test --locked --workspace

test:
needs: [contract, test-linux-rust, test-linux-tooling]
# Preserve the historical aggregate status check. Eligible heavy runs always
# evaluate upstream results so a failed or unexpectedly skipped Linux lane
# cannot turn this required status into a successful or skipped check.
if: >-
always() &&
(
github.event_name == 'push' ||
github.event.pull_request.user.login != github.repository_owner ||
contains(github.event.pull_request.labels.*.name, 'run-ci')
)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require successful Linux lanes
env:
CONTRACT_RESULT: ${{ needs.contract.result }}
RUST_RESULT: ${{ needs.test-linux-rust.result }}
TOOLING_RESULT: ${{ needs.test-linux-tooling.result }}
run: |
echo "contract=$CONTRACT_RESULT rust=$RUST_RESULT tooling=$TOOLING_RESULT"
if [ "$CONTRACT_RESULT" != success ] || \
[ "$RUST_RESULT" != success ] || \
[ "$TOOLING_RESULT" != success ]; then
echo "required Linux CI lane did not succeed" >&2
exit 1
fi

test-macos:
needs: contract
if: >-
github.event_name == 'push' ||
github.event.pull_request.user.login != github.repository_owner ||
Expand All @@ -131,19 +180,16 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- uses: Swatinem/rust-cache@v2
with:
shared-key: macos-ci
- name: Check formatting
run: cargo fmt --all -- --check
- name: Check macOS release production surfaces
run: cargo check --locked -p webcodex -p webcodex-cli -p webcodex-runner
- name: Run native macOS Runner tests
run: cargo test --locked -p webcodex-runner

test-windows:
needs: contract
if: >-
github.event_name == 'push' ||
github.event.pull_request.user.login != github.repository_owner ||
Expand All @@ -158,14 +204,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Check formatting
run: cargo fmt --all -- --check
- name: Run Windows library and CLI tests
run: cargo test --locked -p webcodex-agent-config -p webcodex-process -p webcodex-persistent-shell -p webcodex-cli
- name: Run Windows Runner tests
Expand Down
19 changes: 10 additions & 9 deletions crates/webcodex-runner/src/main_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ use crate::webcodex_runner::{
};
pub(crate) static TEST_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

pub(crate) fn test_env_lock() -> std::sync::MutexGuard<'static, ()> {
TEST_ENV_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}

/// RAII restore for environment variables mutated by tests: restores the
/// previous value (or absence) on drop, even when the test panics, so a
/// failure cannot leak env state into later tests.
Expand All @@ -22,9 +28,9 @@ impl EnvGuard {
}
}

pub(crate) fn set(mut self, name: &'static str, value: &str) -> Self {
pub(crate) fn set(mut self, name: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {
self.capture(name);
std::env::set_var(name, value);
std::env::set_var(name, value.as_ref());
self
}

Expand Down Expand Up @@ -1016,7 +1022,7 @@ fn shell_job_native_exe_nonzero_exit_code_is_preserved() {
#[cfg(windows)]
#[test]
fn shell_job_unicode_stdout_stderr_env_and_cwd() {
let _guard = TEST_ENV_LOCK.lock().unwrap();
let _guard = test_env_lock();
let tmp = tempfile::tempdir().unwrap();
let cfg = test_config(tmp.path().join("config/projects.d"));
let unicode_cwd = tmp.path().join("unicode cwd 测试");
Expand All @@ -1040,8 +1046,7 @@ fn shell_job_unicode_stdout_stderr_env_and_cwd() {
assert_eq!(result.stderr.as_deref(), Some("err 測試"));

// Unicode environment value inherited from the parent process.
let saved = std::env::var_os("WEBCODEX_UNICODE_ENV");
std::env::set_var("WEBCODEX_UNICODE_ENV", "值 测试");
let _env = EnvGuard::new().set("WEBCODEX_UNICODE_ENV", "值 测试");
let result = run_shell(
&cfg.policy,
&ShellConfig::default(),
Expand All @@ -1051,10 +1056,6 @@ fn shell_job_unicode_stdout_stderr_env_and_cwd() {
10,
None,
);
match saved {
Some(value) => std::env::set_var("WEBCODEX_UNICODE_ENV", value),
None => std::env::remove_var("WEBCODEX_UNICODE_ENV"),
}
assert_eq!(result.exit_code, Some(0), "{result:?}");
assert_eq!(result.stdout.as_deref(), Some("值 测试"));

Expand Down
Loading
Loading