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
2 changes: 2 additions & 0 deletions .codespellignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
iTerm
iTerm2
numer
oint
psuedo
SOM
te
TE
WRONLY
12 changes: 10 additions & 2 deletions .github/scripts/check_ci_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,30 @@
"""Fail a terminal CI job unless every serialized dependency succeeded.

Parent workflows pass GitHub's `toJSON(needs)` object through the NEEDS
environment variable. Treat skipped and cancelled dependencies as failures too:
for a required fan-in job, only an explicit success is safe to accept.
environment variable. Only explicit success is accepted, except for skipped
dependencies named with --allow-skipped. Failures and cancellations always fail.
"""

import argparse
import json
import os


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--allow-skipped", action="append", default=[])
args = parser.parse_args()
# Keep result policy in one script so blocking-ci and postmerge-ci cannot
# drift in how they interpret dependency conclusions.
needs = json.loads(os.environ["NEEDS"])
allowed_skips = set(args.allow_skipped)
if unknown := allowed_skips - needs.keys():
raise SystemExit(f"Unknown optional CI dependencies: {sorted(unknown)}")
failures = sorted(
(name, dependency["result"])
for name, dependency in needs.items()
if dependency["result"] != "success"
and not (name in allowed_skips and dependency["result"] == "skipped")
)

