From b38f4ff307a0ca3b21cfd6ddd148aa356c190c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=90=EC=84=B1=EC=A4=80?= Date: Wed, 14 Oct 2026 01:07:31 +0900 Subject: [PATCH] =?UTF-8?q?PR=20CI=EB=A5=BC=20Linux=20=EB=B9=A0=EB=A5=B8?= =?UTF-8?q?=20=EA=B2=8C=EC=9D=B4=ED=8A=B8=20=ED=95=98=EB=82=98=EB=A1=9C=20?= =?UTF-8?q?=EC=A4=84=EC=9D=B4=EA=B3=A0=20=EC=A0=84=EC=B2=B4=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=EC=9D=80=20main=EA=B3=BC=20release=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR마다 6개 job이 돌고 Intel Mac job이 12~26분 걸려 PR 하나 머지에 30분 가까이 기다렸다. 캐시 없이 테스트와 release 빌드를 매번 처음부터 컴파일한 게 원인이다. - PR 필수 check를 `Fast / Linux` 하나로 바꿨다. rust-cache, fmt, clippy, nextest `pr` profile, Python 스크립트 테스트, workflow 계약 검사만 한다. - nextest `pr` profile은 단독 실행 1초 이상인 45개 테스트(실제 자식 프로세스, PTY, 재개 CLI)를 뺀다. 로컬 측정 기준 전체 700개 직렬 368초 중 335초가 이 45개였고, 나머지 655개는 4병렬 14초다. - 기존 Quality / Linux와 5개 Platform job은 main push와 수동 실행에서만 돈다. - release quality gate는 release commit에서 위 6개 check가 모두 success일 때만 진행하고, main ruleset 검증은 `Fast / Linux` 필수 여부로 바꿨다. Co-Authored-By: Claude Opus 5.5 --- .config/nextest.toml | 58 +++++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 56 +++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 38 ++++++++++++++++++----- docs/development/releasing.md | 18 +++++------ 4 files changed, 153 insertions(+), 17 deletions(-) create mode 100644 .config/nextest.toml diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 0000000..fd033e6 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,58 @@ +# Test profiles for cargo-nextest. +# +# `pr` is the required pull-request gate. It skips the tests below, each of which takes 1 s or +# more on its own because it drives real child processes, PTYs or a resumed CLI run. The full +# suite (`default`) still runs on every push to main, on every platform, and in the release +# quality gate. Regenerate the list with `cargo nextest run --workspace -j 1` and keep tests +# that report >= 1 s. + +[profile.pr] +default-filter = """ +not ( + test(=allow_process::tests::executable_specs_are_absolute_deterministic_and_path_redacted) | + test(=bare_xgen_streams_durable_progress_prompts_separate_approvals_and_replays_offline) | + test(=catalog::tests::executable_alias_is_launch_preserved_digest_bound_and_retarget_checked) | + test(=catalog::tests::executable_fingerprints_detect_content_changes_without_exposing_paths) | + test(=changed_safe_environment_is_named_and_blocks_resume_until_restored) | + test(=completion_checks_require_latest_success_after_changes_on_design_and_held_out_inputs) | + test(=composition::tests::execute_flag_approves_only_the_exact_process_catalog_request_shape) | + test(=composition::tests::product_registry_is_bound_to_the_exact_workspace) | + test(=composition::tests::reusable_process_session_is_bound_to_one_physical_workspace) | + test(=composition::tests::search_approval_requires_catalogued_backend_exact_query_and_once_lifetime) | + test(=composition::tests::terminal_approval_is_catalogued_exact_once_and_execute_gated) | + test(=conversation_only_requests_need_no_tool_approvals_and_replay_offline) | + test(=execution::tests::catalogued_symlink_proxy_preserves_argv_zero_alias) | + test(=execution::tests::escaped_stdio_never_blocks_past_the_capture_grace) | + test(=execution::tests::nonzero_exit_and_bounded_streams_are_durable_result_data) | + test(=execution::tests::normal_leader_exit_also_terminates_the_descendant_tree) | + test(=execution::tests::prepared_debug_redacts_command_material_and_rejects_protected_env) | + test(=execution::tests::relative_cwd_and_explicit_environment_reach_the_child) | + test(=execution::tests::shell_metacharacters_are_one_literal_argument) | + test(=execution::tests::timeout_terminates_the_descendant_tree) | + test(=explicit_recovery_preserves_budget_and_verified_effect_before_separate_resume) | + test(=final_contract_projects_process_arguments_exits_and_actual_order_then_replays_offline) | + test(=final_contract_rejects_prose_missing_fields_and_model_written_commands_after_read) | + test(=invocation_diagnostics_distinguish_schema_and_resource_failures_without_values) | + test(=path::tests::cwd_never_follows_a_workspace_symlink) | + test(=path::tests::cwd_rejects_an_ambient_root_retargeted_after_preopen) | + test(=process_execution_requires_separate_approval_and_resumes_without_replay) | + test(=process_outcome_commit_failure_recovers_without_reexecution) | + test(=pty_cli_approves_start_commits_input_output_receipts_and_replays_offline) | + test(=rejected_final_response_resumes_after_restart_without_replaying_writes_or_failed_command) | + test(=sigint_during_model_call_stops_at_a_safe_or_unknown_no_replay_boundary) | + test(=slow_provider_becomes_a_durable_timeout_without_retry) | + test(=terminal::sessions::tests::literal_input_unicode_output_and_non_consuming_offsets) | + test(=terminal::sessions::tests::nonzero_timeout_and_missing_session_remain_observations) | + test(=terminal::sessions::tests::output_ring_is_bounded_and_reports_lost_bytes) | + test(=terminal::sessions::tests::repeated_start_is_not_respawned_and_session_count_is_bounded) | + test(=terminal::sessions::tests::terminal_control_bytes_and_write_deadlines_are_supported) | + test(=terminal::sessions::tests::termination_and_leader_exit_reach_jobs_in_their_own_groups) | + test(=terminal::sessions::tests::termination_normal_exit_and_host_drop_clean_owned_descendants) | + test(=tests::sqlite_cold_audit_indexes_each_event_once_and_warm_append_skips_historical_rows) | + test(=tests::sqlite_receipt_heavy_run_uses_one_lookup_per_binding_and_no_warm_history_scan) | + test(=tests::sqlite_ten_thousand_event_run_has_bounded_warm_verification_and_linear_cold_audit) | + test(=tests::workspace_debug_and_binding_never_expose_ambient_values) | + test(=tool_discovery_is_approved_receipted_and_frozen_across_resume) | + test(=usage_counts_rejected_responses_and_survives_resume_without_recounting_replay) +) +""" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd90a06..5d4e1cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,20 +1,75 @@ name: CI +# Pull requests run only the fast Linux gate (about a minute with a warm cache). The full +# quality and five-platform jobs run after merge on main, on manual dispatch, and again in the +# release workflow before anything is published. on: pull_request: push: branches: [main] + workflow_dispatch: permissions: contents: read +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: CC_aarch64_unknown_linux_musl: musl-gcc CC_x86_64_unknown_linux_musl: musl-gcc jobs: + fast: + name: Fast / Linux + runs-on: ubuntu-24.04 + env: + CARGO_INCREMENTAL: "0" + CARGO_PROFILE_DEV_DEBUG: "0" + CARGO_PROFILE_TEST_DEBUG: "0" + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install pinned Rust toolchain + run: rustup toolchain install --no-self-update + + - name: Restore Rust build cache + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + save-if: ${{ github.ref == 'refs/heads/main' }} + + - name: Install cargo-nextest + uses: taiki-e/install-action@1168f6810a3cb30a3514bf5756a3b7f0422c6df8 # v2.87.28 + with: + tool: cargo-nextest@0.9.148 + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Lint workspace + run: cargo clippy --workspace --all-targets --locked -- -D warnings + + - name: Test workspace without slow process tests + run: cargo nextest run --profile pr --workspace --locked + + - name: Test Python scripts without API calls + run: python3 -m unittest discover -s scripts/tests + + - name: Check release workflows and public documentation + shell: bash + run: | + set -euo pipefail + sh scripts/check-release-workflow.sh + sh scripts/check-npm-distribution-workflow.sh + sh scripts/check-rc3-public-docs.sh + quality: name: Quality / Linux + if: github.event_name != 'pull_request' runs-on: ubuntu-24.04 steps: - name: Checkout @@ -72,6 +127,7 @@ jobs: platform: name: Platform / ${{ matrix.target }} + if: github.event_name != 'pull_request' strategy: fail-fast: false matrix: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 843a1f4..f5f2b9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,6 +22,10 @@ jobs: quality: name: Release quality gate runs-on: ubuntu-24.04 + permissions: + actions: read + checks: read + contents: read steps: - name: Validate trusted release request env: @@ -60,6 +64,31 @@ jobs: .head_sha == $sha ' "$source_run" >/dev/null + - name: Require the full main CI to have passed on the release commit + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ github.event.workflow_run.head_sha }} + shell: bash + run: | + set -euo pipefail + # Pull requests only run the fast gate; the full quality and platform jobs run on main. + gh api --paginate "repos/$GITHUB_REPOSITORY/commits/$RELEASE_SHA/check-runs?per_page=100" \ + --jq '.check_runs[] | select(.app.slug == "github-actions") | [.name, .conclusion // "pending"] | @tsv' \ + > "$RUNNER_TEMP/release-check-runs.tsv" + for check in \ + "Quality / Linux" \ + "Platform / x86_64-unknown-linux-musl" \ + "Platform / aarch64-unknown-linux-musl" \ + "Platform / x86_64-apple-darwin" \ + "Platform / aarch64-apple-darwin" \ + "Platform / x86_64-pc-windows-msvc"; do + if ! awk -F '\t' -v name="$check" '$1 == name && $2 == "success" { found = 1 } END { exit !found }' \ + "$RUNNER_TEMP/release-check-runs.tsv"; then + printf 'main CI check did not pass on %s: %s\n' "$RELEASE_SHA" "$check" >&2 + exit 1 + fi + done + - name: Checkout exact release commit uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -549,14 +578,7 @@ jobs: [.required_status_checks[] | select(.integration_id == $github_actions_integration_id) | .context]] | - any( - (index("Quality / Linux") != null) and - (index("Platform / x86_64-unknown-linux-musl") != null) and - (index("Platform / aarch64-unknown-linux-musl") != null) and - (index("Platform / x86_64-apple-darwin") != null) and - (index("Platform / aarch64-apple-darwin") != null) and - (index("Platform / x86_64-pc-windows-msvc") != null) - )) + any(index("Fast / Linux") != null)) ' >/dev/null; then main_ruleset_ok=true break diff --git a/docs/development/releasing.md b/docs/development/releasing.md index 3a58c1c..7645df8 100644 --- a/docs/development/releasing.md +++ b/docs/development/releasing.md @@ -16,15 +16,15 @@ binary를 감싸는 선택형 설치 채널이며 제품 runtime을 다시 구 - `refs/tags/v*` 또는 모든 tag의 update와 deletion을 막는 active repository ruleset을 exclude와 bypass actor 없이 활성화한다. - `main`에 PR, deletion과 non-fast-forward 차단을 적용하는 active branch ruleset을 exclude와 - bypass actor 없이 활성화한다. Required status checks의 strict policy를 켜고 아래 여섯 context를 - 모두 필수로 지정한다. 각 check의 expected source는 반드시 `GitHub Actions`로 선택한다. 이름만 같고 - source가 다르거나 source가 지정되지 않은 check는 release workflow가 거부한다. - - `Quality / Linux` - - `Platform / x86_64-unknown-linux-musl` - - `Platform / aarch64-unknown-linux-musl` - - `Platform / x86_64-apple-darwin` - - `Platform / aarch64-apple-darwin` - - `Platform / x86_64-pc-windows-msvc` + bypass actor 없이 활성화한다. Required status checks의 strict policy를 켜고 `Fast / Linux`를 + 필수로 지정한다. expected source는 반드시 `GitHub Actions`로 선택한다. 이름만 같고 source가 + 다르거나 source가 지정되지 않은 check는 release workflow가 거부한다. +- PR에서는 `Fast / Linux`만 돈다. fmt, clippy, 단독 1초 이상 걸리는 프로세스·PTY·재개 테스트를 뺀 + nextest `pr` profile, Python 스크립트 테스트, release workflow 계약 검사를 한다. 나머지 여섯 job + (`Quality / Linux`, `Platform / x86_64-unknown-linux-musl`, `Platform / aarch64-unknown-linux-musl`, + `Platform / x86_64-apple-darwin`, `Platform / aarch64-apple-darwin`, + `Platform / x86_64-pc-windows-msvc`)은 main push와 수동 실행에서 전체 테스트로 돈다. release quality + gate는 release commit에서 이 여섯 check가 모두 success인지 확인한 뒤에만 진행한다. 설정 후 `XGEN_MAIN_PROTECTION_ENABLED=true`를 둔다. - Bypass actor가 없음을 관리자가 확인한 뒤 Actions repository variable