diff --git a/.github/workflows/full-release-validation.yml b/.github/workflows/full-release-validation.yml index d1111fe133af..3fd9070208b8 100644 --- a/.github/workflows/full-release-validation.yml +++ b/.github/workflows/full-release-validation.yml @@ -88,6 +88,11 @@ on: required: false default: "" type: string + codex_plugin_spec: + description: Optional Codex plugin install spec for live Docker package checks; blank derives from release_package_spec or packs the selected ref + required: false + default: "" + type: string npm_telegram_provider_mode: description: Provider mode for the package Telegram E2E lane required: false @@ -151,6 +156,7 @@ jobs: RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }} EVIDENCE_PACKAGE_SPEC: ${{ inputs.evidence_package_spec }} PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }} + CODEX_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }} RELEASE_PROFILE: ${{ inputs.release_profile }} RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'full' }} RERUN_GROUP: ${{ inputs.rerun_group }} @@ -208,6 +214,9 @@ jobs: else echo "- Package Acceptance package spec: SHA-built release artifact" fi + if [[ -n "${CODEX_PLUGIN_SPEC// }" ]]; then + echo "- Codex plugin spec: \`${CODEX_PLUGIN_SPEC}\`" + fi } >> "$GITHUB_STEP_SUMMARY" docker_runtime_assets_preflight: @@ -463,6 +472,7 @@ jobs: CROSS_OS_SUITE_FILTER: ${{ inputs.cross_os_suite_filter }} RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }} PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }} + CODEX_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }} run: | set -euo pipefail @@ -558,6 +568,9 @@ jobs: if [[ -n "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then echo "- Package Acceptance package spec: \`${PACKAGE_ACCEPTANCE_PACKAGE_SPEC}\`" fi + if [[ -n "${CODEX_PLUGIN_SPEC// }" ]]; then + echo "- Codex plugin spec: \`${CODEX_PLUGIN_SPEC}\`" + fi } >> "$GITHUB_STEP_SUMMARY" child_rerun_group="$RERUN_GROUP" @@ -586,6 +599,9 @@ jobs: if [[ -n "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then args+=(-f package_acceptance_package_spec="$PACKAGE_ACCEPTANCE_PACKAGE_SPEC") fi + if [[ -n "${CODEX_PLUGIN_SPEC// }" ]]; then + args+=(-f codex_plugin_spec="$CODEX_PLUGIN_SPEC") + fi dispatch_and_wait openclaw-release-checks.yml "${args[@]}" diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index 537b415b54c5..d35fb6d54d83 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -68,6 +68,11 @@ on: required: false default: "" type: string + codex_plugin_spec: + description: Optional Codex plugin install spec for the live package lane; blank packs extensions/codex from the selected ref + required: false + default: "" + type: string include_live_suites: description: Whether to run live-provider coverage required: false @@ -173,6 +178,11 @@ on: required: false default: "" type: string + codex_plugin_spec: + description: Optional Codex plugin install spec for the live package lane; blank packs extensions/codex from the selected ref + required: false + default: "" + type: string include_live_suites: description: Whether to run live-provider coverage required: false @@ -631,7 +641,7 @@ jobs: profiles: stable full - chunk_id: package-update-openai label: package/update OpenAI install - timeout_minutes: 20 + timeout_minutes: 45 profiles: beta minimum stable full - chunk_id: package-update-anthropic label: package/update Anthropic install @@ -734,6 +744,7 @@ jobs: OPENCLAW_DOCKER_E2E_REPO_ROOT: ${{ github.workspace }} OPENCLAW_DOCKER_E2E_SELECTED_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }} OPENCLAW_DOCKER_ALL_RELEASE_PROFILE: ${{ inputs.release_test_profile }} + OPENCLAW_CODEX_NPM_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }} OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: ${{ inputs.published_upgrade_survivor_baseline }} OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ inputs.published_upgrade_survivor_baselines }} @@ -974,6 +985,7 @@ jobs: OPENCLAW_DOCKER_E2E_PACKAGE_ARTIFACT_NAME: ${{ inputs.package_artifact_name || 'docker-e2e-package' }} OPENCLAW_DOCKER_E2E_REPO_ROOT: ${{ github.workspace }} OPENCLAW_DOCKER_E2E_SELECTED_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }} + OPENCLAW_CODEX_NPM_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }} OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: ${{ inputs.published_upgrade_survivor_baseline }} OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ matrix.group.published_upgrade_survivor_baselines || inputs.published_upgrade_survivor_baselines }} diff --git a/.github/workflows/openclaw-release-checks.yml b/.github/workflows/openclaw-release-checks.yml index 8a5a044e246d..88f1bfc258b5 100644 --- a/.github/workflows/openclaw-release-checks.yml +++ b/.github/workflows/openclaw-release-checks.yml @@ -78,6 +78,11 @@ on: required: false default: "" type: string + codex_plugin_spec: + description: Optional Codex plugin install spec for live Docker package checks; blank derives from release_package_spec or packs the selected ref + required: false + default: "" + type: string concurrency: group: openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ inputs.rerun_group }} @@ -112,6 +117,7 @@ jobs: qa_live_slack_enabled: ${{ steps.inputs.outputs.qa_live_slack_enabled }} release_package_spec: ${{ steps.inputs.outputs.release_package_spec }} package_acceptance_package_spec: ${{ steps.inputs.outputs.package_acceptance_package_spec }} + codex_plugin_spec: ${{ steps.inputs.outputs.codex_plugin_spec }} steps: - name: Require trusted workflow ref for release checks env: @@ -262,6 +268,7 @@ jobs: RELEASE_QA_SLACK_LIVE_CI_ENABLED: ${{ vars.OPENCLAW_RELEASE_QA_SLACK_LIVE_CI_ENABLED || 'false' }} RELEASE_PACKAGE_SPEC_INPUT: ${{ inputs.release_package_spec }} RELEASE_PACKAGE_ACCEPTANCE_PACKAGE_SPEC_INPUT: ${{ inputs.package_acceptance_package_spec }} + RELEASE_CODEX_PLUGIN_SPEC_INPUT: ${{ inputs.codex_plugin_spec }} run: | set -euo pipefail qa_live_matrix_enabled=true @@ -307,6 +314,10 @@ jobs: if [[ "$release_profile" == "full" ]]; then run_release_soak=true fi + codex_plugin_spec="$RELEASE_CODEX_PLUGIN_SPEC_INPUT" + if [[ -z "${codex_plugin_spec// }" && "$RELEASE_PACKAGE_SPEC_INPUT" =~ ^openclaw@(.+)$ ]]; then + codex_plugin_spec="npm:@openclaw/codex@${BASH_REMATCH[1]}" + fi filter="$(printf '%s' "$RELEASE_LIVE_SUITE_FILTER_INPUT" | tr '[:upper:]' '[:lower:]')" if [[ -n "${filter// }" ]]; then @@ -387,6 +398,7 @@ jobs: printf 'qa_live_slack_enabled=%s\n' "$qa_live_slack_enabled" printf 'release_package_spec=%s\n' "$RELEASE_PACKAGE_SPEC_INPUT" printf 'package_acceptance_package_spec=%s\n' "$RELEASE_PACKAGE_ACCEPTANCE_PACKAGE_SPEC_INPUT" + printf 'codex_plugin_spec=%s\n' "$codex_plugin_spec" } >> "$GITHUB_OUTPUT" - name: Summarize validated ref @@ -403,6 +415,7 @@ jobs: RELEASE_CROSS_OS_SUITE_FILTER: ${{ inputs.cross_os_suite_filter }} RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }} PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }} + CODEX_PLUGIN_SPEC: ${{ steps.inputs.outputs.codex_plugin_spec }} run: | { echo "## Release checks" @@ -432,6 +445,11 @@ jobs: else echo "- Package Acceptance package spec: prepared release artifact" fi + if [[ -n "${CODEX_PLUGIN_SPEC// }" ]]; then + echo "- Codex plugin spec: \`${CODEX_PLUGIN_SPEC}\`" + else + echo "- Codex plugin spec: packed from selected ref" + fi if [[ "$RUN_RELEASE_SOAK" == "true" ]]; then echo "- This run will execute blocking release validation plus exhaustive live/Docker soak coverage." else @@ -640,6 +658,7 @@ jobs: include_live_suites: false release_test_profile: ${{ needs.resolve_target.outputs.release_profile }} package_artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }} + codex_plugin_spec: ${{ needs.resolve_target.outputs.codex_plugin_spec }} secrets: *live_e2e_release_secrets package_acceptance_release_checks: diff --git a/docs/ci.md b/docs/ci.md index fdf0aa4b86bb..dba615957336 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -154,7 +154,7 @@ Every lane uploads GitHub artifacts. When `CLAWGRIT_REPORTS_TOKEN` is configured ## Full Release Validation -`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, QA Lab parity, Matrix, and Telegram lanes. Stable/default runs keep exhaustive live/E2E and Docker release-path coverage behind `run_release_soak=true`; `release_profile=full` forces that soak coverage on so broad advisory validation remains broad. With `rerun_group=all` and `release_profile=full`, it also runs `NPM Telegram Beta E2E` against the `release-package-under-test` artifact from release checks. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only when Telegram must prove a different package. +`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, QA Lab parity, Matrix, and Telegram lanes. Stable/default runs keep exhaustive live/E2E and Docker release-path coverage behind `run_release_soak=true`; `release_profile=full` forces that soak coverage on so broad advisory validation remains broad. With `rerun_group=all` and `release_profile=full`, it also runs `NPM Telegram Beta E2E` against the `release-package-under-test` artifact from release checks. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only when Telegram must prove a different package. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@` derives `codex_plugin_spec=npm:@openclaw/codex@`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs. See [Full release validation](/reference/full-release-validation) for the stage matrix, exact workflow job names, profile differences, artifacts, and @@ -203,7 +203,7 @@ The umbrella records the dispatched child run ids, and the final `Verify full va For recovery, both `Full Release Validation` and `OpenClaw Release Checks` accept `rerun_group`. Use `all` for a release candidate, `ci` for only the normal full CI child, `plugin-prerelease` for only the plugin prerelease child, `release-checks` for every release child, or a narrower group: `install-smoke`, `cross-os`, `live-e2e`, `package`, `qa`, `qa-parity`, `qa-live`, or `npm-telegram` on the umbrella. This keeps a failed release box rerun bounded after a focused fix. For one failed cross-OS lane, combine `rerun_group=cross-os` with `cross_os_suite_filter`, for example `windows/packaged-upgrade`; long cross-OS commands emit heartbeat lines and packaged-upgrade summaries include per-phase timings. QA release-check lanes are advisory except the standard runtime tool coverage gate, which blocks when required OpenClaw dynamic tools drift or disappear from the standard tier summary. -`OpenClaw Release Checks` uses the trusted workflow ref to resolve the selected ref once into a `release-package-under-test` tarball, then passes that artifact to cross-OS checks and Package Acceptance, plus the live/E2E release-path Docker workflow when soak coverage runs. That keeps the package bytes consistent across release boxes and avoids repacking the same candidate in multiple child jobs. +`OpenClaw Release Checks` uses the trusted workflow ref to resolve the selected ref once into a `release-package-under-test` tarball, then passes that artifact to cross-OS checks and Package Acceptance, plus the live/E2E release-path Docker workflow when soak coverage runs. That keeps the package bytes consistent across release boxes and avoids repacking the same candidate in multiple child jobs. For the Codex npm-plugin live lane, release checks either pass a matching published plugin spec derived from `release_package_spec`, pass the operator-supplied `codex_plugin_spec`, or leave the input blank so the Docker script packs the selected checkout's Codex plugin. Duplicate `Full Release Validation` runs for `ref=main` and `rerun_group=all` supersede the older umbrella. The parent monitor cancels any child workflow it @@ -371,7 +371,7 @@ Release Docker coverage runs smaller chunked jobs with `OPENCLAW_SKIP_DOCKER_BUI - `OPENCLAW_DOCKER_ALL_PROFILE=release-path` - `OPENCLAW_DOCKER_ALL_CHUNK=core | package-update-openai | package-update-anthropic | package-update-core | plugins-runtime-plugins | plugins-runtime-services | plugins-runtime-install-a..h` -Current release Docker chunks are `core`, `package-update-openai`, `package-update-anthropic`, `package-update-core`, `plugins-runtime-plugins`, `plugins-runtime-services`, and `plugins-runtime-install-a` through `plugins-runtime-install-h`. `plugins-runtime-core`, `plugins-runtime`, and `plugins-integrations` remain aggregate plugin/runtime aliases. The `install-e2e` lane alias remains the aggregate manual rerun alias for both provider installer lanes. +Current release Docker chunks are `core`, `package-update-openai`, `package-update-anthropic`, `package-update-core`, `plugins-runtime-plugins`, `plugins-runtime-services`, and `plugins-runtime-install-a` through `plugins-runtime-install-h`. `package-update-openai` includes the live Codex plugin package lane, which installs the candidate OpenClaw package, installs the Codex plugin from `codex_plugin_spec` or a same-ref tarball with explicit Codex CLI install approval, runs Codex CLI preflight, then runs multiple same-session OpenClaw agent turns against OpenAI. `plugins-runtime-core`, `plugins-runtime`, and `plugins-integrations` remain aggregate plugin/runtime aliases. The `install-e2e` lane alias remains the aggregate manual rerun alias for both provider installer lanes. OpenWebUI is folded into `plugins-runtime-services` when full release-path coverage requests it, and keeps a standalone `openwebui` chunk only for OpenWebUI-only dispatches. Bundled-channel update lanes retry once for transient npm network failures. diff --git a/docs/reference/full-release-validation.md b/docs/reference/full-release-validation.md index 5579d3d5e7c6..ae88430be594 100644 --- a/docs/reference/full-release-validation.md +++ b/docs/reference/full-release-validation.md @@ -38,6 +38,12 @@ beta publish, pass `release_package_spec=openclaw@YYYY.M.D-beta.N` to reuse the shipped npm package across release checks, Package Acceptance, cross-OS, release-path Docker, and package Telegram. Use `package_acceptance_package_spec` only when Package Acceptance should intentionally prove a different package. +The Codex plugin live package lane follows the same state: published +`release_package_spec` values derive `codex_plugin_spec=npm:@openclaw/codex@`; +SHA/artifact runs pack `extensions/codex` from the selected ref; and operators +can set `codex_plugin_spec` directly for `npm:`, `npm-pack:`, or `git:` plugin +sources. The lane grants the explicit Codex CLI install approval required by +that plugin, then runs Codex CLI preflight and same-session OpenAI agent turns. ## Top-level stages @@ -81,15 +87,15 @@ or Docker-facing stages need it. The Docker release-path stage runs these chunks when `live_suite_filter` is empty: -| Chunk | Coverage | -| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| `core` | Core Docker release-path smoke lanes. | -| `package-update-openai` | OpenAI package install/update behavior, Codex on-demand install, and Chat Completions tool calls. | -| `package-update-anthropic` | Anthropic package install and update behavior. | -| `package-update-core` | Provider-neutral package and update behavior. | -| `plugins-runtime-plugins` | Plugin runtime lanes that exercise plugin behavior. | -| `plugins-runtime-services` | Service-backed and live plugin runtime lanes; includes OpenWebUI when requested. | -| `plugins-runtime-install-a` through `plugins-runtime-install-h` | Plugin install/runtime batches split for parallel release validation. | +| Chunk | Coverage | +| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `core` | Core Docker release-path smoke lanes. | +| `package-update-openai` | OpenAI package install/update behavior, Codex on-demand install, Codex plugin live turns, and Chat Completions tool calls. | +| `package-update-anthropic` | Anthropic package install and update behavior. | +| `package-update-core` | Provider-neutral package and update behavior. | +| `plugins-runtime-plugins` | Plugin runtime lanes that exercise plugin behavior. | +| `plugins-runtime-services` | Service-backed and live plugin runtime lanes; includes OpenWebUI when requested. | +| `plugins-runtime-install-a` through `plugins-runtime-install-h` | Plugin install/runtime batches split for parallel release validation. | Use targeted `docker_lanes=` on the reusable live/E2E workflow when only one Docker lane failed. The release artifacts include per-lane rerun diff --git a/scripts/e2e/codex-npm-plugin-live-docker.sh b/scripts/e2e/codex-npm-plugin-live-docker.sh index efbacb9a291c..8cbb3ca5d0ff 100644 --- a/scripts/e2e/codex-npm-plugin-live-docker.sh +++ b/scripts/e2e/codex-npm-plugin-live-docker.sh @@ -1,19 +1,27 @@ #!/usr/bin/env bash # Installs OpenClaw from a prepared package tarball, installs @openclaw/codex -# from the real npm registry, and verifies a live Codex app-server turn. +# from a registry/git/tarball spec, and verifies a live Codex app-server turn. set -euo pipefail -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" +SCRIPT_ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +TRUSTED_HARNESS_DIR="${OPENCLAW_LIVE_DOCKER_TRUSTED_HARNESS_DIR:-$SCRIPT_ROOT_DIR}" +CANDIDATE_ROOT="${OPENCLAW_LIVE_DOCKER_REPO_ROOT:-$SCRIPT_ROOT_DIR}" +TRUSTED_HARNESS_DIR="$(cd "$TRUSTED_HARNESS_DIR" && pwd)" +CANDIDATE_ROOT="$(cd "$CANDIDATE_ROOT" && pwd)" +ROOT_DIR="$TRUSTED_HARNESS_DIR" +source "$TRUSTED_HARNESS_DIR/scripts/lib/docker-e2e-image.sh" +source "$TRUSTED_HARNESS_DIR/scripts/lib/docker-e2e-package.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-codex-npm-plugin-live-e2e" OPENCLAW_CODEX_NPM_PLUGIN_E2E_IMAGE)" DOCKER_TARGET="${OPENCLAW_CODEX_NPM_PLUGIN_DOCKER_TARGET:-bare}" HOST_BUILD="${OPENCLAW_CODEX_NPM_PLUGIN_HOST_BUILD:-1}" PACKAGE_TGZ="${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" PROFILE_FILE="${OPENCLAW_CODEX_NPM_PLUGIN_PROFILE_FILE:-${OPENCLAW_TESTBOX_PROFILE_FILE:-$HOME/.openclaw-testbox-live.profile}}" +CODEX_PLUGIN_SPEC="${OPENCLAW_CODEX_NPM_PLUGIN_SPEC:-}" +CODEX_PLUGIN_MOUNT=() +CODEX_PLUGIN_PACK_DIR="" -docker_e2e_build_or_reuse "$IMAGE_NAME" codex-npm-plugin-live "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "$DOCKER_TARGET" +docker_e2e_build_or_reuse "$IMAGE_NAME" codex-npm-plugin-live "$CANDIDATE_ROOT/scripts/e2e/Dockerfile" "$CANDIDATE_ROOT" "$DOCKER_TARGET" prepare_package_tgz() { if [ -n "$PACKAGE_TGZ" ]; then @@ -24,11 +32,60 @@ prepare_package_tgz() { echo "OPENCLAW_CODEX_NPM_PLUGIN_HOST_BUILD=0 requires OPENCLAW_CURRENT_PACKAGE_TGZ" >&2 exit 1 fi + local harness_root="$ROOT_DIR" + ROOT_DIR="$CANDIDATE_ROOT" PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz codex-npm-plugin-live)" + ROOT_DIR="$harness_root" } prepare_package_tgz +prepare_codex_plugin_spec() { + local source_path + local container_path + local pack_output + + if [ -z "$CODEX_PLUGIN_SPEC" ]; then + CODEX_PLUGIN_PACK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-codex-plugin-pack.XXXXXX")" + ( + cd "$CANDIDATE_ROOT" + node scripts/lib/plugin-npm-runtime-build.mjs extensions/codex + node scripts/lib/plugin-npm-package-manifest.mjs --run extensions/codex -- \ + npm pack --json --ignore-scripts --pack-destination "$CODEX_PLUGIN_PACK_DIR" + ) >/tmp/openclaw-codex-plugin-pack.log 2>&1 + pack_output=() + while IFS= read -r packed_file; do + pack_output+=("$packed_file") + done < <(find "$CODEX_PLUGIN_PACK_DIR" -maxdepth 1 -type f -name '*.tgz' | sort) + if [ "${#pack_output[@]}" -ne 1 ]; then + echo "Expected one packed Codex plugin tarball; found ${#pack_output[@]}." >&2 + cat /tmp/openclaw-codex-plugin-pack.log >&2 || true + exit 1 + fi + source_path="${pack_output[0]}" + container_path="/tmp/$(basename "$source_path")" + CODEX_PLUGIN_MOUNT=(-v "$source_path":"$container_path":ro) + CODEX_PLUGIN_SPEC="npm-pack:$container_path" + return 0 + fi + + if [[ "$CODEX_PLUGIN_SPEC" == npm-pack:* ]]; then + source_path="${CODEX_PLUGIN_SPEC#npm-pack:}" + if [[ "$source_path" != /* ]]; then + source_path="$CANDIDATE_ROOT/$source_path" + fi + if [ ! -f "$source_path" ]; then + echo "Codex plugin npm-pack tarball not found: $source_path" >&2 + exit 1 + fi + container_path="/tmp/$(basename "$source_path")" + CODEX_PLUGIN_MOUNT=(-v "$source_path":"$container_path":ro) + CODEX_PLUGIN_SPEC="npm-pack:$container_path" + fi +} + +prepare_codex_plugin_spec + PROFILE_MOUNT=() PROFILE_STATUS="none" if [ -f "$PROFILE_FILE" ] && [ -r "$PROFILE_FILE" ]; then @@ -46,16 +103,18 @@ OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 codex-npm-plug echo "Running Codex npm plugin live Docker E2E..." echo "Profile file: $PROFILE_STATUS" +echo "Codex plugin spec: $CODEX_PLUGIN_SPEC" if ! docker_e2e_run_with_harness \ -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ -e OPENCLAW_CODEX_NPM_PLUGIN_ALLOW_BETA_COMPAT_DIAGNOSTICS="${OPENCLAW_CODEX_NPM_PLUGIN_ALLOW_BETA_COMPAT_DIAGNOSTICS:-0}" \ - -e OPENCLAW_CODEX_NPM_PLUGIN_FORCE_UNSAFE_INSTALL="${OPENCLAW_CODEX_NPM_PLUGIN_FORCE_UNSAFE_INSTALL:-0}" \ + -e OPENCLAW_CODEX_NPM_PLUGIN_FORCE_UNSAFE_INSTALL="${OPENCLAW_CODEX_NPM_PLUGIN_FORCE_UNSAFE_INSTALL:-1}" \ -e OPENCLAW_CODEX_NPM_PLUGIN_MODEL="${OPENCLAW_CODEX_NPM_PLUGIN_MODEL:-codex/gpt-5.4}" \ - -e OPENCLAW_CODEX_NPM_PLUGIN_SPEC="${OPENCLAW_CODEX_NPM_PLUGIN_SPEC:-npm:@openclaw/codex}" \ + -e OPENCLAW_CODEX_NPM_PLUGIN_SPEC="$CODEX_PLUGIN_SPEC" \ -e OPENAI_API_KEY \ -e OPENAI_BASE_URL \ -e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \ "${DOCKER_E2E_PACKAGE_ARGS[@]}" \ + "${CODEX_PLUGIN_MOUNT[@]}" \ "${PROFILE_MOUNT[@]}" \ -i "$IMAGE_NAME" bash -s >"$run_log" 2>&1 <<'EOF'; then set -euo pipefail @@ -108,6 +167,10 @@ dump_debug_logs() { /tmp/openclaw-codex-preflight.log \ /tmp/openclaw-codex-agent.json \ /tmp/openclaw-codex-agent.err \ + /tmp/openclaw-codex-agent-turn1.json \ + /tmp/openclaw-codex-agent-turn1.err \ + /tmp/openclaw-codex-agent-turn2.json \ + /tmp/openclaw-codex-agent-turn2.err \ /tmp/openclaw-codex-plugin-uninstall.log \ /tmp/openclaw-codex-plugins-list-after-uninstall.json \ /tmp/openclaw-codex-agent-after-uninstall.json \ @@ -121,7 +184,7 @@ chmod 700 "$XDG_CACHE_HOME" "$NPM_CONFIG_CACHE" || true openclaw_e2e_install_package /tmp/openclaw-install.log command -v openclaw >/dev/null -echo "Installing Codex plugin from npm: $CODEX_PLUGIN_SPEC" +echo "Installing Codex plugin: $CODEX_PLUGIN_SPEC" openclaw plugins install "$CODEX_PLUGIN_SPEC" "${PLUGIN_INSTALL_FLAGS[@]}" >/tmp/openclaw-codex-plugin-install.log 2>&1 node scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs configure "$MODEL_REF" @@ -137,43 +200,101 @@ node scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs assert-npm-deps CODEX_BIN="$(node scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs print-codex-bin)" printf '%s\n' "$OPENAI_API_KEY" | "$CODEX_BIN" login --with-api-key >/dev/null +print_agent_reply() { + node -e ' +const fs = require("node:fs"); +const file = process.argv[1]; +const marker = process.argv[2]; +const label = process.argv[3]; +const response = JSON.parse(fs.readFileSync(file, "utf8")); +const text = (response.payloads || []) + .map((payload) => (payload && typeof payload.text === "string" ? payload.text : "")) + .filter(Boolean) + .join("\n") + .trim(); +console.log(`${label}: ${text}`); +if (!text.includes(marker)) { + console.error(`missing marker ${marker} in ${file}`); + process.exit(1); +} +' "$1" "$2" "$3" +} + +run_agent_turn() { + local label="$1" + local marker="$2" + local message="$3" + local out="$4" + local err="$5" + + echo "${label}_prompt: $message" + openclaw agent --local \ + --agent main \ + --session-id "$SESSION_ID" \ + --model "$MODEL_REF" \ + --message "$message" \ + --thinking low \ + --timeout 420 \ + --json >"$out" 2>"$err" /tmp/openclaw-codex-preflight.log 2>&1 + "Reply exactly: ${SUCCESS_MARKER}-PREFLIGHT" >/tmp/openclaw-codex-preflight.log 2>&1 /tmp/openclaw-codex-agent.json 2>/tmp/openclaw-codex-agent.err +echo "Running OpenClaw local agent turns through npm-installed Codex plugin..." +run_agent_turn \ + "turn1" \ + "${SUCCESS_MARKER}-TURN-1" \ + "Reply in one short sentence. Include token ${SUCCESS_MARKER}-TURN-1 and say hello from the OpenClaw Codex plugin." \ + /tmp/openclaw-codex-agent-turn1.json \ + /tmp/openclaw-codex-agent-turn1.err +run_agent_turn \ + "turn2" \ + "${SUCCESS_MARKER}-TURN-2" \ + "Using this same conversation, name the exact token from your previous reply, then include token ${SUCCESS_MARKER}-TURN-2." \ + /tmp/openclaw-codex-agent-turn2.json \ + /tmp/openclaw-codex-agent-turn2.err +run_agent_turn \ + "turn3" \ + "$SUCCESS_MARKER" \ + "Answer 7 plus 8, include token $SUCCESS_MARKER, and mention whether you saw ${SUCCESS_MARKER}-TURN-2 earlier." \ + /tmp/openclaw-codex-agent.json \ + /tmp/openclaw-codex-agent.err node scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs assert-agent-turn "$SUCCESS_MARKER" "$SESSION_ID" "$MODEL_REF" +echo "TRANSCRIPT_END" echo "Uninstalling Codex plugin and verifying the configured harness now fails..." openclaw plugins uninstall codex --force >/tmp/openclaw-codex-plugin-uninstall.log 2>&1 openclaw plugins list --json >/tmp/openclaw-codex-plugins-list-after-uninstall.json node scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs assert-uninstalled -set +e -openclaw agent --local \ +if openclaw agent --local \ --agent main \ --session-id "${SESSION_ID}-after-uninstall" \ --model "$MODEL_REF" \ --message "Reply exactly: ${SUCCESS_MARKER}-AFTER-UNINSTALL" \ --thinking low \ --timeout 120 \ - --json >/tmp/openclaw-codex-agent-after-uninstall.json 2>/tmp/openclaw-codex-agent-after-uninstall.err -after_uninstall_status=$? -set -e -node scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs assert-agent-error "$after_uninstall_status" + --json >/tmp/openclaw-codex-agent-after-uninstall.json 2>/tmp/openclaw-codex-agent-after-uninstall.err; then + echo "Expected OpenClaw agent to fail after Codex uninstall, got status 0" >&2 + exit 1 +fi +if ! grep -Fq 'Requested agent harness "codex" is not registered' /tmp/openclaw-codex-agent-after-uninstall.err && + ! grep -Fq 'Unknown model: codex/' /tmp/openclaw-codex-agent-after-uninstall.err; then + echo "Unexpected post-uninstall agent error:" >&2 + cat /tmp/openclaw-codex-agent-after-uninstall.err >&2 || true + exit 1 +fi echo "Codex npm plugin live Docker E2E passed" EOF @@ -182,5 +303,6 @@ EOF exit 1 fi +awk '/TRANSCRIPT_BEGIN/{printing=1} printing{print} /TRANSCRIPT_END/{printing=0}' "$run_log" rm -f "$run_log" echo "Codex npm plugin live Docker E2E passed" diff --git a/scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs b/scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs index 846cab235963..04a1e1b4f809 100644 --- a/scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs +++ b/scripts/e2e/lib/codex-npm-plugin-live/assertions.mjs @@ -95,6 +95,32 @@ function readInstallRecords() { return index.installRecords || index.records || {}; } +function normalizePluginSpec(spec) { + if (spec.startsWith("npm:")) { + return { + expectedSpec: spec.slice("npm:".length), + source: "npm", + }; + } + if (spec.startsWith("npm-pack:")) { + return { + artifactKind: "npm-pack", + source: "npm", + sourcePath: spec.slice("npm-pack:".length), + }; + } + if (spec.startsWith("git:")) { + return { + expectedSpec: spec, + source: "git", + }; + } + return { + expectedSpec: spec, + source: "npm", + }; +} + function assertPlugin() { const spec = process.argv[3] || "npm:@openclaw/codex"; const list = readJson("/tmp/openclaw-codex-plugins-list.json"); @@ -141,16 +167,32 @@ function assertPlugin() { } const record = readInstallRecord(); - const expectedSpec = spec.replace(/^npm:/u, ""); - if (record.source !== "npm") { - throw new Error(`expected codex npm install record, got source=${record.source}`); + const expected = normalizePluginSpec(spec); + if (record.source !== expected.source) { + throw new Error( + `expected codex ${expected.source} install record, got source=${record.source}`, + ); } - if (record.spec !== expectedSpec) { - throw new Error(`expected codex npm spec ${expectedSpec}, got ${record.spec}`); + if (expected.expectedSpec && record.spec !== expected.expectedSpec) { + throw new Error(`expected codex install spec ${expected.expectedSpec}, got ${record.spec}`); } - if (!record.resolvedVersion || !record.resolvedSpec) { + if (expected.artifactKind && record.artifactKind !== expected.artifactKind) { + throw new Error( + `expected codex artifact kind ${expected.artifactKind}, got ${record.artifactKind}`, + ); + } + if ( + expected.sourcePath && + realPathMaybe(record.sourcePath || "") !== realPathMaybe(expected.sourcePath) + ) { + throw new Error(`expected codex source path ${expected.sourcePath}, got ${record.sourcePath}`); + } + if (record.source === "npm" && (!record.resolvedVersion || !record.resolvedSpec)) { throw new Error(`missing codex npm resolution metadata: ${JSON.stringify(record)}`); } + if (record.source === "git" && !record.gitCommit) { + throw new Error(`missing codex git resolution metadata: ${JSON.stringify(record)}`); + } } function managedNpmRoot() { @@ -307,6 +349,12 @@ function assertAgentTurn() { `OpenClaw agent reply did not contain ${marker}:\nstdout=${stdout}\nstderr=${stderr}`, ); } + const executionTrace = response.meta?.executionTrace; + if (!executionTrace || executionTrace.winnerProvider !== "codex") { + throw new Error( + `expected Codex plugin to win the agent turn, got ${JSON.stringify(executionTrace)}`, + ); + } const sessionsDir = path.join(stateDir(), "agents", "main", "sessions"); const storePath = path.join(sessionsDir, "sessions.json"); @@ -333,17 +381,22 @@ function assertAgentTurn() { if (binding.model !== modelRef.split("/").slice(1).join("/")) { throw new Error(`unexpected Codex binding model: ${binding.model}`); } - if (binding.modelProvider && binding.modelProvider !== "codex") { + if (binding.modelProvider && !["codex", "openai"].includes(binding.modelProvider)) { throw new Error(`unexpected Codex binding provider: ${binding.modelProvider}`); } - const codexHome = path.join(stateDir(), "agents", "main", "agent", "codex-home"); - const nativeHome = path.join(codexHome, "home"); - if (!fs.existsSync(codexHome) || !fs.existsSync(nativeHome)) { - throw new Error(`missing isolated Codex home: ${codexHome}`); + const agentDir = path.join(stateDir(), "agents", "main"); + const codexHomes = [ + path.join(agentDir, "codex-home"), + path.join(agentDir, "agent", "codex-home"), + path.join(path.dirname(agentDir), "codex-home"), + ].filter((entry, index, entries) => entries.indexOf(entry) === index); + const codexHome = codexHomes.find((entry) => fs.existsSync(entry)); + if (!codexHome) { + throw new Error(`missing isolated Codex home; checked ${codexHomes.join(", ")}`); } const codexSessionRoot = path.join(codexHome, "sessions"); - const nativeSessionRoot = path.join(nativeHome, ".codex", "sessions"); + const nativeSessionRoot = path.join(codexHome, "home", ".codex", "sessions"); assertNativeCodexSessionEvidence({ codexHome, marker, @@ -387,7 +440,10 @@ function assertAgentError() { ? fs.readFileSync("/tmp/openclaw-codex-agent-after-uninstall.err", "utf8") : ""; const combined = `${stdout}\n${stderr}`; - if (!combined.includes('Requested agent harness "codex" is not registered')) { + if ( + !combined.includes('Requested agent harness "codex" is not registered') && + !combined.includes("Unknown model: codex/") + ) { throw new Error(`unexpected post-uninstall agent error:\nstdout=${stdout}\nstderr=${stderr}`); } } diff --git a/scripts/lib/docker-e2e-scenarios.mjs b/scripts/lib/docker-e2e-scenarios.mjs index cb3518e8f825..a4dc48a5a94d 100644 --- a/scripts/lib/docker-e2e-scenarios.mjs +++ b/scripts/lib/docker-e2e-scenarios.mjs @@ -159,6 +159,22 @@ function liveOpenAiChatToolsLane() { ); } +function liveCodexNpmPluginLane() { + return liveLane( + "live-codex-npm-plugin", + liveDockerScriptCommand("e2e/codex-npm-plugin-live-docker.sh"), + { + cacheKey: "codex-npm-plugin", + e2eImageKind: "bare", + provider: "openai", + resources: ["npm"], + stateScenario: "empty", + timeoutMs: 30 * 60 * 1000, + weight: 3, + }, + ); +} + export const mainLanes = [ liveLane("live-models", liveDockerScriptCommand("test-live-models-docker.sh"), { providers: ["claude-cli", "google-gemini-cli"], @@ -484,19 +500,7 @@ export const tailLanes = [ weight: 3, }, ), - liveLane( - "live-codex-npm-plugin", - "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-codex-npm-plugin", - { - cacheKey: "codex-npm-plugin", - e2eImageKind: "bare", - provider: "openai", - resources: ["npm"], - stateScenario: "empty", - timeoutMs: 30 * 60 * 1000, - weight: 3, - }, - ), + liveCodexNpmPluginLane(), livePluginToolLane(), liveLane( "live-acp-bind-claude", @@ -633,6 +637,7 @@ const releasePathPackageInstallOpenAiLanes = [ }, ), liveOpenAiChatToolsLane(), + liveCodexNpmPluginLane(), npmLane("codex-on-demand", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:codex-on-demand", { resources: ["service"], stateScenario: "empty", diff --git a/test/scripts/docker-e2e-plan.test.ts b/test/scripts/docker-e2e-plan.test.ts index 2c8385e33917..3b52147524c0 100644 --- a/test/scripts/docker-e2e-plan.test.ts +++ b/test/scripts/docker-e2e-plan.test.ts @@ -113,6 +113,7 @@ describe("scripts/lib/docker-e2e-plan", () => { expect(plan.credentials).toEqual(["anthropic", "openai"]); expect(plan.lanes.map((lane) => lane.name)).toContain("install-e2e-openai"); expect(plan.lanes.map((lane) => lane.name)).toContain("openai-chat-tools"); + expect(plan.lanes.map((lane) => lane.name)).toContain("live-codex-npm-plugin"); expect(plan.lanes.map((lane) => lane.name)).toContain("codex-on-demand"); expect(plan.lanes.map((lane) => lane.name)).toContain("install-e2e-anthropic"); expect(plan.lanes.map((lane) => lane.name)).toContain("mcp-channels"); @@ -157,6 +158,7 @@ describe("scripts/lib/docker-e2e-plan", () => { expect(plan.releaseProfile).toBe("beta"); expect(laneNames).toContain("install-e2e-openai"); expect(laneNames).toContain("openai-chat-tools"); + expect(laneNames).toContain("live-codex-npm-plugin"); expect(laneNames).toContain("install-e2e-anthropic"); expect(laneNames).toContain("update-channel-switch"); expect(laneNames).not.toContain("plugins"); @@ -246,6 +248,7 @@ describe("scripts/lib/docker-e2e-plan", () => { expect(packageInstallOpenAi.lanes.map((lane) => lane.name)).toEqual([ "install-e2e-openai", "openai-chat-tools", + "live-codex-npm-plugin", "codex-on-demand", ]); expect(packageInstallAnthropic.lanes.map((lane) => lane.name)).toEqual([ @@ -483,6 +486,7 @@ describe("scripts/lib/docker-e2e-plan", () => { expect(packageUpdate.lanes.map((lane) => lane.name)).toEqual([ "install-e2e-openai", "openai-chat-tools", + "live-codex-npm-plugin", "codex-on-demand", "install-e2e-anthropic", "npm-onboard-channel-agent", @@ -653,7 +657,8 @@ describe("scripts/lib/docker-e2e-plan", () => { expect(plan.credentials).toEqual(["openai"]); expect(plan.lanes.map(summarizeLane)).toEqual([ { - command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-codex-npm-plugin", + command: + 'OPENCLAW_SKIP_DOCKER_BUILD=1 bash -c \'harness="${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-}"; if [ -z "$harness" ]; then if [ -d .release-harness/scripts ]; then harness=.release-harness; else harness=.; fi; fi; OPENCLAW_LIVE_DOCKER_REPO_ROOT="${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}" bash "$harness/scripts/e2e/codex-npm-plugin-live-docker.sh"\'', imageKind: "bare", live: true, name: "live-codex-npm-plugin", diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 5f9fdebfacde..f9dfa1818b2d 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -166,6 +166,14 @@ describe("package acceptance workflow", () => { expect(workflow).toContain( 'args+=(-f package_acceptance_package_spec="$PACKAGE_ACCEPTANCE_PACKAGE_SPEC")', ); + expect(workflow).toContain("codex_plugin_spec:"); + expect(workflow).toContain('args+=(-f codex_plugin_spec="$CODEX_PLUGIN_SPEC")'); + expect(releaseChecksWorkflow).toContain( + 'codex_plugin_spec="npm:@openclaw/codex@${BASH_REMATCH[1]}"', + ); + expect(releaseChecksWorkflow).toContain( + "codex_plugin_spec: ${{ needs.resolve_target.outputs.codex_plugin_spec }}", + ); expect(workflow).toContain("--json status,conclusion,url,attempt,headSha,jobs"); expect(workflow).toContain( '[[ "$CHILD_WORKFLOW_REF" == release-ci/* && -n "${TARGET_SHA// }" && "$head_sha" != "$TARGET_SHA" ]]', @@ -525,6 +533,9 @@ describe("package artifact reuse", () => { expect(scenarios).toMatch(/liveDockerScriptCommand\(\s*"test-live-cli-backend-docker\.sh"/u); expect(scenarios).toMatch(/liveDockerScriptCommand\(\s*"test-live-acp-bind-docker\.sh"/u); expect(scenarios).toMatch(/liveDockerScriptCommand\(\s*"test-live-codex-harness-docker\.sh"/u); + expect(scenarios).toMatch( + /liveDockerScriptCommand\(\s*"e2e\/codex-npm-plugin-live-docker\.sh"/u, + ); expect(scenarios).toMatch( /liveDockerScriptCommand\(\s*"test-live-subagent-announce-docker\.sh"/u, );