if failures:
Expand Down
63 changes: 63 additions & 0 deletions .github/scripts/fork_ci_changes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Select hosted fork CI coverage without requiring Rust or repository secrets."""

import argparse
import subprocess
from pathlib import PurePosixPath


def coverage(paths: list[str], *, full: bool = False) -> dict[str, bool]:
rust = full or any(
path.startswith("codex-rs/")
and (
PurePosixPath(path).suffix == ".rs"
or PurePosixPath(path).name
in {"Cargo.toml", "Cargo.lock", "rust-toolchain.toml"}
or path.startswith("codex-rs/.cargo/")
or path.startswith("codex-rs/.config/")
)
for path in paths
)
sdk = rust or any(
path.startswith("sdk/")
or path in {"package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"}
or path == ".github/workflows/sdk.yml"
for path in paths
)
return {"rust_full": rust, "sdk": sdk}


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base")
parser.add_argument("--head", default="HEAD")
parser.add_argument("--full", action="store_true")
args = parser.parse_args()
if args.full:
paths = []
elif not args.base or set(args.base) == {"0"}:
# Initial pushes and manual runs have no usable comparison range.
args.full = True
paths = []
else:
paths = (
subprocess.check_output(
[
"git",
"diff",
"--name-only",
"--no-renames",
"-z",
args.base,
args.head,
]
)
.decode("utf-8")
.split("\0")
)
for key, value in coverage(paths, full=args.full).items():
print(f"{key}={str(value).lower()}")


if __name__ == "__main__":
main()
50 changes: 50 additions & 0 deletions .github/scripts/test_check_ci_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import json
import os
from pathlib import Path
import subprocess
import sys
import unittest


class CiResultsTests(unittest.TestCase):
def check(self, needs, *args):
return subprocess.run(
[
sys.executable,
str(Path(__file__).with_name("check_ci_results.py")),
*args,
],
env={**os.environ, "NEEDS": json.dumps(needs)},
capture_output=True,
text=True,
).returncode

def test_success(self):
self.assertEqual(self.check({"tests": {"result": "success"}}), 0)

def test_required_checks_must_succeed(self):
for result in ("failure", "cancelled", "skipped"):
with self.subTest(result=result):
self.assertNotEqual(self.check({"tests": {"result": result}}), 0)

def test_only_explicit_optional_skips_are_allowed(self):
self.assertEqual(
self.check({"sdk": {"result": "skipped"}}, "--allow-skipped", "sdk"),
0,
)
self.assertNotEqual(
self.check({"tests": {"result": "skipped"}}, "--allow-skipped", "sdk"),
0,
)

def test_optional_failures_and_cancellations_still_fail(self):
for result in ("failure", "cancelled"):
with self.subTest(result=result):
self.assertNotEqual(
self.check({"sdk": {"result": result}}, "--allow-skipped", "sdk"),
0,
)


if __name__ == "__main__":
unittest.main()
40 changes: 40 additions & 0 deletions .github/scripts/test_fork_ci_changes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import unittest

from fork_ci_changes import coverage


class ForkCoverageTests(unittest.TestCase):
def test_setup_and_docs_keep_native_smoke_coverage(self):
self.assertEqual(
coverage(["codex-rs/scripts/setup-windows.ps1", "docs/install.md"]),
{"rust_full": False, "sdk": False},
)

def test_rust_sources_and_build_configuration_require_full_coverage(self):
for path in (
"codex-rs/core/src/lib.rs",
"codex-rs/utils/pty/tests/test.rs",
"codex-rs/Cargo.lock",
"codex-rs/core/Cargo.toml",
"codex-rs/rust-toolchain.toml",
"codex-rs/.cargo/config.toml",
"codex-rs/.config/nextest.toml",
):
with self.subTest(path=path):
self.assertEqual(coverage([path]), {"rust_full": True, "sdk": True})

def test_sdk_and_dependency_changes_require_sdk_coverage(self):
for path in (
"sdk/python/src/example.py",
"sdk/typescript/src/index.ts",
"pnpm-lock.yaml",
):
with self.subTest(path=path):
self.assertEqual(coverage([path]), {"rust_full": False, "sdk": True})

def test_manual_full_run_cannot_skip_expensive_checks(self):
self.assertEqual(coverage([], full=True), {"rust_full": True, "sdk": True})


if __name__ == "__main__":
unittest.main()
3 changes: 1 addition & 2 deletions .github/scripts/verify_cargo_workspace_manifests.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
"path-utils": "codex-utils-path",
}
MANIFEST_FEATURE_EXCEPTIONS = {
"codex-rs/code-mode/Cargo.toml": {"sandbox": ("v8/v8_enable_sandbox",)},
"codex-rs/v8-poc/Cargo.toml": {"sandbox": ("v8/v8_enable_sandbox",)},
}
OPTIONAL_DEPENDENCY_EXCEPTIONS = set()
Expand Down Expand Up @@ -227,7 +226,7 @@ def is_workspace_reference(value: object) -> bool:


def manifest_key(path: Path) -> str:
return str(path.relative_to(ROOT))
return path.relative_to(ROOT).as_posix()


def normalize_feature_mapping(value: object) -> dict[str, tuple[str, ...]] | None:
Expand Down
4 changes: 2 additions & 2 deletions .github/scripts/verify_tui_core_boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def main() -> int:


def manifest_failures() -> list[str]:
manifest = tomllib.loads(TUI_MANIFEST.read_text())
manifest = tomllib.loads(TUI_MANIFEST.read_text(encoding="utf-8"))
failures = []
for section_name, dependencies in dependency_sections(manifest):
if FORBIDDEN_PACKAGE in dependencies:
Expand Down Expand Up @@ -74,7 +74,7 @@ def dependency_sections(manifest: dict) -> list[tuple[str, dict]]:
def source_failures() -> list[str]:
failures = []
for path in sorted(TUI_ROOT.glob("**/*.rs")):
text = path.read_text()
text = path.read_text(encoding="utf-8")
for line_number, line in enumerate(text.splitlines(), start=1):
if any(pattern.search(line) for pattern in FORBIDDEN_SOURCE_PATTERNS):
failures.append(
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
# Workflow Strategy

## Forks

`fork-ci.yml` is the entrypoint for fork pull requests and pushes to `main`.
It uses standard GitHub-hosted Ubuntu, Windows 2025 and macOS 15 runners with
read-only repository permissions. It needs no OpenAI secrets, runner groups,
BuildBuddy account, protected environments or paid larger runners.

- Every run checks repository policies, formatting, unused dependencies,
spelling and dependency advisories, and runs the Windows setup tests under
Windows PowerShell 5.1 and PowerShell 7 with mocked installers.
- Native Cargo clippy and nextest always cover `codex-shell-command`,
`codex-utils-pty` and `codex-utils-path` on all three operating systems. This
includes native MSVC Windows builds and PowerShell-related shell handling.
- Rust sources, Cargo manifests/lockfiles, toolchain or Cargo/nextest configuration
changes expand native checks to the full Rust workspace and enable SDK tests.
SDK sources, JavaScript dependency files and `sdk.yml` changes enable SDK tests
against a Cargo-built CLI on hosted Ubuntu.
- The **Run workflow** button defaults to full native Rust and SDK coverage.
Clear **full** to repeat the faster smoke checks. Cargo caches are isolated by
operating system, architecture, toolchain, dependency lock and coverage scope.
- Require **Fork CI required** in the fork's branch rules. It fails on failed,
cancelled or unexpectedly skipped dependencies. SDK checks may be skipped only
when the change detector says they are unnecessary.

The native fork suite does not replace OpenAI's Bazel, source-built V8,
cross-compilation, release/signing or custom argument-comment-lint coverage.
Those workflows are gated to `openai/codex`; they remain available there with
their original infrastructure. Fork CI does not publish releases or deploy.
Cargo shear reports informational warnings about unlinked source files without
failing; unused dependencies remain failures.

## OpenAI Upstream

The workflows in this directory are split so that pull requests get fast, review-friendly signal while `main` still gets the full cross-platform verification pass.

## Pull Requests
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/bazel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ concurrency:

jobs:
test:
if: github.repository == 'openai/codex'
# PRs use the sharded Windows cross-compiled test jobs below. Post-merge
# pushes to main also run the native Windows test job for broader Windows
# signal without putting PR latency back on the critical path. When
Expand Down Expand Up @@ -130,6 +131,7 @@ jobs:
uses: ./.github/actions/check-clean-worktree

test-windows-shard:
if: github.repository == 'openai/codex'
# Split the Windows Bazel test leg across separate Windows hosts. Jobs with
# BuildBuddy credentials use Linux RBE for build actions; test execution
# remains on a Windows runner.
Expand Down Expand Up @@ -234,7 +236,7 @@ jobs:
test-windows:
# Preserve the existing required-check surface while the real work happens
# in the sharded Windows jobs above.
if: always()
if: ${{ github.repository == 'openai/codex' && (always()) }}
needs: test-windows-shard
runs-on: ubuntu-24.04
name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm
Expand All @@ -252,7 +254,7 @@ jobs:
# Native Windows Bazel tests are slower and frequently approach the
# 30-minute PR budget. Run this only for post-merge commits to main and give
# it a larger timeout.
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
if: ${{ github.repository == 'openai/codex' && (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
timeout-minutes: 40
runs-on:
group: ${{ github.event.repository.name }}-runners
Expand Down Expand Up @@ -330,6 +332,7 @@ jobs:
uses: ./.github/actions/check-clean-worktree

clippy:
if: github.repository == 'openai/codex'
timeout-minutes: 30
strategy:
fail-fast: false
Expand Down Expand Up @@ -433,6 +436,7 @@ jobs:
uses: ./.github/actions/check-clean-worktree

verify-release-build:
if: github.repository == 'openai/codex'
timeout-minutes: 30
strategy:
fail-fast: false
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/blob-size-policy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ jobs:
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
base='${{ github.event.pull_request.base.sha }}'
head='${{ github.event.pull_request.head.sha }}'
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
base="$(git rev-parse HEAD^)"
head='${{ github.sha }}'
else
base='${{ github.event.before }}'
head='${{ github.sha }}'
fi
if [[ "$base" =~ ^0+$ ]]; then
base="$(git hash-object -t tree /dev/null)"
fi

echo "base=$base" >> "$GITHUB_OUTPUT"
echo "head=$head" >> "$GITHUB_OUTPUT"
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/blocking-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,43 @@ jobs:
# Keep reusable workflow calls alphabetized. The `required` job below is the
# version-controlled list that the main-branch ruleset should require.
bazel:
if: github.repository == 'openai/codex'
name: Bazel
uses: ./.github/workflows/bazel.yml
secrets: inherit

blob-size-policy:
if: github.repository == 'openai/codex'
name: Blob size policy
uses: ./.github/workflows/blob-size-policy.yml
secrets: inherit

cargo-deny:
if: github.repository == 'openai/codex'
name: cargo-deny
uses: ./.github/workflows/cargo-deny.yml
secrets: inherit

codespell:
if: github.repository == 'openai/codex'
name: Codespell
uses: ./.github/workflows/codespell.yml
secrets: inherit

repo-checks:
if: github.repository == 'openai/codex'
name: repo-checks
uses: ./.github/workflows/repo-checks.yml
secrets: inherit

rust-ci:
if: github.repository == 'openai/codex'
name: rust-ci
uses: ./.github/workflows/rust-ci.yml
secrets: inherit

sdk:
if: github.repository == 'openai/codex'
name: sdk
uses: ./.github/workflows/sdk.yml
secrets: inherit
Expand All @@ -49,7 +56,7 @@ jobs:
name: CI required
# Without `always()`, GitHub skips this job after a failed dependency and a
# required check can appear successful instead of reporting the failure.
if: ${{ always() }}
if: ${{ github.repository == 'openai/codex' && (always()) }}
needs:
- bazel
- blob-size-policy
Expand Down
Loading
Loading