feat(development-system): distribute MCP release bundles #665
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| # Public repo: pull_request is reachable from forks, so every job below stays | |
| # on GitHub-hosted Ubuntu runners. Direct pushes to trunk run the same gate, | |
| # and merge_group preserves coverage for repositories that opt into PR mode. | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| merge_group: | |
| types: [checks_requested] | |
| permissions: | |
| contents: read | |
| # Cancel only an older revision of the same delivery stream. Event type keeps | |
| # main pushes, pull requests, and merge-queue validation isolated; ref keeps | |
| # separate pull requests and merge groups from canceling one another. | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| quality: | |
| name: Quality gate | |
| # Ubuntu 24.04's AppArmor policy strips capabilities from unprivileged user | |
| # namespaces, so the Nix Bubblewrap binary cannot construct the verifier's | |
| # mount namespace. Keep the sandbox tests on the supported 22.04 image. | |
| runs-on: ubuntu-22.04 | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Install Nix | |
| uses: DeterminateSystems/nix-installer-action@main | |
| - name: Cache Nix store | |
| uses: DeterminateSystems/magic-nix-cache-action@main | |
| - name: Provision delegated user systemd | |
| run: | | |
| uid="$(id -u)" | |
| user="$(id -un)" | |
| runtime_dir="/run/user/$uid" | |
| delegate_source="$RUNNER_TEMP/ai-plugins-user-delegate.conf" | |
| delegate_drop_in="/run/systemd/system/user@${uid}.service.d/ai-plugins-ci.conf" | |
| user_manager_unit="user@$uid.service" | |
| runtime_unit="user-runtime-dir@$uid.service" | |
| sudo systemctl is-active --quiet systemd-logind.service | |
| # Delegation is allocation-time state, so Jammy cannot apply it with | |
| # `systemctl set-property` after the user manager already exists. | |
| printf '%s\n' \ | |
| '[Service]' \ | |
| 'Delegate=cpu memory pids' \ | |
| 'LimitSTACK=67108864' > "$delegate_source" | |
| sudo install -D -m 0644 "$delegate_source" "$delegate_drop_in" | |
| sudo systemctl daemon-reload | |
| # Load the override before enabling linger so an activation raced by | |
| # logind cannot start the manager with the old delegation contract. | |
| sudo loginctl enable-linger "$user" | |
| activate_user_manager() { | |
| if sudo systemctl is-active --quiet "$user_manager_unit"; then | |
| sudo systemctl restart "$user_manager_unit" || return | |
| else | |
| sudo systemctl reset-failed "$user_manager_unit" "$runtime_unit" || true | |
| sudo systemctl start "$user_manager_unit" || return | |
| fi | |
| sudo systemctl is-active --quiet "$user_manager_unit" | |
| } | |
| diagnose_user_manager() { | |
| sudo systemctl status --no-pager --full \ | |
| "$user_manager_unit" "$runtime_unit" || true | |
| sudo systemctl show "$user_manager_unit" "$runtime_unit" \ | |
| --property=ActiveState,SubState,Result,ExecMainStatus,ControlGroup,DelegateControllers || true | |
| sudo journalctl --boot --no-pager --lines=100 \ | |
| --unit="$user_manager_unit" --unit="$runtime_unit" || true | |
| } | |
| activation_succeeded=false | |
| for attempt in 1 2 3; do | |
| if activate_user_manager; then | |
| activation_succeeded=true | |
| break | |
| fi | |
| echo "user systemd manager activation attempt $attempt failed" >&2 | |
| diagnose_user_manager | |
| sudo systemctl reset-failed "$user_manager_unit" "$runtime_unit" || true | |
| if [ "$attempt" -lt 3 ]; then | |
| sleep "$attempt" | |
| fi | |
| done | |
| if [ "$activation_succeeded" != true ]; then | |
| exit 1 | |
| fi | |
| delegated_controllers="$( | |
| sudo systemctl show "$user_manager_unit" \ | |
| --property=DelegateControllers --value | |
| )" | |
| for controller in cpu memory pids; do | |
| case " $delegated_controllers " in | |
| *" $controller "*) ;; | |
| *) | |
| echo "user systemd manager is missing delegated $controller control" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| done | |
| # The trusted Rust scorer caps compiler subprocesses at a 64 MiB | |
| # stack. GitHub's runner session supplies a lower hard limit, which | |
| # an unprivileged user manager cannot raise for its own units. | |
| manager_stack_limit="$( | |
| sudo systemctl show "$user_manager_unit" \ | |
| --property=LimitSTACK --value | |
| )" | |
| [ "$manager_stack_limit" = 67108864 ] || { | |
| echo "user systemd manager has stack limit $manager_stack_limit; expected 67108864" >&2 | |
| exit 1 | |
| } | |
| # Ubuntu's PAM stack can reapply the runner session's lower hard | |
| # limit after PID 1 applies the unit property. Correct and verify | |
| # the live manager so it can honor the transient gate service's | |
| # explicit 64 MiB limit. | |
| manager_pid="$( | |
| sudo systemctl show "$user_manager_unit" \ | |
| --property=MainPID --value | |
| )" | |
| case "$manager_pid" in | |
| ''|*[!0-9]*|0) | |
| echo "user systemd manager has invalid MainPID: $manager_pid" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| sudo prlimit --pid "$manager_pid" --stack=67108864:67108864 | |
| read -r manager_stack_soft manager_stack_hard manager_stack_units < <( | |
| sudo prlimit --pid "$manager_pid" --stack --raw --noheadings \ | |
| --output=SOFT,HARD,UNITS | |
| ) | |
| [ "$manager_stack_soft" = 67108864 ] || { | |
| echo "user systemd manager has soft stack limit $manager_stack_soft; expected 67108864" >&2 | |
| exit 1 | |
| } | |
| [ "$manager_stack_hard" = 67108864 ] || { | |
| echo "user systemd manager has hard stack limit $manager_stack_hard; expected 67108864" >&2 | |
| exit 1 | |
| } | |
| [ "$manager_stack_units" = bytes ] | |
| test ! -L "$runtime_dir" | |
| test "$(stat -c '%u:%a' "$runtime_dir")" = "$uid:700" | |
| test -S "$runtime_dir/systemd/private" | |
| XDG_RUNTIME_DIR="$runtime_dir" systemctl --user show-environment >/dev/null | |
| echo "XDG_RUNTIME_DIR=$runtime_dir" >> "$GITHUB_ENV" | |
| - name: Full gate | |
| run: | | |
| runtime_dir="$XDG_RUNTIME_DIR" | |
| nix_bin="$(command -v nix)" | |
| # Command substitutions in the single-quoted payload intentionally | |
| # expand in the inner Bash process, not in this workflow shell. | |
| # shellcheck disable=SC2016 | |
| XDG_RUNTIME_DIR="$runtime_dir" \ | |
| systemd-run \ | |
| --user \ | |
| --wait \ | |
| --pipe \ | |
| --quiet \ | |
| --collect \ | |
| --unit="ai-plugins-ci-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \ | |
| --property=Type=exec \ | |
| --property=LimitSTACK=67108864 \ | |
| --working-directory="$GITHUB_WORKSPACE" \ | |
| --setenv=PATH="$PATH" \ | |
| --setenv=HOME="$HOME" \ | |
| --setenv=CI=true \ | |
| --setenv=GITHUB_ACTIONS=true \ | |
| --setenv=XDG_RUNTIME_DIR="$runtime_dir" \ | |
| "$nix_bin" develop -c bash -eu -o pipefail -c ' | |
| [ "$(ulimit -Hs)" = 65536 ] || { | |
| echo "full gate hard stack limit is $(ulimit -Hs) KiB; expected 65536" >&2 | |
| exit 1 | |
| } | |
| bats --print-output-on-failure \ | |
| --filter "scorer contains its full process tree in the fixed aggregate systemd scope|direct public verifier uses the fixed aggregate scope without ambient secrets" \ | |
| scripts/tests/evals-code-quality-scorer-security.bats | |
| just ci | |
| ' | |
| codex-manifests: | |
| name: Codex manifests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Install Nix | |
| uses: DeterminateSystems/nix-installer-action@main | |
| - name: Cache Nix store | |
| uses: DeterminateSystems/magic-nix-cache-action@main | |
| # Structural check: every Codex manifest is valid. Full `codex`-CLI | |
| # runtime verification runs where a Codex login is available. | |
| - name: Codex manifests are valid JSON | |
| run: | | |
| nix develop -c bash -c ' | |
| jq empty .agents/plugins/marketplace.json | |
| find plugins -path "*/.codex-plugin/plugin.json" -exec jq empty {} \; | |
| ' | |
| eval-config: | |
| name: Eval config dry-run | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Install Nix | |
| uses: DeterminateSystems/nix-installer-action@main | |
| - name: Cache Nix store | |
| uses: DeterminateSystems/magic-nix-cache-action@main | |
| - name: Validate provider-backed eval configuration | |
| run: | | |
| nix develop -c scripts/evals/run.sh --dry-run | |
| nix develop -c scripts/evals/run.sh --suite canary --dry-run | |
| # Single aggregate status: succeeds only when every required job did. | |
| gate: | |
| name: CI gate | |
| runs-on: ubuntu-latest | |
| needs: [quality, codex-manifests, eval-config] | |
| if: always() | |
| steps: | |
| - name: Require all required jobs to have succeeded | |
| run: | | |
| required="${{ needs.quality.result }} ${{ needs.codex-manifests.result }} ${{ needs.eval-config.result }}" | |
| echo "required job results: $required" | |
| for result in $required; do | |
| if [ "$result" != "success" ]; then | |
| echo "a required job did not succeed: $result" | |
| exit 1 | |
| fi | |
| done |