SaylorTwift HF Staff commited on
Commit
f15fb1d
·
verified ·
1 Parent(s): 2569bec

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/ISSUE_TEMPLATE/1-codex-app.yml +54 -0
  2. .github/ISSUE_TEMPLATE/2-extension.yml +61 -0
  3. .github/ISSUE_TEMPLATE/3-cli.yml +81 -0
  4. .github/ISSUE_TEMPLATE/4-bug-report.yml +37 -0
  5. .github/ISSUE_TEMPLATE/5-feature-request.yml +32 -0
  6. .github/ISSUE_TEMPLATE/6-docs-issue.yml +27 -0
  7. .github/actions/check-clean-worktree/action.yml +19 -0
  8. .github/actions/linux-code-sign/action.yml +49 -0
  9. .github/actions/prepare-bazel-ci/action.yml +59 -0
  10. .github/actions/setup-bazel-ci/action.yml +94 -0
  11. .github/scripts/archive-release-symbols-and-strip-binaries.sh +119 -0
  12. .github/scripts/build-codex-package-archive.sh +243 -0
  13. .github/scripts/build-zsh-release-artifact.sh +61 -0
  14. .github/scripts/check_ci_results.py +34 -0
  15. .github/scripts/compute-bazel-windows-path.ps1 +113 -0
  16. .github/scripts/install-musl-build-tools.sh +290 -0
  17. .github/scripts/install-musl-openssl.sh +51 -0
  18. .github/scripts/publish_r2_release.py +532 -0
  19. .github/scripts/resolve_python_cli_release.py +123 -0
  20. .github/scripts/run-argument-comment-lint-bazel.sh +57 -0
  21. .github/scripts/run-bazel-ci.sh +420 -0
  22. .github/scripts/run-bazel-query-ci.sh +42 -0
  23. .github/scripts/run_bazel_with_buildbuddy.py +217 -0
  24. .github/scripts/rusty_v8_bazel.py +411 -0
  25. .github/scripts/rusty_v8_module_bazel.py +243 -0
  26. .github/scripts/setup-dev-drive.ps1 +63 -0
  27. .github/scripts/setup-voice-windows.ps1 +96 -0
  28. .github/scripts/test_macos_signing_entitlements.py +37 -0
  29. .github/scripts/test_resolve_python_cli_release.py +250 -0
  30. .github/scripts/test_run_bazel_with_buildbuddy.py +290 -0
  31. .github/scripts/test_rusty_v8_bazel.py +439 -0
  32. .github/scripts/test_v8_canary_changes.py +111 -0
  33. .github/scripts/test_verify_pypi_release.py +96 -0
  34. .github/scripts/test_voice_cygwin_inputs.py +83 -0
  35. .github/scripts/v8_canary_changes.py +193 -0
  36. .github/scripts/verify-zsh-manifest.sh +12 -0
  37. .github/scripts/verify_bazel_clippy_lints.py +234 -0
  38. .github/scripts/verify_cargo_workspace_manifests.py +395 -0
  39. .github/scripts/verify_pypi_release.py +76 -0
  40. .github/scripts/verify_tui_core_boundary.py +91 -0
  41. .github/scripts/voice-cygwin-inputs.py +98 -0
  42. .github/scripts/voice-cygwin-snapshot.json +145 -0
  43. .github/scripts/voice_windows_tools.py +80 -0
  44. .github/scripts/watch_voice_bazel.py +79 -0
  45. .github/workflows/Dockerfile.bazel +20 -0
  46. .github/workflows/README.md +37 -0
  47. .github/workflows/bazel.yml +554 -0
  48. .github/workflows/blob-size-policy.yml +53 -0
  49. .github/workflows/blocking-ci.yml +72 -0
  50. .github/workflows/cla.yml +49 -0
.github/ISSUE_TEMPLATE/1-codex-app.yml ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: 🖥️ Codex App Bug
2
+ description: Report an issue with the Codex App
3
+ labels:
4
+ - app
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Before submitting a new issue, please search for existing issues to see if your issue has already been reported.
10
+ If it has, please add a 👍 reaction (no need to leave a comment) to the existing issue instead of creating a new one.
11
+
12
+ - type: input
13
+ id: version
14
+ attributes:
15
+ label: What version of the Codex App are you using (From “About Codex” dialog)?
16
+ validations:
17
+ required: true
18
+ - type: input
19
+ id: plan
20
+ attributes:
21
+ label: What subscription do you have?
22
+ validations:
23
+ required: true
24
+ - type: input
25
+ id: platform
26
+ attributes:
27
+ label: What platform is your computer?
28
+ description: |
29
+ For macOS and Linux: copy the output of `uname -mprs`
30
+ For Windows: copy the output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in the PowerShell console
31
+ - type: textarea
32
+ id: actual
33
+ attributes:
34
+ label: What issue are you seeing?
35
+ description: Please include the full error messages and prompts with PII redacted. If possible, please provide text instead of a screenshot.
36
+ validations:
37
+ required: true
38
+ - type: textarea
39
+ id: steps
40
+ attributes:
41
+ label: What steps can reproduce the bug?
42
+ description: Explain the bug and provide a code snippet that can reproduce it. Please include session id, token limit usage, context window usage if applicable.
43
+ validations:
44
+ required: true
45
+ - type: textarea
46
+ id: expected
47
+ attributes:
48
+ label: What is the expected behavior?
49
+ description: If possible, please provide text instead of a screenshot.
50
+ - type: textarea
51
+ id: notes
52
+ attributes:
53
+ label: Additional information
54
+ description: Is there anything else you think we should know?
.github/ISSUE_TEMPLATE/2-extension.yml ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: 🧑‍💻 IDE Extension Bug
2
+ description: Report an issue with the IDE extension
3
+ labels:
4
+ - extension
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Before submitting a new issue, please search for existing issues to see if your issue has already been reported.
10
+ If it has, please add a 👍 reaction (no need to leave a comment) to the existing issue instead of creating a new one.
11
+
12
+ - type: input
13
+ id: version
14
+ attributes:
15
+ label: What version of the IDE extension are you using?
16
+ validations:
17
+ required: true
18
+ - type: input
19
+ id: plan
20
+ attributes:
21
+ label: What subscription do you have?
22
+ validations:
23
+ required: true
24
+ - type: input
25
+ id: ide
26
+ attributes:
27
+ label: Which IDE are you using?
28
+ description: Like `VS Code`, `Cursor`, `Windsurf`, etc.
29
+ validations:
30
+ required: true
31
+ - type: input
32
+ id: platform
33
+ attributes:
34
+ label: What platform is your computer?
35
+ description: |
36
+ For macOS and Linux: copy the output of `uname -mprs`
37
+ For Windows: copy the output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in the PowerShell console
38
+ - type: textarea
39
+ id: actual
40
+ attributes:
41
+ label: What issue are you seeing?
42
+ description: Please include the full error messages and prompts with PII redacted. If possible, please provide text instead of a screenshot.
43
+ validations:
44
+ required: true
45
+ - type: textarea
46
+ id: steps
47
+ attributes:
48
+ label: What steps can reproduce the bug?
49
+ description: Explain the bug and provide a code snippet that can reproduce it.
50
+ validations:
51
+ required: true
52
+ - type: textarea
53
+ id: expected
54
+ attributes:
55
+ label: What is the expected behavior?
56
+ description: If possible, please provide text instead of a screenshot.
57
+ - type: textarea
58
+ id: notes
59
+ attributes:
60
+ label: Additional information
61
+ description: Is there anything else you think we should know?
.github/ISSUE_TEMPLATE/3-cli.yml ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: 💻 CLI Bug
2
+ description: Report an issue in the Codex CLI
3
+ labels:
4
+ - bug
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Before submitting a new issue, please search for existing issues to see if your issue has already been reported.
10
+ If it has, please add a 👍 reaction (no need to leave a comment) to the existing issue instead of creating a new one.
11
+
12
+ Make sure you are running the [latest](https://npmjs.com/package/@openai/codex) version of Codex CLI. The bug you are experiencing may already have been fixed.
13
+
14
+ If your version supports it, please run `codex doctor --json` and paste the output in the "Codex doctor report" field below. This helps us diagnose install, config, auth, terminal, MCP, network, and local state issues.
15
+
16
+ - type: input
17
+ id: version
18
+ attributes:
19
+ label: What version of Codex CLI is running?
20
+ description: use `codex --version`
21
+ validations:
22
+ required: true
23
+ - type: input
24
+ id: plan
25
+ attributes:
26
+ label: What subscription do you have?
27
+ validations:
28
+ required: true
29
+ - type: input
30
+ id: model
31
+ attributes:
32
+ label: Which model were you using?
33
+ description: Like `gpt-5.2`, `gpt-5.2-codex`, etc.
34
+ - type: input
35
+ id: platform
36
+ attributes:
37
+ label: What platform is your computer?
38
+ description: |
39
+ For macOS and Linux: copy the output of `uname -mprs`
40
+ For Windows: copy the output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in the PowerShell console
41
+ - type: input
42
+ id: terminal
43
+ attributes:
44
+ label: What terminal emulator and version are you using (if applicable)?
45
+ description: |
46
+ Also note any multiplexer in use (screen / tmux / zellij).
47
+ E.g., VS Code, Terminal.app, iTerm2, Ghostty, Windows Terminal (WSL / PowerShell)
48
+ - type: textarea
49
+ id: doctor
50
+ attributes:
51
+ label: Codex doctor report
52
+ description: |
53
+ If available, run `codex doctor --json` and paste the full output here.
54
+
55
+ The report is designed to redact secrets, but please review it before submitting.
56
+ If your Codex version does not support `doctor`, write `not available`.
57
+ render: json
58
+ - type: textarea
59
+ id: actual
60
+ attributes:
61
+ label: What issue are you seeing?
62
+ description: Please include the full error messages and prompts with PII redacted. If possible, please provide text instead of a screenshot.
63
+ validations:
64
+ required: true
65
+ - type: textarea
66
+ id: steps
67
+ attributes:
68
+ label: What steps can reproduce the bug?
69
+ description: Explain the bug and provide a code snippet that can reproduce it. Please include thread id if applicable.
70
+ validations:
71
+ required: true
72
+ - type: textarea
73
+ id: expected
74
+ attributes:
75
+ label: What is the expected behavior?
76
+ description: If possible, please provide text instead of a screenshot.
77
+ - type: textarea
78
+ id: notes
79
+ attributes:
80
+ label: Additional information
81
+ description: Is there anything else you think we should know?
.github/ISSUE_TEMPLATE/4-bug-report.yml ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: 🪲 Other Bug
2
+ description: Report an issue in Codex Web, integrations, or other Codex components
3
+ labels:
4
+ - bug
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Before submitting a new issue, please search for existing issues to see if your issue has already been reported.
10
+ If it has, please add a 👍 reaction (no need to leave a comment) to the existing issue instead of creating a new one.
11
+
12
+ If you need help or support using Codex and are not reporting a bug, please post on [codex/discussions](https://github.com/openai/codex/discussions), where you can ask questions or engage with others on ideas for how to improve codex.
13
+
14
+ - type: textarea
15
+ id: actual
16
+ attributes:
17
+ label: What issue are you seeing?
18
+ description: Please include the full error messages and prompts with PII redacted. If possible, please provide text instead of a screenshot.
19
+ validations:
20
+ required: true
21
+ - type: textarea
22
+ id: steps
23
+ attributes:
24
+ label: What steps can reproduce the bug?
25
+ description: Explain the bug and provide a code snippet that can reproduce it.
26
+ validations:
27
+ required: true
28
+ - type: textarea
29
+ id: expected
30
+ attributes:
31
+ label: What is the expected behavior?
32
+ description: If possible, please provide text instead of a screenshot.
33
+ - type: textarea
34
+ id: notes
35
+ attributes:
36
+ label: Additional information
37
+ description: Is there anything else you think we should know?
.github/ISSUE_TEMPLATE/5-feature-request.yml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: 🎁 Feature Request
2
+ description: Propose a new feature for Codex
3
+ labels:
4
+ - enhancement
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Is Codex missing a feature that you'd like to see? Feel free to propose it here.
10
+
11
+ Before you submit a feature:
12
+ 1. Search existing issues for similar features. If you find one, 👍 it rather than opening a new one.
13
+ 2. The Codex team will try to balance the varying needs of the community when prioritizing or rejecting new features. Not all features will be accepted. See [Contributing](https://github.com/openai/codex/blob/main/docs/contributing.md) for more details.
14
+
15
+ - type: input
16
+ id: variant
17
+ attributes:
18
+ label: What variant of Codex are you using?
19
+ description: (e.g., App, IDE Extension, CLI, Web)
20
+ validations:
21
+ required: true
22
+ - type: textarea
23
+ id: feature
24
+ attributes:
25
+ label: What feature would you like to see?
26
+ validations:
27
+ required: true
28
+ - type: textarea
29
+ id: notes
30
+ attributes:
31
+ label: Additional information
32
+ description: Is there anything else you think we should know?
.github/ISSUE_TEMPLATE/6-docs-issue.yml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: 📗 Documentation Issue
2
+ description: Tell us if there is missing or incorrect documentation
3
+ labels: [documentation]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Thank you for submitting a documentation request. It helps make Codex better.
9
+ - type: dropdown
10
+ attributes:
11
+ label: What is the type of issue?
12
+ multiple: true
13
+ options:
14
+ - Documentation is missing
15
+ - Documentation is incorrect
16
+ - Documentation is confusing
17
+ - Example code is not working
18
+ - Something else
19
+ - type: textarea
20
+ attributes:
21
+ label: What is the issue?
22
+ validations:
23
+ required: true
24
+ - type: textarea
25
+ attributes:
26
+ label: Where did you find it?
27
+ description: If possible, please provide the URL(s) where you found this issue.
.github/actions/check-clean-worktree/action.yml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: check-clean-worktree
2
+ description: Fail when a CI job leaves tracked changes or untracked files in the repository worktree.
3
+
4
+ runs:
5
+ using: composite
6
+ steps:
7
+ - name: Check for a clean worktree
8
+ shell: bash
9
+ run: |
10
+ set -euo pipefail
11
+
12
+ status="$(git -C "${GITHUB_WORKSPACE}" status --porcelain=v1 --untracked-files=normal --ignore-submodules=none)"
13
+ if [[ -z "${status}" ]]; then
14
+ exit 0
15
+ fi
16
+
17
+ echo "::error::CI job left tracked changes or untracked files in the repository worktree"
18
+ printf '%s\n' "${status}"
19
+ exit 1
.github/actions/linux-code-sign/action.yml ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: linux-code-sign
2
+ description: Sign Linux artifacts with cosign.
3
+ inputs:
4
+ target:
5
+ description: Target triple for the artifacts to sign.
6
+ required: true
7
+ artifacts-dir:
8
+ description: Absolute path to the directory containing built binaries to sign.
9
+ required: true
10
+ binaries:
11
+ description: Space-delimited binary basenames to sign.
12
+ default: "codex codex-responses-api-proxy"
13
+
14
+ runs:
15
+ using: composite
16
+ steps:
17
+ - name: Install cosign
18
+ uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
19
+
20
+ - name: Cosign Linux artifacts
21
+ shell: bash
22
+ env:
23
+ ARTIFACTS_DIR: ${{ inputs.artifacts-dir }}
24
+ BINARIES: ${{ inputs.binaries }}
25
+ COSIGN_EXPERIMENTAL: "1"
26
+ COSIGN_YES: "true"
27
+ COSIGN_OIDC_CLIENT_ID: "sigstore"
28
+ COSIGN_OIDC_ISSUER: "https://oauth2.sigstore.dev/auth"
29
+ run: |
30
+ set -euo pipefail
31
+
32
+ dest="$ARTIFACTS_DIR"
33
+ if [[ ! -d "$dest" ]]; then
34
+ echo "Destination $dest does not exist"
35
+ exit 1
36
+ fi
37
+
38
+ for binary in ${BINARIES}; do
39
+ artifact="${dest}/${binary}"
40
+ if [[ ! -f "$artifact" ]]; then
41
+ echo "Binary $artifact not found"
42
+ exit 1
43
+ fi
44
+
45
+ cosign sign-blob \
46
+ --yes \
47
+ --bundle "${artifact}.sigstore" \
48
+ "$artifact"
49
+ done
.github/actions/prepare-bazel-ci/action.yml ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: prepare-bazel-ci
2
+ description: Prepare a Bazel CI job with shared setup, repository cache restore, and execution logs.
3
+ inputs:
4
+ target:
5
+ description: Target triple used for setup and cache namespacing.
6
+ required: true
7
+ cache-scope:
8
+ description: Logical namespace used to keep concurrent Bazel jobs from reserving the same repository cache key.
9
+ required: true
10
+ outputs:
11
+ repository-cache-path:
12
+ description: Filesystem path used for the Bazel repository cache.
13
+ value: ${{ steps.setup_bazel.outputs.repository-cache-path }}
14
+ repository-cache-key:
15
+ description: Primary actions/cache key for the Bazel repository cache.
16
+ value: ${{ steps.cache_bazel_repository_key.outputs.repository-cache-key }}
17
+ repository-cache-hit:
18
+ description: Whether the Bazel repository cache restore found an exact key match.
19
+ value: ${{ steps.cache_bazel_repository_restore.outputs.cache-hit }}
20
+
21
+ runs:
22
+ using: composite
23
+ steps:
24
+ - name: Set up Bazel CI
25
+ id: setup_bazel
26
+ uses: ./.github/actions/setup-bazel-ci
27
+ with:
28
+ target: ${{ inputs.target }}
29
+
30
+ - name: Compute bazel repository cache key
31
+ id: cache_bazel_repository_key
32
+ shell: bash
33
+ env:
34
+ CACHE_SCOPE: ${{ inputs.cache-scope }}
35
+ TARGET: ${{ inputs.target }}
36
+ CACHE_HASH: ${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }}
37
+ run: |
38
+ echo "repository-cache-key=bazel-cache-${CACHE_SCOPE}-${TARGET}-${CACHE_HASH}" >> "${GITHUB_OUTPUT}"
39
+ echo "repository-cache-restore-key=bazel-cache-${CACHE_SCOPE}-${TARGET}-" >> "${GITHUB_OUTPUT}"
40
+
41
+ # Restore the Bazel repository cache explicitly so external dependencies
42
+ # do not need to be re-downloaded on every CI run. Keep restore failures
43
+ # non-fatal so transient cache-service errors degrade to a cold build
44
+ # instead of failing the job.
45
+ - name: Restore bazel repository cache
46
+ id: cache_bazel_repository_restore
47
+ continue-on-error: true
48
+ uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
49
+ with:
50
+ path: ${{ steps.setup_bazel.outputs.repository-cache-path }}
51
+ key: ${{ steps.cache_bazel_repository_key.outputs.repository-cache-key }}
52
+ restore-keys: |
53
+ ${{ steps.cache_bazel_repository_key.outputs.repository-cache-restore-key }}
54
+
55
+ - name: Set up Bazel execution logs
56
+ shell: bash
57
+ run: |
58
+ mkdir -p "${RUNNER_TEMP}/bazel-execution-logs"
59
+ echo "CODEX_BAZEL_EXECUTION_LOG_COMPACT_DIR=${RUNNER_TEMP}/bazel-execution-logs" >> "${GITHUB_ENV}"
.github/actions/setup-bazel-ci/action.yml ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: setup-bazel-ci
2
+ description: Prepare a Bazel CI runner with shared caches.
3
+ inputs:
4
+ target:
5
+ description: Target triple used for cache namespacing.
6
+ required: true
7
+ outputs:
8
+ repository-cache-path:
9
+ description: Filesystem path used for the Bazel repository cache.
10
+ value: ${{ steps.configure_bazel_repository_cache.outputs.repository-cache-path }}
11
+
12
+ runs:
13
+ using: composite
14
+ steps:
15
+ - id: setup_ci
16
+ uses: ./.github/actions/setup-ci
17
+
18
+ - name: Set up Bazel
19
+ uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
20
+ # Without an explicit Bazelisk version, setup-bazel leaves PATH unchanged
21
+ # and Windows can use the runner's standalone Bazel, ignoring .bazelversion.
22
+ with:
23
+ bazelisk-version: 1.28.1
24
+ # setup-bazel writes an explicit output_base, which otherwise overrides
25
+ # BAZEL_OUTPUT_USER_ROOT and leaves Bazel's I/O-heavy trees on C:.
26
+ output-base: ${{ steps.setup_ci.outputs.bazel-output-base }}
27
+
28
+ - name: Configure Bazel repository cache
29
+ id: configure_bazel_repository_cache
30
+ shell: pwsh
31
+ run: |
32
+ "repository-cache-path=$env:BAZEL_REPOSITORY_CACHE" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
33
+
34
+ - name: Expose MSVC SDK environment (Windows)
35
+ if: runner.os == 'Windows'
36
+ shell: pwsh
37
+ run: |
38
+ # Bazel exec-side Rust build scripts do not reliably inherit the MSVC developer
39
+ # shell on GitHub-hosted Windows runners, so discover the latest VS install and
40
+ # ask `VsDevCmd.bat` to materialize the x64/x64 compiler + SDK environment.
41
+ $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
42
+ if (-not (Test-Path $vswhere)) {
43
+ throw "vswhere.exe not found"
44
+ }
45
+
46
+ $installPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>$null
47
+ if (-not $installPath) {
48
+ throw "Could not locate a Visual Studio installation with VC tools"
49
+ }
50
+
51
+ $vsDevCmd = Join-Path $installPath 'Common7\Tools\VsDevCmd.bat'
52
+ if (-not (Test-Path $vsDevCmd)) {
53
+ throw "VsDevCmd.bat not found at $vsDevCmd"
54
+ }
55
+
56
+ # Keep the export surface explicit: these are the paths and SDK roots that the
57
+ # MSVC toolchain probes need later when Bazel runs Windows exec-platform build
58
+ # scripts such as `aws-lc-sys`.
59
+ $varsToExport = @(
60
+ 'INCLUDE',
61
+ 'LIB',
62
+ 'LIBPATH',
63
+ 'PATH',
64
+ 'UCRTVersion',
65
+ 'UniversalCRTSdkDir',
66
+ 'VCINSTALLDIR',
67
+ 'VCToolsInstallDir',
68
+ 'WindowsLibPath',
69
+ 'WindowsSdkBinPath',
70
+ 'WindowsSdkDir',
71
+ 'WindowsSDKLibVersion',
72
+ 'WindowsSDKVersion'
73
+ )
74
+
75
+ # `VsDevCmd.bat` is a batch file, so invoke it under `cmd.exe`, suppress its
76
+ # banner, then dump the resulting environment with `set`. Re-export only the
77
+ # approved keys into `GITHUB_ENV` so later steps inherit the same MSVC context.
78
+ $envLines = & cmd.exe /c ('"{0}" -no_logo -arch=x64 -host_arch=x64 >nul && set' -f $vsDevCmd)
79
+ foreach ($line in $envLines) {
80
+ if ($line -notmatch '^(.*?)=(.*)$') {
81
+ continue
82
+ }
83
+
84
+ $name = $matches[1]
85
+ $value = $matches[2]
86
+ if ($varsToExport -contains $name) {
87
+ "$name=$value" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
88
+ }
89
+ }
90
+
91
+ - name: Compute cache-stable Windows Bazel PATH
92
+ if: runner.os == 'Windows'
93
+ shell: pwsh
94
+ run: ./.github/scripts/compute-bazel-windows-path.ps1
.github/scripts/archive-release-symbols-and-strip-binaries.sh ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ Usage: archive-release-symbols-and-strip-binaries.sh \
7
+ --target <rust-target> \
8
+ --artifact-name <artifact-name> \
9
+ --release-dir <dir> \
10
+ --archive-dir <dir> \
11
+ --binaries "<space-delimited binary basenames>"
12
+ EOF
13
+ }
14
+
15
+ target=""
16
+ artifact_name=""
17
+ release_dir=""
18
+ archive_dir=""
19
+ binaries=""
20
+
21
+ while [[ $# -gt 0 ]]; do
22
+ case "$1" in
23
+ --target)
24
+ target="${2:?--target requires a value}"
25
+ shift 2
26
+ ;;
27
+ --artifact-name)
28
+ artifact_name="${2:?--artifact-name requires a value}"
29
+ shift 2
30
+ ;;
31
+ --release-dir)
32
+ release_dir="${2:?--release-dir requires a value}"
33
+ shift 2
34
+ ;;
35
+ --archive-dir)
36
+ archive_dir="${2:?--archive-dir requires a value}"
37
+ shift 2
38
+ ;;
39
+ --binaries)
40
+ binaries="${2:?--binaries requires a value}"
41
+ shift 2
42
+ ;;
43
+ -h|--help)
44
+ usage
45
+ exit 0
46
+ ;;
47
+ *)
48
+ echo "Unexpected argument: $1" >&2
49
+ usage >&2
50
+ exit 1
51
+ ;;
52
+ esac
53
+ done
54
+
55
+ if [[ -z "$target" || -z "$artifact_name" || -z "$release_dir" || -z "$archive_dir" || -z "$binaries" ]]; then
56
+ usage >&2
57
+ exit 1
58
+ fi
59
+
60
+ symbols_root="${RUNNER_TEMP:-/tmp}/codex-symbols-${artifact_name}"
61
+ symbols_dir="${symbols_root}/codex-symbols-${artifact_name}"
62
+ archive_path="${archive_dir%/}/codex-symbols-${artifact_name}.tar.gz"
63
+ rm -rf "$symbols_root"
64
+ mkdir -p "$symbols_dir" "$archive_dir"
65
+ read -r -a binary_names <<< "$binaries"
66
+
67
+ case "$target" in
68
+ *apple-darwin)
69
+ for binary in "${binary_names[@]}"; do
70
+ binary_path="${release_dir%/}/${binary}"
71
+ dsym_path="${binary_path}.dSYM"
72
+ if [[ ! -f "$binary_path" ]]; then
73
+ echo "Binary $binary_path not found" >&2
74
+ exit 1
75
+ fi
76
+ if [[ ! -d "$dsym_path" ]]; then
77
+ echo "dSYM $dsym_path not found" >&2
78
+ exit 1
79
+ fi
80
+
81
+ cp -RL "$dsym_path" "${symbols_dir}/${binary}.dSYM"
82
+ strip -S -x "$binary_path"
83
+ done
84
+ ;;
85
+ *linux*)
86
+ objcopy_bin="${OBJCOPY:-objcopy}"
87
+ strip_bin="${STRIP:-strip}"
88
+ for binary in "${binary_names[@]}"; do
89
+ binary_path="${release_dir%/}/${binary}"
90
+ debug_path="${symbols_dir}/${binary}.debug"
91
+ if [[ ! -f "$binary_path" ]]; then
92
+ echo "Binary $binary_path not found" >&2
93
+ exit 1
94
+ fi
95
+
96
+ "$objcopy_bin" --only-keep-debug "$binary_path" "$debug_path"
97
+ "$strip_bin" --strip-debug --strip-unneeded "$binary_path"
98
+ "$objcopy_bin" --add-gnu-debuglink="$debug_path" "$binary_path"
99
+ done
100
+ ;;
101
+ *windows*)
102
+ for binary in "${binary_names[@]}"; do
103
+ pdb_path="${release_dir%/}/${binary}.pdb"
104
+ if [[ ! -f "$pdb_path" ]]; then
105
+ echo "PDB $pdb_path not found" >&2
106
+ exit 1
107
+ fi
108
+
109
+ cp "$pdb_path" "${symbols_dir}/${binary}.pdb"
110
+ done
111
+ ;;
112
+ *)
113
+ echo "No symbols packaging support for target: $target" >&2
114
+ exit 1
115
+ ;;
116
+ esac
117
+
118
+ rm -f "$archive_path"
119
+ tar -C "$symbols_root" -czf "$archive_path" "codex-symbols-${artifact_name}"
.github/scripts/build-codex-package-archive.sh ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ Usage: build-codex-package-archive.sh \
7
+ --target <rust-target> \
8
+ --bundle <primary|app-server> \
9
+ --entrypoint-dir <dir> \
10
+ --archive-dir <dir> \
11
+ [--bwrap-bin <path>] \
12
+ [--code-mode-host-bin <path>] \
13
+ [--rg-bin <path>] \
14
+ [--zsh-bin <path>] \
15
+ [--zsh-manifest <path>] \
16
+ [--codex-command-runner-bin <path>] \
17
+ [--codex-windows-sandbox-setup-bin <path>] \
18
+ [--voice-release-dir <path> --release-version <release-version>] \
19
+ [--target-suffixed-entrypoint]
20
+ EOF
21
+ }
22
+
23
+ target=""
24
+ bundle=""
25
+ entrypoint_dir=""
26
+ archive_dir=""
27
+ target_suffixed_entrypoint="false"
28
+ resource_args=()
29
+ bwrap_bin_provided="false"
30
+ code_mode_host_bin_provided="false"
31
+ command_runner_bin_provided="false"
32
+ sandbox_setup_bin_provided="false"
33
+ voice_release_dir=""
34
+ release_version=""
35
+
36
+ while [[ $# -gt 0 ]]; do
37
+ case "$1" in
38
+ --target)
39
+ target="${2:?--target requires a value}"
40
+ shift 2
41
+ ;;
42
+ --bundle)
43
+ bundle="${2:?--bundle requires a value}"
44
+ shift 2
45
+ ;;
46
+ --entrypoint-dir)
47
+ entrypoint_dir="${2:?--entrypoint-dir requires a value}"
48
+ shift 2
49
+ ;;
50
+ --archive-dir)
51
+ archive_dir="${2:?--archive-dir requires a value}"
52
+ shift 2
53
+ ;;
54
+ --bwrap-bin)
55
+ resource_args+=(--bwrap-bin "${2:?--bwrap-bin requires a value}")
56
+ bwrap_bin_provided="true"
57
+ shift 2
58
+ ;;
59
+ --code-mode-host-bin)
60
+ resource_args+=(--code-mode-host-bin "${2:?--code-mode-host-bin requires a value}")
61
+ code_mode_host_bin_provided="true"
62
+ shift 2
63
+ ;;
64
+ --rg-bin)
65
+ resource_args+=(--rg-bin "${2:?--rg-bin requires a value}")
66
+ shift 2
67
+ ;;
68
+ --zsh-bin)
69
+ resource_args+=(--zsh-bin "${2:?--zsh-bin requires a value}")
70
+ shift 2
71
+ ;;
72
+ --zsh-manifest)
73
+ resource_args+=(--zsh-manifest "${2:?--zsh-manifest requires a value}")
74
+ shift 2
75
+ ;;
76
+ --codex-command-runner-bin)
77
+ resource_args+=(
78
+ --codex-command-runner-bin
79
+ "${2:?--codex-command-runner-bin requires a value}"
80
+ )
81
+ command_runner_bin_provided="true"
82
+ shift 2
83
+ ;;
84
+ --codex-windows-sandbox-setup-bin)
85
+ resource_args+=(
86
+ --codex-windows-sandbox-setup-bin
87
+ "${2:?--codex-windows-sandbox-setup-bin requires a value}"
88
+ )
89
+ sandbox_setup_bin_provided="true"
90
+ shift 2
91
+ ;;
92
+ --target-suffixed-entrypoint)
93
+ target_suffixed_entrypoint="true"
94
+ shift
95
+ ;;
96
+ --voice-release-dir)
97
+ voice_release_dir="${2:?--voice-release-dir requires a value}"
98
+ shift 2
99
+ ;;
100
+ --release-version)
101
+ release_version="${2:?--release-version requires a value}"
102
+ shift 2
103
+ ;;
104
+ -h|--help)
105
+ usage
106
+ exit 0
107
+ ;;
108
+ *)
109
+ echo "Unexpected argument: $1" >&2
110
+ usage >&2
111
+ exit 1
112
+ ;;
113
+ esac
114
+ done
115
+
116
+ if [[ -z "$target" || -z "$bundle" || -z "$entrypoint_dir" || -z "$archive_dir" ]]; then
117
+ usage >&2
118
+ exit 1
119
+ fi
120
+ if [[ ( -n "$voice_release_dir" || -n "$release_version" ) && ( -z "$voice_release_dir" || -z "$release_version" || "$bundle" != "primary" || ( "$target" != *-apple-darwin && "$target" != *-unknown-linux-musl && "$target" != *-pc-windows-msvc ) ) ]]; then
121
+ echo "Voice resources require a primary supported release package version" >&2
122
+ exit 1
123
+ fi
124
+
125
+ case "$bundle" in
126
+ primary)
127
+ variant="codex"
128
+ entrypoint="codex"
129
+ archive_stem="codex-package"
130
+ ;;
131
+ app-server)
132
+ variant="codex-app-server"
133
+ entrypoint="codex-app-server"
134
+ archive_stem="codex-app-server-package"
135
+ ;;
136
+ *)
137
+ echo "No Codex package variant for bundle: $bundle" >&2
138
+ exit 1
139
+ ;;
140
+ esac
141
+
142
+ exe_suffix=""
143
+ case "$target" in
144
+ *windows*)
145
+ exe_suffix=".exe"
146
+ ;;
147
+ esac
148
+
149
+ code_mode_host_bin="${entrypoint_dir%/}/codex-code-mode-host${exe_suffix}"
150
+ if [[ "$code_mode_host_bin_provided" == "false" && -f "$code_mode_host_bin" ]]; then
151
+ resource_args+=(--code-mode-host-bin "$code_mode_host_bin")
152
+ fi
153
+
154
+ entrypoint_name="$entrypoint"
155
+ if [[ "$target_suffixed_entrypoint" == "true" ]]; then
156
+ entrypoint_name="${entrypoint_name}-${target}"
157
+ fi
158
+
159
+ case "$target" in
160
+ *linux*)
161
+ bwrap_bin="${entrypoint_dir%/}/bwrap"
162
+ if [[ "$bwrap_bin_provided" == "false" && -f "$bwrap_bin" ]]; then
163
+ resource_args+=(--bwrap-bin "$bwrap_bin")
164
+ fi
165
+ ;;
166
+ *windows*)
167
+ command_runner_bin="${entrypoint_dir%/}/codex-command-runner.exe"
168
+ sandbox_setup_bin="${entrypoint_dir%/}/codex-windows-sandbox-setup.exe"
169
+ if [[ "$command_runner_bin_provided" == "false" && -f "$command_runner_bin" ]]; then
170
+ resource_args+=(--codex-command-runner-bin "$command_runner_bin")
171
+ fi
172
+ if [[ "$sandbox_setup_bin_provided" == "false" && -f "$sandbox_setup_bin" ]]; then
173
+ resource_args+=(--codex-windows-sandbox-setup-bin "$sandbox_setup_bin")
174
+ fi
175
+ ;;
176
+ esac
177
+
178
+ repo_root="${GITHUB_WORKSPACE:-}"
179
+ if [[ -z "$repo_root" ]]; then
180
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
181
+ fi
182
+
183
+ if command -v python3 >/dev/null 2>&1; then
184
+ python_bin="python3"
185
+ else
186
+ python_bin="python"
187
+ fi
188
+
189
+ if ! command -v zstd >/dev/null 2>&1 && [[ -x "${repo_root}/.github/workflows/zstd" ]]; then
190
+ export PATH="${repo_root}/.github/workflows:${PATH}"
191
+ fi
192
+
193
+ mkdir -p "$archive_dir"
194
+ package_dir="${RUNNER_TEMP:-/tmp}/${archive_stem}-${target}"
195
+ gzip_archive_path="${archive_dir}/${archive_stem}-${target}.tar.gz"
196
+ zstd_archive_path="${archive_dir}/${archive_stem}-${target}.tar.zst"
197
+ rm -rf "$package_dir"
198
+
199
+ python_args=(
200
+ "${repo_root}/scripts/build_codex_package.py"
201
+ --target "$target"
202
+ --variant "$variant"
203
+ --entrypoint-bin "${entrypoint_dir%/}/${entrypoint_name}${exe_suffix}"
204
+ --cargo-profile release
205
+ --package-dir "$package_dir"
206
+ )
207
+ if [[ -z "$voice_release_dir" ]]; then
208
+ python_args+=(--archive-output "$gzip_archive_path" --archive-output "$zstd_archive_path")
209
+ fi
210
+ if ((${#resource_args[@]} > 0)); then
211
+ python_args+=("${resource_args[@]}")
212
+ fi
213
+ python_args+=(--force)
214
+
215
+ "$python_bin" "${python_args[@]}"
216
+
217
+ if [[ -n "$voice_release_dir" ]]; then
218
+ voice_target="$target"
219
+ if [[ "$target" == *-unknown-linux-musl ]]; then
220
+ voice_target="${target%-musl}-gnu"
221
+ fi
222
+ voice_package="${RUNNER_TEMP:-/tmp}/${archive_stem}-voice-${target}"
223
+ rm -rf "$voice_package"
224
+ voice_helper="${voice_release_dir%/}/codex-voice-host${exe_suffix}"
225
+ "$python_bin" "${repo_root}/third_party/voice/assemble_package.py" \
226
+ --package "$package_dir" \
227
+ --helper "$voice_helper" \
228
+ --runtime "${voice_release_dir%/}/runtime" \
229
+ --voice-target "$voice_target" \
230
+ --build-commit "$(git -C "$repo_root" rev-parse HEAD)" \
231
+ --release-version "$release_version" \
232
+ --output "$voice_package"
233
+ PYTHONPATH="${repo_root}/scripts" "$python_bin" - \
234
+ "$voice_package" "$gzip_archive_path" "$zstd_archive_path" <<'PY'
235
+ import sys
236
+ from pathlib import Path
237
+ from codex_package.archive import write_archive
238
+
239
+ package = Path(sys.argv[1])
240
+ for archive in sys.argv[2:]:
241
+ write_archive(package, Path(archive), force=True)
242
+ PY
243
+ fi
.github/scripts/build-zsh-release-artifact.sh ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ if [[ "$#" -ne 1 ]]; then
6
+ echo "usage: $0 <archive-path>" >&2
7
+ exit 1
8
+ fi
9
+
10
+ archive_path="$1"
11
+ workspace="${GITHUB_WORKSPACE:?missing GITHUB_WORKSPACE}"
12
+ zsh_commit="${ZSH_COMMIT:?missing ZSH_COMMIT}"
13
+ zsh_patch="${ZSH_PATCH:?missing ZSH_PATCH}"
14
+ temp_root="${RUNNER_TEMP:-/tmp}"
15
+ work_root="$(mktemp -d "${temp_root%/}/codex-zsh-release.XXXXXX")"
16
+ trap 'rm -rf "$work_root"' EXIT
17
+
18
+ source_root="${work_root}/zsh"
19
+ package_root="${work_root}/codex-zsh"
20
+ wrapper_path="${work_root}/exec-wrapper"
21
+ stdout_path="${work_root}/stdout.txt"
22
+ wrapper_log_path="${work_root}/wrapper.log"
23
+
24
+ git clone https://git.code.sf.net/p/zsh/code "$source_root"
25
+ cd "$source_root"
26
+ git checkout "$zsh_commit"
27
+ git apply "${workspace}/${zsh_patch}"
28
+ ./Util/preconfig
29
+ ./configure
30
+
31
+ cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)"
32
+ make -j"${cores}"
33
+
34
+ cat > "$wrapper_path" <<'EOF'
35
+ #!/usr/bin/env bash
36
+ set -euo pipefail
37
+ : "${CODEX_WRAPPER_LOG:?missing CODEX_WRAPPER_LOG}"
38
+ printf '%s\n' "$@" > "$CODEX_WRAPPER_LOG"
39
+ file="$1"
40
+ shift
41
+ if [[ "$#" -eq 0 ]]; then
42
+ exec "$file"
43
+ fi
44
+ arg0="$1"
45
+ shift
46
+ exec -a "$arg0" "$file" "$@"
47
+ EOF
48
+ chmod +x "$wrapper_path"
49
+
50
+ CODEX_WRAPPER_LOG="$wrapper_log_path" \
51
+ EXEC_WRAPPER="$wrapper_path" \
52
+ "${source_root}/Src/zsh" -fc '/bin/echo smoke-zsh' > "$stdout_path"
53
+
54
+ grep -Fx "smoke-zsh" "$stdout_path"
55
+ grep -Fx "/bin/echo" "$wrapper_log_path"
56
+
57
+ mkdir -p "$package_root/bin" "$(dirname "${workspace}/${archive_path}")"
58
+ cp "${source_root}/Src/zsh" "$package_root/bin/zsh"
59
+ chmod +x "$package_root/bin/zsh"
60
+
61
+ (cd "$work_root" && tar -czf "${workspace}/${archive_path}" codex-zsh)
.github/scripts/check_ci_results.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ """Fail a terminal CI job unless every serialized dependency succeeded.
4
+
5
+ Parent workflows pass GitHub's `toJSON(needs)` object through the NEEDS
6
+ environment variable. Treat skipped and cancelled dependencies as failures too:
7
+ for a required fan-in job, only an explicit success is safe to accept.
8
+ """
9
+
10
+ import json
11
+ import os
12
+
13
+
14
+ def main() -> None:
15
+ # Keep result policy in one script so blocking-ci and postmerge-ci cannot
16
+ # drift in how they interpret dependency conclusions.
17
+ needs = json.loads(os.environ["NEEDS"])
18
+ failures = sorted(
19
+ (name, dependency["result"])
20
+ for name, dependency in needs.items()
21
+ if dependency["result"] != "success"
22
+ )
23
+
24
+ if failures:
25
+ print("CI dependencies did not succeed:")
26
+ for name, result in failures:
27
+ print(f"{name}: {result}")
28
+ raise SystemExit(1)
29
+
30
+ print("All CI dependencies succeeded.")
31
+
32
+
33
+ if __name__ == "__main__":
34
+ main()
.github/scripts/compute-bazel-windows-path.ps1 ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <#
2
+ BuildBuddy cache keys include the action and test environment, so Bazel should
3
+ not inherit the full hosted-runner PATH on Windows. That PATH includes volatile
4
+ tool entries, such as Maven, that can change independently of this repo and
5
+ cause avoidable cache misses.
6
+
7
+ This script derives a smaller, cache-stable PATH that keeps the Windows
8
+ toolchain entries Bazel-backed CI tasks need: MSVC and Windows SDK paths,
9
+ MinGW runtime DLL paths for gnullvm-built tests, Git, PowerShell, Node, Python,
10
+ DotSlash, and the standard Windows system directories.
11
+ `setup-bazel-ci` runs this after exporting the MSVC environment, and the script
12
+ publishes the result via `GITHUB_ENV` as `CODEX_BAZEL_WINDOWS_PATH` so later
13
+ steps can pass that explicit PATH to Bazel.
14
+ #>
15
+
16
+ $stablePathEntries = New-Object System.Collections.Generic.List[string]
17
+ $seenEntries = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
18
+ $windowsAppsPath = if ([string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) {
19
+ $null
20
+ } else {
21
+ "$($env:LOCALAPPDATA)\Microsoft\WindowsApps"
22
+ }
23
+ $windowsDir = if ($env:WINDIR) {
24
+ $env:WINDIR
25
+ } elseif ($env:SystemRoot) {
26
+ $env:SystemRoot
27
+ } else {
28
+ $null
29
+ }
30
+
31
+ function Add-StablePathEntry {
32
+ param([string]$PathEntry)
33
+
34
+ if ([string]::IsNullOrWhiteSpace($PathEntry)) {
35
+ return
36
+ }
37
+
38
+ if ($seenEntries.Add($PathEntry)) {
39
+ [void]$stablePathEntries.Add($PathEntry)
40
+ }
41
+ }
42
+
43
+ foreach ($pathEntry in ($env:PATH -split ';')) {
44
+ if ([string]::IsNullOrWhiteSpace($pathEntry)) {
45
+ continue
46
+ }
47
+
48
+ if (
49
+ $pathEntry -like '*Microsoft Visual Studio*' -or
50
+ $pathEntry -like '*Windows Kits*' -or
51
+ $pathEntry -like '*Microsoft SDKs*' -or
52
+ $pathEntry -eq 'C:\mingw64\bin' -or
53
+ $pathEntry -like 'C:\msys64\*\bin' -or
54
+ $pathEntry -like 'C:\Program Files\Git\*' -or
55
+ $pathEntry -like 'C:\Program Files\PowerShell\*' -or
56
+ $pathEntry -like 'C:\hostedtoolcache\windows\node\*' -or
57
+ $pathEntry -like 'C:\hostedtoolcache\windows\Python\*' -or
58
+ $pathEntry -eq 'D:\a\_temp\install-dotslash\bin' -or
59
+ ($windowsDir -and ($pathEntry -eq $windowsDir -or $pathEntry -like "${windowsDir}\*"))
60
+ ) {
61
+ Add-StablePathEntry $pathEntry
62
+ }
63
+ }
64
+
65
+ $gitCommand = Get-Command git -ErrorAction SilentlyContinue
66
+ if ($gitCommand) {
67
+ Add-StablePathEntry (Split-Path $gitCommand.Source -Parent)
68
+ }
69
+
70
+ $nodeCommand = Get-Command node -ErrorAction SilentlyContinue
71
+ if ($nodeCommand) {
72
+ Add-StablePathEntry (Split-Path $nodeCommand.Source -Parent)
73
+ }
74
+
75
+ $python3Command = Get-Command python3 -ErrorAction SilentlyContinue
76
+ if ($python3Command) {
77
+ Add-StablePathEntry (Split-Path $python3Command.Source -Parent)
78
+ }
79
+
80
+ $pythonCommand = Get-Command python -ErrorAction SilentlyContinue
81
+ if ($pythonCommand) {
82
+ Add-StablePathEntry (Split-Path $pythonCommand.Source -Parent)
83
+ }
84
+
85
+ $pwshCommand = Get-Command pwsh -ErrorAction SilentlyContinue
86
+ if ($pwshCommand) {
87
+ Add-StablePathEntry (Split-Path $pwshCommand.Source -Parent)
88
+ }
89
+
90
+ foreach ($mingwPath in @('C:\mingw64\bin', 'C:\msys64\mingw64\bin', 'C:\msys64\ucrt64\bin')) {
91
+ if (Test-Path $mingwPath) {
92
+ Add-StablePathEntry $mingwPath
93
+ }
94
+ }
95
+
96
+ if ($windowsAppsPath) {
97
+ Add-StablePathEntry $windowsAppsPath
98
+ }
99
+
100
+ if ($stablePathEntries.Count -eq 0) {
101
+ throw 'Failed to derive cache-stable Windows PATH.'
102
+ }
103
+
104
+ if ([string]::IsNullOrWhiteSpace($env:GITHUB_ENV)) {
105
+ throw 'GITHUB_ENV must be set.'
106
+ }
107
+
108
+ $stablePath = $stablePathEntries -join ';'
109
+ Write-Host 'Derived CODEX_BAZEL_WINDOWS_PATH entries:'
110
+ foreach ($pathEntry in $stablePathEntries) {
111
+ Write-Host " $pathEntry"
112
+ }
113
+ "CODEX_BAZEL_WINDOWS_PATH=$stablePath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
.github/scripts/install-musl-build-tools.sh ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ : "${TARGET:?TARGET environment variable is required}"
5
+ : "${GITHUB_ENV:?GITHUB_ENV environment variable is required}"
6
+
7
+ apt_update_args=()
8
+ if [[ -n "${APT_UPDATE_ARGS:-}" ]]; then
9
+ # shellcheck disable=SC2206
10
+ apt_update_args=(${APT_UPDATE_ARGS})
11
+ fi
12
+
13
+ apt_install_args=()
14
+ if [[ -n "${APT_INSTALL_ARGS:-}" ]]; then
15
+ # shellcheck disable=SC2206
16
+ apt_install_args=(${APT_INSTALL_ARGS})
17
+ fi
18
+
19
+ sudo apt-get update "${apt_update_args[@]}"
20
+ sudo apt-get install -y "${apt_install_args[@]}" ca-certificates curl musl-tools pkg-config libcap-dev g++ clang libc++-dev libc++abi-dev lld xz-utils perl make
21
+
22
+ case "${TARGET}" in
23
+ x86_64-unknown-linux-musl)
24
+ arch="x86_64"
25
+ ;;
26
+ aarch64-unknown-linux-musl)
27
+ arch="aarch64"
28
+ ;;
29
+ *)
30
+ echo "Unexpected musl target: ${TARGET}" >&2
31
+ exit 1
32
+ ;;
33
+ esac
34
+
35
+ libcap_version="2.75"
36
+ libcap_sha256="de4e7e064c9ba451d5234dd46e897d7c71c96a9ebf9a0c445bc04f4742d83632"
37
+ libcap_tarball_name="libcap-${libcap_version}.tar.xz"
38
+ libcap_download_url="https://mirrors.edge.kernel.org/pub/linux/libs/security/linux-privs/libcap2/${libcap_tarball_name}"
39
+
40
+ # Use the musl toolchain as the Rust linker to avoid Zig injecting its own CRT.
41
+ if command -v "${arch}-linux-musl-gcc" >/dev/null; then
42
+ musl_linker="$(command -v "${arch}-linux-musl-gcc")"
43
+ elif command -v musl-gcc >/dev/null; then
44
+ musl_linker="$(command -v musl-gcc)"
45
+ else
46
+ echo "musl gcc not found after install; arch=${arch}" >&2
47
+ exit 1
48
+ fi
49
+
50
+ OPENSSL_CC="${musl_linker}" bash "$(dirname "${BASH_SOURCE[0]}")/install-musl-openssl.sh"
51
+
52
+ zig_target="${TARGET/-unknown-linux-musl/-linux-musl}"
53
+ runner_temp="${RUNNER_TEMP:-/tmp}"
54
+ tool_root="${runner_temp}/codex-musl-tools-${TARGET}"
55
+ mkdir -p "${tool_root}"
56
+
57
+ libcap_root="${tool_root}/libcap-${libcap_version}"
58
+ libcap_src_root="${libcap_root}/src"
59
+ libcap_prefix="${libcap_root}/prefix"
60
+ libcap_pkgconfig_dir="${libcap_prefix}/lib/pkgconfig"
61
+
62
+ if [[ ! -f "${libcap_prefix}/lib/libcap.a" ]]; then
63
+ mkdir -p "${libcap_src_root}" "${libcap_prefix}/lib" "${libcap_prefix}/include/sys" "${libcap_prefix}/include/linux" "${libcap_pkgconfig_dir}"
64
+ libcap_tarball="${libcap_root}/${libcap_tarball_name}"
65
+
66
+ curl -fsSL "${libcap_download_url}" -o "${libcap_tarball}"
67
+ echo "${libcap_sha256} ${libcap_tarball}" | sha256sum -c -
68
+
69
+ tar -xJf "${libcap_tarball}" -C "${libcap_src_root}"
70
+ libcap_source_dir="${libcap_src_root}/libcap-${libcap_version}"
71
+ make -C "${libcap_source_dir}/libcap" -j"$(nproc)" \
72
+ CC="${musl_linker}" \
73
+ AR=ar \
74
+ RANLIB=ranlib
75
+
76
+ cp "${libcap_source_dir}/libcap/libcap.a" "${libcap_prefix}/lib/libcap.a"
77
+ cp "${libcap_source_dir}/libcap/include/uapi/linux/capability.h" "${libcap_prefix}/include/linux/capability.h"
78
+ cp "${libcap_source_dir}/libcap/../libcap/include/sys/capability.h" "${libcap_prefix}/include/sys/capability.h"
79
+
80
+ cat > "${libcap_pkgconfig_dir}/libcap.pc" <<EOF
81
+ prefix=${libcap_prefix}
82
+ exec_prefix=\${prefix}
83
+ libdir=\${prefix}/lib
84
+ includedir=\${prefix}/include
85
+
86
+ Name: libcap
87
+ Description: Linux capabilities
88
+ Version: ${libcap_version}
89
+ Libs: -L\${libdir} -lcap
90
+ Cflags: -I\${includedir}
91
+ EOF
92
+ fi
93
+
94
+ sysroot=""
95
+ if command -v zig >/dev/null; then
96
+ zig_bin="$(command -v zig)"
97
+ cc="${tool_root}/zigcc"
98
+ cxx="${tool_root}/zigcxx"
99
+
100
+ cat >"${cc}" <<EOF
101
+ #!/usr/bin/env bash
102
+ set -euo pipefail
103
+
104
+ args=()
105
+ skip_next=0
106
+ pending_include=0
107
+ for arg in "\$@"; do
108
+ if [[ "\${pending_include}" -eq 1 ]]; then
109
+ pending_include=0
110
+ if [[ "\${arg}" == /usr/include || "\${arg}" == /usr/include/* ]]; then
111
+ # Keep host-only headers available, but after the target sysroot headers.
112
+ args+=("-idirafter" "\${arg}")
113
+ else
114
+ args+=("-I" "\${arg}")
115
+ fi
116
+ continue
117
+ fi
118
+
119
+ if [[ "\${skip_next}" -eq 1 ]]; then
120
+ skip_next=0
121
+ continue
122
+ fi
123
+ case "\${arg}" in
124
+ --target)
125
+ skip_next=1
126
+ continue
127
+ ;;
128
+ --target=*|-target=*|-target)
129
+ # Drop any explicit --target/-target flags. Zig expects -target and
130
+ # rejects Rust triples like *-unknown-linux-musl.
131
+ if [[ "\${arg}" == "-target" ]]; then
132
+ skip_next=1
133
+ fi
134
+ continue
135
+ ;;
136
+ -I)
137
+ pending_include=1
138
+ continue
139
+ ;;
140
+ -I/usr/include|-I/usr/include/*)
141
+ # Avoid making glibc headers win over musl headers.
142
+ args+=("-idirafter" "\${arg#-I}")
143
+ continue
144
+ ;;
145
+ -Wp,-U_FORTIFY_SOURCE)
146
+ # aws-lc-sys emits this GCC preprocessor forwarding form in debug
147
+ # builds, but zig cc expects the define flag directly.
148
+ args+=("-U_FORTIFY_SOURCE")
149
+ continue
150
+ ;;
151
+ esac
152
+ args+=("\${arg}")
153
+ done
154
+
155
+ # Zig enables UBSan for debug C builds by default. Rust links these objects
156
+ # without Zig's sanitizer runtime, so keep native dependencies uninstrumented.
157
+ exec "${zig_bin}" cc -target "${zig_target}" "\${args[@]}" -fno-sanitize=undefined
158
+ EOF
159
+ cat >"${cxx}" <<EOF
160
+ #!/usr/bin/env bash
161
+ set -euo pipefail
162
+
163
+ args=()
164
+ skip_next=0
165
+ pending_include=0
166
+ for arg in "\$@"; do
167
+ if [[ "\${pending_include}" -eq 1 ]]; then
168
+ pending_include=0
169
+ if [[ "\${arg}" == /usr/include || "\${arg}" == /usr/include/* ]]; then
170
+ # Keep host-only headers available, but after the target sysroot headers.
171
+ args+=("-idirafter" "\${arg}")
172
+ else
173
+ args+=("-I" "\${arg}")
174
+ fi
175
+ continue
176
+ fi
177
+
178
+ if [[ "\${skip_next}" -eq 1 ]]; then
179
+ skip_next=0
180
+ continue
181
+ fi
182
+ case "\${arg}" in
183
+ --target)
184
+ # Drop explicit --target and its value: we always pass zig's -target below.
185
+ skip_next=1
186
+ continue
187
+ ;;
188
+ --target=*|-target=*|-target)
189
+ # Zig expects -target and rejects Rust triples like *-unknown-linux-musl.
190
+ if [[ "\${arg}" == "-target" ]]; then
191
+ skip_next=1
192
+ fi
193
+ continue
194
+ ;;
195
+ -I)
196
+ pending_include=1
197
+ continue
198
+ ;;
199
+ -I/usr/include|-I/usr/include/*)
200
+ # Avoid making glibc headers win over musl headers.
201
+ args+=("-idirafter" "\${arg#-I}")
202
+ continue
203
+ ;;
204
+ -Wp,-U_FORTIFY_SOURCE)
205
+ # aws-lc-sys emits this GCC forwarding form in debug builds; zig c++
206
+ # expects the define flag directly.
207
+ args+=("-U_FORTIFY_SOURCE")
208
+ continue
209
+ ;;
210
+ esac
211
+ args+=("\${arg}")
212
+ done
213
+
214
+ # Zig enables UBSan for debug C++ builds by default. Rust links these objects
215
+ # without Zig's sanitizer runtime, so keep native dependencies uninstrumented.
216
+ exec "${zig_bin}" c++ -target "${zig_target}" "\${args[@]}" -fno-sanitize=undefined
217
+ EOF
218
+ chmod +x "${cc}" "${cxx}"
219
+
220
+ sysroot="$("${zig_bin}" cc -target "${zig_target}" -print-sysroot 2>/dev/null || true)"
221
+ else
222
+ cc="${musl_linker}"
223
+
224
+ if command -v "${arch}-linux-musl-g++" >/dev/null; then
225
+ cxx="$(command -v "${arch}-linux-musl-g++")"
226
+ elif command -v musl-g++ >/dev/null; then
227
+ cxx="$(command -v musl-g++)"
228
+ else
229
+ cxx="${cc}"
230
+ fi
231
+ fi
232
+
233
+ if [[ -n "${sysroot}" && "${sysroot}" != "/" ]]; then
234
+ echo "BORING_BSSL_SYSROOT=${sysroot}" >> "$GITHUB_ENV"
235
+ boring_sysroot_var="BORING_BSSL_SYSROOT_${TARGET}"
236
+ boring_sysroot_var="${boring_sysroot_var//-/_}"
237
+ echo "${boring_sysroot_var}=${sysroot}" >> "$GITHUB_ENV"
238
+ fi
239
+
240
+ cflags="-pthread"
241
+ cxxflags="-pthread"
242
+ if [[ "${TARGET}" == "aarch64-unknown-linux-musl" ]]; then
243
+ # BoringSSL enables -Wframe-larger-than=25344 under clang and treats warnings as errors.
244
+ cflags="${cflags} -Wno-error=frame-larger-than"
245
+ cxxflags="${cxxflags} -Wno-error=frame-larger-than"
246
+ fi
247
+
248
+ echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
249
+ echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"
250
+ echo "CC=${cc}" >> "$GITHUB_ENV"
251
+ echo "TARGET_CC=${cc}" >> "$GITHUB_ENV"
252
+ target_cc_var="CC_${TARGET}"
253
+ target_cc_var="${target_cc_var//-/_}"
254
+ echo "${target_cc_var}=${cc}" >> "$GITHUB_ENV"
255
+ echo "CXX=${cxx}" >> "$GITHUB_ENV"
256
+ echo "TARGET_CXX=${cxx}" >> "$GITHUB_ENV"
257
+ target_cxx_var="CXX_${TARGET}"
258
+ target_cxx_var="${target_cxx_var//-/_}"
259
+ echo "${target_cxx_var}=${cxx}" >> "$GITHUB_ENV"
260
+
261
+ cargo_linker_var="CARGO_TARGET_${TARGET^^}_LINKER"
262
+ cargo_linker_var="${cargo_linker_var//-/_}"
263
+ echo "${cargo_linker_var}=${musl_linker}" >> "$GITHUB_ENV"
264
+
265
+ echo "CMAKE_C_COMPILER=${cc}" >> "$GITHUB_ENV"
266
+ echo "CMAKE_CXX_COMPILER=${cxx}" >> "$GITHUB_ENV"
267
+ echo "CMAKE_ARGS=-DCMAKE_HAVE_THREADS_LIBRARY=1 -DCMAKE_USE_PTHREADS_INIT=1 -DCMAKE_THREAD_LIBS_INIT=-pthread -DTHREADS_PREFER_PTHREAD_FLAG=ON" >> "$GITHUB_ENV"
268
+
269
+ # Allow pkg-config resolution during cross-compilation.
270
+ echo "PKG_CONFIG_ALLOW_CROSS=1" >> "$GITHUB_ENV"
271
+ pkg_config_path="${libcap_pkgconfig_dir}"
272
+ if [[ -n "${PKG_CONFIG_PATH:-}" ]]; then
273
+ pkg_config_path="${pkg_config_path}:${PKG_CONFIG_PATH}"
274
+ fi
275
+ echo "PKG_CONFIG_PATH=${pkg_config_path}" >> "$GITHUB_ENV"
276
+ pkg_config_path_var="PKG_CONFIG_PATH_${TARGET}"
277
+ pkg_config_path_var="${pkg_config_path_var//-/_}"
278
+ echo "${pkg_config_path_var}=${libcap_pkgconfig_dir}" >> "$GITHUB_ENV"
279
+ pkg_config_libdir_var="PKG_CONFIG_LIBDIR_${TARGET}"
280
+ pkg_config_libdir_var="${pkg_config_libdir_var//-/_}"
281
+ # Do not let musl cross-builds resolve native libraries from the host glibc
282
+ # pkg-config directories. libcap is the only target package provided here.
283
+ echo "${pkg_config_libdir_var}=${libcap_pkgconfig_dir}" >> "$GITHUB_ENV"
284
+
285
+ if [[ -n "${sysroot}" && "${sysroot}" != "/" ]]; then
286
+ echo "PKG_CONFIG_SYSROOT_DIR=${sysroot}" >> "$GITHUB_ENV"
287
+ pkg_config_sysroot_var="PKG_CONFIG_SYSROOT_DIR_${TARGET}"
288
+ pkg_config_sysroot_var="${pkg_config_sysroot_var//-/_}"
289
+ echo "${pkg_config_sysroot_var}=${sysroot}" >> "$GITHUB_ENV"
290
+ fi
.github/scripts/install-musl-openssl.sh ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ : "${TARGET:?TARGET environment variable is required}"
5
+ : "${GITHUB_ENV:?GITHUB_ENV environment variable is required}"
6
+ : "${OPENSSL_CC:?OPENSSL_CC must name the target musl compiler}"
7
+
8
+ case "${TARGET}" in
9
+ x86_64-unknown-linux-musl) openssl_target="linux-x86_64" ;;
10
+ aarch64-unknown-linux-musl) openssl_target="linux-aarch64" ;;
11
+ *) echo "Unexpected musl target: ${TARGET}" >&2; exit 1 ;;
12
+ esac
13
+
14
+ # openssl-src's latest 300.x crate still contains 3.6.3. Build the upstream
15
+ # security release until that crate catches up, keeping the existing 3.x ABI.
16
+ openssl_version="3.6.4"
17
+ openssl_sha256="9bffaa1ad1e07b354c21bd3324ec02fa15579f45a7d0494b3e74bc449b7333ef"
18
+ openssl_root="${RUNNER_TEMP:-/tmp}/codex-musl-tools-${TARGET}/openssl-${openssl_version}"
19
+ openssl_prefix="${openssl_root}/prefix"
20
+
21
+ if [[ ! -f "${openssl_prefix}/.complete" ]]; then
22
+ mkdir -p "${openssl_root}"
23
+ archive="${openssl_root}/openssl-${openssl_version}.tar.gz"
24
+ curl -fsSL "https://github.com/openssl/openssl/releases/download/openssl-${openssl_version}/openssl-${openssl_version}.tar.gz" -o "${archive}"
25
+ echo "${openssl_sha256} ${archive}" | sha256sum -c -
26
+ tar -xzf "${archive}" -C "${openssl_root}"
27
+
28
+ (
29
+ cd "${openssl_root}/openssl-${openssl_version}"
30
+ # Match openssl-src's default/legacy configuration for musl, including
31
+ # disabling shared libraries, zlib, engines, and unsupported async APIs.
32
+ CC="${OPENSSL_CC}" perl ./Configure "${openssl_target}" \
33
+ "--prefix=${openssl_prefix}" --openssldir=/usr/local/ssl --libdir=lib \
34
+ no-shared no-module no-tests no-comp no-zlib no-zlib-dynamic \
35
+ no-ssl3 no-md2 no-rc5 no-weak-ssl-ciphers no-camellia no-idea no-seed \
36
+ no-engine no-async -DOPENSSL_NO_SECURE_MEMORY
37
+ make -j"${OPENSSL_BUILD_JOBS:-$(nproc)}" build_libs
38
+ make install_dev
39
+ )
40
+ touch "${openssl_prefix}/.complete"
41
+ fi
42
+
43
+ # Scope these overrides to the target so host build dependencies are unaffected.
44
+ # openssl-sys honors OPENSSL_NO_VENDOR even when Cargo enables `vendored`.
45
+ target_env="${TARGET^^}"
46
+ target_env="${target_env//-/_}"
47
+ {
48
+ echo "${target_env}_OPENSSL_DIR=${openssl_prefix}"
49
+ echo "${target_env}_OPENSSL_NO_VENDOR=1"
50
+ echo "${target_env}_OPENSSL_STATIC=1"
51
+ } >> "${GITHUB_ENV}"
.github/scripts/publish_r2_release.py ADDED
@@ -0,0 +1,532 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Mirror a Codex GitHub Release to Cloudflare R2.
3
+
4
+ Cloudflare R2 exposes an S3-compatible API, so the built-in AWS CLI uses
5
+ standard AWS credentials and the R2 endpoint from ``AWS_ENDPOINT_URL``.
6
+ Objects are created under ``codex/releases/<version>/`` with a validated upload
7
+ checksum and checked using object metadata before the run succeeds. The
8
+ versioned prefix includes every release asset plus installer-facing
9
+ ``release.json`` metadata derived from the verified downloads. Once those
10
+ objects are verified, the same metadata advances ``codex/channels/latest`` when
11
+ the release is marked latest and ``codex/channels/prerelease`` for prereleases.
12
+ Stable releases also update the mutable ``codex/install.sh`` and
13
+ ``codex/install.ps1`` bootstrap aliases from their verified versioned assets.
14
+ """
15
+
16
+ import argparse
17
+ import hashlib
18
+ import json
19
+ import os
20
+ import re
21
+ import subprocess
22
+ import sys
23
+ import tempfile
24
+ from concurrent.futures import ThreadPoolExecutor, as_completed
25
+ from pathlib import Path
26
+ from typing import Any, NamedTuple, NoReturn
27
+ from urllib.parse import quote
28
+
29
+ BUCKET = "releases"
30
+ PREFIX = "codex"
31
+ REPOSITORY = "openai/codex"
32
+ RELEASE_METADATA_NAME = "release.json"
33
+ INSTALLER_NAMES = ("install.sh", "install.ps1")
34
+ MAX_UPLOAD_WORKERS = 8
35
+ # Keep this pattern in sync with release-tag validation in
36
+ # .github/workflows/rust-release.yml.
37
+ VERSION_RE = re.compile(
38
+ r"^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:alpha(?:\.[0-9]+){0,2}"
39
+ r"|beta(?:\.[0-9]+)?))?$"
40
+ )
41
+ CRC64_RE = re.compile(r"^[A-Za-z0-9+/]{11}=$")
42
+ SHA256_RE = re.compile(r"^sha256:([0-9a-f]{64})$")
43
+ MISSING_OBJECT_RE = re.compile(r"\((?:404|NoSuchKey|NotFound)\)")
44
+
45
+
46
+ class PublishError(RuntimeError):
47
+ pass
48
+
49
+
50
+ class ReleaseAsset(NamedTuple):
51
+ path: Path
52
+ size: int
53
+ sha256: str
54
+
55
+
56
+ def run_command(args: list[str]) -> str:
57
+ result = subprocess.run(
58
+ args,
59
+ stdout=subprocess.PIPE,
60
+ stderr=subprocess.PIPE,
61
+ text=True,
62
+ )
63
+ if result.stdout:
64
+ print(result.stdout, end="", file=sys.stderr)
65
+ if result.stderr:
66
+ print(result.stderr, end="", file=sys.stderr)
67
+ result.check_returncode()
68
+ return result.stdout or ""
69
+
70
+
71
+ def get_release_metadata(tag: str, directory: Path, stage: str) -> list[ReleaseAsset]:
72
+ try:
73
+ metadata = json.loads(
74
+ run_command(
75
+ [
76
+ "gh",
77
+ "release",
78
+ "view",
79
+ tag,
80
+ "--repo",
81
+ REPOSITORY,
82
+ "--json",
83
+ "assets",
84
+ "--jq",
85
+ "[.assets[] | {name, size, state, digest}]",
86
+ ]
87
+ )
88
+ )
89
+ except (OSError, subprocess.CalledProcessError) as error:
90
+ raise PublishError(
91
+ f"GitHub release metadata request failed for {tag}: {error}"
92
+ ) from error
93
+ except json.JSONDecodeError as error:
94
+ raise PublishError(
95
+ f"invalid GitHub release metadata for {tag}: {error}"
96
+ ) from error
97
+
98
+ expected = {}
99
+ if not isinstance(metadata, list):
100
+ raise PublishError(f"GitHub returned invalid release metadata for {tag}")
101
+ for asset in metadata:
102
+ if not isinstance(asset, dict):
103
+ raise PublishError(
104
+ f"GitHub returned invalid release metadata for {tag}: {asset!r}"
105
+ )
106
+ # DotSlash runs concurrently, so defer its in-progress assets to finalization.
107
+ if stage == "assets" and asset.get("state") != "uploaded":
108
+ continue
109
+ name = asset.get("name")
110
+ size = asset.get("size")
111
+ digest = asset.get("digest")
112
+ match = SHA256_RE.fullmatch(digest) if isinstance(digest, str) else None
113
+ if (
114
+ not isinstance(name, str)
115
+ or not name
116
+ or name == RELEASE_METADATA_NAME
117
+ or name in expected
118
+ or type(size) is not int
119
+ or size < 0
120
+ or asset.get("state") != "uploaded"
121
+ or match is None
122
+ ):
123
+ raise PublishError(
124
+ f"GitHub returned invalid release metadata for {tag}: {asset!r}"
125
+ )
126
+ expected[name] = ReleaseAsset(directory / name, size, match.group(1))
127
+
128
+ if not expected:
129
+ raise PublishError(f"GitHub Release {tag} has no assets")
130
+ return sorted(expected.values(), key=lambda asset: asset.path.name)
131
+
132
+
133
+ def download_assets(tag: str, directory: Path, assets: list[ReleaseAsset]) -> None:
134
+ if not assets:
135
+ return
136
+
137
+ command = [
138
+ "gh",
139
+ "release",
140
+ "download",
141
+ tag,
142
+ "--repo",
143
+ REPOSITORY,
144
+ "--dir",
145
+ str(directory),
146
+ ]
147
+ for asset in assets:
148
+ command.extend(["--pattern", asset.path.name])
149
+
150
+ try:
151
+ run_command(command)
152
+ except (OSError, subprocess.CalledProcessError) as error:
153
+ raise PublishError(
154
+ f"GitHub release download failed for {tag}: {error}"
155
+ ) from error
156
+
157
+ if any(not asset.path.is_file() for asset in assets):
158
+ raise PublishError("GitHub returned invalid release assets")
159
+
160
+
161
+ def stream_digest(source: Any) -> tuple[int, str]:
162
+ digest = hashlib.sha256()
163
+ size = 0
164
+ while chunk := source.read(1024 * 1024):
165
+ digest.update(chunk)
166
+ size += len(chunk)
167
+ return size, digest.hexdigest()
168
+
169
+
170
+ def validate_asset(asset: ReleaseAsset) -> None:
171
+ with asset.path.open("rb") as source:
172
+ size, sha256 = stream_digest(source)
173
+ if size != asset.size or sha256 != asset.sha256:
174
+ raise PublishError(
175
+ f"GitHub asset mismatch for {asset.path.name}: expected "
176
+ f"size={asset.size} sha256={asset.sha256}, got "
177
+ f"size={size} sha256={sha256}"
178
+ )
179
+
180
+
181
+ def raise_s3(
182
+ action: str, key: str, error: Exception, detail: str | None = None
183
+ ) -> NoReturn:
184
+ raise PublishError(
185
+ f"could not {action} s3://{BUCKET}/{key}: {detail or error}"
186
+ ) from error
187
+
188
+
189
+ def put_object(
190
+ endpoint: str,
191
+ key: str,
192
+ path: Path,
193
+ sha256: str,
194
+ *,
195
+ extra_args: list[str],
196
+ ) -> None:
197
+ try:
198
+ run_command(
199
+ [
200
+ "aws",
201
+ "s3",
202
+ "cp",
203
+ str(path),
204
+ f"s3://{BUCKET}/{key}",
205
+ *extra_args,
206
+ "--checksum-algorithm",
207
+ "CRC64NVME",
208
+ "--metadata",
209
+ f"sha256={sha256}",
210
+ "--endpoint-url",
211
+ endpoint,
212
+ ]
213
+ )
214
+ except subprocess.CalledProcessError as error:
215
+ raise_s3("upload", key, error, (error.stderr or "").strip())
216
+ except OSError as error:
217
+ raise_s3("upload", key, error)
218
+
219
+
220
+ def verify_remote(
221
+ endpoint: str,
222
+ key: str,
223
+ expected_size: int,
224
+ expected_sha256: str,
225
+ ) -> None:
226
+ try:
227
+ response = json.loads(
228
+ run_command(
229
+ [
230
+ "aws",
231
+ "s3api",
232
+ "head-object",
233
+ "--bucket",
234
+ BUCKET,
235
+ "--key",
236
+ key,
237
+ "--checksum-mode",
238
+ "ENABLED",
239
+ "--endpoint-url",
240
+ endpoint,
241
+ ]
242
+ )
243
+ )
244
+ except subprocess.CalledProcessError as error:
245
+ raise_s3("inspect", key, error, (error.stderr or "").strip())
246
+ except OSError as error:
247
+ raise_s3("inspect", key, error)
248
+ except json.JSONDecodeError as error:
249
+ raise PublishError(f"invalid object metadata for {key}: {error}") from error
250
+
251
+ metadata = response.get("Metadata") if isinstance(response, dict) else None
252
+ size = response.get("ContentLength") if isinstance(response, dict) else None
253
+ crc64 = response.get("ChecksumCRC64NVME") if isinstance(response, dict) else None
254
+ sha256 = metadata.get("sha256") if isinstance(metadata, dict) else None
255
+ if (
256
+ size != expected_size
257
+ or sha256 != expected_sha256
258
+ or not isinstance(crc64, str)
259
+ or not CRC64_RE.fullmatch(crc64)
260
+ ):
261
+ raise PublishError(
262
+ f"object metadata mismatch for {key}: expected size={expected_size} "
263
+ f"sha256={expected_sha256}, got size={size} sha256={sha256} "
264
+ f"crc64nvme={crc64}"
265
+ )
266
+
267
+
268
+ def publish_installers(endpoint: str, tag: str, assets: list[ReleaseAsset]) -> None:
269
+ installers = {asset.path.name: asset for asset in assets}
270
+ missing = sorted(set(INSTALLER_NAMES) - installers.keys())
271
+ if missing:
272
+ raise PublishError(
273
+ f"GitHub Release {tag} is missing installer assets: {', '.join(missing)}"
274
+ )
275
+ for name in INSTALLER_NAMES:
276
+ asset = installers[name]
277
+ validate_asset(asset)
278
+ installer_key = f"{PREFIX}/{name}"
279
+ put_object(endpoint, installer_key, asset.path, asset.sha256, extra_args=[])
280
+ verify_remote(endpoint, installer_key, asset.size, asset.sha256)
281
+ print(
282
+ f"published and verified s3://{BUCKET}/{installer_key} "
283
+ f"size={asset.size} sha256={asset.sha256}",
284
+ file=sys.stderr,
285
+ )
286
+
287
+
288
+ def publish_asset(
289
+ endpoint: str, version: str, asset: ReleaseAsset, *, verify: bool = True
290
+ ) -> dict[str, Any]:
291
+ validate_asset(asset)
292
+ key = f"{PREFIX}/releases/{version}/{asset.path.name}"
293
+ put_object(endpoint, key, asset.path, asset.sha256, extra_args=["--no-overwrite"])
294
+ if verify:
295
+ verify_remote(endpoint, key, asset.size, asset.sha256)
296
+ status = "published and verified" if verify else "published"
297
+ print(
298
+ f"{status} s3://{BUCKET}/{key} size={asset.size} sha256={asset.sha256}",
299
+ file=sys.stderr,
300
+ )
301
+ return {
302
+ "key": key,
303
+ "name": asset.path.name,
304
+ "sha256": asset.sha256,
305
+ "size": asset.size,
306
+ }
307
+
308
+
309
+ def publish_assets(
310
+ endpoint: str, version: str, assets: list[ReleaseAsset], *, verify: bool = True
311
+ ) -> list[dict[str, Any]]:
312
+ if not assets:
313
+ return []
314
+
315
+ published = {}
316
+ with ThreadPoolExecutor(
317
+ max_workers=min(MAX_UPLOAD_WORKERS, len(assets))
318
+ ) as executor:
319
+ futures = {
320
+ executor.submit(
321
+ publish_asset, endpoint, version, asset, verify=verify
322
+ ): asset
323
+ for asset in assets
324
+ }
325
+ for future in as_completed(futures):
326
+ asset = futures[future]
327
+ published[asset.path.name] = future.result()
328
+ return [published[asset.path.name] for asset in assets]
329
+
330
+
331
+ def find_published_assets(
332
+ endpoint: str, version: str, assets: list[ReleaseAsset]
333
+ ) -> dict[str, dict[str, Any]]:
334
+ published = {}
335
+ with ThreadPoolExecutor(
336
+ max_workers=min(MAX_UPLOAD_WORKERS, len(assets))
337
+ ) as executor:
338
+ futures = {
339
+ executor.submit(
340
+ verify_remote,
341
+ endpoint,
342
+ f"{PREFIX}/releases/{version}/{asset.path.name}",
343
+ asset.size,
344
+ asset.sha256,
345
+ ): asset
346
+ for asset in assets
347
+ }
348
+ for future in as_completed(futures):
349
+ asset = futures[future]
350
+ try:
351
+ future.result()
352
+ except PublishError as error:
353
+ cause = error.__cause__
354
+ if isinstance(cause, subprocess.CalledProcessError) and (
355
+ MISSING_OBJECT_RE.search(cause.stderr or "")
356
+ ):
357
+ continue
358
+ raise
359
+ published[asset.path.name] = {
360
+ "key": f"{PREFIX}/releases/{version}/{asset.path.name}",
361
+ "name": asset.path.name,
362
+ "sha256": asset.sha256,
363
+ "size": asset.size,
364
+ }
365
+ return published
366
+
367
+
368
+ def parse_args() -> argparse.Namespace:
369
+ parser = argparse.ArgumentParser(description=__doc__)
370
+ parser.add_argument("--tag", required=True)
371
+ parser.add_argument("--make-latest", choices=("true", "false"), required=True)
372
+ parser.add_argument("--prerelease", choices=("true", "false"), required=True)
373
+ parser.add_argument("--stage", choices=("assets", "finalize"), required=True)
374
+ return parser.parse_args()
375
+
376
+
377
+ def main() -> int:
378
+ args = parse_args()
379
+ try:
380
+ endpoint = os.environ.get("AWS_ENDPOINT_URL")
381
+ if not os.environ.get("GH_TOKEN"):
382
+ raise PublishError("GH_TOKEN is required")
383
+ if not endpoint:
384
+ raise PublishError("AWS_ENDPOINT_URL is required for the R2 S3 endpoint")
385
+
386
+ version = args.tag.removeprefix("rust-v")
387
+ if args.tag == version or not VERSION_RE.fullmatch(version):
388
+ raise PublishError(f"invalid rust release tag: {args.tag}")
389
+ with tempfile.TemporaryDirectory() as temp_dir:
390
+ assets_directory = Path(temp_dir) / "assets"
391
+ assets_directory.mkdir()
392
+ assets = get_release_metadata(args.tag, assets_directory, args.stage)
393
+
394
+ if args.stage == "assets":
395
+ download_assets(args.tag, assets_directory, assets)
396
+ published = publish_assets(endpoint, version, assets, verify=False)
397
+ print(
398
+ json.dumps(
399
+ {
400
+ "assetCount": len(published),
401
+ "assets": published,
402
+ "releasePrefix": f"{PREFIX}/releases/{version}/",
403
+ "stage": args.stage,
404
+ "tag": args.tag,
405
+ "version": version,
406
+ },
407
+ sort_keys=True,
408
+ )
409
+ )
410
+ return 0
411
+
412
+ previously_published = find_published_assets(endpoint, version, assets)
413
+ remaining = [
414
+ asset for asset in assets if asset.path.name not in previously_published
415
+ ]
416
+ required_downloads = list(remaining)
417
+ if args.prerelease == "false":
418
+ required_downloads.extend(
419
+ asset
420
+ for asset in assets
421
+ if asset.path.name in INSTALLER_NAMES
422
+ and asset.path.name in previously_published
423
+ )
424
+ download_assets(args.tag, assets_directory, required_downloads)
425
+ additionally_published = {
426
+ asset["name"]: asset
427
+ for asset in publish_assets(endpoint, version, remaining)
428
+ }
429
+ published = [
430
+ previously_published.get(asset.path.name)
431
+ or additionally_published[asset.path.name]
432
+ for asset in assets
433
+ ]
434
+ metadata_assets = [
435
+ {
436
+ "name": asset.path.name,
437
+ "digest": f"sha256:{asset.sha256}",
438
+ "browser_download_url": (
439
+ f"https://releases.openai.com/{PREFIX}/releases/"
440
+ f"{version}/{quote(asset.path.name, safe='')}"
441
+ ),
442
+ }
443
+ for asset in assets
444
+ ]
445
+
446
+ metadata_path = Path(temp_dir) / RELEASE_METADATA_NAME
447
+ metadata_path.write_text(
448
+ json.dumps(
449
+ {
450
+ "assets": metadata_assets,
451
+ "tag_name": args.tag,
452
+ },
453
+ indent=2,
454
+ )
455
+ + "\n",
456
+ encoding="utf-8",
457
+ )
458
+ with metadata_path.open("rb") as source:
459
+ metadata_size, metadata_sha256 = stream_digest(source)
460
+ metadata_key = f"{PREFIX}/releases/{version}/{RELEASE_METADATA_NAME}"
461
+ put_object(
462
+ endpoint,
463
+ metadata_key,
464
+ metadata_path,
465
+ metadata_sha256,
466
+ extra_args=["--no-overwrite"],
467
+ )
468
+ verify_remote(
469
+ endpoint,
470
+ metadata_key,
471
+ metadata_size,
472
+ metadata_sha256,
473
+ )
474
+ print(
475
+ f"published and verified s3://{BUCKET}/{metadata_key} "
476
+ f"size={metadata_size} sha256={metadata_sha256}",
477
+ file=sys.stderr,
478
+ )
479
+ if args.prerelease == "false":
480
+ publish_installers(endpoint, args.tag, assets)
481
+ channels = []
482
+ if args.make_latest == "true":
483
+ channels.append("latest")
484
+ if args.prerelease == "true":
485
+ channels.append("prerelease")
486
+ for channel in channels:
487
+ channel_key = f"{PREFIX}/channels/{channel}"
488
+ put_object(
489
+ endpoint,
490
+ channel_key,
491
+ metadata_path,
492
+ metadata_sha256,
493
+ extra_args=["--content-type", "application/json"],
494
+ )
495
+ verify_remote(
496
+ endpoint,
497
+ channel_key,
498
+ metadata_size,
499
+ metadata_sha256,
500
+ )
501
+ print(
502
+ f"published and verified s3://{BUCKET}/{channel_key} "
503
+ f"size={metadata_size} sha256={metadata_sha256}",
504
+ file=sys.stderr,
505
+ )
506
+
507
+ print(
508
+ json.dumps(
509
+ {
510
+ "assetCount": len(published),
511
+ "assets": published,
512
+ "releaseMetadata": {
513
+ "key": metadata_key,
514
+ "sha256": metadata_sha256,
515
+ "size": metadata_size,
516
+ },
517
+ "releasePrefix": f"{PREFIX}/releases/{version}/",
518
+ "stage": args.stage,
519
+ "tag": args.tag,
520
+ "version": version,
521
+ },
522
+ sort_keys=True,
523
+ )
524
+ )
525
+ return 0
526
+ except PublishError as error:
527
+ print(f"publish failed: {error}", file=sys.stderr)
528
+ return 1
529
+
530
+
531
+ if __name__ == "__main__":
532
+ raise SystemExit(main())
.github/scripts/resolve_python_cli_release.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Bind downstream Python publication to a published stable CLI release."""
3
+
4
+ import argparse
5
+ import itertools
6
+ import json
7
+ import re
8
+ import subprocess
9
+ from pathlib import Path
10
+
11
+
12
+ def github_api(path: str):
13
+ return json.loads(subprocess.check_output(["gh", "api", path], text=True))
14
+
15
+
16
+ def github_items(path: str, key: str | None = None):
17
+ separator = "&" if "?" in path else "?"
18
+ for page in itertools.count(1):
19
+ response = github_api(f"{path}{separator}per_page=100&page={page}")
20
+ items = response[key] if key else response
21
+ yield from items
22
+ if len(items) < 100:
23
+ return
24
+
25
+
26
+ def resolve_release(
27
+ repository: str, run_id: str, *, run: dict | None = None
28
+ ) -> dict[str, str] | None:
29
+ if repository != "openai/codex" or not re.fullmatch(r"[1-9][0-9]*", run_id):
30
+ raise ValueError("Expected an openai/codex Rust release run ID")
31
+ prefix = f"repos/{repository}"
32
+ if run is None:
33
+ run = github_api(f"{prefix}/actions/runs/{run_id}")
34
+ if (
35
+ str(run["id"]) != run_id
36
+ or run["path"] != ".github/workflows/rust-release.yml"
37
+ or run["event"] != "push"
38
+ or run["status"] != "completed"
39
+ or run["head_repository"]["full_name"] != repository
40
+ ):
41
+ raise ValueError("Python publication requires a completed Rust release run")
42
+ tag = run["head_branch"]
43
+ if not isinstance(tag, str) or not tag.startswith("rust-v"):
44
+ raise ValueError("The Rust release run must identify a release tag")
45
+ match = re.fullmatch(r"rust-v([0-9]+\.[0-9]+\.[0-9]+)", tag)
46
+ if match is None:
47
+ return None # CLI prereleases do not publish the Python SDK.
48
+ revision = run["head_sha"]
49
+ if not re.fullmatch(r"[0-9a-f]{40}", revision):
50
+ raise ValueError("The Rust release run must identify an exact commit")
51
+
52
+ # A partial rerun of an unrelated publisher may not contain the release job.
53
+ # Inspect all attempts and use the last release job that actually ran.
54
+ release_job = max(
55
+ (
56
+ job
57
+ for job in github_items(
58
+ f"{prefix}/actions/runs/{run_id}/jobs?filter=all", "jobs"
59
+ )
60
+ if job["name"] == "release" and job["conclusion"] != "skipped"
61
+ ),
62
+ key=lambda job: (job["run_attempt"], job["id"]),
63
+ default=None,
64
+ )
65
+ if release_job is None or release_job["conclusion"] != "success":
66
+ raise ValueError("Python publication requires a successful release job")
67
+
68
+ target = github_api(f"{prefix}/git/ref/tags/{tag}")["object"]
69
+ for _ in range(8):
70
+ if target["type"] != "tag":
71
+ break
72
+ target = github_api(f"{prefix}/git/tags/{target['sha']}")["object"]
73
+ if target.get("type") != "commit" or target.get("sha") != revision:
74
+ raise ValueError("The release tag no longer matches the successful CLI run")
75
+ release = github_api(f"{prefix}/releases/tags/{tag}")
76
+ if release["draft"] or release["prerelease"] or release["tag_name"] != tag:
77
+ raise ValueError("The CLI release must be published and stable")
78
+
79
+ # The runtime builder downloads these six wheels and builds the two musl
80
+ # wheels from the release's package archives.
81
+ required_assets = {
82
+ f"openai_codex_cli_bin-{match[1]}-py3-none-{platform}.whl"
83
+ for platform in (
84
+ "macosx_10_9_x86_64",
85
+ "macosx_11_0_arm64",
86
+ "manylinux_2_17_aarch64",
87
+ "manylinux_2_17_x86_64",
88
+ "win_amd64",
89
+ "win_arm64",
90
+ )
91
+ } | {
92
+ f"codex-package-{arch}-unknown-linux-musl.tar.gz"
93
+ for arch in ("aarch64", "x86_64")
94
+ }
95
+ available_assets = {
96
+ asset["name"]
97
+ for asset in github_items(f"{prefix}/releases/{release['id']}/assets")
98
+ if asset["state"] == "uploaded" and asset["size"] > 0
99
+ }
100
+ if missing := required_assets - available_assets:
101
+ raise ValueError(f"The CLI release is missing Python inputs: {sorted(missing)}")
102
+ return {"source_sha": revision, "release_tag": tag, "version": match[1]}
103
+
104
+
105
+ def main(argv: list[str] | None = None) -> None:
106
+ parser = argparse.ArgumentParser(description=__doc__)
107
+ parser.add_argument("run_id")
108
+ parser.add_argument("--repository", required=True)
109
+ parser.add_argument("--event-path", type=Path)
110
+ parser.add_argument("--github-output", type=Path, required=True)
111
+ args = parser.parse_args(argv)
112
+ event = json.loads(args.event_path.read_text()) if args.event_path else {}
113
+ release = resolve_release(
114
+ args.repository, args.run_id, run=event.get("workflow_run")
115
+ )
116
+ with args.github_output.open("a") as output:
117
+ print(f"publish={str(release is not None).lower()}", file=output)
118
+ for name, value in (release or {}).items():
119
+ print(f"{name}={value}", file=output)
120
+
121
+
122
+ if __name__ == "__main__":
123
+ main()
.github/scripts/run-argument-comment-lint-bazel.sh ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ bazel_lint_args=("$@")
6
+ if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
7
+ # Some Rust top-level targets are still intentionally incompatible with the
8
+ # local Windows exec platform. Skip those explicit targets so the native
9
+ # lint aspect can run across the compatible crate graph instead of failing the
10
+ # whole build after analysis.
11
+ bazel_lint_args+=("--skip_incompatible_explicit_targets")
12
+ fi
13
+
14
+ read_query_labels() {
15
+ local query="$1"
16
+ local query_stdout
17
+ local query_stderr
18
+ query_stdout="$(mktemp)"
19
+ query_stderr="$(mktemp)"
20
+
21
+ if ! ./.github/scripts/run-bazel-query-ci.sh \
22
+ --keep_going \
23
+ --output=label \
24
+ -- "$query" >"$query_stdout" 2>"$query_stderr"; then
25
+ cat "$query_stderr" >&2
26
+ rm -f "$query_stdout" "$query_stderr"
27
+ exit 1
28
+ fi
29
+
30
+ cat "$query_stdout"
31
+ rm -f "$query_stdout" "$query_stderr"
32
+ }
33
+
34
+ final_build_targets=(//codex-rs/...)
35
+ if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
36
+ # Bazel's local Windows platform currently lacks a default test toolchain for
37
+ # `rust_test`, so target the concrete Rust crate rules directly. The lint
38
+ # aspect still walks their crate graph, which preserves incremental reuse for
39
+ # non-test code while avoiding non-Rust wrapper targets such as platform_data.
40
+ final_build_targets=()
41
+ while IFS= read -r label; do
42
+ [[ -n "$label" ]] || continue
43
+ final_build_targets+=("$label")
44
+ done < <(read_query_labels 'kind("rust_(library|binary|proc_macro) rule", //codex-rs/...)')
45
+
46
+ if [[ ${#final_build_targets[@]} -eq 0 ]]; then
47
+ echo "Failed to discover Windows Bazel lint targets." >&2
48
+ exit 1
49
+ fi
50
+ fi
51
+
52
+ ./.github/scripts/run-bazel-ci.sh \
53
+ -- \
54
+ build \
55
+ "${bazel_lint_args[@]}" \
56
+ -- \
57
+ "${final_build_targets[@]}"
.github/scripts/run-bazel-ci.sh ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ print_failed_bazel_test_logs=0
6
+ print_failed_bazel_action_summary=0
7
+ remote_download_toplevel=0
8
+ windows_msvc_host_platform=0
9
+ windows_cross_compile=0
10
+
11
+ while [[ $# -gt 0 ]]; do
12
+ case "$1" in
13
+ --print-failed-test-logs)
14
+ print_failed_bazel_test_logs=1
15
+ shift
16
+ ;;
17
+ --print-failed-action-summary)
18
+ print_failed_bazel_action_summary=1
19
+ shift
20
+ ;;
21
+ --remote-download-toplevel)
22
+ remote_download_toplevel=1
23
+ shift
24
+ ;;
25
+ --windows-msvc-host-platform)
26
+ windows_msvc_host_platform=1
27
+ shift
28
+ ;;
29
+ --windows-cross-compile)
30
+ windows_cross_compile=1
31
+ shift
32
+ ;;
33
+ --)
34
+ shift
35
+ break
36
+ ;;
37
+ *)
38
+ echo "Unknown option: $1" >&2
39
+ exit 1
40
+ ;;
41
+ esac
42
+ done
43
+
44
+ if [[ $# -eq 0 ]]; then
45
+ echo "Usage: $0 [--print-failed-test-logs] [--print-failed-action-summary] [--remote-download-toplevel] [--windows-msvc-host-platform] [--windows-cross-compile] -- <bazel args> -- <targets>" >&2
46
+ exit 1
47
+ fi
48
+
49
+ bazel_startup_args=()
50
+ if [[ -n "${BAZEL_HOST_JVM_ARG:-}" ]]; then
51
+ bazel_startup_args+=("--host_jvm_args=${BAZEL_HOST_JVM_ARG}")
52
+ fi
53
+ if [[ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]]; then
54
+ bazel_startup_args+=("--output_user_root=${BAZEL_OUTPUT_USER_ROOT}")
55
+ fi
56
+
57
+ run_bazel() {
58
+ if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
59
+ MSYS2_ARG_CONV_EXCL='*' "$(dirname "${BASH_SOURCE[0]}")/run_bazel_with_buildbuddy.py" "$@"
60
+ return
61
+ fi
62
+
63
+ "$(dirname "${BASH_SOURCE[0]}")/run_bazel_with_buildbuddy.py" "$@"
64
+ }
65
+
66
+ run_bazel_with_startup_args() {
67
+ if (( ${#bazel_startup_args[@]} > 0 )); then
68
+ run_bazel "${bazel_startup_args[@]}" "$@"
69
+ return
70
+ fi
71
+
72
+ run_bazel "$@"
73
+ }
74
+
75
+ ci_config=ci-linux
76
+ case "${RUNNER_OS:-}" in
77
+ macOS)
78
+ ci_config=ci-macos
79
+ ;;
80
+ Windows)
81
+ if [[ $windows_cross_compile -eq 1 ]]; then
82
+ ci_config=ci-windows-cross
83
+ else
84
+ ci_config=ci-windows
85
+ fi
86
+ ;;
87
+ esac
88
+
89
+ print_bazel_test_log_tails() {
90
+ local console_log="$1"
91
+ local testlogs_dir
92
+
93
+ local -a bazel_info_args=(info)
94
+ if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
95
+ # `bazel info` needs the same CI config as the failed test invocation so
96
+ # platform-specific output roots match. On Windows, omitting `ci-windows`
97
+ # would point at `local_windows-fastbuild` even when the test ran with the
98
+ # MSVC host platform under `local_windows_msvc-fastbuild`.
99
+ bazel_info_args+=("--config=${ci_config}")
100
+ fi
101
+
102
+ # Only pass flags that affect Bazel's output-root selection or repository
103
+ # lookup. Test/build-only flags such as execution logs or remote download
104
+ # mode can make `bazel info` fail, which would hide the real test log path.
105
+ for arg in "${post_config_bazel_args[@]}"; do
106
+ case "$arg" in
107
+ --host_platform=* | --repo_contents_cache=* | --repository_cache=*)
108
+ bazel_info_args+=("$arg")
109
+ ;;
110
+ esac
111
+ done
112
+
113
+ testlogs_dir="$(run_bazel_with_startup_args \
114
+ --noexperimental_remote_repo_contents_cache \
115
+ "${bazel_info_args[@]}" \
116
+ bazel-testlogs 2>/dev/null || echo bazel-testlogs)"
117
+
118
+ local failed_targets=()
119
+ while IFS= read -r target; do
120
+ failed_targets+=("$target")
121
+ done < <(
122
+ grep -E '^(FAIL: //|ERROR: .* Testing //)' "$console_log" \
123
+ | sed -E 's#^FAIL: (//[^ ]+).*#\1#; s#^ERROR: .* Testing (//[^ ]+) failed:.*#\1#' \
124
+ | sort -u
125
+ )
126
+
127
+ if [[ ${#failed_targets[@]} -eq 0 ]]; then
128
+ echo "No failed Bazel test targets were found in console output."
129
+ return
130
+ fi
131
+
132
+ for target in "${failed_targets[@]}"; do
133
+ local rel_path="${target#//}"
134
+ rel_path="${rel_path/://}"
135
+ local test_log="${testlogs_dir}/${rel_path}/test.log"
136
+ local reported_test_log
137
+ reported_test_log="$(grep -F "FAIL: ${target} " "$console_log" | sed -nE 's#.* \(see (.*[\\/]test\.log)\).*#\1#p' | head -n 1 || true)"
138
+ if [[ -n "$reported_test_log" ]]; then
139
+ reported_test_log="${reported_test_log//\\//}"
140
+ test_log="$reported_test_log"
141
+ fi
142
+
143
+ echo "::group::Bazel test log tail for ${target}"
144
+ if [[ -f "$test_log" ]]; then
145
+ tail -n 200 "$test_log"
146
+ else
147
+ echo "Missing test log: $test_log"
148
+ fi
149
+ echo "::endgroup::"
150
+ done
151
+ }
152
+
153
+ print_bazel_action_failure_summary() {
154
+ local console_log="$1"
155
+ local escaped_summary
156
+ local summary
157
+
158
+ summary="$(
159
+ awk '
160
+ function clean(line) {
161
+ gsub(sprintf("%c", 27) "\\[[0-9;]*m", "", line)
162
+ sub(/^.*\t[^\t]*\t[0-9TZ:._-]+ /, "", line)
163
+ return line
164
+ }
165
+
166
+ function is_diagnostic(line) {
167
+ return line ~ /^(error(\[[^]]+\])?:|warning:|note:|help:)/ ||
168
+ line ~ /^[[:space:]]+-->/ ||
169
+ line ~ /^[[:space:]]*[0-9]+[[:space:]]+\|/ ||
170
+ line ~ /^[[:space:]]*\|/ ||
171
+ line ~ /^[[:space:]]+= (note|help):/ ||
172
+ line ~ /^[[:space:]]*\^[[:space:]^~-]*$/ ||
173
+ line ~ /^For more information/ ||
174
+ line ~ /^error: aborting/
175
+ }
176
+
177
+ {
178
+ line = clean($0)
179
+ }
180
+
181
+ line ~ /^ERROR: .* failed:/ {
182
+ if (printed) {
183
+ print ""
184
+ }
185
+ print line
186
+ in_failure = 1
187
+ seen_diagnostic = 0
188
+ printed = 1
189
+ next
190
+ }
191
+
192
+ in_failure && is_diagnostic(line) {
193
+ print line
194
+ seen_diagnostic = 1
195
+ next
196
+ }
197
+
198
+ in_failure && seen_diagnostic && line == "" {
199
+ print ""
200
+ next
201
+ }
202
+
203
+ in_failure && seen_diagnostic {
204
+ in_failure = 0
205
+ seen_diagnostic = 0
206
+ next
207
+ }
208
+ ' "$console_log"
209
+ )"
210
+
211
+ if [[ -z "$summary" ]]; then
212
+ summary="$(grep -E '^ERROR: |^FAILED: ' "$console_log" | tail -n 50 || true)"
213
+ fi
214
+
215
+ if [[ -z "$summary" ]]; then
216
+ echo "No Bazel action failures were found in the captured console output."
217
+ return
218
+ fi
219
+
220
+ if [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then
221
+ escaped_summary="$(
222
+ printf '%s' "$summary" \
223
+ | awk 'BEGIN { ORS = "" } {
224
+ gsub(/%/, "%25")
225
+ gsub(/\r/, "%0D")
226
+ print sep $0
227
+ sep = "%0A"
228
+ }'
229
+ )"
230
+ echo "::error title=Bazel failed action diagnostics::${escaped_summary}"
231
+ fi
232
+
233
+ echo
234
+ echo "Bazel failed action diagnostics:"
235
+ echo "--------------------------------"
236
+ printf '%s\n' "$summary"
237
+ echo "--------------------------------"
238
+ }
239
+
240
+ bazel_args=()
241
+ bazel_targets=()
242
+ found_target_separator=0
243
+ for arg in "$@"; do
244
+ if [[ "$arg" == "--" && $found_target_separator -eq 0 ]]; then
245
+ found_target_separator=1
246
+ continue
247
+ fi
248
+
249
+ if [[ $found_target_separator -eq 0 ]]; then
250
+ bazel_args+=("$arg")
251
+ else
252
+ bazel_targets+=("$arg")
253
+ fi
254
+ done
255
+
256
+ if [[ ${#bazel_args[@]} -eq 0 || ${#bazel_targets[@]} -eq 0 ]]; then
257
+ echo "Expected Bazel args and targets separated by --" >&2
258
+ exit 1
259
+ fi
260
+
261
+ if [[ "${RUNNER_OS:-}" == "Windows" && $windows_cross_compile -eq 1 && -z "${BUILDBUDDY_API_KEY:-}" ]]; then
262
+ # Windows cross-compilation depends on authenticated RBE. Preserve the local
263
+ # Windows build shape when credentials are unavailable.
264
+ ci_config=ci-windows
265
+ windows_msvc_host_platform=1
266
+ fi
267
+
268
+ post_config_bazel_args=()
269
+ if [[ "${RUNNER_OS:-}" == "Windows" && $windows_msvc_host_platform -eq 1 ]]; then
270
+ has_host_platform_override=0
271
+ for arg in "${bazel_args[@]}"; do
272
+ if [[ "$arg" == --host_platform=* ]]; then
273
+ has_host_platform_override=1
274
+ break
275
+ fi
276
+ done
277
+
278
+ if [[ $has_host_platform_override -eq 0 ]]; then
279
+ # Use the MSVC Windows platform for jobs that need helper binaries like
280
+ # Rust test wrappers and V8 generators to resolve a compatible toolchain.
281
+ # Callers that need a different Windows target platform should pass an
282
+ # explicit `--platforms=...` flag.
283
+ post_config_bazel_args+=("--host_platform=//:local_windows_msvc")
284
+ fi
285
+ fi
286
+
287
+ if [[ $remote_download_toplevel -eq 1 ]]; then
288
+ # Override the CI config's remote_download_minimal setting when callers need
289
+ # the built artifact to exist on disk after the command completes.
290
+ post_config_bazel_args+=(--remote_download_toplevel)
291
+ fi
292
+
293
+ if [[ "${RUNNER_OS:-}" == "Windows" && $windows_cross_compile -eq 1 && -n "${BUILDBUDDY_API_KEY:-}" ]]; then
294
+ # `--enable_platform_specific_config` expands `common:windows` on Windows
295
+ # hosts after ordinary rc configs, which can override `ci-windows-cross`'s
296
+ # RBE host platform. Repeat the host platform on the command line so V8 and
297
+ # other genrules execute on Linux RBE workers instead of Git Bash locally.
298
+ #
299
+ # Bazel also derives the default genrule shell from the client host. Without
300
+ # an explicit shell executable, remote Linux actions can be asked to run
301
+ # `C:\Program Files\Git\usr\bin\bash.exe`.
302
+ post_config_bazel_args+=(--host_platform=//:rbe --shell_executable=/bin/bash)
303
+ fi
304
+
305
+ if [[ "${RUNNER_OS:-}" == "Windows" && $windows_cross_compile -eq 1 && -z "${BUILDBUDDY_API_KEY:-}" ]]; then
306
+ # The Windows cross-compile config depends on authenticated remote
307
+ # execution. When credentials are unavailable, keep the local build shape
308
+ # and its lower concurrency cap.
309
+ post_config_bazel_args+=(--jobs=8)
310
+ fi
311
+
312
+ if [[ -n "${BAZEL_REPO_CONTENTS_CACHE:-}" ]]; then
313
+ # Windows self-hosted runners can run multiple Bazel jobs concurrently. Give
314
+ # each job its own repo contents cache so they do not fight over the shared
315
+ # path configured in `ci-windows`.
316
+ post_config_bazel_args+=("--repo_contents_cache=${BAZEL_REPO_CONTENTS_CACHE}")
317
+ fi
318
+
319
+ if [[ -n "${BAZEL_REPOSITORY_CACHE:-}" ]]; then
320
+ post_config_bazel_args+=("--repository_cache=${BAZEL_REPOSITORY_CACHE}")
321
+ fi
322
+
323
+ if [[ -n "${CODEX_BAZEL_EXECUTION_LOG_COMPACT_DIR:-}" ]]; then
324
+ post_config_bazel_args+=(
325
+ "--execution_log_compact_file=${CODEX_BAZEL_EXECUTION_LOG_COMPACT_DIR}/execution-log-${bazel_args[0]}-${GITHUB_JOB:-local}-$$.zst"
326
+ )
327
+ fi
328
+
329
+ if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
330
+ pass_windows_build_env=1
331
+ if [[ $windows_cross_compile -eq 1 && -n "${BUILDBUDDY_API_KEY:-}" ]]; then
332
+ # Remote build actions execute on Linux RBE workers. Passing the Windows
333
+ # runner's build environment there makes Bazel genrules try to execute
334
+ # C:\Program Files\Git\usr\bin\bash.exe on Linux.
335
+ pass_windows_build_env=0
336
+ fi
337
+
338
+ if [[ $pass_windows_build_env -eq 1 ]]; then
339
+ windows_action_env_vars=(
340
+ INCLUDE
341
+ LIB
342
+ LIBPATH
343
+ UCRTVersion
344
+ UniversalCRTSdkDir
345
+ VCINSTALLDIR
346
+ VCToolsInstallDir
347
+ WindowsLibPath
348
+ WindowsSdkBinPath
349
+ WindowsSdkDir
350
+ WindowsSDKLibVersion
351
+ WindowsSDKVersion
352
+ )
353
+
354
+ for env_var in "${windows_action_env_vars[@]}"; do
355
+ if [[ -n "${!env_var:-}" ]]; then
356
+ post_config_bazel_args+=("--action_env=${env_var}" "--host_action_env=${env_var}")
357
+ fi
358
+ done
359
+ fi
360
+
361
+ if [[ -z "${CODEX_BAZEL_WINDOWS_PATH:-}" ]]; then
362
+ echo "CODEX_BAZEL_WINDOWS_PATH must be set for Windows Bazel CI." >&2
363
+ exit 1
364
+ fi
365
+
366
+ if [[ $pass_windows_build_env -eq 1 ]]; then
367
+ post_config_bazel_args+=(
368
+ "--action_env=PATH=${CODEX_BAZEL_WINDOWS_PATH}"
369
+ "--host_action_env=PATH=${CODEX_BAZEL_WINDOWS_PATH}"
370
+ )
371
+ elif [[ $windows_cross_compile -eq 1 ]]; then
372
+ # Remote build actions run on Linux RBE workers. Give their shell snippets
373
+ # a Linux PATH while preserving CODEX_BAZEL_WINDOWS_PATH below for local
374
+ # Windows test execution.
375
+ post_config_bazel_args+=(
376
+ "--action_env=PATH=/usr/bin:/bin"
377
+ "--host_action_env=PATH=/usr/bin:/bin"
378
+ )
379
+ fi
380
+ post_config_bazel_args+=("--test_env=PATH=${CODEX_BAZEL_WINDOWS_PATH}")
381
+ fi
382
+
383
+ bazel_console_log="$(mktemp)"
384
+ trap 'rm -f "$bazel_console_log"' EXIT
385
+
386
+ bazel_run_args=(
387
+ "${bazel_args[@]}"
388
+ )
389
+ if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
390
+ echo "BuildBuddy API key is available; using remote Bazel configuration."
391
+ bazel_run_args+=("--config=${ci_config}")
392
+ else
393
+ echo "BuildBuddy API key is not available; using local Bazel configuration."
394
+ fi
395
+ if (( ${#post_config_bazel_args[@]} > 0 )); then
396
+ bazel_run_args+=("${post_config_bazel_args[@]}")
397
+ fi
398
+ set +e
399
+ # Work around Bazel 9 remote repo contents cache / overlay materialization
400
+ # failures seen in CI (for example "is not a symlink" or permission errors
401
+ # while materializing external repos such as rules_perl). This only disables
402
+ # the startup-level repo contents cache; keyed runs still use BuildBuddy.
403
+ run_bazel_with_startup_args \
404
+ --noexperimental_remote_repo_contents_cache \
405
+ "${bazel_run_args[@]}" \
406
+ -- \
407
+ "${bazel_targets[@]}" \
408
+ 2>&1 | tee "$bazel_console_log"
409
+ bazel_status=${PIPESTATUS[0]}
410
+ set -e
411
+
412
+ if [[ ${bazel_status:-0} -ne 0 ]]; then
413
+ if [[ $print_failed_bazel_action_summary -eq 1 ]]; then
414
+ print_bazel_action_failure_summary "$bazel_console_log"
415
+ fi
416
+ if [[ $print_failed_bazel_test_logs -eq 1 ]]; then
417
+ print_bazel_test_log_tails "$bazel_console_log"
418
+ fi
419
+ exit "$bazel_status"
420
+ fi
.github/scripts/run-bazel-query-ci.sh ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ # Run target-discovery queries with the same startup settings as the main
6
+ # build/test invocation so they can reuse the same Bazel server. Queries only
7
+ # enumerate labels, so they intentionally do not select a CI build/test config
8
+ # or remote execution.
9
+
10
+ if [[ $# -lt 2 || "${@: -2:1}" != "--" ]]; then
11
+ echo "Usage: $0 [<bazel query args>...] -- <query expression>" >&2
12
+ exit 1
13
+ fi
14
+
15
+ query_args=("${@:1:$#-2}")
16
+ query_expression="${@: -1}"
17
+
18
+ run_bazel() {
19
+ if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
20
+ MSYS2_ARG_CONV_EXCL='*' "$(dirname "${BASH_SOURCE[0]}")/run_bazel_with_buildbuddy.py" "$@"
21
+ return
22
+ fi
23
+
24
+ "$(dirname "${BASH_SOURCE[0]}")/run_bazel_with_buildbuddy.py" "$@"
25
+ }
26
+
27
+ bazel_query_args=(query)
28
+
29
+ if [[ -n "${BAZEL_REPO_CONTENTS_CACHE:-}" ]]; then
30
+ bazel_query_args+=("--repo_contents_cache=${BAZEL_REPO_CONTENTS_CACHE}")
31
+ fi
32
+
33
+ if [[ -n "${BAZEL_REPOSITORY_CACHE:-}" ]]; then
34
+ bazel_query_args+=("--repository_cache=${BAZEL_REPOSITORY_CACHE}")
35
+ fi
36
+
37
+ if (( ${#query_args[@]} > 0 )); then
38
+ bazel_query_args+=("${query_args[@]}")
39
+ fi
40
+ bazel_query_args+=("$query_expression")
41
+
42
+ run_bazel "${bazel_query_args[@]}"
.github/scripts/run_bazel_with_buildbuddy.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from collections.abc import Mapping
8
+ from collections.abc import Sequence
9
+ from pathlib import Path
10
+
11
+
12
+ OPENAI_REPOSITORY = "openai/codex"
13
+ # Remote configurations select cache/BES/download endpoints. Their -rbe forms
14
+ # also select the matching remote executor endpoint.
15
+ GENERIC_REMOTE_CONFIG = "buildbuddy-generic"
16
+ OPENAI_REMOTE_CONFIG = "buildbuddy-openai"
17
+ # These CI configurations require remote build execution. The wrapper supplies
18
+ # an RBE configuration, which also includes the common `remote` settings.
19
+ REMOTE_EXECUTION_CONFIGS = {
20
+ "--config=ci-linux",
21
+ "--config=ci-macos",
22
+ "--config=ci-v8",
23
+ "--config=ci-windows-cross",
24
+ }
25
+ # Honor either explicit setting so the wrapper never overrides the caller's
26
+ # choice when it supplies the CI default below.
27
+ REMOTE_REPO_CONTENTS_CACHE_STARTUP_OPTIONS = {
28
+ "--experimental_remote_repo_contents_cache",
29
+ "--noexperimental_remote_repo_contents_cache",
30
+ }
31
+
32
+
33
+ def startup_args(args: Sequence[str], env: Mapping[str, str]) -> list[str]:
34
+ """Return shared startup options that are missing from a Bazel invocation.
35
+
36
+ Bazel startup options must precede the command, and changing them restarts
37
+ the server and discards its analysis cache. GitHub Actions invokes Bazel
38
+ through several helpers, so normalize their startup options here while
39
+ preserving any explicit choice made by the caller.
40
+ """
41
+ command_idx = next(
42
+ (idx for idx, arg in enumerate(args) if not arg.startswith("-")),
43
+ len(args),
44
+ )
45
+ configured_startup_args = args[:command_idx]
46
+ injected_args = []
47
+
48
+ output_user_root = env.get("BAZEL_OUTPUT_USER_ROOT")
49
+ if output_user_root and not any(
50
+ arg.startswith("--output_user_root=") for arg in configured_startup_args
51
+ ):
52
+ injected_args.append(f"--output_user_root={output_user_root}")
53
+
54
+ if env.get("GITHUB_ACTIONS") == "true" and not any(
55
+ arg in REMOTE_REPO_CONTENTS_CACHE_STARTUP_OPTIONS
56
+ for arg in configured_startup_args
57
+ ):
58
+ # Work around Bazel 9 overlay materialization failures seen in CI. This
59
+ # disables only the startup-level repo contents cache; keyed runs still
60
+ # use BuildBuddy.
61
+ injected_args.append("--noexperimental_remote_repo_contents_cache")
62
+
63
+ return injected_args
64
+
65
+
66
+ # Only authenticated workflow runs executing trusted upstream code may use the
67
+ # OpenAI BuildBuddy host. A pull request event without proof that its head is
68
+ # in the upstream repository fails closed to the generic host.
69
+ def is_trusted_upstream_run(env: Mapping[str, str]) -> bool:
70
+ # `GITHUB_REPOSITORY` is easy to set locally. Requiring GitHub's workflow
71
+ # marker prevents a local command from opting itself into the OpenAI host.
72
+ if (
73
+ env.get("GITHUB_ACTIONS") != "true"
74
+ or env.get("GITHUB_REPOSITORY") != OPENAI_REPOSITORY
75
+ ):
76
+ return False
77
+ # Non-PR workflow runs in `openai/codex` execute upstream refs, so they are
78
+ # trusted. Fork code reaches these workflows only through pull requests.
79
+ if env.get("GITHUB_EVENT_NAME") != "pull_request":
80
+ return True
81
+
82
+ event_path = env.get("GITHUB_EVENT_PATH")
83
+ if not event_path:
84
+ return False
85
+ try:
86
+ event = json.loads(Path(event_path).read_text(encoding="utf-8"))
87
+ except (OSError, json.JSONDecodeError):
88
+ return False
89
+
90
+ try:
91
+ return event["pull_request"]["head"]["repo"]["fork"] is False
92
+ except (KeyError, TypeError):
93
+ return False
94
+
95
+
96
+ def uses_openai_host(env: Mapping[str, str]) -> bool:
97
+ return bool(env.get("BUILDBUDDY_API_KEY")) and is_trusted_upstream_run(env)
98
+
99
+
100
+ def uses_remote_execution(args: Sequence[str]) -> bool:
101
+ try:
102
+ separator_idx = args.index("--")
103
+ except ValueError:
104
+ separator_idx = len(args)
105
+ return any(arg in REMOTE_EXECUTION_CONFIGS for arg in args[:separator_idx])
106
+
107
+
108
+ def remote_config(args: Sequence[str], env: Mapping[str, str]) -> str | None:
109
+ if not env.get("BUILDBUDDY_API_KEY"):
110
+ return None
111
+
112
+ config = OPENAI_REMOTE_CONFIG if uses_openai_host(env) else GENERIC_REMOTE_CONFIG
113
+ if uses_remote_execution(args):
114
+ config += "-rbe"
115
+ return config
116
+
117
+
118
+ def bazel_args_without_remote_execution(args: Sequence[str]) -> list[str]:
119
+ # Remote CI configs require BuildBuddy credentials. Removing them preserves
120
+ # the local fallback used for fork pull requests.
121
+ try:
122
+ separator_idx = args.index("--")
123
+ except ValueError:
124
+ separator_idx = len(args)
125
+ return [
126
+ *(arg for arg in args[:separator_idx] if arg not in REMOTE_EXECUTION_CONFIGS),
127
+ *args[separator_idx:],
128
+ ]
129
+
130
+
131
+ def bazel_args_with_remote_config(
132
+ args: Sequence[str], env: Mapping[str, str]
133
+ ) -> list[str]:
134
+ command_idx = next(
135
+ (idx for idx, arg in enumerate(args) if not arg.startswith("-")),
136
+ None,
137
+ )
138
+ if command_idx is None:
139
+ raise ValueError("expected a Bazel command")
140
+
141
+ config = remote_config(args, env)
142
+ if config is None:
143
+ configured_args = bazel_args_without_remote_execution(args)
144
+ else:
145
+ # `remote_config()` returns a configuration only when this key is present.
146
+ api_key = env["BUILDBUDDY_API_KEY"]
147
+ remote_args = [
148
+ f"--config={config}",
149
+ f"--remote_header=x-buildbuddy-api-key={api_key}",
150
+ ]
151
+
152
+ # Insert immediately after the Bazel command. This keeps wrapper-added
153
+ # options out of positional payloads and lets later CI configs override
154
+ # shared RBE defaults such as the Windows cross-compilation exec platforms.
155
+ configured_args = [
156
+ *args[: command_idx + 1],
157
+ *remote_args,
158
+ *args[command_idx + 1 :],
159
+ ]
160
+
161
+ try:
162
+ separator_idx = configured_args.index("--")
163
+ except ValueError:
164
+ separator_idx = len(configured_args)
165
+
166
+ cache_args = [
167
+ f"{option_prefix}{env[env_name]}"
168
+ for env_name, option_prefix in (
169
+ ("BAZEL_REPO_CONTENTS_CACHE", "--repo_contents_cache="),
170
+ ("BAZEL_REPOSITORY_CACHE", "--repository_cache="),
171
+ )
172
+ if env.get(env_name)
173
+ and not any(
174
+ arg.startswith(option_prefix) for arg in configured_args[:separator_idx]
175
+ )
176
+ ]
177
+ return [
178
+ *configured_args[:separator_idx],
179
+ *cache_args,
180
+ *configured_args[separator_idx:],
181
+ ]
182
+
183
+
184
+ def bazel_command(*args: str, env: Mapping[str, str] | None = None) -> list[str]:
185
+ env = os.environ if env is None else env
186
+ bazel = env.get("CODEX_BAZEL_BIN", "bazel")
187
+ return [bazel, *startup_args(args, env), *bazel_args_with_remote_config(args, env)]
188
+
189
+
190
+ def main() -> None:
191
+ config = remote_config(sys.argv[1:], os.environ)
192
+ if config is None:
193
+ print(
194
+ "BuildBuddy key unavailable; using local Bazel configuration.",
195
+ file=sys.stderr,
196
+ )
197
+ else:
198
+ host_description = (
199
+ "OpenAI tenant" if uses_openai_host(os.environ) else "generic"
200
+ )
201
+ print(
202
+ f"Using {host_description} BuildBuddy configuration: {config}.",
203
+ file=sys.stderr,
204
+ )
205
+
206
+ command = bazel_command(*sys.argv[1:])
207
+ if os.name == "nt":
208
+ # Windows CRT exec can split arguments containing spaces and lose the
209
+ # eventual child exit status. Wait for Bazel and propagate its status.
210
+ result = subprocess.run(command, check=False)
211
+ raise SystemExit(result.returncode)
212
+
213
+ os.execvp(command[0], command)
214
+
215
+
216
+ if __name__ == "__main__":
217
+ main()
.github/scripts/rusty_v8_bazel.py ADDED
@@ -0,0 +1,411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import gzip
7
+ import hashlib
8
+ import re
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ import tomllib
13
+ from pathlib import Path
14
+
15
+ from run_bazel_with_buildbuddy import bazel_command
16
+ from rusty_v8_module_bazel import (
17
+ RustyV8ChecksumError,
18
+ check_module_bazel,
19
+ rusty_v8_http_file_versions,
20
+ update_module_bazel,
21
+ )
22
+
23
+
24
+ ROOT = Path(__file__).resolve().parents[2]
25
+ MODULE_BAZEL = ROOT / "MODULE.bazel"
26
+ RUSTY_V8_CHECKSUMS_DIR = ROOT / "third_party" / "v8"
27
+ RELEASE_ARTIFACT_PROFILE = "release"
28
+ SANDBOX_ARTIFACT_PROFILE = "ptrcomp_sandbox_release"
29
+ ARTIFACT_BAZEL_CONFIGS = ["rusty-v8-upstream-libcxx"]
30
+
31
+
32
+ def bazel_execroot() -> Path:
33
+ output = subprocess.check_output(
34
+ bazel_command("info", "execution_root"),
35
+ cwd=ROOT,
36
+ text=True,
37
+ )
38
+ return Path(output.strip())
39
+
40
+
41
+ def bazel_output_base() -> Path:
42
+ output = subprocess.check_output(
43
+ bazel_command("info", "output_base"),
44
+ cwd=ROOT,
45
+ text=True,
46
+ )
47
+ return Path(output.strip())
48
+
49
+
50
+ def bazel_output_path(path: str) -> Path:
51
+ if path.startswith("external/"):
52
+ return bazel_output_base() / path
53
+ return bazel_execroot() / path
54
+
55
+
56
+ def bazel_output_files(
57
+ platform: str,
58
+ labels: list[str],
59
+ compilation_mode: str = "fastbuild",
60
+ bazel_configs: list[str] | None = None,
61
+ ) -> list[Path]:
62
+ expression = "set(" + " ".join(labels) + ")"
63
+ bazel_configs = bazel_configs or []
64
+ output = subprocess.check_output(
65
+ bazel_command(
66
+ "cquery",
67
+ "-c",
68
+ compilation_mode,
69
+ f"--platforms=@llvm//platforms:{platform}",
70
+ *[f"--config={config}" for config in bazel_configs],
71
+ "--output=files",
72
+ expression,
73
+ ),
74
+ cwd=ROOT,
75
+ text=True,
76
+ )
77
+ return [
78
+ bazel_output_path(line.strip()) for line in output.splitlines() if line.strip()
79
+ ]
80
+
81
+
82
+ def bazel_build(
83
+ platform: str,
84
+ labels: list[str],
85
+ compilation_mode: str = "fastbuild",
86
+ bazel_configs: list[str] | None = None,
87
+ download_toplevel: bool = False,
88
+ ) -> None:
89
+ bazel_configs = bazel_configs or []
90
+ download_args = ["--remote_download_toplevel"] if download_toplevel else []
91
+ subprocess.run(
92
+ bazel_command(
93
+ "build",
94
+ "-c",
95
+ compilation_mode,
96
+ f"--platforms=@llvm//platforms:{platform}",
97
+ *[f"--config={config}" for config in bazel_configs],
98
+ *download_args,
99
+ *labels,
100
+ ),
101
+ cwd=ROOT,
102
+ check=True,
103
+ )
104
+
105
+
106
+ def ensure_bazel_output_files(
107
+ platform: str,
108
+ labels: list[str],
109
+ compilation_mode: str = "fastbuild",
110
+ bazel_configs: list[str] | None = None,
111
+ ) -> list[Path]:
112
+ # Bazel output paths can be reused across config flips, so existence alone
113
+ # does not prove the files match the requested flags.
114
+ bazel_build(
115
+ platform,
116
+ labels,
117
+ compilation_mode,
118
+ bazel_configs,
119
+ download_toplevel=True,
120
+ )
121
+ outputs = bazel_output_files(platform, labels, compilation_mode, bazel_configs)
122
+ missing = [str(path) for path in outputs if not path.exists()]
123
+ if missing:
124
+ raise SystemExit(f"missing built outputs for {labels}: {missing}")
125
+ return outputs
126
+
127
+
128
+ def artifact_bazel_configs(bazel_configs: list[str] | None = None) -> list[str]:
129
+ configured = list(ARTIFACT_BAZEL_CONFIGS)
130
+ for config in bazel_configs or []:
131
+ if config not in configured:
132
+ configured.append(config)
133
+ return configured
134
+
135
+
136
+ def release_pair_label(target: str, sandbox: bool = False) -> str:
137
+ target_suffix = target.replace("-", "_")
138
+ pair_kind = "sandbox_release_pair" if sandbox else "release_pair"
139
+ return f"//third_party/v8:rusty_v8_{pair_kind}_{target_suffix}"
140
+
141
+
142
+ def resolved_v8_crate_version() -> str:
143
+ cargo_lock = tomllib.loads((ROOT / "codex-rs" / "Cargo.lock").read_text())
144
+ versions = sorted(
145
+ {
146
+ package["version"]
147
+ for package in cargo_lock["package"]
148
+ if package["name"] == "v8"
149
+ }
150
+ )
151
+ if len(versions) == 1:
152
+ return versions[0]
153
+ if len(versions) > 1:
154
+ raise SystemExit(f"expected exactly one resolved v8 version, found: {versions}")
155
+
156
+ module_bazel = (ROOT / "MODULE.bazel").read_text()
157
+ matches = sorted(
158
+ set(
159
+ re.findall(
160
+ r"https://static\.crates\.io/crates/v8/v8-([0-9]+\.[0-9]+\.[0-9]+)\.crate",
161
+ module_bazel,
162
+ )
163
+ )
164
+ )
165
+ if len(matches) != 1:
166
+ raise SystemExit(
167
+ "expected exactly one pinned v8 crate version in MODULE.bazel, "
168
+ f"found: {matches}"
169
+ )
170
+ return matches[0]
171
+
172
+
173
+ def rusty_v8_checksum_manifest_path(version: str) -> Path:
174
+ return RUSTY_V8_CHECKSUMS_DIR / f"rusty_v8_{version.replace('.', '_')}.sha256"
175
+
176
+
177
+ def command_version(version: str | None) -> str:
178
+ if version is not None:
179
+ return version
180
+
181
+ manifest_versions = rusty_v8_http_file_versions(MODULE_BAZEL.read_text())
182
+ if len(manifest_versions) == 1:
183
+ return manifest_versions[0]
184
+ if len(manifest_versions) > 1:
185
+ raise SystemExit(
186
+ "expected at most one rusty_v8 http_file version in MODULE.bazel, "
187
+ f"found: {manifest_versions}; pass --version explicitly"
188
+ )
189
+
190
+ return resolved_v8_crate_version()
191
+
192
+
193
+ def command_manifest_path(manifest: Path | None, version: str) -> Path:
194
+ if manifest is None:
195
+ return rusty_v8_checksum_manifest_path(version)
196
+ if manifest.is_absolute():
197
+ return manifest
198
+ return ROOT / manifest
199
+
200
+
201
+ def staged_archive_name(target: str, source_path: Path, artifact_profile: str) -> str:
202
+ if target.endswith("-pc-windows-msvc"):
203
+ return f"rusty_v8_{artifact_profile}_{target}.lib.gz"
204
+ return f"librusty_v8_{artifact_profile}_{target}.a.gz"
205
+
206
+
207
+ def staged_binding_name(target: str, artifact_profile: str) -> str:
208
+ return f"src_binding_{artifact_profile}_{target}.rs"
209
+
210
+
211
+ def staged_checksums_name(target: str, artifact_profile: str) -> str:
212
+ return f"rusty_v8_{artifact_profile}_{target}.sha256"
213
+
214
+
215
+ def stage_artifacts(
216
+ target: str,
217
+ lib_path: Path,
218
+ binding_path: Path,
219
+ output_dir: Path,
220
+ sandbox: bool,
221
+ ) -> None:
222
+ missing_paths = [
223
+ str(path) for path in [lib_path, binding_path] if not path.exists()
224
+ ]
225
+ if missing_paths:
226
+ raise SystemExit(f"missing release outputs for {target}: {missing_paths}")
227
+
228
+ output_dir.mkdir(parents=True, exist_ok=True)
229
+ artifact_profile = SANDBOX_ARTIFACT_PROFILE if sandbox else RELEASE_ARTIFACT_PROFILE
230
+ staged_library = output_dir / staged_archive_name(
231
+ target, lib_path, artifact_profile
232
+ )
233
+ staged_binding = output_dir / staged_binding_name(target, artifact_profile)
234
+
235
+ with lib_path.open("rb") as src, staged_library.open("wb") as dst:
236
+ with gzip.GzipFile(
237
+ filename="",
238
+ mode="wb",
239
+ fileobj=dst,
240
+ compresslevel=6,
241
+ mtime=0,
242
+ ) as gz:
243
+ shutil.copyfileobj(src, gz)
244
+
245
+ shutil.copyfile(binding_path, staged_binding)
246
+
247
+ staged_checksums = output_dir / staged_checksums_name(target, artifact_profile)
248
+ with staged_checksums.open("w", encoding="utf-8", newline="\n") as checksums:
249
+ for path in [staged_library, staged_binding]:
250
+ digest = hashlib.sha256()
251
+ with path.open("rb") as artifact:
252
+ for chunk in iter(lambda: artifact.read(1024 * 1024), b""):
253
+ digest.update(chunk)
254
+ checksums.write(f"{digest.hexdigest()} {path.name}\n")
255
+
256
+ print(staged_library)
257
+ print(staged_binding)
258
+ print(staged_checksums)
259
+
260
+
261
+ def upstream_release_pair_paths(
262
+ target: str,
263
+ target_dir: Path,
264
+ ) -> tuple[Path, Path]:
265
+ lib_name = (
266
+ "rusty_v8.lib" if target.endswith("-pc-windows-msvc") else "librusty_v8.a"
267
+ )
268
+ gn_out = target_dir / target / "release" / "gn_out"
269
+ return gn_out / "obj" / lib_name, gn_out / "src_binding.rs"
270
+
271
+
272
+ def stage_upstream_release_pair(
273
+ target: str,
274
+ output_dir: Path,
275
+ target_dir: Path,
276
+ sandbox: bool = False,
277
+ ) -> None:
278
+ lib_path, binding_path = upstream_release_pair_paths(
279
+ target,
280
+ target_dir,
281
+ )
282
+ stage_artifacts(target, lib_path, binding_path, output_dir, sandbox)
283
+
284
+
285
+ def stage_release_pair(
286
+ platform: str,
287
+ target: str,
288
+ output_dir: Path,
289
+ compilation_mode: str = "fastbuild",
290
+ bazel_configs: list[str] | None = None,
291
+ sandbox: bool = False,
292
+ ) -> None:
293
+ bazel_configs = artifact_bazel_configs(bazel_configs)
294
+ outputs = ensure_bazel_output_files(
295
+ platform,
296
+ [release_pair_label(target, sandbox)],
297
+ compilation_mode,
298
+ bazel_configs,
299
+ )
300
+
301
+ try:
302
+ lib_path = next(path for path in outputs if path.suffix in {".a", ".lib"})
303
+ except StopIteration as exc:
304
+ raise SystemExit(f"missing static library output for {target}") from exc
305
+
306
+ try:
307
+ binding_path = next(path for path in outputs if path.suffix == ".rs")
308
+ except StopIteration as exc:
309
+ raise SystemExit(f"missing Rust binding output for {target}") from exc
310
+
311
+ stage_artifacts(target, lib_path, binding_path, output_dir, sandbox)
312
+
313
+
314
+ def parse_args() -> argparse.Namespace:
315
+ parser = argparse.ArgumentParser()
316
+ subparsers = parser.add_subparsers(dest="command", required=True)
317
+
318
+ stage_release_pair_parser = subparsers.add_parser("stage-release-pair")
319
+ stage_release_pair_parser.add_argument("--platform", required=True)
320
+ stage_release_pair_parser.add_argument("--target", required=True)
321
+ stage_release_pair_parser.add_argument("--output-dir", required=True)
322
+ stage_release_pair_parser.add_argument("--sandbox", action="store_true")
323
+ stage_release_pair_parser.add_argument(
324
+ "--bazel-config",
325
+ action="append",
326
+ default=[],
327
+ dest="bazel_configs",
328
+ )
329
+ stage_release_pair_parser.add_argument(
330
+ "--compilation-mode",
331
+ default="fastbuild",
332
+ choices=["fastbuild", "opt", "dbg"],
333
+ )
334
+
335
+ stage_upstream_release_pair_parser = subparsers.add_parser(
336
+ "stage-upstream-release-pair"
337
+ )
338
+ stage_upstream_release_pair_parser.add_argument(
339
+ "--target-dir", type=Path, required=True
340
+ )
341
+ stage_upstream_release_pair_parser.add_argument("--target", required=True)
342
+ stage_upstream_release_pair_parser.add_argument("--output-dir", required=True)
343
+ stage_upstream_release_pair_parser.add_argument("--sandbox", action="store_true")
344
+
345
+ subparsers.add_parser("resolved-v8-crate-version")
346
+
347
+ check_module_bazel_parser = subparsers.add_parser("check-module-bazel")
348
+ check_module_bazel_parser.add_argument("--version")
349
+ check_module_bazel_parser.add_argument("--manifest", type=Path)
350
+ check_module_bazel_parser.add_argument(
351
+ "--module-bazel",
352
+ type=Path,
353
+ default=MODULE_BAZEL,
354
+ )
355
+
356
+ update_module_bazel_parser = subparsers.add_parser("update-module-bazel")
357
+ update_module_bazel_parser.add_argument("--version")
358
+ update_module_bazel_parser.add_argument("--manifest", type=Path)
359
+ update_module_bazel_parser.add_argument(
360
+ "--module-bazel",
361
+ type=Path,
362
+ default=MODULE_BAZEL,
363
+ )
364
+
365
+ return parser.parse_args()
366
+
367
+
368
+ def main() -> int:
369
+ args = parse_args()
370
+ if args.command == "stage-release-pair":
371
+ stage_release_pair(
372
+ platform=args.platform,
373
+ target=args.target,
374
+ output_dir=Path(args.output_dir),
375
+ compilation_mode=args.compilation_mode,
376
+ bazel_configs=args.bazel_configs,
377
+ sandbox=args.sandbox,
378
+ )
379
+ return 0
380
+ if args.command == "stage-upstream-release-pair":
381
+ stage_upstream_release_pair(
382
+ target=args.target,
383
+ output_dir=Path(args.output_dir),
384
+ target_dir=args.target_dir,
385
+ sandbox=args.sandbox,
386
+ )
387
+ return 0
388
+ if args.command == "resolved-v8-crate-version":
389
+ print(resolved_v8_crate_version())
390
+ return 0
391
+ if args.command == "check-module-bazel":
392
+ version = command_version(args.version)
393
+ manifest_path = command_manifest_path(args.manifest, version)
394
+ try:
395
+ check_module_bazel(args.module_bazel, manifest_path, version)
396
+ except RustyV8ChecksumError as exc:
397
+ raise SystemExit(str(exc)) from exc
398
+ return 0
399
+ if args.command == "update-module-bazel":
400
+ version = command_version(args.version)
401
+ manifest_path = command_manifest_path(args.manifest, version)
402
+ try:
403
+ update_module_bazel(args.module_bazel, manifest_path, version)
404
+ except RustyV8ChecksumError as exc:
405
+ raise SystemExit(str(exc)) from exc
406
+ return 0
407
+ raise SystemExit(f"unsupported command: {args.command}")
408
+
409
+
410
+ if __name__ == "__main__":
411
+ sys.exit(main())
.github/scripts/rusty_v8_module_bazel.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+
10
+ SHA256_RE = re.compile(r"[0-9a-f]{64}")
11
+ HTTP_FILE_BLOCK_RE = re.compile(r"(?ms)^http_file\(\n.*?^\)\n?")
12
+ HTTP_FILE_VERSION_RE = re.compile(r"^rusty_v8_([0-9]+)_([0-9]+)_([0-9]+)_")
13
+
14
+
15
+ class RustyV8ChecksumError(ValueError):
16
+ pass
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class RustyV8HttpFile:
21
+ start: int
22
+ end: int
23
+ block: str
24
+ name: str
25
+ downloaded_file_path: str
26
+ sha256: str | None
27
+
28
+
29
+ def parse_checksum_manifest(path: Path) -> dict[str, str]:
30
+ try:
31
+ lines = path.read_text(encoding="utf-8").splitlines()
32
+ except FileNotFoundError as exc:
33
+ raise RustyV8ChecksumError(f"missing checksum manifest: {path}") from exc
34
+
35
+ checksums: dict[str, str] = {}
36
+ for line_number, line in enumerate(lines, 1):
37
+ if not line.strip():
38
+ continue
39
+ parts = line.split()
40
+ if len(parts) != 2:
41
+ raise RustyV8ChecksumError(
42
+ f"{path}:{line_number}: expected '<sha256> <filename>'"
43
+ )
44
+ checksum, filename = parts
45
+ if not SHA256_RE.fullmatch(checksum):
46
+ raise RustyV8ChecksumError(
47
+ f"{path}:{line_number}: invalid SHA-256 digest for {filename}"
48
+ )
49
+ if not filename or filename in {".", ".."} or "/" in filename:
50
+ raise RustyV8ChecksumError(
51
+ f"{path}:{line_number}: expected a bare artifact filename"
52
+ )
53
+ if filename in checksums:
54
+ raise RustyV8ChecksumError(
55
+ f"{path}:{line_number}: duplicate checksum for {filename}"
56
+ )
57
+ checksums[filename] = checksum
58
+
59
+ if not checksums:
60
+ raise RustyV8ChecksumError(f"empty checksum manifest: {path}")
61
+ return checksums
62
+
63
+
64
+ def string_field(block: str, field: str) -> str | None:
65
+ # Matches one-line string fields inside http_file blocks, e.g. `sha256 = "...",`.
66
+ match = re.search(rf'^\s*{re.escape(field)}\s*=\s*"([^"]+)",\s*$', block, re.M)
67
+ if match:
68
+ return match.group(1)
69
+ return None
70
+
71
+
72
+ def rusty_v8_http_files(module_bazel: str, version: str) -> list[RustyV8HttpFile]:
73
+ version_slug = version.replace(".", "_")
74
+ name_prefix = f"rusty_v8_{version_slug}_"
75
+ entries = []
76
+ for match in HTTP_FILE_BLOCK_RE.finditer(module_bazel):
77
+ block = match.group(0)
78
+ name = string_field(block, "name")
79
+ if not name or not name.startswith(name_prefix):
80
+ continue
81
+ downloaded_file_path = string_field(block, "downloaded_file_path")
82
+ if not downloaded_file_path:
83
+ raise RustyV8ChecksumError(
84
+ f"MODULE.bazel {name} is missing downloaded_file_path"
85
+ )
86
+ entries.append(
87
+ RustyV8HttpFile(
88
+ start=match.start(),
89
+ end=match.end(),
90
+ block=block,
91
+ name=name,
92
+ downloaded_file_path=downloaded_file_path,
93
+ sha256=string_field(block, "sha256"),
94
+ )
95
+ )
96
+ return entries
97
+
98
+
99
+ def rusty_v8_http_file_versions(module_bazel: str) -> list[str]:
100
+ versions = set()
101
+ for match in HTTP_FILE_BLOCK_RE.finditer(module_bazel):
102
+ name = string_field(match.group(0), "name")
103
+ if not name:
104
+ continue
105
+ version_match = HTTP_FILE_VERSION_RE.match(name)
106
+ if version_match:
107
+ versions.add(".".join(version_match.groups()))
108
+ return sorted(versions)
109
+
110
+
111
+ def module_entry_set_errors(
112
+ entries: list[RustyV8HttpFile],
113
+ checksums: dict[str, str],
114
+ version: str,
115
+ ) -> list[str]:
116
+ errors = []
117
+ if not entries:
118
+ errors.append(f"MODULE.bazel has no rusty_v8 http_file entries for {version}")
119
+ return errors
120
+
121
+ module_files: dict[str, RustyV8HttpFile] = {}
122
+ duplicate_files = set()
123
+ for entry in entries:
124
+ if entry.downloaded_file_path in module_files:
125
+ duplicate_files.add(entry.downloaded_file_path)
126
+ module_files[entry.downloaded_file_path] = entry
127
+
128
+ for filename in sorted(duplicate_files):
129
+ errors.append(f"MODULE.bazel has duplicate http_file entries for {filename}")
130
+
131
+ for filename in sorted(set(module_files) - set(checksums)):
132
+ entry = module_files[filename]
133
+ errors.append(f"MODULE.bazel {entry.name} has no checksum in the manifest")
134
+
135
+ for filename in sorted(set(checksums) - set(module_files)):
136
+ errors.append(f"manifest has {filename}, but MODULE.bazel has no http_file")
137
+
138
+ return errors
139
+
140
+
141
+ def module_checksum_errors(
142
+ entries: list[RustyV8HttpFile],
143
+ checksums: dict[str, str],
144
+ ) -> list[str]:
145
+ errors = []
146
+ for entry in entries:
147
+ expected = checksums.get(entry.downloaded_file_path)
148
+ if expected is None:
149
+ continue
150
+ if entry.sha256 is None:
151
+ errors.append(f"MODULE.bazel {entry.name} is missing sha256")
152
+ elif entry.sha256 != expected:
153
+ errors.append(
154
+ f"MODULE.bazel {entry.name} has sha256 {entry.sha256}, "
155
+ f"expected {expected}"
156
+ )
157
+ return errors
158
+
159
+
160
+ def raise_checksum_errors(message: str, errors: list[str]) -> None:
161
+ if errors:
162
+ formatted_errors = "\n".join(f"- {error}" for error in errors)
163
+ raise RustyV8ChecksumError(f"{message}:\n{formatted_errors}")
164
+
165
+
166
+ def check_module_bazel_text(
167
+ module_bazel: str,
168
+ checksums: dict[str, str],
169
+ version: str,
170
+ ) -> None:
171
+ entries = rusty_v8_http_files(module_bazel, version)
172
+ errors = [
173
+ *module_entry_set_errors(entries, checksums, version),
174
+ *module_checksum_errors(entries, checksums),
175
+ ]
176
+ raise_checksum_errors("rusty_v8 MODULE.bazel checksum drift", errors)
177
+
178
+
179
+ def block_with_sha256(block: str, checksum: str) -> str:
180
+ sha256_line_re = re.compile(r'(?m)^(\s*)sha256\s*=\s*"[0-9a-f]+",\s*$')
181
+ if sha256_line_re.search(block):
182
+ return sha256_line_re.sub(
183
+ lambda match: f'{match.group(1)}sha256 = "{checksum}",',
184
+ block,
185
+ count=1,
186
+ )
187
+
188
+ downloaded_file_path_match = re.search(
189
+ r'(?m)^(\s*)downloaded_file_path\s*=\s*"[^"]+",\n',
190
+ block,
191
+ )
192
+ if not downloaded_file_path_match:
193
+ raise RustyV8ChecksumError("http_file block is missing downloaded_file_path")
194
+ insert_at = downloaded_file_path_match.end()
195
+ indent = downloaded_file_path_match.group(1)
196
+ return f'{block[:insert_at]}{indent}sha256 = "{checksum}",\n{block[insert_at:]}'
197
+
198
+
199
+ def update_module_bazel_text(
200
+ module_bazel: str,
201
+ checksums: dict[str, str],
202
+ version: str,
203
+ ) -> str:
204
+ entries = rusty_v8_http_files(module_bazel, version)
205
+ errors = module_entry_set_errors(entries, checksums, version)
206
+ raise_checksum_errors("cannot update rusty_v8 MODULE.bazel checksums", errors)
207
+
208
+ updated = []
209
+ previous_end = 0
210
+ for entry in entries:
211
+ updated.append(module_bazel[previous_end : entry.start])
212
+ updated.append(
213
+ block_with_sha256(entry.block, checksums[entry.downloaded_file_path])
214
+ )
215
+ previous_end = entry.end
216
+ updated.append(module_bazel[previous_end:])
217
+ return "".join(updated)
218
+
219
+
220
+ def check_module_bazel(
221
+ module_bazel_path: Path,
222
+ manifest_path: Path,
223
+ version: str,
224
+ ) -> None:
225
+ checksums = parse_checksum_manifest(manifest_path)
226
+ module_bazel = module_bazel_path.read_text(encoding="utf-8")
227
+ check_module_bazel_text(module_bazel, checksums, version)
228
+ print(f"{module_bazel_path} rusty_v8 {version} checksums match {manifest_path}")
229
+
230
+
231
+ def update_module_bazel(
232
+ module_bazel_path: Path,
233
+ manifest_path: Path,
234
+ version: str,
235
+ ) -> None:
236
+ checksums = parse_checksum_manifest(manifest_path)
237
+ module_bazel = module_bazel_path.read_text(encoding="utf-8")
238
+ updated_module_bazel = update_module_bazel_text(module_bazel, checksums, version)
239
+ if updated_module_bazel == module_bazel:
240
+ print(f"{module_bazel_path} rusty_v8 {version} checksums are already current")
241
+ return
242
+ module_bazel_path.write_text(updated_module_bazel, encoding="utf-8")
243
+ print(f"updated {module_bazel_path} rusty_v8 {version} checksums")
.github/scripts/setup-dev-drive.ps1 ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Configure a fast drive for Windows CI jobs.
2
+ #
3
+ # GitHub-hosted Windows runners do not always expose a secondary D: volume. When
4
+ # they do not, create a Dev Drive VHD. CI depends on this path for its
5
+ # build directories where CI spends significant time doing I/O, so fail the
6
+ # job if no real Dev Drive is available.
7
+
8
+ function Test-DevDrive {
9
+ param([string]$Drive)
10
+
11
+ & fsutil devdrv query $Drive *> $null
12
+ return $LASTEXITCODE -eq 0
13
+ }
14
+
15
+ function Invoke-BestEffort {
16
+ param([scriptblock]$Script, [string]$Description)
17
+
18
+ try {
19
+ & $Script
20
+ } catch {
21
+ Write-Warning "$Description failed: $($_.Exception.Message)"
22
+ }
23
+ }
24
+
25
+ if ((Test-Path "D:\") -and (Test-DevDrive "D:")) {
26
+ Write-Output "Using existing Dev Drive at D:"
27
+ $Drive = "D:"
28
+ } else {
29
+ if (Test-Path "D:\") {
30
+ Write-Output "Existing D: volume is not a Dev Drive; provisioning a new Dev Drive VHD."
31
+ }
32
+
33
+ try {
34
+ $VhdPath = Join-Path $env:RUNNER_TEMP "codex-dev-drive.vhdx"
35
+ $SizeBytes = 64GB
36
+
37
+ if (Test-Path $VhdPath) {
38
+ Remove-Item -Path $VhdPath -Force
39
+ }
40
+
41
+ New-VHD -Path $VhdPath -SizeBytes $SizeBytes -Dynamic -ErrorAction Stop | Out-Null
42
+ $Mounted = Mount-VHD -Path $VhdPath -Passthru -ErrorAction Stop
43
+ $Disk = $Mounted | Get-Disk -ErrorAction Stop
44
+ $Disk | Initialize-Disk -PartitionStyle GPT -ErrorAction Stop
45
+ $Partition = $Disk | New-Partition -AssignDriveLetter -UseMaximumSize -ErrorAction Stop
46
+ $Volume = $Partition | Format-Volume -FileSystem ReFS -NewFileSystemLabel "CodexDevDrive" -DevDrive -Confirm:$false -Force -ErrorAction Stop
47
+
48
+ $Drive = "$($Volume.DriveLetter):"
49
+
50
+ if (-not (Test-DevDrive $Drive)) {
51
+ throw "Provisioned volume at $Drive did not pass Dev Drive verification."
52
+ }
53
+
54
+ Invoke-BestEffort { fsutil devdrv trust $Drive } "Trusting Dev Drive $Drive"
55
+ Invoke-BestEffort { fsutil devdrv enable /disallowAv } "Disabling AV filter attachment for Dev Drives"
56
+
57
+ Write-Output "Using Dev Drive at $Drive"
58
+ } catch {
59
+ throw "Failed to create Dev Drive: $($_.Exception.Message)"
60
+ }
61
+ }
62
+
63
+ "CI_BUILD_ROOT=$Drive" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
.github/scripts/setup-voice-windows.ps1 ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CI-only prerequisites. Never install these tools into a Codex package.
2
+ param(
3
+ [Parameter(Mandatory = $true)][string]$Target,
4
+ [Parameter(Mandatory = $true)][string]$SnapshotArchive
5
+ )
6
+
7
+ $ErrorActionPreference = "Stop"
8
+ $PkgHashes = @{
9
+ "x86_64-pc-windows-msvc" = @("x64", "5604cf25ef38bb6a09520cff25ae9f0ecd8c2443053b15e40df4ad1eae0e4405")
10
+ "aarch64-pc-windows-msvc" = @("arm64", "d5752ce2ac2296c8abb91fb12c12e75ee99ba8a18b52ce5febf325847b6f062b")
11
+ }
12
+ if (-not $PkgHashes.ContainsKey($Target)) { throw "Unsupported target: $Target" }
13
+ $Root = Join-Path $env:RUNNER_TEMP "voice-windows-build-tools"
14
+ $Evidence = Join-Path $Root "evidence"
15
+ $Cygwin = Join-Path $Root "cygwin"
16
+ $Cache = Join-Path $Root "cache"
17
+ New-Item -ItemType Directory -Path $Root, $Evidence | Out-Null
18
+ $ManifestPath = Join-Path $PSScriptRoot "voice-cygwin-snapshot.json"
19
+ $Manifest = Get-Content -Raw $ManifestPath | ConvertFrom-Json
20
+ $SnapshotTool = Join-Path $PSScriptRoot "voice-cygwin-inputs.py"
21
+ & python $SnapshotTool extract --archive $SnapshotArchive --directory (Join-Path $Cache $Manifest.cacheDirectory)
22
+ if ($LASTEXITCODE -ne 0) { throw "Cygwin snapshot could not be verified and extracted" }
23
+ Copy-Item $ManifestPath (Join-Path $Evidence "cygwin-snapshot.json")
24
+
25
+ # Official https://cygwin.com/setup/sha512.sum and pkgconf release asset digests.
26
+ # The complete package closure is pinned separately in cygwin-snapshot.json.
27
+ $Inputs = @(
28
+ @{
29
+ url = "https://cygwin.com/setup/setup-2.937.x86_64.exe"
30
+ file = "setup.exe"
31
+ algorithm = "SHA512"
32
+ digest = "6acea47c59781c9e7f544a18d53935d59df6e44d5d52ac95ee165671b8e388820455eebf30ccc7d254b00c3d0eb694269a0f8dc84b17944c1f355dac9c5aafcc"
33
+ },
34
+ @{
35
+ url = "https://github.com/pkgconf/pkgconf/releases/download/pkgconf-3.0.6/pkgconf-$($PkgHashes[$Target][0])-3.0.6.msi"
36
+ file = "pkgconf.msi"
37
+ algorithm = "SHA256"
38
+ digest = $PkgHashes[$Target][1]
39
+ }
40
+ )
41
+ foreach ($InputFile in $Inputs) {
42
+ $Destination = Join-Path $Root $InputFile.file
43
+ Invoke-WebRequest -Uri $InputFile.url -OutFile $Destination
44
+ $Actual = (Get-FileHash -Path $Destination -Algorithm $InputFile.algorithm).Hash.ToLowerInvariant()
45
+ if ($Actual -ne $InputFile.digest) { throw "Build input digest mismatch: $($InputFile.file)" }
46
+ }
47
+ $Inputs | ConvertTo-Json | Set-Content (Join-Path $Evidence "bootstrap-inputs.json")
48
+ $Packages = $Manifest.requestedPackages -join ","
49
+ # Only setup.xz + setup.xz.sig are present: local mode still verifies that
50
+ # signature. Never substitute an unsigned setup.ini or enable a network fallback.
51
+ $Setup = Start-Process -FilePath (Join-Path $Root "setup.exe") -Wait -PassThru -ArgumentList @(
52
+ "--quiet-mode", "--no-admin", "--no-shortcuts", "--only-site",
53
+ "--local-install", "--no-version-check",
54
+ "--root", "`"$Cygwin`"", "--local-package-dir", "`"$Cache`"",
55
+ "--packages", $Packages
56
+ )
57
+ if ($Setup.ExitCode -ne 0) { throw "Cygwin setup failed: $($Setup.ExitCode)" }
58
+ Copy-Item (Join-Path $Cygwin "var/log/setup.log*") $Evidence
59
+ Copy-Item (Join-Path $Cygwin "etc/setup/installed.db") $Evidence
60
+ & (Join-Path $Cygwin "bin/cygcheck.exe") -cd | Set-Content (Join-Path $Evidence "packages.txt")
61
+ if ($LASTEXITCODE -ne 0) { throw "Cannot inventory Cygwin packages" }
62
+ & python $SnapshotTool check-installed --inventory (Join-Path $Evidence "packages.txt")
63
+ if ($LASTEXITCODE -ne 0) { throw "Installed Cygwin package set differs from the snapshot" }
64
+ Get-ChildItem $Cache -File -Recurse | ForEach-Object {
65
+ @{ file = [IO.Path]::GetRelativePath($Cache, $_.FullName); bytes = $_.Length;
66
+ sha512 = (Get-FileHash $_.FullName -Algorithm SHA512).Hash.ToLowerInvariant() }
67
+ } | ConvertTo-Json | Set-Content (Join-Path $Evidence "package-cache.json")
68
+
69
+ # Administrative extraction uses TARGETDIR, not a normal MSI installation:
70
+ # no product registration or system PATH modification. Fail rather than fall
71
+ # back to an ambient executable or ordinary installer if the payload is absent.
72
+ $Image = Join-Path $Root "pkgconf-image"
73
+ $Extract = Start-Process msiexec.exe -Wait -PassThru -ArgumentList @(
74
+ "/a", "`"$(Join-Path $Root 'pkgconf.msi')`"", "/qn", "/norestart",
75
+ "TARGETDIR=`"$Image`"", "/L*v", "`"$(Join-Path $Evidence 'pkgconf-extract.log')`""
76
+ )
77
+ if ($Extract.ExitCode -ne 0) { throw "pkgconf extraction failed: $($Extract.ExitCode)" }
78
+ $Candidates = @(Get-ChildItem $Image -Filter pkgconf.exe -File -Recurse)
79
+ if ($Candidates.Count -ne 1) { throw "Expected exactly one native pkgconf executable" }
80
+ # Bazel shell-quotes execpaths containing spaces; Cargo executes PKG_CONFIG
81
+ # directly. Keep the executable and its support files at a space-free execpath.
82
+ $Native = Join-Path $Image "native"
83
+ if (Test-Path $Native) { throw "Native pkgconf destination must be fresh" }
84
+ Move-Item -LiteralPath $Candidates[0].Directory.FullName -Destination $Native
85
+ $PkgConfig = Join-Path $Native "pkgconf.exe"
86
+ $Version = & $PkgConfig --version
87
+ if ($LASTEXITCODE -ne 0 -or $Version -ne "3.0.6") { throw "Unexpected native pkgconf version" }
88
+ @{ target = $Target; cygwin_root = $Cygwin; pkg_config = $PkgConfig;
89
+ pkgconf_version = $Version; requested_packages = $Packages } |
90
+ ConvertTo-Json | Set-Content (Join-Path $Evidence "tools.json")
91
+ # Declare complete installed support trees only after snapshot and tool checks.
92
+ & python (Join-Path $PSScriptRoot "voice_windows_tools.py") --root $Root --target $Target --pkg-config $PkgConfig
93
+ if ($LASTEXITCODE -ne 0) { throw "Cannot declare Windows Bazel tool inputs" }
94
+ "VOICE_WINDOWS_BAZEL_REPOSITORY=$Root" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append
95
+ "VOICE_CYGWIN_ROOT=$Cygwin" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append
96
+ "VOICE_PKG_CONFIG=$PkgConfig" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append
.github/scripts/test_macos_signing_entitlements.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import plistlib
2
+ import unittest
3
+ from pathlib import Path
4
+
5
+
6
+ ROOT = Path(__file__).resolve().parents[2]
7
+ SIGNING_DIR = ROOT / ".github" / "scripts" / "macos-signing"
8
+ ALLOW_JIT = "com.apple.security.cs.allow-jit"
9
+ ALLOW_UNSIGNED_EXECUTABLE_MEMORY = (
10
+ "com.apple.security.cs.allow-unsigned-executable-memory"
11
+ )
12
+
13
+
14
+ class MacosSigningEntitlementsTest(unittest.TestCase):
15
+ def load(self, binary: str) -> dict[str, bool]:
16
+ path = SIGNING_DIR / f"{binary}.entitlements.plist"
17
+ with path.open("rb") as file:
18
+ return plistlib.load(file)
19
+
20
+ def test_v8_binaries_allow_unsigned_executable_memory(self) -> None:
21
+ expected = {
22
+ ALLOW_JIT: True,
23
+ ALLOW_UNSIGNED_EXECUTABLE_MEMORY: True,
24
+ }
25
+ for binary in ["codex", "codex-app-server", "codex-code-mode-host"]:
26
+ with self.subTest(binary=binary):
27
+ self.assertEqual(self.load(binary), expected)
28
+
29
+ def test_responses_proxy_keeps_existing_entitlements(self) -> None:
30
+ self.assertEqual(
31
+ self.load("codex-responses-api-proxy"),
32
+ {ALLOW_JIT: True},
33
+ )
34
+
35
+
36
+ if __name__ == "__main__":
37
+ unittest.main()
.github/scripts/test_resolve_python_cli_release.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import json
3
+ import tempfile
4
+ import unittest
5
+ from pathlib import Path
6
+ from unittest.mock import patch
7
+
8
+ import resolve_python_cli_release as resolver
9
+
10
+
11
+ class ResolvePythonCliReleaseTest(unittest.TestCase):
12
+ def setUp(self) -> None:
13
+ self.revision = "a" * 40
14
+ self.run = {
15
+ "id": 123,
16
+ "path": ".github/workflows/rust-release.yml",
17
+ "event": "push",
18
+ "status": "completed",
19
+ "conclusion": "success",
20
+ "head_repository": {"full_name": "openai/codex"},
21
+ "head_branch": "rust-v1.2.3",
22
+ "head_sha": self.revision,
23
+ }
24
+ self.job = {
25
+ "id": 456,
26
+ "run_attempt": 1,
27
+ "name": "release",
28
+ "conclusion": "success",
29
+ }
30
+ self.commit = {"type": "commit", "sha": self.revision}
31
+ self.release = {
32
+ "id": 789,
33
+ "draft": False,
34
+ "prerelease": False,
35
+ "tag_name": "rust-v1.2.3",
36
+ }
37
+ self.assets = [
38
+ {"name": name, "state": "uploaded", "size": 100}
39
+ for name in [
40
+ "openai_codex_cli_bin-1.2.3-py3-none-macosx_10_9_x86_64.whl",
41
+ "openai_codex_cli_bin-1.2.3-py3-none-macosx_11_0_arm64.whl",
42
+ "openai_codex_cli_bin-1.2.3-py3-none-manylinux_2_17_aarch64.whl",
43
+ "openai_codex_cli_bin-1.2.3-py3-none-manylinux_2_17_x86_64.whl",
44
+ "openai_codex_cli_bin-1.2.3-py3-none-win_amd64.whl",
45
+ "openai_codex_cli_bin-1.2.3-py3-none-win_arm64.whl",
46
+ "codex-package-aarch64-unknown-linux-musl.tar.gz",
47
+ "codex-package-x86_64-unknown-linux-musl.tar.gz",
48
+ ]
49
+ ]
50
+ self.expected = {
51
+ "source_sha": self.revision,
52
+ "release_tag": "rust-v1.2.3",
53
+ "version": "1.2.3",
54
+ }
55
+
56
+ def responses(self) -> list:
57
+ return copy.deepcopy(
58
+ [
59
+ self.run,
60
+ {"jobs": [self.job]},
61
+ {"object": self.commit},
62
+ self.release,
63
+ self.assets,
64
+ ]
65
+ )
66
+
67
+ def test_resolves_lightweight_and_annotated_tags_to_the_run_revision(self) -> None:
68
+ for annotated in (False, True):
69
+ with self.subTest(annotated=annotated):
70
+ responses = self.responses()
71
+ if annotated:
72
+ responses.insert(2, {"object": {"type": "tag", "sha": "b" * 40}})
73
+ with patch.object(resolver, "github_api", side_effect=responses):
74
+ self.assertEqual(
75
+ resolver.resolve_release("openai/codex", "123"), self.expected
76
+ )
77
+
78
+ def test_skips_cli_prereleases_before_reading_jobs_or_assets(self) -> None:
79
+ for suffix in ("-alpha", "-alpha.1", "-alpha.1.2", "-beta", "-beta.1"):
80
+ with self.subTest(suffix=suffix):
81
+ run = {**self.run, "head_branch": f"rust-v1.2.3{suffix}"}
82
+ with patch.object(resolver, "github_api", return_value=run) as api:
83
+ self.assertIsNone(resolver.resolve_release("openai/codex", "123"))
84
+ api.assert_called_once_with("repos/openai/codex/actions/runs/123")
85
+
86
+ def test_rejects_unrelated_or_incomplete_runs(self) -> None:
87
+ for field, value in (
88
+ ("id", 999),
89
+ ("path", ".github/workflows/sdk.yml"),
90
+ ("event", "pull_request"),
91
+ ("status", "in_progress"),
92
+ ("head_branch", "main"),
93
+ ("head_sha", "main"),
94
+ ("head_repository", {"full_name": "other/codex"}),
95
+ ):
96
+ with self.subTest(field=field, value=value):
97
+ with patch.object(
98
+ resolver, "github_api", return_value={**self.run, field: value}
99
+ ):
100
+ with self.assertRaises(ValueError):
101
+ resolver.resolve_release("openai/codex", "123")
102
+
103
+ def test_accepts_failed_ancillary_publisher_and_its_partial_rerun(self) -> None:
104
+ for overall in ("failure", "cancelled"):
105
+ with self.subTest(overall=overall):
106
+ responses = self.responses()
107
+ responses[0]["conclusion"] = overall
108
+ responses[1]["jobs"].extend(
109
+ [
110
+ {
111
+ "id": 999,
112
+ "run_attempt": 2,
113
+ "name": "publish-winget",
114
+ "conclusion": "failure",
115
+ },
116
+ {
117
+ **self.job,
118
+ "id": 998,
119
+ "run_attempt": 2,
120
+ "conclusion": "skipped",
121
+ },
122
+ ]
123
+ )
124
+ with patch.object(resolver, "github_api", side_effect=responses):
125
+ self.assertEqual(
126
+ resolver.resolve_release("openai/codex", "123"), self.expected
127
+ )
128
+
129
+ def test_requires_success_from_latest_executed_release_job(self) -> None:
130
+ for jobs in (
131
+ [],
132
+ [{**self.job, "conclusion": "skipped"}],
133
+ [{**self.job, "conclusion": "failure"}],
134
+ [{**self.job, "conclusion": "cancelled"}],
135
+ [
136
+ {**self.job, "id": 999, "run_attempt": 2, "conclusion": "failure"},
137
+ self.job,
138
+ ],
139
+ ):
140
+ with self.subTest(jobs=jobs):
141
+ with patch.object(
142
+ resolver, "github_api", side_effect=[self.run, {"jobs": jobs}]
143
+ ):
144
+ with self.assertRaisesRegex(ValueError, "successful release job"):
145
+ resolver.resolve_release("openai/codex", "123")
146
+
147
+ def test_reads_all_pages_of_jobs_and_assets(self) -> None:
148
+ responses = self.responses()
149
+ responses.insert(1, {"jobs": [{**self.job, "name": "other"}] * 100})
150
+ responses.insert(-1, [{"name": "other", "state": "uploaded", "size": 1}] * 100)
151
+ with patch.object(resolver, "github_api", side_effect=responses) as api:
152
+ self.assertEqual(
153
+ resolver.resolve_release("openai/codex", "123"), self.expected
154
+ )
155
+ api.assert_any_call(
156
+ "repos/openai/codex/actions/runs/123/jobs?filter=all&per_page=100&page=2"
157
+ )
158
+ api.assert_any_call(
159
+ "repos/openai/codex/releases/789/assets?per_page=100&page=2"
160
+ )
161
+
162
+ def test_rejects_a_moved_tag(self) -> None:
163
+ responses = self.responses()
164
+ responses[2] = {"object": {"type": "commit", "sha": "b" * 40}}
165
+ with patch.object(resolver, "github_api", side_effect=responses):
166
+ with self.assertRaisesRegex(ValueError, "no longer matches"):
167
+ resolver.resolve_release("openai/codex", "123")
168
+
169
+ def test_bounds_annotated_tag_resolution(self) -> None:
170
+ tag = {"object": {"type": "tag", "sha": "b" * 40}}
171
+ with patch.object(
172
+ resolver,
173
+ "github_api",
174
+ side_effect=[self.run, {"jobs": [self.job]}, *([tag] * 9)],
175
+ ) as api:
176
+ with self.assertRaisesRegex(ValueError, "no longer matches"):
177
+ resolver.resolve_release("openai/codex", "123")
178
+ self.assertEqual(api.call_count, 11)
179
+
180
+ def test_requires_a_published_stable_release_for_the_tag(self) -> None:
181
+ for field, value in (
182
+ ("draft", True),
183
+ ("prerelease", True),
184
+ ("tag_name", "rust-v9.9.9"),
185
+ ):
186
+ responses = self.responses()
187
+ responses[3][field] = value
188
+ with self.subTest(field=field):
189
+ with patch.object(resolver, "github_api", side_effect=responses):
190
+ with self.assertRaisesRegex(ValueError, "published and stable"):
191
+ resolver.resolve_release("openai/codex", "123")
192
+
193
+ def test_requires_all_runtime_inputs_to_be_uploaded_and_nonempty(self) -> None:
194
+ for assets in (
195
+ self.assets[:-1],
196
+ [*self.assets[:-1], {**self.assets[-1], "size": 0}],
197
+ [*self.assets[:-1], {**self.assets[-1], "state": "new"}],
198
+ ):
199
+ with self.subTest(assets=assets):
200
+ responses = self.responses()
201
+ responses[-1] = assets
202
+ with patch.object(resolver, "github_api", side_effect=responses):
203
+ with self.assertRaisesRegex(ValueError, "missing Python inputs"):
204
+ resolver.resolve_release("openai/codex", "123")
205
+
206
+ def test_rejects_invalid_inputs_before_github_access(self) -> None:
207
+ with patch.object(resolver, "github_api") as api:
208
+ for repository, run_id in (
209
+ ("other/codex", "123"),
210
+ ("openai/codex", "../123"),
211
+ ):
212
+ with self.assertRaises(ValueError):
213
+ resolver.resolve_release(repository, run_id)
214
+ api.assert_not_called()
215
+
216
+ def test_event_and_manual_retry_emit_the_same_revision_and_version(self) -> None:
217
+ with tempfile.TemporaryDirectory() as directory:
218
+ output = Path(directory) / "output"
219
+ event = Path(directory) / "event.json"
220
+ for automatic in (True, False):
221
+ output.write_text("")
222
+ event.write_text(
223
+ json.dumps({"workflow_run": self.run} if automatic else {})
224
+ )
225
+ responses = self.responses()[1:] if automatic else self.responses()
226
+ with patch.object(resolver, "github_api", side_effect=responses) as api:
227
+ resolver.main(
228
+ [
229
+ "123",
230
+ "--repository",
231
+ "openai/codex",
232
+ "--event-path",
233
+ str(event),
234
+ "--github-output",
235
+ str(output),
236
+ ]
237
+ )
238
+ self.assertEqual(
239
+ output.read_text(),
240
+ f"publish=true\nsource_sha={self.revision}\nrelease_tag=rust-v1.2.3\nversion=1.2.3\n",
241
+ )
242
+ if automatic:
243
+ self.assertNotIn(
244
+ unittest.mock.call("repos/openai/codex/actions/runs/123"),
245
+ api.call_args_list,
246
+ )
247
+
248
+
249
+ if __name__ == "__main__":
250
+ unittest.main()
.github/scripts/test_run_bazel_with_buildbuddy.py ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ import unittest
8
+ from pathlib import Path
9
+ from tempfile import TemporaryDirectory
10
+
11
+ import run_bazel_with_buildbuddy
12
+
13
+
14
+ class RunBazelWithBuildBuddyTest(unittest.TestCase):
15
+ def github_env(
16
+ self,
17
+ temp_dir: str,
18
+ *,
19
+ repository: str = "openai/codex",
20
+ fork: bool = False,
21
+ event_name: str = "pull_request",
22
+ ) -> dict[str, str]:
23
+ event_path = Path(temp_dir) / "event.json"
24
+ event_path.write_text(
25
+ json.dumps({"pull_request": {"head": {"repo": {"fork": fork}}}}),
26
+ encoding="utf-8",
27
+ )
28
+ return {
29
+ "BUILDBUDDY_API_KEY": "token",
30
+ "GITHUB_ACTIONS": "true",
31
+ "GITHUB_EVENT_NAME": event_name,
32
+ "GITHUB_EVENT_PATH": str(event_path),
33
+ "GITHUB_REPOSITORY": repository,
34
+ }
35
+
36
+ def test_keyless_invocation_drops_remote_ci_configuration(self) -> None:
37
+ self.assertIsNone(
38
+ run_bazel_with_buildbuddy.remote_config(
39
+ ["build", "--config=ci-linux", "//codex-rs/cli:codex"],
40
+ {},
41
+ )
42
+ )
43
+ self.assertEqual(
44
+ run_bazel_with_buildbuddy.bazel_args_with_remote_config(
45
+ ["build", "--config=ci-linux", "--", "//codex-rs/cli:codex"],
46
+ {},
47
+ ),
48
+ ["build", "--", "//codex-rs/cli:codex"],
49
+ )
50
+
51
+ def test_program_arguments_after_separator_do_not_select_or_lose_rbe(self) -> None:
52
+ args = ["run", "//codex-rs/cli:codex", "--", "--config=remote"]
53
+
54
+ self.assertEqual(
55
+ run_bazel_with_buildbuddy.bazel_args_with_remote_config(args, {}),
56
+ args,
57
+ )
58
+ self.assertEqual(
59
+ run_bazel_with_buildbuddy.remote_config(
60
+ args, {"BUILDBUDDY_API_KEY": "fork-token"}
61
+ ),
62
+ "buildbuddy-generic",
63
+ )
64
+
65
+ def test_upstream_push_selects_openai_rbe_before_target_separator(self) -> None:
66
+ with TemporaryDirectory() as temp_dir:
67
+ env = self.github_env(temp_dir, event_name="push")
68
+
69
+ self.assertEqual(
70
+ run_bazel_with_buildbuddy.bazel_args_with_remote_config(
71
+ ["build", "--config=ci-linux", "--", "//codex-rs/cli:codex"],
72
+ env,
73
+ ),
74
+ [
75
+ "build",
76
+ "--config=buildbuddy-openai-rbe",
77
+ "--remote_header=x-buildbuddy-api-key=token",
78
+ "--config=ci-linux",
79
+ "--",
80
+ "//codex-rs/cli:codex",
81
+ ],
82
+ )
83
+
84
+ def test_windows_cross_ci_configuration_follows_remote_configuration(self) -> None:
85
+ env = {"BUILDBUDDY_API_KEY": "fork-token"}
86
+
87
+ self.assertEqual(
88
+ run_bazel_with_buildbuddy.bazel_args_with_remote_config(
89
+ ["build", "--config=ci-windows-cross", "//codex-rs/cli:codex"],
90
+ env,
91
+ ),
92
+ [
93
+ "build",
94
+ "--config=buildbuddy-generic-rbe",
95
+ "--remote_header=x-buildbuddy-api-key=fork-token",
96
+ "--config=ci-windows-cross",
97
+ "//codex-rs/cli:codex",
98
+ ],
99
+ )
100
+
101
+ def test_query_remote_configuration_is_inserted_before_expression(self) -> None:
102
+ expression = 'kind("rust_library rule", //codex-rs/...)'
103
+ env = {"BUILDBUDDY_API_KEY": "fork-token"}
104
+
105
+ for command in ("query", "cquery", "aquery"):
106
+ with self.subTest(command=command):
107
+ self.assertEqual(
108
+ run_bazel_with_buildbuddy.bazel_args_with_remote_config(
109
+ [
110
+ command,
111
+ "--config=ci-windows-cross",
112
+ "--output=label",
113
+ expression,
114
+ ],
115
+ env,
116
+ ),
117
+ [
118
+ command,
119
+ "--config=buildbuddy-generic-rbe",
120
+ "--remote_header=x-buildbuddy-api-key=fork-token",
121
+ "--config=ci-windows-cross",
122
+ "--output=label",
123
+ expression,
124
+ ],
125
+ )
126
+
127
+ def test_same_repository_pull_request_selects_openai_host(self) -> None:
128
+ with TemporaryDirectory() as temp_dir:
129
+ self.assertEqual(
130
+ run_bazel_with_buildbuddy.remote_config(
131
+ ["build", "--config=ci-v8"], self.github_env(temp_dir)
132
+ ),
133
+ "buildbuddy-openai-rbe",
134
+ )
135
+
136
+ def test_fork_pull_request_cannot_select_openai_host(self) -> None:
137
+ with TemporaryDirectory() as temp_dir:
138
+ env = self.github_env(temp_dir, fork=True)
139
+
140
+ self.assertEqual(
141
+ run_bazel_with_buildbuddy.remote_config(
142
+ ["build", "--config=ci-v8"], env
143
+ ),
144
+ "buildbuddy-generic-rbe",
145
+ )
146
+
147
+ def test_run_in_fork_repository_cannot_select_openai_host(self) -> None:
148
+ with TemporaryDirectory() as temp_dir:
149
+ env = self.github_env(temp_dir, repository="contributor/codex")
150
+
151
+ self.assertEqual(
152
+ run_bazel_with_buildbuddy.remote_config(
153
+ ["build", "--config=ci-v8"], env
154
+ ),
155
+ "buildbuddy-generic-rbe",
156
+ )
157
+
158
+ def test_pull_request_without_readable_event_payload_fails_closed(self) -> None:
159
+ for event_path in (None, "missing-event.json"):
160
+ env = {
161
+ "BUILDBUDDY_API_KEY": "token",
162
+ "GITHUB_ACTIONS": "true",
163
+ "GITHUB_EVENT_NAME": "pull_request",
164
+ "GITHUB_REPOSITORY": "openai/codex",
165
+ }
166
+ if event_path is not None:
167
+ env["GITHUB_EVENT_PATH"] = event_path
168
+
169
+ with self.subTest(event_path=event_path):
170
+ self.assertEqual(
171
+ run_bazel_with_buildbuddy.remote_config(["build"], env),
172
+ "buildbuddy-generic",
173
+ )
174
+
175
+ def test_bazel_command_uses_configured_binary_locally(self) -> None:
176
+ self.assertEqual(
177
+ run_bazel_with_buildbuddy.bazel_command(
178
+ "info",
179
+ "execution_root",
180
+ env={"CODEX_BAZEL_BIN": "fake-bazel"},
181
+ ),
182
+ ["fake-bazel", "info", "execution_root"],
183
+ )
184
+
185
+ def test_bazel_command_normalizes_github_actions_startup_options(self) -> None:
186
+ env = {
187
+ "BAZEL_OUTPUT_USER_ROOT": "/tmp/bazel-output",
188
+ "GITHUB_ACTIONS": "true",
189
+ }
190
+
191
+ self.assertEqual(
192
+ run_bazel_with_buildbuddy.bazel_command("build", "//codex-rs/...", env=env),
193
+ [
194
+ "bazel",
195
+ "--output_user_root=/tmp/bazel-output",
196
+ "--noexperimental_remote_repo_contents_cache",
197
+ "build",
198
+ "//codex-rs/...",
199
+ ],
200
+ )
201
+ self.assertEqual(
202
+ run_bazel_with_buildbuddy.bazel_command(
203
+ "--experimental_remote_repo_contents_cache",
204
+ "build",
205
+ "//codex-rs/...",
206
+ env=env,
207
+ ),
208
+ [
209
+ "bazel",
210
+ "--output_user_root=/tmp/bazel-output",
211
+ "--experimental_remote_repo_contents_cache",
212
+ "build",
213
+ "//codex-rs/...",
214
+ ],
215
+ )
216
+
217
+ def test_bazel_command_uses_configured_local_caches(self) -> None:
218
+ env = {
219
+ "BAZEL_REPO_CONTENTS_CACHE": "/tmp/bazel-repo-contents",
220
+ "BAZEL_REPOSITORY_CACHE": "/tmp/bazel-repository",
221
+ }
222
+
223
+ self.assertEqual(
224
+ run_bazel_with_buildbuddy.bazel_command(
225
+ "build",
226
+ "--config=local",
227
+ "//codex-rs/...",
228
+ env=env,
229
+ ),
230
+ [
231
+ "bazel",
232
+ "build",
233
+ "--config=local",
234
+ "//codex-rs/...",
235
+ "--repo_contents_cache=/tmp/bazel-repo-contents",
236
+ "--repository_cache=/tmp/bazel-repository",
237
+ ],
238
+ )
239
+
240
+ def test_bazel_command_adds_local_caches_before_separator(self) -> None:
241
+ self.assertEqual(
242
+ run_bazel_with_buildbuddy.bazel_command(
243
+ "build",
244
+ "//codex-rs/...",
245
+ "--",
246
+ "--program-arg",
247
+ env={"BAZEL_REPOSITORY_CACHE": "/tmp/bazel-repository"},
248
+ ),
249
+ [
250
+ "bazel",
251
+ "build",
252
+ "//codex-rs/...",
253
+ "--repository_cache=/tmp/bazel-repository",
254
+ "--",
255
+ "--program-arg",
256
+ ],
257
+ )
258
+
259
+ def test_main_preserves_spaced_argument_and_child_exit_status(self) -> None:
260
+ spaced_arg = (
261
+ r"--test_env=PATH=C:\Program Files\PowerShell\7;C:\Program Files\Git\bin"
262
+ )
263
+ child_code = (
264
+ f"import sys; sys.exit(37 if sys.argv[1] == {spaced_arg!r} else 91)"
265
+ )
266
+ env = os.environ.copy()
267
+ env["CODEX_BAZEL_BIN"] = sys.executable
268
+ env.pop("BAZEL_OUTPUT_USER_ROOT", None)
269
+ env.pop("BUILDBUDDY_API_KEY", None)
270
+ env.pop("GITHUB_ACTIONS", None)
271
+
272
+ result = subprocess.run(
273
+ [
274
+ sys.executable,
275
+ str(Path(run_bazel_with_buildbuddy.__file__)),
276
+ "-c",
277
+ child_code,
278
+ spaced_arg,
279
+ ],
280
+ env=env,
281
+ check=False,
282
+ capture_output=True,
283
+ text=True,
284
+ )
285
+
286
+ self.assertEqual(result.returncode, 37, result.stderr)
287
+
288
+
289
+ if __name__ == "__main__":
290
+ unittest.main()
.github/scripts/test_rusty_v8_bazel.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import textwrap
6
+ import unittest
7
+ from os import environ
8
+ from pathlib import Path
9
+ from tempfile import TemporaryDirectory
10
+ from unittest.mock import patch
11
+
12
+ import rusty_v8_bazel
13
+ import rusty_v8_module_bazel
14
+
15
+
16
+ class RustyV8BazelTest(unittest.TestCase):
17
+ def test_consumer_selectors_track_resolved_crate_version(self) -> None:
18
+ build_bazel = (
19
+ rusty_v8_bazel.ROOT / "third_party" / "v8" / "BUILD.bazel"
20
+ ).read_text()
21
+ version_suffix = rusty_v8_bazel.resolved_v8_crate_version().replace(".", "_")
22
+
23
+ for selector in [
24
+ "aarch64_apple_darwin_bazel",
25
+ "aarch64_pc_windows_gnullvm",
26
+ "aarch64_pc_windows_msvc",
27
+ "aarch64_unknown_linux_gnu_bazel",
28
+ "aarch64_unknown_linux_musl_release_base",
29
+ "x86_64_apple_darwin_bazel",
30
+ "x86_64_pc_windows_gnullvm",
31
+ "x86_64_pc_windows_msvc",
32
+ "x86_64_unknown_linux_gnu_bazel",
33
+ "x86_64_unknown_linux_musl_release",
34
+ ]:
35
+ self.assertIn(
36
+ f":v8_{version_suffix}_{selector}",
37
+ build_bazel,
38
+ )
39
+
40
+ for selector in [
41
+ "aarch64_apple_darwin",
42
+ "aarch64_pc_windows_gnullvm",
43
+ "aarch64_pc_windows_msvc",
44
+ "aarch64_unknown_linux_gnu",
45
+ "aarch64_unknown_linux_musl",
46
+ "x86_64_apple_darwin",
47
+ "x86_64_pc_windows_gnullvm",
48
+ "x86_64_pc_windows_msvc",
49
+ "x86_64_unknown_linux_gnu",
50
+ "x86_64_unknown_linux_musl",
51
+ ]:
52
+ self.assertIn(
53
+ f":src_binding_release_{selector}_{version_suffix}_release",
54
+ build_bazel,
55
+ )
56
+
57
+ def test_command_version_tracks_remaining_http_file_assets(self) -> None:
58
+ with TemporaryDirectory() as temp_dir:
59
+ module_bazel = Path(temp_dir) / "MODULE.bazel"
60
+ module_bazel.write_text(
61
+ textwrap.dedent(
62
+ """\
63
+ http_file(
64
+ name = "rusty_v8_146_4_0_x86_64_unknown_linux_gnu_archive",
65
+ downloaded_file_path = "librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
66
+ urls = ["https://example.test/archive.gz"],
67
+ )
68
+ """
69
+ )
70
+ )
71
+
72
+ with patch.object(rusty_v8_bazel, "MODULE_BAZEL", module_bazel):
73
+ self.assertEqual("146.4.0", rusty_v8_bazel.command_version(None))
74
+
75
+ def test_artifact_bazel_configs_always_enable_upstream_libcxx(self) -> None:
76
+ self.assertEqual(
77
+ ["rusty-v8-upstream-libcxx"],
78
+ rusty_v8_bazel.artifact_bazel_configs(),
79
+ )
80
+ self.assertEqual(
81
+ ["rusty-v8-upstream-libcxx", "v8-release-compat"],
82
+ rusty_v8_bazel.artifact_bazel_configs(["v8-release-compat"]),
83
+ )
84
+ self.assertEqual(
85
+ ["rusty-v8-upstream-libcxx", "v8-release-compat"],
86
+ rusty_v8_bazel.artifact_bazel_configs(
87
+ ["rusty-v8-upstream-libcxx", "v8-release-compat"]
88
+ ),
89
+ )
90
+
91
+ def test_bazel_commands_use_shared_buildbuddy_remote_config_library(self) -> None:
92
+ with patch.dict(environ, {}, clear=True):
93
+ self.assertEqual(
94
+ [
95
+ "bazel",
96
+ "build",
97
+ "//third_party/v8:release",
98
+ ],
99
+ rusty_v8_bazel.bazel_command(
100
+ "build",
101
+ "--config=ci-v8",
102
+ "//third_party/v8:release",
103
+ ),
104
+ )
105
+ with patch.dict(environ, {"BUILDBUDDY_API_KEY": "token"}, clear=True):
106
+ self.assertEqual(
107
+ [
108
+ "bazel",
109
+ "build",
110
+ "--config=buildbuddy-generic-rbe",
111
+ "--remote_header=x-buildbuddy-api-key=token",
112
+ "--config=ci-v8",
113
+ "//third_party/v8:release",
114
+ ],
115
+ rusty_v8_bazel.bazel_command(
116
+ "build",
117
+ "--config=ci-v8",
118
+ "//third_party/v8:release",
119
+ ),
120
+ )
121
+
122
+ def test_release_pair_labels_and_staged_names_distinguish_sandbox_artifacts(
123
+ self,
124
+ ) -> None:
125
+ self.assertEqual(
126
+ "//third_party/v8:rusty_v8_release_pair_x86_64_unknown_linux_musl",
127
+ rusty_v8_bazel.release_pair_label("x86_64-unknown-linux-musl"),
128
+ )
129
+ self.assertEqual(
130
+ "//third_party/v8:rusty_v8_sandbox_release_pair_x86_64_unknown_linux_musl",
131
+ rusty_v8_bazel.release_pair_label(
132
+ "x86_64-unknown-linux-musl", sandbox=True
133
+ ),
134
+ )
135
+ self.assertEqual(
136
+ "//third_party/v8:rusty_v8_sandbox_release_pair_x86_64_apple_darwin",
137
+ rusty_v8_bazel.release_pair_label("x86_64-apple-darwin", sandbox=True),
138
+ )
139
+ self.assertEqual(
140
+ "librusty_v8_release_x86_64-unknown-linux-musl.a.gz",
141
+ rusty_v8_bazel.staged_archive_name(
142
+ "x86_64-unknown-linux-musl",
143
+ Path("libv8.a"),
144
+ rusty_v8_bazel.RELEASE_ARTIFACT_PROFILE,
145
+ ),
146
+ )
147
+ self.assertEqual(
148
+ "rusty_v8_ptrcomp_sandbox_release_x86_64-pc-windows-msvc.lib.gz",
149
+ rusty_v8_bazel.staged_archive_name(
150
+ "x86_64-pc-windows-msvc",
151
+ Path("v8.a"),
152
+ rusty_v8_bazel.SANDBOX_ARTIFACT_PROFILE,
153
+ ),
154
+ )
155
+ self.assertEqual(
156
+ "src_binding_ptrcomp_sandbox_release_x86_64-unknown-linux-musl.rs",
157
+ rusty_v8_bazel.staged_binding_name(
158
+ "x86_64-unknown-linux-musl",
159
+ rusty_v8_bazel.SANDBOX_ARTIFACT_PROFILE,
160
+ ),
161
+ )
162
+ self.assertEqual(
163
+ "rusty_v8_ptrcomp_sandbox_release_x86_64-unknown-linux-musl.sha256",
164
+ rusty_v8_bazel.staged_checksums_name(
165
+ "x86_64-unknown-linux-musl",
166
+ rusty_v8_bazel.SANDBOX_ARTIFACT_PROFILE,
167
+ ),
168
+ )
169
+
170
+ def test_stage_artifacts(self) -> None:
171
+ with TemporaryDirectory() as source_dir, TemporaryDirectory() as output_dir:
172
+ source_root = Path(source_dir)
173
+ archive = source_root / "librusty_v8.a"
174
+ binding = source_root / "src_binding.rs"
175
+ archive.write_bytes(b"archive")
176
+ binding.write_text("binding")
177
+
178
+ rusty_v8_bazel.stage_artifacts(
179
+ "aarch64-apple-darwin",
180
+ archive,
181
+ binding,
182
+ Path(output_dir),
183
+ sandbox=True,
184
+ )
185
+
186
+ self.assertEqual(
187
+ {
188
+ "librusty_v8_ptrcomp_sandbox_release_aarch64-apple-darwin.a.gz",
189
+ "src_binding_ptrcomp_sandbox_release_aarch64-apple-darwin.rs",
190
+ "rusty_v8_ptrcomp_sandbox_release_aarch64-apple-darwin.sha256",
191
+ },
192
+ {path.name for path in Path(output_dir).iterdir()},
193
+ )
194
+ checksums = (
195
+ Path(output_dir)
196
+ / "rusty_v8_ptrcomp_sandbox_release_aarch64-apple-darwin.sha256"
197
+ ).read_bytes()
198
+ self.assertNotIn(b"\r", checksums)
199
+
200
+ def test_upstream_release_pair_paths(self) -> None:
201
+ self.assertEqual(
202
+ (
203
+ Path(
204
+ "/tmp/rusty_v8/target/x86_64-apple-darwin/release/gn_out/obj/"
205
+ "librusty_v8.a"
206
+ ),
207
+ Path(
208
+ "/tmp/rusty_v8/target/x86_64-apple-darwin/release/gn_out/"
209
+ "src_binding.rs"
210
+ ),
211
+ ),
212
+ rusty_v8_bazel.upstream_release_pair_paths(
213
+ "x86_64-apple-darwin",
214
+ Path("/tmp/rusty_v8/target"),
215
+ ),
216
+ )
217
+ self.assertEqual(
218
+ (
219
+ Path(
220
+ "/tmp/rusty_v8/target/x86_64-pc-windows-msvc/release/gn_out/"
221
+ "obj/rusty_v8.lib"
222
+ ),
223
+ Path(
224
+ "/tmp/rusty_v8/target/x86_64-pc-windows-msvc/release/gn_out/"
225
+ "src_binding.rs"
226
+ ),
227
+ ),
228
+ rusty_v8_bazel.upstream_release_pair_paths(
229
+ "x86_64-pc-windows-msvc",
230
+ Path("/tmp/rusty_v8/target"),
231
+ ),
232
+ )
233
+
234
+ def test_stage_upstream_release_pair(self) -> None:
235
+ with (
236
+ TemporaryDirectory() as target_dir,
237
+ TemporaryDirectory() as output_dir,
238
+ ):
239
+ gn_out = Path(target_dir) / "x86_64-pc-windows-msvc" / "release" / "gn_out"
240
+ (gn_out / "obj").mkdir(parents=True)
241
+ (gn_out / "obj" / "rusty_v8.lib").write_bytes(b"archive")
242
+ (gn_out / "src_binding.rs").write_text("binding")
243
+
244
+ rusty_v8_bazel.stage_upstream_release_pair(
245
+ "x86_64-pc-windows-msvc",
246
+ Path(output_dir),
247
+ Path(target_dir),
248
+ sandbox=True,
249
+ )
250
+
251
+ self.assertEqual(
252
+ {
253
+ "rusty_v8_ptrcomp_sandbox_release_x86_64-pc-windows-msvc.lib.gz",
254
+ "src_binding_ptrcomp_sandbox_release_x86_64-pc-windows-msvc.rs",
255
+ "rusty_v8_ptrcomp_sandbox_release_x86_64-pc-windows-msvc.sha256",
256
+ },
257
+ {path.name for path in Path(output_dir).iterdir()},
258
+ )
259
+
260
+ def test_ensure_bazel_output_files_rebuilds_existing_outputs(self) -> None:
261
+ with TemporaryDirectory() as output_dir:
262
+ output = Path(output_dir) / "libv8.a"
263
+ output.write_bytes(b"archive")
264
+
265
+ with (
266
+ patch.object(rusty_v8_bazel, "bazel_build") as bazel_build,
267
+ patch.object(
268
+ rusty_v8_bazel,
269
+ "bazel_output_files",
270
+ return_value=[output],
271
+ ) as bazel_output_files,
272
+ ):
273
+ self.assertEqual(
274
+ [output],
275
+ rusty_v8_bazel.ensure_bazel_output_files(
276
+ "macos_arm64",
277
+ ["//third_party/v8:pair"],
278
+ "opt",
279
+ ["rusty-v8-upstream-libcxx"],
280
+ ),
281
+ )
282
+
283
+ bazel_build.assert_called_once_with(
284
+ "macos_arm64",
285
+ ["//third_party/v8:pair"],
286
+ "opt",
287
+ ["rusty-v8-upstream-libcxx"],
288
+ download_toplevel=True,
289
+ )
290
+ bazel_output_files.assert_called_once_with(
291
+ "macos_arm64",
292
+ ["//third_party/v8:pair"],
293
+ "opt",
294
+ ["rusty-v8-upstream-libcxx"],
295
+ )
296
+
297
+ def test_update_module_bazel_replaces_and_inserts_sha256(self) -> None:
298
+ module_bazel = textwrap.dedent(
299
+ """\
300
+ http_file(
301
+ name = "rusty_v8_146_4_0_x86_64_unknown_linux_gnu_archive",
302
+ downloaded_file_path = "librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
303
+ sha256 = "0000000000000000000000000000000000000000000000000000000000000000",
304
+ urls = [
305
+ "https://example.test/librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
306
+ ],
307
+ )
308
+
309
+ http_file(
310
+ name = "rusty_v8_146_4_0_x86_64_unknown_linux_musl_binding",
311
+ downloaded_file_path = "src_binding_release_x86_64-unknown-linux-musl.rs",
312
+ urls = [
313
+ "https://example.test/src_binding_release_x86_64-unknown-linux-musl.rs",
314
+ ],
315
+ )
316
+
317
+ http_file(
318
+ name = "rusty_v8_145_0_0_x86_64_unknown_linux_gnu_archive",
319
+ downloaded_file_path = "librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
320
+ sha256 = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
321
+ urls = [
322
+ "https://example.test/old.gz",
323
+ ],
324
+ )
325
+ """
326
+ )
327
+ checksums = {
328
+ "librusty_v8_release_x86_64-unknown-linux-gnu.a.gz": (
329
+ "1111111111111111111111111111111111111111111111111111111111111111"
330
+ ),
331
+ "src_binding_release_x86_64-unknown-linux-musl.rs": (
332
+ "2222222222222222222222222222222222222222222222222222222222222222"
333
+ ),
334
+ }
335
+
336
+ updated = rusty_v8_module_bazel.update_module_bazel_text(
337
+ module_bazel,
338
+ checksums,
339
+ "146.4.0",
340
+ )
341
+
342
+ self.assertEqual(
343
+ textwrap.dedent(
344
+ """\
345
+ http_file(
346
+ name = "rusty_v8_146_4_0_x86_64_unknown_linux_gnu_archive",
347
+ downloaded_file_path = "librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
348
+ sha256 = "1111111111111111111111111111111111111111111111111111111111111111",
349
+ urls = [
350
+ "https://example.test/librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
351
+ ],
352
+ )
353
+
354
+ http_file(
355
+ name = "rusty_v8_146_4_0_x86_64_unknown_linux_musl_binding",
356
+ downloaded_file_path = "src_binding_release_x86_64-unknown-linux-musl.rs",
357
+ sha256 = "2222222222222222222222222222222222222222222222222222222222222222",
358
+ urls = [
359
+ "https://example.test/src_binding_release_x86_64-unknown-linux-musl.rs",
360
+ ],
361
+ )
362
+
363
+ http_file(
364
+ name = "rusty_v8_145_0_0_x86_64_unknown_linux_gnu_archive",
365
+ downloaded_file_path = "librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
366
+ sha256 = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
367
+ urls = [
368
+ "https://example.test/old.gz",
369
+ ],
370
+ )
371
+ """
372
+ ),
373
+ updated,
374
+ )
375
+ rusty_v8_module_bazel.check_module_bazel_text(updated, checksums, "146.4.0")
376
+
377
+ def test_check_module_bazel_rejects_manifest_drift(self) -> None:
378
+ module_bazel = textwrap.dedent(
379
+ """\
380
+ http_file(
381
+ name = "rusty_v8_146_4_0_x86_64_unknown_linux_gnu_archive",
382
+ downloaded_file_path = "librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
383
+ sha256 = "1111111111111111111111111111111111111111111111111111111111111111",
384
+ urls = [
385
+ "https://example.test/librusty_v8_release_x86_64-unknown-linux-gnu.a.gz",
386
+ ],
387
+ )
388
+ """
389
+ )
390
+ checksums = {
391
+ "librusty_v8_release_x86_64-unknown-linux-gnu.a.gz": (
392
+ "1111111111111111111111111111111111111111111111111111111111111111"
393
+ ),
394
+ "orphan.gz": (
395
+ "2222222222222222222222222222222222222222222222222222222222222222"
396
+ ),
397
+ }
398
+
399
+ with self.assertRaisesRegex(
400
+ rusty_v8_module_bazel.RustyV8ChecksumError,
401
+ "manifest has orphan.gz",
402
+ ):
403
+ rusty_v8_module_bazel.check_module_bazel_text(
404
+ module_bazel,
405
+ checksums,
406
+ "146.4.0",
407
+ )
408
+
409
+ def test_rusty_v8_http_file_versions(self) -> None:
410
+ module_bazel = textwrap.dedent(
411
+ """\
412
+ http_file(
413
+ name = "rusty_v8_146_4_0_x86_64_unknown_linux_gnu_archive",
414
+ downloaded_file_path = "archive.gz",
415
+ urls = ["https://example.test/archive.gz"],
416
+ )
417
+
418
+ http_file(
419
+ name = "rusty_v8_147_4_0_x86_64_unknown_linux_gnu_archive",
420
+ downloaded_file_path = "new-archive.gz",
421
+ urls = ["https://example.test/new-archive.gz"],
422
+ )
423
+
424
+ http_file(
425
+ name = "unrelated_archive",
426
+ downloaded_file_path = "other.gz",
427
+ urls = ["https://example.test/other.gz"],
428
+ )
429
+ """
430
+ )
431
+
432
+ self.assertEqual(
433
+ ["146.4.0", "147.4.0"],
434
+ rusty_v8_module_bazel.rusty_v8_http_file_versions(module_bazel),
435
+ )
436
+
437
+
438
+ if __name__ == "__main__":
439
+ unittest.main()
.github/scripts/test_v8_canary_changes.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import tempfile
3
+ import unittest
4
+ from pathlib import Path
5
+
6
+ from v8_canary_changes import changed_files
7
+ from v8_canary_changes import canary_required
8
+ from v8_canary_changes import merge_base
9
+ from v8_canary_changes import resolved_v8_version
10
+ from v8_canary_changes import windows_source_required
11
+
12
+
13
+ class V8CanaryChangesTest(unittest.TestCase):
14
+ def test_resolved_v8_version(self) -> None:
15
+ cargo_lock = b"""\
16
+ [[package]]
17
+ name = "other"
18
+ version = "1.0.0"
19
+
20
+ [[package]]
21
+ name = "v8"
22
+ version = "149.2.0"
23
+ """
24
+
25
+ self.assertEqual(resolved_v8_version(cargo_lock), "149.2.0")
26
+
27
+ def test_unrelated_cargo_manifest_change_does_not_require_source_build(
28
+ self,
29
+ ) -> None:
30
+ self.assertFalse(
31
+ windows_source_required(
32
+ {"codex-rs/Cargo.toml"},
33
+ "149.2.0",
34
+ "149.2.0",
35
+ )
36
+ )
37
+
38
+ def test_v8_version_change_requires_source_build(self) -> None:
39
+ self.assertTrue(windows_source_required(set(), "149.2.0", "150.0.0"))
40
+
41
+ def test_module_helper_change_requires_source_build(self) -> None:
42
+ self.assertTrue(
43
+ windows_source_required(
44
+ {".github/scripts/rusty_v8_module_bazel.py"},
45
+ "149.2.0",
46
+ "149.2.0",
47
+ )
48
+ )
49
+
50
+ def test_shared_ci_setup_changes_require_canary_and_source_build(self) -> None:
51
+ for path in (
52
+ ".github/actions/setup-ci/action.yml",
53
+ ".github/scripts/setup-dev-drive.ps1",
54
+ ):
55
+ with self.subTest(path=path):
56
+ changed_files = {path}
57
+ self.assertTrue(canary_required(changed_files, "149.2.0", "149.2.0"))
58
+ self.assertTrue(
59
+ windows_source_required(changed_files, "149.2.0", "149.2.0")
60
+ )
61
+
62
+ def test_manual_dispatch_requires_source_build(self) -> None:
63
+ self.assertTrue(
64
+ windows_source_required(
65
+ set(),
66
+ "149.2.0",
67
+ "149.2.0",
68
+ force=True,
69
+ )
70
+ )
71
+
72
+ def test_changed_files_excludes_changes_made_only_on_base_branch(self) -> None:
73
+ with tempfile.TemporaryDirectory() as temp_dir:
74
+ root = Path(temp_dir)
75
+ self.run_git(root, "init", "--initial-branch=main")
76
+ self.run_git(root, "config", "user.name", "Test User")
77
+ self.run_git(root, "config", "user.email", "test@example.com")
78
+
79
+ self.write_and_commit(root, "initial", "initial.txt")
80
+ common = self.run_git(root, "rev-parse", "HEAD")
81
+ self.run_git(root, "switch", "-c", "feature")
82
+ self.run_git(root, "switch", "main")
83
+ self.write_and_commit(root, "base-only", "base-only.txt")
84
+ base = self.run_git(root, "rev-parse", "HEAD")
85
+
86
+ self.run_git(root, "switch", "feature")
87
+ self.write_and_commit(root, "feature-only", "feature-only.txt")
88
+ head = self.run_git(root, "rev-parse", "HEAD")
89
+
90
+ self.assertEqual(
91
+ changed_files(base, head, root=root),
92
+ {"feature-only.txt"},
93
+ )
94
+ self.assertEqual(merge_base(base, head, root=root), common)
95
+
96
+ def write_and_commit(self, root: Path, contents: str, path: str) -> None:
97
+ (root / path).write_text(contents)
98
+ self.run_git(root, "add", path)
99
+ self.run_git(root, "commit", "-m", contents)
100
+
101
+ def run_git(self, root: Path, *args: str) -> str:
102
+ return subprocess.check_output(
103
+ ["git", *args],
104
+ cwd=root,
105
+ stderr=subprocess.PIPE,
106
+ text=True,
107
+ ).strip()
108
+
109
+
110
+ if __name__ == "__main__":
111
+ unittest.main()
.github/scripts/test_verify_pypi_release.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import http.client
2
+ import io
3
+ import json
4
+ import unittest
5
+ import urllib.error
6
+ from unittest.mock import MagicMock, patch
7
+
8
+ from verify_pypi_release import verify_release
9
+
10
+
11
+ class VerifyPyPIReleaseTest(unittest.TestCase):
12
+ def test_waits_for_complete_release_after_registry_error(self) -> None:
13
+ wheel = "openai_codex-1.2.3-py3-none-any.whl"
14
+ sdist = "openai_codex-1.2.3.tar.gz"
15
+ reset_response = MagicMock()
16
+ reset_response.__enter__.return_value.read.side_effect = ConnectionResetError(
17
+ "connection reset while reading response"
18
+ )
19
+ responses = [
20
+ urllib.error.URLError("registry unavailable"),
21
+ TimeoutError("registry timed out"),
22
+ http.client.IncompleteRead(b"{", 20),
23
+ reset_response,
24
+ io.StringIO('{"urls":'),
25
+ *[
26
+ io.StringIO(json.dumps(data))
27
+ for data in (
28
+ None,
29
+ [],
30
+ {},
31
+ {"urls": None},
32
+ {"urls": {}},
33
+ {"urls": [None]},
34
+ {"urls": [{}]},
35
+ {"urls": [{"filename": 42}]},
36
+ )
37
+ ],
38
+ io.StringIO(json.dumps({"urls": [{"filename": wheel}]})),
39
+ io.StringIO(
40
+ json.dumps({"urls": [{"filename": wheel}, {"filename": sdist}]})
41
+ ),
42
+ ]
43
+ with (
44
+ patch(
45
+ "verify_pypi_release.urllib.request.urlopen", side_effect=responses
46
+ ) as urlopen,
47
+ patch("verify_pypi_release.time.sleep") as sleep,
48
+ ):
49
+ verify_release("openai-codex", "1.2.3")
50
+
51
+ self.assertEqual(urlopen.call_count, 15)
52
+ self.assertEqual(sleep.call_count, 14)
53
+ urlopen.assert_called_with(
54
+ "https://pypi.org/pypi/openai-codex/1.2.3/json", timeout=30
55
+ )
56
+
57
+ def test_uses_canonical_version_for_lookup_and_artifact_names(self) -> None:
58
+ data = {
59
+ "urls": [
60
+ {"filename": name}
61
+ for name in (
62
+ "openai_codex-1.2.3b1-py3-none-any.whl",
63
+ "openai_codex-1.2.3b1.tar.gz",
64
+ )
65
+ ]
66
+ }
67
+ with (
68
+ patch(
69
+ "verify_pypi_release.urllib.request.urlopen",
70
+ return_value=io.StringIO(json.dumps(data)),
71
+ ) as urlopen,
72
+ patch("verify_pypi_release.time.sleep") as sleep,
73
+ ):
74
+ verify_release("openai-codex", "1.2.3b01")
75
+ urlopen.assert_called_once_with(
76
+ "https://pypi.org/pypi/openai-codex/1.2.3b1/json", timeout=30
77
+ )
78
+ sleep.assert_not_called()
79
+
80
+ def test_fails_after_bounded_retries_when_runtime_wheels_are_missing(self) -> None:
81
+ with (
82
+ patch(
83
+ "verify_pypi_release.urllib.request.urlopen",
84
+ side_effect=lambda *args, **kwargs: io.StringIO('{"urls": []}'),
85
+ ) as urlopen,
86
+ patch("verify_pypi_release.time.sleep") as sleep,
87
+ self.assertRaisesRegex(SystemExit, "did not become available on PyPI"),
88
+ ):
89
+ verify_release("openai-codex-cli-bin", "1.2.3a4.post5")
90
+
91
+ self.assertEqual(urlopen.call_count, 30)
92
+ self.assertEqual(sleep.call_count, 29)
93
+
94
+
95
+ if __name__ == "__main__":
96
+ unittest.main()
.github/scripts/test_voice_cygwin_inputs.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Offline checks for the public Windows build-input bootstrap."""
2
+
3
+ import hashlib
4
+ import importlib.util
5
+ import io
6
+ from pathlib import Path
7
+ import tarfile
8
+ import tempfile
9
+ import unittest
10
+
11
+
12
+ SCRIPT = Path(__file__).with_name("voice-cygwin-inputs.py")
13
+ SPEC = importlib.util.spec_from_file_location("voice_cygwin_inputs", SCRIPT)
14
+ inputs = importlib.util.module_from_spec(SPEC)
15
+ SPEC.loader.exec_module(inputs)
16
+
17
+
18
+ class VoiceCygwinInputsTests(unittest.TestCase):
19
+ def test_input_paths_cannot_escape_the_local_cache(self):
20
+ manifest = {
21
+ "metadata": [
22
+ {"file": "x86_64/setup.xz"},
23
+ {"file": "x86_64/setup.xz.sig"},
24
+ ],
25
+ "packages": [{"file": "../outside.tar.xz"}],
26
+ }
27
+ with self.assertRaisesRegex(ValueError, "unsafe Cygwin input"):
28
+ inputs.records(manifest)
29
+
30
+ def test_extract_verifies_archive_and_every_input_before_offline_installation(self):
31
+ with tempfile.TemporaryDirectory() as directory:
32
+ root = Path(directory)
33
+ payloads = {
34
+ "x86_64/setup.xz": b"metadata",
35
+ "x86_64/setup.xz.sig": b"signature",
36
+ "x86_64/release/bash/bash.tar.xz": b"package",
37
+ }
38
+ records = [
39
+ {
40
+ "file": name,
41
+ "bytes": len(data),
42
+ "sha512": hashlib.sha512(data).hexdigest(),
43
+ }
44
+ for name, data in payloads.items()
45
+ ]
46
+ archive = root / "inputs.tar.gz"
47
+
48
+ def write_archive(contents):
49
+ with tarfile.open(archive, "w:gz") as bundle:
50
+ for name, data in contents.items():
51
+ info = tarfile.TarInfo(name)
52
+ info.size = len(data)
53
+ bundle.addfile(info, io.BytesIO(data))
54
+ return {
55
+ "bytes": archive.stat().st_size,
56
+ "sha256": hashlib.sha256(archive.read_bytes()).hexdigest(),
57
+ }
58
+
59
+ manifest = {
60
+ "metadata": records[:2],
61
+ "packages": records[2:],
62
+ "archive": write_archive(payloads),
63
+ }
64
+ destination = root / "cache"
65
+ inputs.extract(manifest, archive, destination)
66
+ self.assertEqual(
67
+ {
68
+ record["file"]: (destination / record["file"]).read_bytes()
69
+ for record in records
70
+ },
71
+ payloads,
72
+ )
73
+
74
+ (destination / records[2]["file"]).unlink()
75
+ payloads[records[2]["file"]] = b"wrong"
76
+ manifest["archive"] = write_archive(payloads)
77
+ with self.assertRaisesRegex(ValueError, "Cygwin input mismatch"):
78
+ inputs.extract(manifest, archive, root / "rejected")
79
+ self.assertFalse((root / "rejected").exists())
80
+
81
+
82
+ if __name__ == "__main__":
83
+ unittest.main()
.github/scripts/v8_canary_changes.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ """Decide which V8 canary work is needed for a commit range.
4
+
5
+ The workflow deliberately has no trigger-level path filters because it is both
6
+ directly triggered for pull requests and called by postmerge-ci. Keeping the
7
+ patterns here gives those entrypoints one source of truth; unrelated events
8
+ still run metadata but skip the expensive build matrices.
9
+ """
10
+
11
+ import argparse
12
+ import subprocess
13
+ import tomllib
14
+ from fnmatch import fnmatchcase
15
+ from pathlib import Path
16
+
17
+
18
+ ROOT = Path(__file__).resolve().parents[2]
19
+ # These patterns replace the old pull_request/push path filters. Include parent
20
+ # workflow changes because they can alter whether the canary is invoked.
21
+ CANARY_PATH_PATTERNS = {
22
+ ".bazelrc",
23
+ ".github/actions/setup-bazel-ci/**",
24
+ ".github/actions/setup-ci/**",
25
+ ".github/scripts/run_bazel_with_buildbuddy.py",
26
+ ".github/scripts/rusty_v8_bazel.py",
27
+ ".github/scripts/rusty_v8_module_bazel.py",
28
+ ".github/scripts/setup-dev-drive.ps1",
29
+ ".github/scripts/v8_canary_changes.py",
30
+ ".github/workflows/postmerge-ci.yml",
31
+ ".github/workflows/rusty-v8-release.yml",
32
+ ".github/workflows/v8-canary.yml",
33
+ "MODULE.bazel",
34
+ "MODULE.bazel.lock",
35
+ "codex-rs/Cargo.toml",
36
+ "patches/BUILD.bazel",
37
+ "patches/llvm_*.patch",
38
+ "patches/rules_cc_*.patch",
39
+ "patches/v8_*.patch",
40
+ "third_party/v8/**",
41
+ }
42
+ # Windows source builds are a narrower, more expensive subset of the canary.
43
+ # A V8 version change also requires them even when no path below changed.
44
+ WINDOWS_SOURCE_BUILD_PATHS = {
45
+ ".github/actions/setup-ci/**",
46
+ ".github/scripts/rusty_v8_bazel.py",
47
+ ".github/scripts/rusty_v8_module_bazel.py",
48
+ ".github/scripts/setup-dev-drive.ps1",
49
+ ".github/scripts/v8_canary_changes.py",
50
+ ".github/workflows/rusty-v8-release.yml",
51
+ ".github/workflows/v8-canary.yml",
52
+ }
53
+
54
+
55
+ def matching_canary_paths(changed_files: set[str]) -> set[str]:
56
+ """Return changed paths that require the general V8 build matrix."""
57
+ return {
58
+ path
59
+ for path in changed_files
60
+ if any(fnmatchcase(path, pattern) for pattern in CANARY_PATH_PATTERNS)
61
+ }
62
+
63
+
64
+ def canary_required(
65
+ changed_files: set[str],
66
+ base_v8_version: str,
67
+ head_v8_version: str,
68
+ *,
69
+ force: bool = False,
70
+ ) -> bool:
71
+ """Return whether the general V8 build matrix should run."""
72
+ return (
73
+ force
74
+ or base_v8_version != head_v8_version
75
+ or bool(matching_canary_paths(changed_files))
76
+ )
77
+
78
+
79
+ def matching_windows_source_paths(changed_files: set[str]) -> set[str]:
80
+ """Return changed paths that require Windows rusty_v8 source builds."""
81
+ return {
82
+ path
83
+ for path in changed_files
84
+ if any(fnmatchcase(path, pattern) for pattern in WINDOWS_SOURCE_BUILD_PATHS)
85
+ }
86
+
87
+
88
+ def resolved_v8_version(cargo_lock: bytes) -> str:
89
+ versions = sorted(
90
+ {
91
+ package["version"]
92
+ for package in tomllib.loads(cargo_lock.decode())["package"]
93
+ if package["name"] == "v8"
94
+ }
95
+ )
96
+ if len(versions) != 1:
97
+ raise ValueError(f"expected exactly one resolved v8 version, found: {versions}")
98
+ return versions[0]
99
+
100
+
101
+ def windows_source_required(
102
+ changed_files: set[str],
103
+ base_v8_version: str,
104
+ head_v8_version: str,
105
+ *,
106
+ force: bool = False,
107
+ ) -> bool:
108
+ """Return whether Windows must rebuild rusty_v8 from source."""
109
+ return (
110
+ force
111
+ or base_v8_version != head_v8_version
112
+ or bool(matching_windows_source_paths(changed_files))
113
+ )
114
+
115
+
116
+ def git_output(*args: str, root: Path = ROOT) -> bytes:
117
+ return subprocess.check_output(["git", *args], cwd=root)
118
+
119
+
120
+ def v8_version_at_revision(revision: str, *, root: Path = ROOT) -> str:
121
+ return resolved_v8_version(
122
+ git_output("show", f"{revision}:codex-rs/Cargo.lock", root=root)
123
+ )
124
+
125
+
126
+ def merge_base(base: str, head: str, *, root: Path = ROOT) -> str:
127
+ return git_output("merge-base", base, head, root=root).decode().strip()
128
+
129
+
130
+ def changed_files(base: str, head: str, *, root: Path = ROOT) -> set[str]:
131
+ # Three-dot diff gives PRs merge-base semantics while remaining equivalent
132
+ # to before/after for ordinary linear pushes to main.
133
+ output = git_output(
134
+ "diff",
135
+ "--name-only",
136
+ "--no-renames",
137
+ f"{base}...{head}",
138
+ root=root,
139
+ )
140
+ return set(output.decode().splitlines())
141
+
142
+
143
+ def parse_args() -> argparse.Namespace:
144
+ parser = argparse.ArgumentParser()
145
+ parser.add_argument("--base")
146
+ parser.add_argument("--head")
147
+ parser.add_argument("--force", action="store_true")
148
+ return parser.parse_args()
149
+
150
+
151
+ def main() -> None:
152
+ args = parse_args()
153
+ if args.force:
154
+ # workflow_dispatch has no comparison range, and callers use it as a
155
+ # manual retry path, so it intentionally runs every variant.
156
+ canary = True
157
+ canary_reason = "manual workflow dispatch"
158
+ windows_source = True
159
+ windows_source_reason = "manual workflow dispatch"
160
+ elif not args.base or not args.head:
161
+ raise SystemExit("--base and --head are required unless --force is set")
162
+ else:
163
+ files = changed_files(args.base, args.head)
164
+ base_version = v8_version_at_revision(merge_base(args.base, args.head))
165
+ head_version = v8_version_at_revision(args.head)
166
+
167
+ matched_canary_paths = sorted(matching_canary_paths(files))
168
+ canary = canary_required(files, base_version, head_version)
169
+ windows_source = windows_source_required(files, base_version, head_version)
170
+ if base_version != head_version:
171
+ canary_reason = f"v8 version changed from {base_version} to {head_version}"
172
+ windows_source_reason = canary_reason
173
+ else:
174
+ canary_reason = (
175
+ ", ".join(matched_canary_paths)
176
+ if matched_canary_paths
177
+ else "no relevant changes"
178
+ )
179
+ matched_windows_paths = sorted(matching_windows_source_paths(files))
180
+ windows_source_reason = (
181
+ ", ".join(matched_windows_paths)
182
+ if matched_windows_paths
183
+ else "no relevant changes"
184
+ )
185
+
186
+ print(f"canary_required={str(canary).lower()}")
187
+ print(f"canary_reason={canary_reason}")
188
+ print(f"windows_source_required={str(windows_source).lower()}")
189
+ print(f"windows_source_reason={windows_source_reason}")
190
+
191
+
192
+ if __name__ == "__main__":
193
+ main()
.github/scripts/verify-zsh-manifest.sh ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ manifest="${1:?missing zsh manifest path}"
6
+ expected_sha256="${2:?missing expected zsh manifest SHA-256}"
7
+
8
+ if command -v sha256sum >/dev/null 2>&1; then
9
+ printf '%s %s\n' "$expected_sha256" "$manifest" | sha256sum --check -
10
+ else
11
+ printf '%s %s\n' "$expected_sha256" "$manifest" | shasum -a 256 --check -
12
+ fi
.github/scripts/verify_bazel_clippy_lints.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import re
7
+ import sys
8
+ import tomllib
9
+ from pathlib import Path
10
+
11
+
12
+ ROOT = Path(__file__).resolve().parents[2]
13
+ DEFAULT_CARGO_TOML = ROOT / "codex-rs" / "Cargo.toml"
14
+ DEFAULT_BAZELRC = ROOT / ".bazelrc"
15
+ BAZEL_CLIPPY_FLAG_PREFIX = "build:clippy --@rules_rust//rust/settings:clippy_flag="
16
+ BAZEL_SPECIAL_FLAGS = {"-Dwarnings"}
17
+ VALID_LEVELS = {"allow", "warn", "deny", "forbid"}
18
+ LONG_FLAG_RE = re.compile(
19
+ r"^--(?P<level>allow|warn|deny|forbid)=clippy::(?P<lint>[a-z0-9_]+)$"
20
+ )
21
+ SHORT_FLAG_RE = re.compile(r"^-(?P<level>[AWDF])clippy::(?P<lint>[a-z0-9_]+)$")
22
+ SHORT_LEVEL_NAMES = {
23
+ "A": "allow",
24
+ "W": "warn",
25
+ "D": "deny",
26
+ "F": "forbid",
27
+ }
28
+
29
+
30
+ def main() -> int:
31
+ parser = argparse.ArgumentParser(
32
+ description=(
33
+ "Verify that Bazel clippy flags in .bazelrc stay in sync with "
34
+ "codex-rs/Cargo.toml [workspace.lints.clippy]."
35
+ )
36
+ )
37
+ parser.add_argument(
38
+ "--cargo-toml",
39
+ type=Path,
40
+ default=DEFAULT_CARGO_TOML,
41
+ help="Path to the workspace Cargo.toml to inspect.",
42
+ )
43
+ parser.add_argument(
44
+ "--bazelrc",
45
+ type=Path,
46
+ default=DEFAULT_BAZELRC,
47
+ help="Path to the .bazelrc file to inspect.",
48
+ )
49
+ args = parser.parse_args()
50
+
51
+ cargo_toml = args.cargo_toml.resolve()
52
+ bazelrc = args.bazelrc.resolve()
53
+
54
+ cargo_lints = load_workspace_clippy_lints(cargo_toml)
55
+ bazel_lints = load_bazel_clippy_lints(bazelrc)
56
+
57
+ missing = sorted(cargo_lints.keys() - bazel_lints.keys())
58
+ extra = sorted(bazel_lints.keys() - cargo_lints.keys())
59
+ mismatched = sorted(
60
+ lint
61
+ for lint in cargo_lints.keys() & bazel_lints.keys()
62
+ if cargo_lints[lint] != bazel_lints[lint]
63
+ )
64
+
65
+ if missing or extra or mismatched:
66
+ print_sync_error(
67
+ cargo_toml=cargo_toml,
68
+ bazelrc=bazelrc,
69
+ cargo_lints=cargo_lints,
70
+ bazel_lints=bazel_lints,
71
+ missing=missing,
72
+ extra=extra,
73
+ mismatched=mismatched,
74
+ )
75
+ return 1
76
+
77
+ print(
78
+ "Bazel clippy flags in "
79
+ f"{display_path(bazelrc)} match "
80
+ f"{display_path(cargo_toml)} [workspace.lints.clippy]."
81
+ )
82
+ return 0
83
+
84
+
85
+ def load_workspace_clippy_lints(cargo_toml: Path) -> dict[str, str]:
86
+ workspace = tomllib.loads(cargo_toml.read_text())["workspace"]
87
+ clippy_lints = workspace["lints"]["clippy"]
88
+ parsed: dict[str, str] = {}
89
+ for lint, level in clippy_lints.items():
90
+ if not isinstance(level, str):
91
+ raise SystemExit(
92
+ f"expected string lint level for clippy::{lint} in {cargo_toml}, got {level!r}"
93
+ )
94
+ normalized = level.strip().lower()
95
+ if normalized not in VALID_LEVELS:
96
+ raise SystemExit(
97
+ f"unsupported lint level {level!r} for clippy::{lint} in {cargo_toml}"
98
+ )
99
+ parsed[lint] = normalized
100
+ return parsed
101
+
102
+
103
+ def load_bazel_clippy_lints(bazelrc: Path) -> dict[str, str]:
104
+ parsed: dict[str, str] = {}
105
+ line_numbers: dict[str, int] = {}
106
+
107
+ for lineno, line in enumerate(bazelrc.read_text().splitlines(), start=1):
108
+ if not line.startswith(BAZEL_CLIPPY_FLAG_PREFIX):
109
+ continue
110
+
111
+ flag = line.removeprefix(BAZEL_CLIPPY_FLAG_PREFIX).strip()
112
+ if flag in BAZEL_SPECIAL_FLAGS:
113
+ continue
114
+
115
+ parsed_flag = parse_bazel_lint_flag(flag)
116
+ if parsed_flag is None:
117
+ continue
118
+
119
+ lint, level = parsed_flag
120
+ if lint in parsed:
121
+ raise SystemExit(
122
+ f"duplicate Bazel clippy entry for clippy::{lint} at "
123
+ f"{bazelrc}:{line_numbers[lint]} and {bazelrc}:{lineno}"
124
+ )
125
+ parsed[lint] = level
126
+ line_numbers[lint] = lineno
127
+
128
+ return parsed
129
+
130
+
131
+ def parse_bazel_lint_flag(flag: str) -> tuple[str, str] | None:
132
+ long_match = LONG_FLAG_RE.match(flag)
133
+ if long_match:
134
+ return long_match["lint"], long_match["level"]
135
+
136
+ short_match = SHORT_FLAG_RE.match(flag)
137
+ if short_match:
138
+ return short_match["lint"], SHORT_LEVEL_NAMES[short_match["level"]]
139
+
140
+ return None
141
+
142
+
143
+ def print_sync_error(
144
+ *,
145
+ cargo_toml: Path,
146
+ bazelrc: Path,
147
+ cargo_lints: dict[str, str],
148
+ bazel_lints: dict[str, str],
149
+ missing: list[str],
150
+ extra: list[str],
151
+ mismatched: list[str],
152
+ ) -> None:
153
+ cargo_toml_display = display_path(cargo_toml)
154
+ bazelrc_display = display_path(bazelrc)
155
+ example_manifest = find_workspace_lints_example_manifest()
156
+
157
+ print(
158
+ "ERROR: Bazel clippy flags are out of sync with Cargo workspace clippy lints.",
159
+ file=sys.stderr,
160
+ )
161
+ print(file=sys.stderr)
162
+ print(
163
+ f"Cargo defines the source of truth in {cargo_toml_display} "
164
+ "[workspace.lints.clippy].",
165
+ file=sys.stderr,
166
+ )
167
+ if example_manifest is not None:
168
+ print(
169
+ "Cargo applies those lint levels to member crates that opt into "
170
+ f"`[lints] workspace = true`, for example {example_manifest}.",
171
+ file=sys.stderr,
172
+ )
173
+ print(
174
+ "Bazel clippy does not ingest Cargo lint levels automatically, and "
175
+ "`clippy.toml` can configure lint behavior but cannot set allow/warn/deny/forbid.",
176
+ file=sys.stderr,
177
+ )
178
+ print(
179
+ f"Update {bazelrc_display} so its `build:clippy` "
180
+ "`clippy_flag` entries match Cargo.",
181
+ file=sys.stderr,
182
+ )
183
+
184
+ if missing:
185
+ print(file=sys.stderr)
186
+ print("Missing Bazel entries:", file=sys.stderr)
187
+ for lint in missing:
188
+ print(f" {render_bazelrc_line(lint, cargo_lints[lint])}", file=sys.stderr)
189
+
190
+ if mismatched:
191
+ print(file=sys.stderr)
192
+ print("Mismatched lint levels:", file=sys.stderr)
193
+ for lint in mismatched:
194
+ cargo_level = cargo_lints[lint]
195
+ bazel_level = bazel_lints[lint]
196
+ print(
197
+ f" clippy::{lint}: Cargo has {cargo_level}, Bazel has {bazel_level}",
198
+ file=sys.stderr,
199
+ )
200
+ print(
201
+ f" expected: {render_bazelrc_line(lint, cargo_level)}",
202
+ file=sys.stderr,
203
+ )
204
+
205
+ if extra:
206
+ print(file=sys.stderr)
207
+ print("Extra Bazel entries with no Cargo counterpart:", file=sys.stderr)
208
+ for lint in extra:
209
+ print(f" {render_bazelrc_line(lint, bazel_lints[lint])}", file=sys.stderr)
210
+
211
+
212
+ def render_bazelrc_line(lint: str, level: str) -> str:
213
+ return f"{BAZEL_CLIPPY_FLAG_PREFIX}--{level}=clippy::{lint}"
214
+
215
+
216
+ def display_path(path: Path) -> str:
217
+ try:
218
+ return str(path.relative_to(ROOT))
219
+ except ValueError:
220
+ return str(path)
221
+
222
+
223
+ def find_workspace_lints_example_manifest() -> str | None:
224
+ for cargo_toml in sorted((ROOT / "codex-rs").glob("**/Cargo.toml")):
225
+ if cargo_toml == DEFAULT_CARGO_TOML:
226
+ continue
227
+ data = tomllib.loads(cargo_toml.read_text())
228
+ if data.get("lints", {}).get("workspace") is True:
229
+ return str(cargo_toml.relative_to(ROOT))
230
+ return None
231
+
232
+
233
+ if __name__ == "__main__":
234
+ sys.exit(main())
.github/scripts/verify_cargo_workspace_manifests.py ADDED
@@ -0,0 +1,395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ """Verify that codex-rs Cargo manifests follow workspace manifest policy.
4
+
5
+ Checks:
6
+ - Crates inherit `[workspace.package]` metadata.
7
+ - Crates opt into `[lints] workspace = true`.
8
+ - Crate names follow the codex-rs directory naming conventions.
9
+ - Workspace manifests do not introduce workspace crate feature toggles.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import sys
15
+ import tomllib
16
+ from pathlib import Path
17
+
18
+
19
+ ROOT = Path(__file__).resolve().parents[2]
20
+ CARGO_RS_ROOT = ROOT / "codex-rs"
21
+ WORKSPACE_PACKAGE_FIELDS = ("version", "edition", "license")
22
+ TOP_LEVEL_NAME_EXCEPTIONS = {
23
+ "windows-sandbox-rs": "codex-windows-sandbox",
24
+ }
25
+ UTILITY_NAME_EXCEPTIONS = {
26
+ "path-utils": "codex-utils-path",
27
+ }
28
+ MANIFEST_FEATURE_EXCEPTIONS = {
29
+ "codex-rs/code-mode/Cargo.toml": {"sandbox": ("v8/v8_enable_sandbox",)},
30
+ "codex-rs/v8-poc/Cargo.toml": {"sandbox": ("v8/v8_enable_sandbox",)},
31
+ }
32
+ OPTIONAL_DEPENDENCY_EXCEPTIONS = set()
33
+ INTERNAL_DEPENDENCY_FEATURE_EXCEPTIONS = {}
34
+
35
+
36
+ def main() -> int:
37
+ internal_package_names = workspace_package_names()
38
+ used_manifest_feature_exceptions: set[str] = set()
39
+ used_optional_dependency_exceptions: set[tuple[str, str, str]] = set()
40
+ used_internal_dependency_feature_exceptions: set[tuple[str, str, str]] = set()
41
+ failures_by_path: dict[str, list[str]] = {}
42
+
43
+ for path in manifests_to_verify():
44
+ if errors := manifest_errors(
45
+ path,
46
+ internal_package_names,
47
+ used_manifest_feature_exceptions,
48
+ used_optional_dependency_exceptions,
49
+ used_internal_dependency_feature_exceptions,
50
+ ):
51
+ failures_by_path[manifest_key(path)] = errors
52
+
53
+ add_unused_exception_errors(
54
+ failures_by_path,
55
+ used_manifest_feature_exceptions,
56
+ used_optional_dependency_exceptions,
57
+ used_internal_dependency_feature_exceptions,
58
+ )
59
+
60
+ if not failures_by_path:
61
+ return 0
62
+
63
+ print(
64
+ "Cargo manifests under codex-rs must inherit workspace package metadata, "
65
+ "opt into workspace lints, and avoid introducing new workspace crate "
66
+ "features."
67
+ )
68
+ print(
69
+ "Workspace crate features are disallowed because our Bazel build setup "
70
+ "does not honor them today, which can let issues hidden behind feature "
71
+ "gates go unnoticed, and because they add extra crate build "
72
+ "permutations we want to avoid."
73
+ )
74
+ print(
75
+ "Cargo only applies `codex-rs/Cargo.toml` `[workspace.lints.clippy]` "
76
+ "entries to a crate when that crate declares:"
77
+ )
78
+ print()
79
+ print("[lints]")
80
+ print("workspace = true")
81
+ print()
82
+ print(
83
+ "Without that opt-in, `cargo clippy` can miss violations that Bazel clippy "
84
+ "catches."
85
+ )
86
+ print()
87
+ print(
88
+ "Package-name checks apply to `codex-rs/<crate>/Cargo.toml` and "
89
+ "`codex-rs/utils/<crate>/Cargo.toml`."
90
+ )
91
+ print(
92
+ "Workspace crate features are forbidden; add a targeted exception here "
93
+ "only if there is a deliberate temporary migration in flight."
94
+ )
95
+ print()
96
+ for path in sorted(failures_by_path):
97
+ errors = failures_by_path[path]
98
+ print(f"{path}:")
99
+ for error in errors:
100
+ print(f" - {error}")
101
+
102
+ return 1
103
+
104
+
105
+ def manifest_errors(
106
+ path: Path,
107
+ internal_package_names: set[str],
108
+ used_manifest_feature_exceptions: set[str],
109
+ used_optional_dependency_exceptions: set[tuple[str, str, str]],
110
+ used_internal_dependency_feature_exceptions: set[tuple[str, str, str]],
111
+ ) -> list[str]:
112
+ manifest = load_manifest(path)
113
+ package = manifest.get("package")
114
+ if not isinstance(package, dict) and path != CARGO_RS_ROOT / "Cargo.toml":
115
+ return []
116
+
117
+ errors = []
118
+ if isinstance(package, dict):
119
+ for field in WORKSPACE_PACKAGE_FIELDS:
120
+ if not is_workspace_reference(package.get(field)):
121
+ errors.append(f"set `{field}.workspace = true` in `[package]`")
122
+
123
+ lints = manifest.get("lints")
124
+ if not (isinstance(lints, dict) and lints.get("workspace") is True):
125
+ errors.append("add `[lints]` with `workspace = true`")
126
+
127
+ expected_name = expected_package_name(path)
128
+ if expected_name is not None:
129
+ actual_name = package.get("name")
130
+ if actual_name != expected_name:
131
+ errors.append(
132
+ f"set `[package].name` to `{expected_name}` (found `{actual_name}`)"
133
+ )
134
+
135
+ path_key = manifest_key(path)
136
+ features = manifest.get("features")
137
+ if features is not None:
138
+ normalized_features = normalize_feature_mapping(features)
139
+ expected_features = MANIFEST_FEATURE_EXCEPTIONS.get(path_key)
140
+ if expected_features is None:
141
+ errors.append(
142
+ "remove `[features]`; new workspace crate features are not allowed"
143
+ )
144
+ else:
145
+ used_manifest_feature_exceptions.add(path_key)
146
+ if normalized_features != expected_features:
147
+ errors.append(
148
+ "limit `[features]` to the existing exception list while "
149
+ "workspace crate features are being removed "
150
+ f"(expected {render_feature_mapping(expected_features)})"
151
+ )
152
+
153
+ for section_name, dependencies in dependency_sections(manifest):
154
+ for dependency_name, dependency in dependencies.items():
155
+ if not isinstance(dependency, dict):
156
+ continue
157
+
158
+ if dependency.get("optional") is True:
159
+ exception_key = (path_key, section_name, dependency_name)
160
+ if exception_key in OPTIONAL_DEPENDENCY_EXCEPTIONS:
161
+ used_optional_dependency_exceptions.add(exception_key)
162
+ else:
163
+ errors.append(
164
+ "remove `optional = true` from "
165
+ f"`{dependency_entry_label(section_name, dependency_name)}`; "
166
+ "new optional dependencies are not allowed because they "
167
+ "create crate features"
168
+ )
169
+
170
+ if not is_internal_dependency(
171
+ path, dependency_name, dependency, internal_package_names
172
+ ):
173
+ continue
174
+
175
+ dependency_features = dependency.get("features")
176
+ if dependency_features is not None:
177
+ normalized_dependency_features = normalize_string_list(
178
+ dependency_features
179
+ )
180
+ exception_key = (path_key, section_name, dependency_name)
181
+ expected_dependency_features = (
182
+ INTERNAL_DEPENDENCY_FEATURE_EXCEPTIONS.get(exception_key)
183
+ )
184
+ if expected_dependency_features is None:
185
+ errors.append(
186
+ "remove `features = [...]` from workspace dependency "
187
+ f"`{dependency_entry_label(section_name, dependency_name)}`; "
188
+ "new workspace crate feature activations are not allowed"
189
+ )
190
+ else:
191
+ used_internal_dependency_feature_exceptions.add(exception_key)
192
+ if normalized_dependency_features != expected_dependency_features:
193
+ errors.append(
194
+ "limit workspace dependency features on "
195
+ f"`{dependency_entry_label(section_name, dependency_name)}` "
196
+ "to the existing exception list while workspace crate "
197
+ "features are being removed "
198
+ f"(expected {render_string_list(expected_dependency_features)})"
199
+ )
200
+
201
+ if dependency.get("default-features") is False:
202
+ errors.append(
203
+ "remove `default-features = false` from workspace dependency "
204
+ f"`{dependency_entry_label(section_name, dependency_name)}`; "
205
+ "new workspace crate feature toggles are not allowed"
206
+ )
207
+
208
+ return errors
209
+
210
+
211
+ def expected_package_name(path: Path) -> str | None:
212
+ parts = path.relative_to(CARGO_RS_ROOT).parts
213
+ if len(parts) == 2 and parts[1] == "Cargo.toml":
214
+ directory = parts[0]
215
+ return TOP_LEVEL_NAME_EXCEPTIONS.get(
216
+ directory,
217
+ directory if directory.startswith("codex-") else f"codex-{directory}",
218
+ )
219
+ if len(parts) == 3 and parts[0] == "utils" and parts[2] == "Cargo.toml":
220
+ directory = parts[1]
221
+ return UTILITY_NAME_EXCEPTIONS.get(directory, f"codex-utils-{directory}")
222
+ return None
223
+
224
+
225
+ def is_workspace_reference(value: object) -> bool:
226
+ return isinstance(value, dict) and value.get("workspace") is True
227
+
228
+
229
+ def manifest_key(path: Path) -> str:
230
+ return str(path.relative_to(ROOT))
231
+
232
+
233
+ def normalize_feature_mapping(value: object) -> dict[str, tuple[str, ...]] | None:
234
+ if not isinstance(value, dict):
235
+ return None
236
+
237
+ normalized = {}
238
+ for key, features in value.items():
239
+ if not isinstance(key, str):
240
+ return None
241
+ normalized_features = normalize_string_list(features)
242
+ if normalized_features is None:
243
+ return None
244
+ normalized[key] = normalized_features
245
+ return normalized
246
+
247
+
248
+ def normalize_string_list(value: object) -> tuple[str, ...] | None:
249
+ if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
250
+ return None
251
+ return tuple(value)
252
+
253
+
254
+ def render_feature_mapping(features: dict[str, tuple[str, ...]]) -> str:
255
+ entries = [
256
+ f"{name} = {render_string_list(items)}" for name, items in features.items()
257
+ ]
258
+ return ", ".join(entries)
259
+
260
+
261
+ def render_string_list(items: tuple[str, ...]) -> str:
262
+ return "[" + ", ".join(f'"{item}"' for item in items) + "]"
263
+
264
+
265
+ def dependency_sections(manifest: dict) -> list[tuple[str, dict]]:
266
+ sections = []
267
+ for section_name in ("dependencies", "dev-dependencies", "build-dependencies"):
268
+ dependencies = manifest.get(section_name)
269
+ if isinstance(dependencies, dict):
270
+ sections.append((section_name, dependencies))
271
+
272
+ workspace = manifest.get("workspace")
273
+ if isinstance(workspace, dict):
274
+ workspace_dependencies = workspace.get("dependencies")
275
+ if isinstance(workspace_dependencies, dict):
276
+ sections.append(("workspace.dependencies", workspace_dependencies))
277
+
278
+ target = manifest.get("target")
279
+ if not isinstance(target, dict):
280
+ return sections
281
+
282
+ for target_name, tables in target.items():
283
+ if not isinstance(tables, dict):
284
+ continue
285
+ for section_name in ("dependencies", "dev-dependencies", "build-dependencies"):
286
+ dependencies = tables.get(section_name)
287
+ if isinstance(dependencies, dict):
288
+ sections.append((f"target.{target_name}.{section_name}", dependencies))
289
+
290
+ return sections
291
+
292
+
293
+ def dependency_entry_label(section_name: str, dependency_name: str) -> str:
294
+ return f"[{section_name}].{dependency_name}"
295
+
296
+
297
+ def is_internal_dependency(
298
+ manifest_path: Path,
299
+ dependency_name: str,
300
+ dependency: dict,
301
+ internal_package_names: set[str],
302
+ ) -> bool:
303
+ package_name = dependency.get("package", dependency_name)
304
+ if isinstance(package_name, str) and package_name in internal_package_names:
305
+ return True
306
+
307
+ dependency_path = dependency.get("path")
308
+ if not isinstance(dependency_path, str):
309
+ return False
310
+
311
+ resolved_dependency_path = (manifest_path.parent / dependency_path).resolve()
312
+ try:
313
+ resolved_dependency_path.relative_to(CARGO_RS_ROOT)
314
+ except ValueError:
315
+ return False
316
+ return True
317
+
318
+
319
+ def add_unused_exception_errors(
320
+ failures_by_path: dict[str, list[str]],
321
+ used_manifest_feature_exceptions: set[str],
322
+ used_optional_dependency_exceptions: set[tuple[str, str, str]],
323
+ used_internal_dependency_feature_exceptions: set[tuple[str, str, str]],
324
+ ) -> None:
325
+ for path_key in sorted(
326
+ set(MANIFEST_FEATURE_EXCEPTIONS) - used_manifest_feature_exceptions
327
+ ):
328
+ add_failure(
329
+ failures_by_path,
330
+ path_key,
331
+ "remove the stale `[features]` exception from "
332
+ "`MANIFEST_FEATURE_EXCEPTIONS`",
333
+ )
334
+
335
+ for path_key, section_name, dependency_name in sorted(
336
+ OPTIONAL_DEPENDENCY_EXCEPTIONS - used_optional_dependency_exceptions
337
+ ):
338
+ add_failure(
339
+ failures_by_path,
340
+ path_key,
341
+ "remove the stale optional-dependency exception for "
342
+ f"`{dependency_entry_label(section_name, dependency_name)}` from "
343
+ "`OPTIONAL_DEPENDENCY_EXCEPTIONS`",
344
+ )
345
+
346
+ for path_key, section_name, dependency_name in sorted(
347
+ set(INTERNAL_DEPENDENCY_FEATURE_EXCEPTIONS)
348
+ - used_internal_dependency_feature_exceptions
349
+ ):
350
+ add_failure(
351
+ failures_by_path,
352
+ path_key,
353
+ "remove the stale internal dependency feature exception for "
354
+ f"`{dependency_entry_label(section_name, dependency_name)}` from "
355
+ "`INTERNAL_DEPENDENCY_FEATURE_EXCEPTIONS`",
356
+ )
357
+
358
+
359
+ def add_failure(
360
+ failures_by_path: dict[str, list[str]], path_key: str, error: str
361
+ ) -> None:
362
+ failures_by_path.setdefault(path_key, []).append(error)
363
+
364
+
365
+ def workspace_package_names() -> set[str]:
366
+ package_names = set()
367
+ for path in cargo_manifests():
368
+ manifest = load_manifest(path)
369
+ package = manifest.get("package")
370
+ if not isinstance(package, dict):
371
+ continue
372
+ package_name = package.get("name")
373
+ if isinstance(package_name, str):
374
+ package_names.add(package_name)
375
+ return package_names
376
+
377
+
378
+ def load_manifest(path: Path) -> dict:
379
+ return tomllib.loads(path.read_text())
380
+
381
+
382
+ def cargo_manifests() -> list[Path]:
383
+ return sorted(
384
+ path
385
+ for path in CARGO_RS_ROOT.rglob("Cargo.toml")
386
+ if path != CARGO_RS_ROOT / "Cargo.toml"
387
+ )
388
+
389
+
390
+ def manifests_to_verify() -> list[Path]:
391
+ return [CARGO_RS_ROOT / "Cargo.toml", *cargo_manifests()]
392
+
393
+
394
+ if __name__ == "__main__":
395
+ sys.exit(main())
.github/scripts/verify_pypi_release.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Wait for every Python release artifact to become visible on PyPI."""
3
+
4
+ import argparse
5
+ import http.client
6
+ import json
7
+ import time
8
+ import urllib.request
9
+
10
+ from packaging.version import Version
11
+
12
+ RUNTIME_PLATFORM_TAGS = {
13
+ "macosx_10_9_x86_64",
14
+ "macosx_11_0_arm64",
15
+ "manylinux_2_17_aarch64",
16
+ "manylinux_2_17_x86_64",
17
+ "musllinux_1_1_aarch64",
18
+ "musllinux_1_1_x86_64",
19
+ "win_amd64",
20
+ "win_arm64",
21
+ }
22
+
23
+
24
+ def verify_release(package: str, version: str) -> None:
25
+ version = str(Version(version))
26
+ name = package.replace("-", "_")
27
+ if package == "openai-codex-cli-bin":
28
+ expected = {
29
+ f"{name}-{version}-py3-none-{tag}.whl" for tag in RUNTIME_PLATFORM_TAGS
30
+ }
31
+ else:
32
+ expected = {f"{name}-{version}-py3-none-any.whl", f"{name}-{version}.tar.gz"}
33
+
34
+ for attempt in range(30):
35
+ try:
36
+ with urllib.request.urlopen(
37
+ f"https://pypi.org/pypi/{package}/{version}/json", timeout=30
38
+ ) as response:
39
+ data = json.load(response)
40
+ if not isinstance(data, dict) or not isinstance(data.get("urls"), list):
41
+ raise ValueError("Expected a PyPI response with a urls list")
42
+ if any(
43
+ not isinstance(file, dict)
44
+ or not isinstance(file.get("filename"), str)
45
+ for file in data["urls"]
46
+ ):
47
+ raise ValueError("Expected each PyPI file to have a filename")
48
+ actual = {file["filename"] for file in data["urls"]}
49
+ except (
50
+ OSError,
51
+ http.client.HTTPException,
52
+ ValueError,
53
+ ) as error:
54
+ print(f"Could not read {package} {version} from PyPI: {error}.")
55
+ else:
56
+ if actual == expected:
57
+ print(f"All {package} {version} files are available on PyPI.")
58
+ return
59
+ print(f"Missing files: {sorted(expected - actual)}")
60
+ print(f"Unexpected files: {sorted(actual - expected)}")
61
+ if attempt < 29:
62
+ time.sleep(10)
63
+
64
+ raise SystemExit(f"{package} {version} files did not become available on PyPI.")
65
+
66
+
67
+ def main() -> None:
68
+ parser = argparse.ArgumentParser(description=__doc__)
69
+ parser.add_argument("package", choices=["openai-codex-cli-bin", "openai-codex"])
70
+ parser.add_argument("version")
71
+ args = parser.parse_args()
72
+ verify_release(args.package, args.version)
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()
.github/scripts/verify_tui_core_boundary.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ """Verify codex-tui does not depend on or import codex-core directly."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import re
8
+ import sys
9
+ import tomllib
10
+ from pathlib import Path
11
+
12
+
13
+ ROOT = Path(__file__).resolve().parents[2]
14
+ TUI_ROOT = ROOT / "codex-rs" / "tui"
15
+ TUI_MANIFEST = TUI_ROOT / "Cargo.toml"
16
+ FORBIDDEN_PACKAGE = "codex-core"
17
+ FORBIDDEN_SOURCE_PATTERNS = (
18
+ re.compile(r"\bcodex_core::"),
19
+ re.compile(r"\buse\s+codex_core\b"),
20
+ re.compile(r"\bextern\s+crate\s+codex_core\b"),
21
+ )
22
+
23
+
24
+ def main() -> int:
25
+ failures = []
26
+ failures.extend(manifest_failures())
27
+ failures.extend(source_failures())
28
+
29
+ if not failures:
30
+ return 0
31
+
32
+ print("codex-tui must not depend on or import codex-core directly.")
33
+ print(
34
+ "Use the app-server protocol/client boundary instead; temporary embedded "
35
+ "startup gaps belong behind codex_app_server_client::legacy_core."
36
+ )
37
+ print()
38
+ for failure in failures:
39
+ print(f"- {failure}")
40
+
41
+ return 1
42
+
43
+
44
+ def manifest_failures() -> list[str]:
45
+ manifest = tomllib.loads(TUI_MANIFEST.read_text())
46
+ failures = []
47
+ for section_name, dependencies in dependency_sections(manifest):
48
+ if FORBIDDEN_PACKAGE in dependencies:
49
+ failures.append(
50
+ f"{relative_path(TUI_MANIFEST)} declares `{FORBIDDEN_PACKAGE}` "
51
+ f"in `[{section_name}]`"
52
+ )
53
+ return failures
54
+
55
+
56
+ def dependency_sections(manifest: dict) -> list[tuple[str, dict]]:
57
+ sections: list[tuple[str, dict]] = []
58
+ for section_name in ("dependencies", "dev-dependencies", "build-dependencies"):
59
+ dependencies = manifest.get(section_name)
60
+ if isinstance(dependencies, dict):
61
+ sections.append((section_name, dependencies))
62
+
63
+ for target_name, target in manifest.get("target", {}).items():
64
+ if not isinstance(target, dict):
65
+ continue
66
+ for section_name in ("dependencies", "dev-dependencies", "build-dependencies"):
67
+ dependencies = target.get(section_name)
68
+ if isinstance(dependencies, dict):
69
+ sections.append((f"target.{target_name}.{section_name}", dependencies))
70
+
71
+ return sections
72
+
73
+
74
+ def source_failures() -> list[str]:
75
+ failures = []
76
+ for path in sorted(TUI_ROOT.glob("**/*.rs")):
77
+ text = path.read_text()
78
+ for line_number, line in enumerate(text.splitlines(), start=1):
79
+ if any(pattern.search(line) for pattern in FORBIDDEN_SOURCE_PATTERNS):
80
+ failures.append(
81
+ f"{relative_path(path)}:{line_number} imports `codex_core`"
82
+ )
83
+ return failures
84
+
85
+
86
+ def relative_path(path: Path) -> str:
87
+ return str(path.relative_to(ROOT))
88
+
89
+
90
+ if __name__ == "__main__":
91
+ sys.exit(main())
.github/scripts/voice-cygwin-inputs.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Verify the pinned Cygwin build-input archive before offline installation."""
2
+
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ from pathlib import Path, PurePosixPath
7
+ import shutil
8
+ import tarfile
9
+
10
+
11
+ def records(manifest):
12
+ metadata = manifest["metadata"]
13
+ if {item["file"] for item in metadata} != {
14
+ "x86_64/setup.xz",
15
+ "x86_64/setup.xz.sig",
16
+ }:
17
+ raise ValueError("signed setup metadata is required")
18
+ entries = metadata + manifest["packages"]
19
+ expected = {entry["file"]: entry for entry in entries}
20
+ if len(expected) != len(entries):
21
+ raise ValueError("duplicate Cygwin input")
22
+ for name in expected:
23
+ path = PurePosixPath(name)
24
+ if (
25
+ not path.parts
26
+ or path.is_absolute()
27
+ or ".." in path.parts
28
+ or path.as_posix() != name
29
+ or "\\" in name
30
+ or ":" in name
31
+ ):
32
+ raise ValueError(f"unsafe Cygwin input: {name}")
33
+ return expected
34
+
35
+
36
+ def extract(manifest, archive, destination):
37
+ expected = records(manifest)
38
+ pin = manifest["archive"]
39
+ with archive.open("rb") as stream:
40
+ if (
41
+ archive.stat().st_size != pin["bytes"]
42
+ or hashlib.file_digest(stream, "sha256").hexdigest() != pin["sha256"]
43
+ ):
44
+ raise ValueError("Cygwin archive digest or size mismatch")
45
+ stream.seek(0)
46
+ with tarfile.open(fileobj=stream, mode="r:gz") as bundle:
47
+ members = bundle.getmembers()
48
+ if len(members) != len(expected) or {m.name for m in members} != set(
49
+ expected
50
+ ):
51
+ raise ValueError("missing, extra, or duplicate Cygwin inputs")
52
+ for member in members:
53
+ if not member.isfile() or member.issparse():
54
+ raise ValueError(f"non-regular Cygwin input: {member.name}")
55
+ with bundle.extractfile(member) as source:
56
+ if (
57
+ member.size != expected[member.name]["bytes"]
58
+ or hashlib.file_digest(source, "sha512").hexdigest()
59
+ != expected[member.name]["sha512"]
60
+ ):
61
+ raise ValueError(f"Cygwin input mismatch: {member.name}")
62
+ destination.mkdir(parents=True)
63
+ try:
64
+ bundle.extractall(destination, members=members, filter="data")
65
+ except BaseException:
66
+ shutil.rmtree(destination)
67
+ raise
68
+
69
+
70
+ def check_installed(manifest, inventory):
71
+ installed = sorted(
72
+ tuple(line.split())
73
+ for line in inventory.read_text().splitlines()
74
+ if len(line.split()) == 2 and not line.startswith("Package ")
75
+ )
76
+ expected = sorted((item["name"], item["version"]) for item in manifest["packages"])
77
+ if installed != expected:
78
+ raise ValueError("installed Cygwin packages differ from reviewed inputs")
79
+
80
+
81
+ if __name__ == "__main__":
82
+ parser = argparse.ArgumentParser(description=__doc__)
83
+ parser.add_argument("command", choices=("extract", "check-installed"))
84
+ parser.add_argument("--archive", type=Path)
85
+ parser.add_argument("--directory", type=Path)
86
+ parser.add_argument("--inventory", type=Path)
87
+ args = parser.parse_args()
88
+ pinned = json.loads(
89
+ Path(__file__).with_name("voice-cygwin-snapshot.json").read_text()
90
+ )
91
+ if args.command == "extract":
92
+ if args.archive is None or args.directory is None:
93
+ parser.error("extract requires --archive and --directory")
94
+ extract(pinned, args.archive, args.directory)
95
+ else:
96
+ if args.inventory is None:
97
+ parser.error("check-installed requires --inventory")
98
+ check_installed(pinned, args.inventory)
.github/scripts/voice-cygwin-snapshot.json ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schemaVersion": 1,
3
+ "sourceMirror": "https://mirrors.kernel.org/sourceware/cygwin/",
4
+ "cacheDirectory": "https%3a%2f%2fmirrors.kernel.org%2fsourceware%2fcygwin%2f",
5
+ "requestedPackages": [
6
+ "bash",
7
+ "make",
8
+ "automake1.18",
9
+ "coreutils",
10
+ "diffutils",
11
+ "findutils",
12
+ "grep",
13
+ "sed",
14
+ "gawk"
15
+ ],
16
+ "signingFingerprint": "56405CF6FCC81574682A5D561A698DE9E2E56300",
17
+ "archive": {
18
+ "tag": "voice-cygwin-108b38cf67cbb731",
19
+ "name": "cygwin-build-inputs.tar.gz",
20
+ "bytes": 62276721,
21
+ "sha256": "108b38cf67cbb731801e5ca8d8412b8f349961e4e71d8e3d5a1768f1a8d15c50"
22
+ },
23
+ "metadata": [
24
+ {
25
+ "file": "x86_64/setup.xz",
26
+ "bytes": 3598392,
27
+ "sha512": "306bffefa1e86178d9c6ff78a8add2e1af3c96f7cd698d62b58d0237a568e1a9f1b30016ed5891f9fe1d1c8a1e58089c3d9cb8989eb7bed758a928f212078e25"
28
+ },
29
+ {
30
+ "file": "x86_64/setup.xz.sig",
31
+ "bytes": 566,
32
+ "sha512": "7c466da6446bbef67762ff4038dd4986018ed09432bd6ee6ca9e33150874dbb9d61c7e4b06bd925f932a55fda345401224689bbaddc135d7300651f67ec8160b"
33
+ }
34
+ ],
35
+ "sourceArchive": {
36
+ "name": "cygwin-build-sources.tar",
37
+ "bytes": 368824320,
38
+ "sha256": "13a42ea4e6465f77c32f91e4162f41bcfeab7c0eca47701bb6a8b9aca1eb99f4"
39
+ },
40
+ "packages": [
41
+ {"name": "_autorebase", "version": "001091-1", "file": "noarch/release/_autorebase/_autorebase-001091-1.tar.zst", "bytes": 4696, "sha512": "a9416ce992ca57f3a6208f46517520d21dfac61988ac84cbb5b17b76e053a0845c15368ccb8ca9c483d8935f9e3db81568433c0d08d0265ec5e6916ea30f2831"},
42
+ {"name": "alternatives", "version": "1.31-1", "file": "x86_64/release/alternatives/alternatives-1.31-1.tar.xz", "bytes": 123748, "sha512": "94adf3376e7fcaf937594be718f54a99f2a2b66367eb2bb294b6fd4a933a9515954a9e9f309ed74cc62fbb353adb053d8500bbaf42994e4355f7180664afa62c"},
43
+ {"name": "autoconf", "version": "15-3", "file": "noarch/release/autoconf/autoconf-15-3-noarch.tar.zst", "bytes": 2396, "sha512": "edaa6c97c5b50e138acef4b80406bfdd0988da3360cc622be2541593f6e926a4fbe1088ff608ddadfacf275b3924605d54f59e140cc7c79b31f4885e5878ad6f"},
44
+ {"name": "autoconf2.1", "version": "2.13-12", "file": "noarch/release/autoconf2.1/autoconf2.1-2.13-12.tar.bz2", "bytes": 204911, "sha512": "96f8f592fff4665512c0023d6a7844fdc281ba99dc98dee9e55c37523b3a1279ae8fd31559fa6426edb1da7559a483b3eae99ae7911d50646ec60338ef209c18"},
45
+ {"name": "autoconf2.5", "version": "2.69-5", "file": "noarch/release/autoconf2.5/autoconf2.5-2.69-5.tar.zst", "bytes": 341597, "sha512": "3739719f29ad3ab1c1045b72ccbfe6c58e3f7696ac063c2ff4e09e11e1a9015f289fa8fc267a62b43cb2f6b682b83d3b4b6450040e733f9f41d086dc4a2f36b9"},
46
+ {"name": "autoconf2.7", "version": "2.73-1", "file": "noarch/release/autoconf2.7/autoconf2.7-2.73-1-noarch.tar.zst", "bytes": 906344, "sha512": "0493d68a7ea9cd61e403b6b7086b5302bad5bc1cc35715349f59605a795ac7a9f901c444cb387e5f44d1860de3e5d0fcfceee4ee2726d68904cfc5e2db6e90e9"},
47
+ {"name": "automake1.18", "version": "1.18.1-1", "file": "noarch/release/automake1.18/automake1.18-1.18.1-1-noarch.tar.zst", "bytes": 921189, "sha512": "416ef8041505ecddc23e9e72394ef3a81311c6c378b8b6d348e989584c77924d40aa427b47ab4d3a9a2b83438d13995bb7c6302afb73af3d3af7f2e9061579c0"},
48
+ {"name": "base-cygwin", "version": "3.8-2", "file": "noarch/release/base-cygwin/base-cygwin-3.8-2.tar.xz", "bytes": 1260, "sha512": "e03949b4ae6b692858e1d29fb764afbda990c07a6b70b0eb7219fc12f28dfba3cf7e757a7570c31e315be79f091b012d6a6ddafdb4b0b8961afe9af9f4977fab"},
49
+ {"name": "base-files", "version": "4.3-3", "file": "noarch/release/base-files/base-files-4.3-3.tar.zst", "bytes": 46481, "sha512": "dfafbe28fbe89d9b72a2eeb10901712e04de71d9089a85cfd2b8d45ffd739bdd49dadb7b57d098f01b8902456f5b839e9d4a10dbd6a8001573e035fde6797b63"},
50
+ {"name": "bash", "version": "5.2.21-1", "file": "x86_64/release/bash/bash-5.2.21-1.tar.xz", "bytes": 1650932, "sha512": "c3a5594f1c248dd27a3b5aa10c31fb1651072eae878479d3d8cab09c28354ce4509a2fe230f176ad200a6484a561bc10c2dc9c74c4e9f03164190dac2e683d65"},
51
+ {"name": "bzip2", "version": "1.0.8-2", "file": "x86_64/release/bzip2/bzip2-1.0.8-2-x86_64.tar.xz", "bytes": 36396, "sha512": "86e4b0b21b64d17b9fb60260a787626125102b0cb786de67893cb1d39a109457637bdb93956c992429f1135f64b07def64dd0116682a6190b635ba12f81395d7"},
52
+ {"name": "ca-certificates", "version": "2025.2.80_v9.0.304-1", "file": "noarch/release/ca-certificates/ca-certificates-2025.2.80_v9.0.304-1-noarch.tar.zst", "bytes": 902180, "sha512": "49d08f490db12855f8794cd5cfc721940504d36deffdd3b9aea0fc75909e27471f795d48c739630498fd4baeecb76ee0adb5e5c4cecab4db2ea39b03391a02d0"},
53
+ {"name": "coreutils", "version": "9.0-1", "file": "x86_64/release/coreutils/coreutils-9.0-1.tar.xz", "bytes": 2767296, "sha512": "530f47640d878f5953580f3556c492a93786191abe573322026b70206a46c7f17d05927784db3e7f2c044d1555f35941880316f7288b887eaec770a9eda4c687"},
54
+ {"name": "crypto-policies", "version": "20190218-1", "file": "noarch/release/crypto-policies/crypto-policies-20190218-1.tar.xz", "bytes": 14032, "sha512": "44d50ff08528df786eeb26ec1d312445ec92d77ecc9f448a3b741f7647e187f11eabdc0b38f8ab8de803e21d2e610f67eb44de7554f3fa83f6e8172e609b0054"},
55
+ {"name": "cygutils", "version": "1.4.17-4", "file": "x86_64/release/cygutils/cygutils-1.4.17-4-x86_64.tar.xz", "bytes": 90580, "sha512": "a9c63f8ac86e529fa8809c9637bb80651721003160726e2058c361687e5df6377c4cdbea2d023677b23e1d166bbce37d5a412f7d1135baf075ae2631fdeb77b2"},
56
+ {"name": "cygwin", "version": "3.6.10-1", "file": "x86_64/release/cygwin/cygwin-3.6.10-1-x86_64.tar.xz", "bytes": 1626060, "sha512": "50e5a100f5229eecba279799363a30d599f8c6ad06967eabc4fc8ed9105736576a522539abc250110282cf18da8536d4e408823cc6c0d2abd8692a91d9fd3341"},
57
+ {"name": "dash", "version": "0.5.12-5", "file": "x86_64/release/dash/dash-0.5.12-5.tar.xz", "bytes": 79612, "sha512": "8891165224ca8531ad7b601309bc1a74f8b68dad24721060fd3db5d12ae504e3c96bbd0217006bbcbe0650d13283f04c1f58412bde54c40f19acb5291dfaced9"},
58
+ {"name": "diffutils", "version": "3.12-1", "file": "x86_64/release/diffutils/diffutils-3.12-1-x86_64.tar.xz", "bytes": 431400, "sha512": "2a9d8fa0acd5a6108f74afe515f55a2a7cade6fa7f96368e46a6f33cf3b49d270b50dc3cee0320740c8f1729edcbf8dd3824f4b268261fba7aeed0d75a4e943c"},
59
+ {"name": "editrights", "version": "1.04-1", "file": "x86_64/release/editrights/editrights-1.04-1.tar.xz", "bytes": 7112, "sha512": "aadc188201441ee1ff5710df0e5550c8ca5cb65421d025c5845921510560a2875c317d1f35b6ea8abbb5077172c0de7fc8f2f55ae95dc8d1e59d26c963c93aa7"},
60
+ {"name": "file", "version": "5.46-1", "file": "x86_64/release/file/file-5.46-1-x86_64.tar.xz", "bytes": 840940, "sha512": "e4b52477d353557d36af625a6a55c97a11ae41053ce17602624210df2a6d2683f46250c2b9f6db5a9f9e5fcccb7b0be4b24705a36d196cee677bf47d90dfee99"},
61
+ {"name": "findutils", "version": "4.11.0-1", "file": "x86_64/release/findutils/findutils-4.11.0-1-x86_64.tar.xz", "bytes": 768172, "sha512": "cbb1359fd81c55f98da1485f84f11f905a7a3fe9ce1676424e86da4d7f0effa5fc298546bd5c62c9707e8ddf93c31597d6da01e60be0610c50b9b22ef9475814"},
62
+ {"name": "gawk", "version": "5.4.0-1", "file": "x86_64/release/gawk/gawk-5.4.0-1-x86_64.tar.xz", "bytes": 1496256, "sha512": "3604c99921d0cff387279aec1d3b332e8afcd2746b052304c0511221efdb6ae6d4a3117b36593e44adc43c67b6f6c4c48bccf6b686e2e1a5cac644c89613d829"},
63
+ {"name": "getent", "version": "2.18.90-5", "file": "x86_64/release/getent/getent-2.18.90-5.tar.xz", "bytes": 17864, "sha512": "b860f3d01f32da7312a0ce118bdaa53e2418c53f0ac5cfd52b3e4267f353fa21ae27fd2649eb9c6bff48b3d10a0a9f7bcaa2d644f770f0f3916f5d969beedd65"},
64
+ {"name": "gettext-locale-alias", "version": "0.26-1", "file": "x86_64/release/gettext/gettext-locale-alias/gettext-locale-alias-0.26-1-x86_64.tar.xz", "bytes": 1596, "sha512": "289feafbe00598a2451527b64b0187ab62115577ceaa960774b020270e5a8c5bff4038f389a44c7d9eb870f7e2741c7bb2b27be507843cc8c4ae5c33bc74c8f3"},
65
+ {"name": "grep", "version": "3.12-1", "file": "x86_64/release/grep/grep-3.12-1-x86_64.tar.xz", "bytes": 418788, "sha512": "2ba28b5d93c352bc3f3f0f232e2052e6651b71cc535841f4ff8cedb23e61e9e4c030d7d5516ed9c797074e1a59cbf3a56732ca678cb0e986e86dcbbf25955976"},
66
+ {"name": "groff", "version": "1.24.1-1", "file": "x86_64/release/groff/groff-1.24.1-1-x86_64.tar.zst", "bytes": 5878965, "sha512": "9e8aaab0e6f464df3fec6b96fed6679673091aaeafaa463ed518081cb156ef7441fb5f9341a251f602334862640ea0b0fc1b158746c08a1e2529cceeb37e9802"},
67
+ {"name": "gzip", "version": "1.14-1", "file": "x86_64/release/gzip/gzip-1.14-1-x86_64.tar.xz", "bytes": 164124, "sha512": "57995d2e90bf435ffff8f61cd79c4970ad7260e0c407ab52d4dbacb0540a74c7f8fd0d8bd905316eff31525d9b0367284f9f250950f78f179e5260ce59be3a98"},
68
+ {"name": "hostname", "version": "3.13-1", "file": "x86_64/release/hostname/hostname-3.13-1.tar.bz2", "bytes": 14024, "sha512": "75114326fa4575b8341434658a881feb81a6565e7e0adf8fd100dde87022d3818b5fc3df430bd9b3298d818a1d23f6b6c4bc7ff33efb3ded5f0ea0bf118c1aae"},
69
+ {"name": "info", "version": "7.3-1", "file": "x86_64/release/texinfo/info/info-7.3-1-x86_64.tar.zst", "bytes": 607040, "sha512": "3af0e7c2edc34a068d88974c6c1b0f529663f4bbce145b5a33f3b7e90671fd908fb4a544e033b98a6906d3244c64a18732104c5f1d323c0ccac8a5134e13f411"},
70
+ {"name": "ipc-utils", "version": "1.1-1", "file": "x86_64/release/ipc-utils/ipc-utils-1.1-1.tar.xz", "bytes": 11120, "sha512": "374ef40c871d00eb3b4774c383bb85872443a2e8b9abf74b8b9b9b27984598abfb6284716c2d2cd264181e6d012cf7dc8cd04f6bab91f9b083f72e9b51e6226b"},
71
+ {"name": "less", "version": "704-1", "file": "x86_64/release/less/less-704-1-x86_64.tar.xz", "bytes": 171024, "sha512": "2d1ecf29272e7c1781bb0df5b0f0531b009972edc8d246d63e17edf7d8314109f0a8f38668b371c6f5e1dc1f948d63384eecdca5409eacb927c4f267f89f4bfe"},
72
+ {"name": "libargp", "version": "20250917-2", "file": "x86_64/release/libargp/libargp-20250917-2-x86_64.tar.xz", "bytes": 19508, "sha512": "a83ef7c1809cae1b10a50e228643a640ea14e1be324429b9f53be9a4c8cee03ddd0efb46b24e159493dfa36569e73b636bc897943d61cdb90511e5e4cbb54257"},
73
+ {"name": "libattr1", "version": "2.5.2-2", "file": "x86_64/release/attr/libattr1/libattr1-2.5.2-2-x86_64.tar.xz", "bytes": 6160, "sha512": "345b8f84b9c4df2eeea5e211dbaff5e0382aaee788435e1f4cc69f546e1603bbb45e9baec02a37d935674fcd55fb782d924c3a1540d2f829bde4969c96945ce4"},
74
+ {"name": "libblkid1", "version": "2.40.2-2", "file": "x86_64/release/util-linux/libblkid1/libblkid1-2.40.2-2.tar.xz", "bytes": 120936, "sha512": "d931c47cce5a7caf18799a16ed526e0f0d0f82c0f6d0af952420d50d88f1e0f0ea9d6c07be72f9d0baa06a3299b2db8c3bd3c43d13d94400edc816e7c00da148"},
75
+ {"name": "libbz2_1", "version": "1.0.8-2", "file": "x86_64/release/bzip2/libbz2_1/libbz2_1-1.0.8-2-x86_64.tar.xz", "bytes": 26884, "sha512": "d15d88e8e0fae69f7c4c3025515e748c273d758a0f470312162cec46c4cf0d340d2ef036e3a4f4d03b5edf4508b4927834ffa0f9aa45585a5f3bc3b3163ff675"},
76
+ {"name": "libcrypt2", "version": "4.5.2-1", "file": "x86_64/release/libxcrypt/libcrypt2/libcrypt2-4.5.2-1-x86_64.tar.xz", "bytes": 106104, "sha512": "7668d29aaebba8ca94e48e810d92108db72b067b2786cea8dcf4a049c29f20f0aeb8653a6e9524869c69e0b1df9d4d0ac499a052a86f0d44178e8a8167002c97"},
77
+ {"name": "libdb18.1", "version": "18.1.40-1", "file": "x86_64/release/db/libdb18.1/libdb18.1-18.1.40-1.tar.xz", "bytes": 733872, "sha512": "51955ae95823c54051b5345a7dbe6927521411754dff416ad6af2061bd3c9b2230ca2ea88f9b921b4b09b7654c7dac924b0ae08af708a93604cc193d6a18f4ff"},
78
+ {"name": "libfdisk1", "version": "2.40.2-2", "file": "x86_64/release/util-linux/libfdisk1/libfdisk1-2.40.2-2.tar.xz", "bytes": 160264, "sha512": "0c8e6854eb72bb8cc51fc33565ee0d36a747e520e98e074a1d2400672a45651807b53be0f5a1c97efa0ce34975063f86953c9465312dcfdaba8104edb1298d62"},
79
+ {"name": "libffi8", "version": "3.8.0-1", "file": "x86_64/release/libffi/libffi8/libffi8-3.8.0-1-x86_64.tar.xz", "bytes": 196924, "sha512": "935a0567f740a314d70ecd4adc8c4cf5ad37c6fb43f2d26dee882d4fda71f08efb2646bee10cee6e2000f52e25cce87fa29ce07665ba6d22532e0daa76bc887c"},
80
+ {"name": "libgc1", "version": "8.2.12-1", "file": "x86_64/release/libgc/libgc1/libgc1-8.2.12-1-x86_64.tar.xz", "bytes": 62392, "sha512": "faf226ea2b58949da714e2c50f1aa95738082c3499e2ae4828af5173bdd617248f000bf4868636b31a2b76b3a905057ab307c4f6a0d7112f23422531a97be92d"},
81
+ {"name": "libgcc1", "version": "14.4.0-1", "file": "x86_64/release/gcc/libgcc1/libgcc1-14.4.0-1-x86_64.tar.zst", "bytes": 59166, "sha512": "5ac8a56e5eee285efcd6dbd05a473e795e181021d7194b8a98dfe2fa201a71f0abe2908e0b4c81490eb5ef472efaeac8e56a61a5f289eee4ad6e90e52a844ff8"},
82
+ {"name": "libgdbm6", "version": "1.26-1", "file": "x86_64/release/gdbm/libgdbm6/libgdbm6-1.26-1-x86_64.tar.xz", "bytes": 25436, "sha512": "8ed666a5b978b40f8cfc69d270d47cd674a31eb41bac499f783137eedfde6aa02ba4346de3d5dc3184d9af10ec80f57730c7517a88732616f4c6e0ef637d4bce"},
83
+ {"name": "libgdbm_compat4", "version": "1.26-1", "file": "x86_64/release/gdbm/libgdbm_compat4/libgdbm_compat4-1.26-1-x86_64.tar.xz", "bytes": 3892, "sha512": "fd026dd74c495c1ee67b790269e77e8525ad78d52d4895923443feab55f7bb32dc07b115d490143c7e0277167d9cdbef5dad4707ecc7ca6df13a9f1a5184f8ca"},
84
+ {"name": "libgmp10", "version": "6.3.0-1", "file": "x86_64/release/gmp/libgmp10/libgmp10-6.3.0-1.tar.zst", "bytes": 254680, "sha512": "947cd56e1945ddf180622b453f17e6655b4ee3e79fcc91238370a8826b88cd07c31d939f1ed7b87d91344d46fee90af2560105267a9207dcca800cba72459191"},
85
+ {"name": "libguile3.0_1", "version": "3.0.11-1", "file": "x86_64/release/guile3.0/libguile3.0_1/libguile3.0_1-3.0.11-1-x86_64.tar.xz", "bytes": 6969492, "sha512": "a559914ed5d62d0a701e4ce3810276436cf1ce54d579c72fc72c3b4688f2af71ef93f3cdae7089ad7f85107c7ca5c87f2e497cac6f2550b38df1bc6ba7f16230"},
86
+ {"name": "libiconv2", "version": "1.19-2", "file": "x86_64/release/libiconv/libiconv2/libiconv2-1.19-2-x86_64.tar.xz", "bytes": 555664, "sha512": "a1b23e24e0a495e7163923bcf40a4b933403e0826cfc0d9bde785bc92c909b8c4fb94b1e6b2c0242f4328a723855e72ff3baab9f557ef855b7fe7a7d901b0943"},
87
+ {"name": "libintl8", "version": "0.22.5-1", "file": "x86_64/release/gettext/libintl8/libintl8-0.22.5-1.tar.xz", "bytes": 43256, "sha512": "370a19af888bc7c9ed95cf8ade0d36d5e7cdb634abdebcba260cad61fa9a4cc05b5e748e315c9709f66861e8756b227d6757d529846591189e19ee2b2d00002d"},
88
+ {"name": "liblastlog2", "version": "2.40.2-2", "file": "x86_64/release/util-linux/liblastlog2/liblastlog2-2.40.2-2.tar.xz", "bytes": 4908, "sha512": "89350921fe76c9065662dd9d72dd464d2f404f1690e806210cbb828ae8325ea5f3f9909d8eb04c8342b80417a4e1e22c3569241515762ad081ecd58d56927c02"},
89
+ {"name": "liblz4_1", "version": "1.9.4-1", "file": "x86_64/release/lz4/liblz4_1/liblz4_1-1.9.4-1.tar.xz", "bytes": 53296, "sha512": "70e6b0f3af2c5e04089c955b46c13293ab935ee4683e4690da0cebfffba4ba91614004aede99dd804f54e4def93461159f55d151e7fbb647d1ca97ca29e619cc"},
90
+ {"name": "liblzma5", "version": "5.8.3-1", "file": "x86_64/release/xz/liblzma5/liblzma5-5.8.3-1-x86_64.tar.zst", "bytes": 87321, "sha512": "7399c6d117f23788a7edb11cf7f5eec77724aad2bf7cf8a437001fc1515941f6ca415f42363981be35361290f43cd60cd04e15bb1b01cffc0fd275f3167037e0"},
91
+ {"name": "libmpfr6", "version": "4.2.2-1", "file": "x86_64/release/mpfr/libmpfr6/libmpfr6-4.2.2-1.tar.zst", "bytes": 284416, "sha512": "8203dc61bf646041c71f7fece9fe59422ded96ac28dd24826ffb00a9607f274f3226673f9bee441f32b424a7e26c2f178281bd7836db26b0253d37da208d06a8"},
92
+ {"name": "libncursesw10", "version": "6.5+20240427-1", "file": "x86_64/release/ncurses/libncursesw10/libncursesw10-6.5+20240427-1.tar.xz", "bytes": 393928, "sha512": "08102f7a7083cb5663fecc46d819f2ece4123444a8f94c42300f03f7eebd40395f556c44b22ab58bf57321b44d9b8fb8b19358e535c7500d36e52d48b05dcca8"},
93
+ {"name": "libp11-kit0", "version": "0.26.5-1", "file": "x86_64/release/p11-kit/libp11-kit0/libp11-kit0-0.26.5-1-x86_64.tar.xz", "bytes": 212700, "sha512": "dd64f129fe71233e26e0845b6711db5cf2eb2e6d2d050f07b6aec0d9dd54d980e87390614744fbf022cf57aa2b3f7edbdc30c548c2f9d480188c22bcde0c2eba"},
94
+ {"name": "libpcre1", "version": "8.45-1", "file": "x86_64/release/pcre/libpcre1/libpcre1-8.45-1.tar.zst", "bytes": 166711, "sha512": "85c793ed3622a00b811773b94c45376882513e267b9f1d2a3efa0bc42ff86a4e4bebd8362a193d302e66ae9e5c9b93befc1bb1096c0fd397e2cd0f5632c808dd"},
95
+ {"name": "libpcre2_8_0", "version": "10.47-1", "file": "x86_64/release/pcre2/libpcre2_8_0/libpcre2_8_0-10.47-1-x86_64.tar.zst", "bytes": 237134, "sha512": "a98b38e1591c19404a5794cef488810747d86cc27ce5ee326e706cfa7069cf088d8a64571933f4c1f7a611fd8c232008837271bebe2e81c04a3f6ec571f0009e"},
96
+ {"name": "libpipeline1", "version": "1.5.8-1", "file": "x86_64/release/libpipeline/libpipeline1/libpipeline1-1.5.8-1-x86_64.tar.xz", "bytes": 24880, "sha512": "1d062ac4909d37fbac3ce66b07ac691df0ae5acbd01f08c5019a5ed897cb2bbef95f9e347c5bc71185ae3317d89383306e46f2c59acae5d28a18fa65f66d4e80"},
97
+ {"name": "libpopt-common", "version": "1.19-1", "file": "x86_64/release/popt/libpopt-common/libpopt-common-1.19-1.tar.xz", "bytes": 18536, "sha512": "4d69e6d7e196cdb4caad558c5d7543f706a4c6d7053476be861be07672506e378084126f22903deab8113557f757d2a07578f4a790ae316a2ea50ba612148a43"},
98
+ {"name": "libpopt0", "version": "1.19-1", "file": "x86_64/release/popt/libpopt0/libpopt0-1.19-1.tar.xz", "bytes": 18748, "sha512": "e820a7bac13cdd3bff07673ac54dcb80f342c6a32917035f218ef255bdb707d654ee6a1684242d7ce8439eebf050ade6566f27f5118896fbf8e0171173b5bfee"},
99
+ {"name": "libreadline7", "version": "8.2-2", "file": "x86_64/release/readline/libreadline7/libreadline7-8.2-2.tar.xz", "bytes": 113340, "sha512": "1d27576cf49b056f35f90ad933e39c5d946bc271ddcd109daa98f95b97123b428256827889ff45ca8a76409cdb1afd0146e99c9b70150b302538356512acfe38"},
100
+ {"name": "libreadline8", "version": "8.3-1", "file": "x86_64/release/readline/libreadline8/libreadline8-8.3-1-x86_64.tar.xz", "bytes": 120084, "sha512": "64142c5f305b9836c5be9a4271e4163d8b3e739a0451c0d152373eeb3338342f7cf5691e4437b8e6d0a8404c30ea7a21832e342e7a1ca3b5f3e8e3403051b96e"},
101
+ {"name": "libsmartcols1", "version": "2.40.2-2", "file": "x86_64/release/util-linux/libsmartcols1/libsmartcols1-2.40.2-2.tar.xz", "bytes": 96124, "sha512": "71ded6f02d264343d068b90450d96f4fdeb4087f278b990709aaa814c16123f4ae947714e5f10020260c30a6d78865c2d35d9ffe57a51b038815db52c3274317"},
102
+ {"name": "libsqlite3_0", "version": "3.49.1-1", "file": "x86_64/release/sqlite3/libsqlite3_0/libsqlite3_0-3.49.1-1.tar.xz", "bytes": 686212, "sha512": "cd81686fe5778a94ab91e4aebdd2a229cb963318ce7b393e0a0299e29ed7648c24135733c1978cbebbb926813d7f994b9e38318ede12c72fe79ad404dbfdeaf2"},
103
+ {"name": "libssl3", "version": "3.5.7-1", "file": "x86_64/release/openssl/libssl3/libssl3-3.5.7-1-x86_64.tar.zst", "bytes": 2081790, "sha512": "18161f1292404554742ced4beaa10548c7e1bbfb9398ec0b733e632401dfaa49c33336ad4b1e80e2b7bd77c20f00c10196c5804c327e37f641d49da2288837b8"},
104
+ {"name": "libstdc++6", "version": "14.4.0-1", "file": "x86_64/release/gcc/libstdc++6/libstdc++6-14.4.0-1-x86_64.tar.zst", "bytes": 651767, "sha512": "3d86409193c952618eee7e2dea32bdeb97f22ee800e13cc46f98b5095d4d0b1b31ac4336dc5819abb2d553ad1c2151619e9f6f337dfe921ba80fda1323f6f4e3"},
105
+ {"name": "libtasn1_6", "version": "4.21.0-1", "file": "x86_64/release/libtasn1/libtasn1_6/libtasn1_6-4.21.0-1-x86_64.tar.xz", "bytes": 32072, "sha512": "d28713b27d5d41408b29caed785e29380da4d8b4ccdc1c479ae13906e6765bedc6d6b9e6c1c22fd2111d678fcf1663187d3de27d7ad2e104cd48c38086189e36"},
106
+ {"name": "libuchardet0", "version": "0.0.8-1", "file": "x86_64/release/uchardet/libuchardet0/libuchardet0-0.0.8-1.tar.zst", "bytes": 72637, "sha512": "1fad5711590a82a8386c2492286dd9c0ad7cbe086aab249d07ad27277757327a6a67264e9718aef7d30307515462fe67d64f23582f08f0ced85a19cc8e0afe23"},
107
+ {"name": "libunistring-devel", "version": "1.4.1-1", "file": "x86_64/release/libunistring/libunistring-devel/libunistring-devel-1.4.1-1-x86_64.tar.xz", "bytes": 43332, "sha512": "14e067719e5c0a973950955a05685c18d6fa97df5defa8d0915125ea3af1d01ac921c2a84565a49cc7fb785b15a8be4640ee209f3f0d2e13cb542eb6c75b51f9"},
108
+ {"name": "libunistring5", "version": "1.4.1-1", "file": "x86_64/release/libunistring/libunistring5/libunistring5-1.4.1-1-x86_64.tar.xz", "bytes": 498220, "sha512": "cbd454f5068bfb9291d440a3b03b0c58437aa0342a199007d5cce6fa006b6b5ea4d3ba10c136fb0257ffb0d0199db73484275c632506e4ec31c81e1746667b80"},
109
+ {"name": "libuuid1", "version": "2.40.2-2", "file": "x86_64/release/util-linux/libuuid1/libuuid1-2.40.2-2.tar.xz", "bytes": 12532, "sha512": "3ba0c5d5a401c88796077e9f45164532408882f2f5d9f0ab99b2fdf6eb915917250124cde3653a7c53b648866e091aa3baf3304d0b243337f0d4e254a7631f55"},
110
+ {"name": "libzstd1", "version": "1.5.7-1", "file": "x86_64/release/zstd/libzstd1/libzstd1-1.5.7-1.tar.zst", "bytes": 253786, "sha512": "f89cdaa004487ec8c4796b8f56887c81f6f497873c1495b9cb585ffe6c8d6e6b8ad4b52617d5238075f965f1fe001da287d9a00d121590274b2d87672b415a0f"},
111
+ {"name": "login", "version": "1.13-1", "file": "x86_64/release/login/login-1.13-1.tar.xz", "bytes": 17264, "sha512": "66acc3255962a27af8d701a8c02846f3b9180b7b1d65d946f6fb27cd24aeedc8e6a9a2601b419e981c94b356ef376a5c565ea31a4a220e5add9d70ae501a11d1"},
112
+ {"name": "m4", "version": "1.4.21-1", "file": "x86_64/release/m4/m4-1.4.21-1-x86_64.tar.xz", "bytes": 363204, "sha512": "0d86c88d4bbbfc59e136bba3101ca3714139fba2ab7fea2883bc773ca884c3876f8a8f3a2ea8b05c29fd5280c1fb2d686401346f2761160917edf0818a2b232b"},
113
+ {"name": "make", "version": "4.4.1-2", "file": "x86_64/release/make/make-4.4.1-2.tar.xz", "bytes": 598004, "sha512": "9a89a0f4ceadf5c2d0390029d16274fbed0190d2410ad6beb76e39b7153f38d0d1169ad0abf57c96929e6ab0523a0a76c345868e9ae46f9cd3d708d23f6fa3e6"},
114
+ {"name": "man-db", "version": "2.13.1-1", "file": "x86_64/release/man-db/man-db-2.13.1-1-x86_64.tar.zst", "bytes": 1301518, "sha512": "a1e8a0d6d5a75465e32be83c13e685a73f084fa41abc5500d9437837a2c9de2f897827d3516c1076fa6cdd07a3d1aaaa4751f3b393fcfe64c370ff8f6b8b7f84"},
115
+ {"name": "mintty", "version": "3.8.3-1", "file": "x86_64/release/mintty/mintty-3.8.3-1-x86_64.tar.xz", "bytes": 1190356, "sha512": "79068708c81a7bf6abba3ad1cd0550a2c3eb6f6eea21adade8b0d2b77339cd2519716f528159da9f0972f93bb624867e394d782c3e3b0b7e841a8db985395b92"},
116
+ {"name": "ncurses", "version": "6.5+20240427-1", "file": "x86_64/release/ncurses/ncurses-6.5+20240427-1.tar.xz", "bytes": 111396, "sha512": "e15664bdab3f4d82bac6481deaa66476adbe34404937079983b5165930fa18cec3aa0345b93fbb2c390f8304ef20f041cd47d268552b92d01a51adb6abde0fc5"},
117
+ {"name": "openssl", "version": "3.5.7-1", "file": "x86_64/release/openssl/openssl-3.5.7-1-x86_64.tar.zst", "bytes": 1392875, "sha512": "c50821b61eec47d58d1624a4a3d5a4013c80a8b48cdc803d81a656e6859079d321b34fa0ff97db4c093735a76b71a68c8a7f12184b2c4f0677014e910d1ba9d3"},
118
+ {"name": "p11-kit", "version": "0.26.5-1", "file": "x86_64/release/p11-kit/p11-kit-0.26.5-1-x86_64.tar.xz", "bytes": 332724, "sha512": "1f057de789a5553f04649c80e6f78385135b57de7ba078e9dd51f2db60afee37717bb6de9dcf0a330cf72fce1359e08c0186988632b5f858992ebf95739bb840"},
119
+ {"name": "p11-kit-trust", "version": "0.26.5-1", "file": "x86_64/release/p11-kit/p11-kit-trust/p11-kit-trust-0.26.5-1-x86_64.tar.xz", "bytes": 110096, "sha512": "2f98ae11e42f69fa4626638f7cefe2ba2fed808ae618093c1e6c568f8a8be79e6690b40e39e20df02a8d363c371cca8b8320fea7b288e8561cd299f15cc5b796"},
120
+ {"name": "perl", "version": "5.44.0-1", "file": "x86_64/release/perl/perl-5.44.0-1-x86_64.tar.zst", "bytes": 5129424, "sha512": "42f80064ff94ea778319a24e011d4a7236944c79d9db22e0ed64a7ca7d9b65bc2df5998130eb64c50aa95c173937776cc42665174c5717e84fe6dca523368f27"},
121
+ {"name": "perl-Algorithm-Diff", "version": "1.2010-5", "file": "noarch/release/perl-Algorithm-Diff/perl-Algorithm-Diff-1.2010-5-noarch.tar.zst", "bytes": 31580, "sha512": "d840b605043448ceab8ded77313e12653c81fd7db08062a99010fb19b334429c15628b1a0a805f88110e7aae4dcd3f667b515c18870ba873169533663f4374de"},
122
+ {"name": "perl-Archive-Zip", "version": "1.680.0-1", "file": "noarch/release/perl-Archive-Zip/perl-Archive-Zip-1.680.0-1-noarch.tar.zst", "bytes": 85917, "sha512": "de23da6dac8aa8c8218c141c845723d092f6eaf2f1e67ced96d7b0385206c2e335b150bb7066bd0c7d257125ad1b3fc7691686dc535c56429b4a0f4535b6c73a"},
123
+ {"name": "perl-Class-Inspector", "version": "1.360.0-1", "file": "noarch/release/perl-Class-Inspector/perl-Class-Inspector-1.360.0-1-noarch.tar.zst", "bytes": 21261, "sha512": "299489c9d0bc86fe8ba625c0ec0530f0e6ecacab3f67b2826760413f6bc58252c6c11f45583063cfcf0944f9127ed2bece9168fd76220e735841f374a57943f5"},
124
+ {"name": "perl-Devel-Cycle", "version": "1.120.0-1", "file": "noarch/release/perl-Devel-Cycle/perl-Devel-Cycle-1.120.0-1-noarch.tar.zst", "bytes": 8884, "sha512": "308211af5b57106629b168be340fd682e23670fd97cc7c7f80c3bac48223984c1a226148c99cf5d904b09e352f1008f13bd4645897223a162a22af819aa7af11"},
125
+ {"name": "perl-File-ShareDir", "version": "1.118.0-1", "file": "noarch/release/perl-File-ShareDir/perl-File-ShareDir-1.118.0-1-noarch.tar.zst", "bytes": 19857, "sha512": "7cb0f5fe4c327e9f870fc9c303f5e7e1483d2d2d012dbb5487f4b08f350170b7eb0a345766bd0a809bb4b8fa138788112eed2528b92718e817a5f21a9b82774d"},
126
+ {"name": "perl-Text-Diff", "version": "1.450.0-1", "file": "noarch/release/perl-Text-Diff/perl-Text-Diff-1.450.0-1-noarch.tar.zst", "bytes": 30982, "sha512": "c19c80564d50d1d8efd44b156013aa5620b79205fd291897b938735c49214c84867a1337dfe9e7857f97e6f924ec919f5a0197df2d3bfeae78e0ec9dc8b9a6ac"},
127
+ {"name": "perl-Win32-API", "version": "0.840.0-1", "file": "x86_64/release/perl-Win32-API/perl-Win32-API-0.840.0-1-x86_64.tar.zst", "bytes": 84514, "sha512": "758b1881d55942e91365780bede1dca63838995ad6574f9a242c15f613e7e3211a029e9bd3a3bd24d834ca6838e7a85c2812411783423ba23ccf808d094b6890"},
128
+ {"name": "perl_autorebase", "version": "5.44.0-1", "file": "x86_64/release/perl/perl_autorebase/perl_autorebase-5.44.0-1-x86_64.tar.zst", "bytes": 175, "sha512": "55cd94604cfa3e2491df45e5a3715f32b2dee8298a656ca2da8de2eba4096329c595228a8c950760e9641b2c6097309571350c0ffe1c8d13759eb76ec841a4f8"},
129
+ {"name": "perl_base", "version": "5.44.0-1", "file": "x86_64/release/perl/perl_base/perl_base-5.44.0-1-x86_64.tar.zst", "bytes": 3772189, "sha512": "3039e7a60c950cd682d8372f948ec83f7ebea27e9b425ab7406ed2eff0bebb80f7622423cf82b9ab07e34deb183bdf1b56fe129c8306bd2cfb04e7cd96beb992"},
130
+ {"name": "rebase", "version": "4.6.6-1", "file": "x86_64/release/rebase/rebase-4.6.6-1.tar.xz", "bytes": 248832, "sha512": "9f3751c4687b09f3c286cf5c4522a0d976fce2d79ab471ab37560b15f14af84f7d13162b6102c02d0d60784448ff0586e76e9291675af4fa0c9320a3713633da"},
131
+ {"name": "run", "version": "1.3.4-2", "file": "x86_64/release/run/run-1.3.4-2.tar.xz", "bytes": 34492, "sha512": "7e3156bbddc5f934fde3530879aa5186c9fdccb757ba848fc958e79e96534b336975f2882e413d5c18174628033a422157d25c48f8b5e90f0f1555ff2fa6343f"},
132
+ {"name": "sed", "version": "4.10-1", "file": "x86_64/release/sed/sed-4.10-1-x86_64.tar.xz", "bytes": 359252, "sha512": "da5475344a1693f90fb560773ccca48eaf13af0e6bbcada9dcea658862bf7f46043b2d7365de0e2e235fb9a874fe784ad83bc29eb2fdbf3207cd8ad5e127b8b9"},
133
+ {"name": "tar", "version": "1.35-2", "file": "x86_64/release/tar/tar-1.35-2.tar.zst", "bytes": 997616, "sha512": "007c7df12bc27ca14862a925680e2b2ff616a5149262b153889e6f580f3db492f98da49b5f34a8c4234194ee25021b67e9a84c987c96c5d6baa7ee0b3ea02425"},
134
+ {"name": "terminfo", "version": "6.5+20240427-1", "file": "x86_64/release/ncurses/terminfo/terminfo-6.5+20240427-1.tar.xz", "bytes": 94760, "sha512": "128fb04f188bbf2af3060416193c9879a9cbbf7e5486febf851f35c3d9d00b9d61a0e2bf4be575d2308fd665a224ce5674485dcfd071125c3a8d199a4b88a042"},
135
+ {"name": "texinfo", "version": "7.3-1", "file": "x86_64/release/texinfo/texinfo-7.3-1-x86_64.tar.zst", "bytes": 1853455, "sha512": "a7a0da042d05bbf2978857fcad1887552db9d77bf745fb8a17e156a2616500cf711adce107610f008ddff5556d02823f148d2bf146bd7eaa8a9466f9300cf2e6"},
136
+ {"name": "tzcode", "version": "2026c-1", "file": "x86_64/release/tzcode/tzcode-2026c-1-x86_64.tar.xz", "bytes": 185512, "sha512": "85ab0f5d8d7c02e38dbd945c99ee6841774d84c486f857cd8484605068441b2028eea1f42a7e36c8fd6801c454f8f786a16f9caeb422afb30e5fcf324434878b"},
137
+ {"name": "tzdata", "version": "2026c-1", "file": "noarch/release/tzdata/tzdata-2026c-1-noarch.tar.xz", "bytes": 184092, "sha512": "567adb6da01050a84b3785a1cb518e62a9c838cfe55e8ab33610c6ee64355677486329de5781361d08f03cd355acd64f94c64ec2a2686d48538813cc405b638c"},
138
+ {"name": "util-linux", "version": "2.40.2-2", "file": "x86_64/release/util-linux/util-linux-2.40.2-2.tar.xz", "bytes": 2053568, "sha512": "4f3473472e32bff586401ec31f1423237c9cb112ae1140cdd8b2797040c5c182632f49f5354eaaa409d2dacba7767d5baa8b8926d7638e2725ad80065b46c10b"},
139
+ {"name": "vim-minimal", "version": "9.1.1825-1", "file": "x86_64/release/vim/vim-minimal/vim-minimal-9.1.1825-1-x86_64.tar.xz", "bytes": 794064, "sha512": "6a252210e2ef08a7038d6e5bc8e5312e0c25d8328ab0aefd7f292eb79b7a32dd699f15f3f293cfbb83687e87cb3f68a335e969b117f55e6ff99b34ebdfbc8119"},
140
+ {"name": "which", "version": "2.23-1", "file": "x86_64/release/which/which-2.23-1-x86_64.tar.xz", "bytes": 31108, "sha512": "98dffe802e1137d7b54359314797fd10080e6c261f37668e32e46aa7b1587b4148a1647b4f4a00cd0d23a9e90f153d5c2081d0ffa4ccea657b73aaa31b538d6e"},
141
+ {"name": "xz", "version": "5.8.3-1", "file": "x86_64/release/xz/xz-5.8.3-1-x86_64.tar.zst", "bytes": 767298, "sha512": "8918389508ade8237226170b6dd02104938ec53b2bb15b310f5005dd64f954dfd67a5391757ba8602afe2a423f901894204771d6ae335402095c074d098daa76"},
142
+ {"name": "zlib0", "version": "1.3.2-1", "file": "x86_64/release/zlib/zlib0/zlib0-1.3.2-1-x86_64.tar.zst", "bytes": 46254, "sha512": "d79ba6098cadf6bf2f32f35d9cd54729293c64b3c9daabb9d080f52861a19db71544d5710fdbd61ff45e7c2ae4eecb3f2e3b8a1e3bc05695b8cfe01c7639c53a"},
143
+ {"name": "zstd", "version": "1.5.7-1", "file": "x86_64/release/zstd/zstd-1.5.7-1.tar.zst", "bytes": 394600, "sha512": "5de9a2fdee75345a35592444d467a1b70504f09f3b2ee5aaf8af8a9d9fda1a4e77d37c5c4c4fee9b96995ef78f9708e6c02adea7a70d9f80f1238e507b2bc0fc"}
144
+ ]
145
+ }
.github/scripts/voice_windows_tools.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Expose verified Windows build-tool installations as local Bazel inputs.
2
+
3
+ This declares the installed support trees; the caller authenticates their
4
+ inputs and verifies the installed package inventory first.
5
+ """
6
+
7
+ import argparse
8
+ import itertools
9
+ import json
10
+ from pathlib import Path
11
+ import stat
12
+
13
+
14
+ def export_repository(root: Path, target: str, pkg_config: Path):
15
+ if target not in ("x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"):
16
+ raise ValueError("unsupported Windows target")
17
+ root = root.resolve(strict=True)
18
+ pkg_config = pkg_config.resolve(strict=True)
19
+ names = {
20
+ "shell": "cygwin/bin/bash.exe",
21
+ "make": "cygwin/bin/make.exe",
22
+ "cygpath": "cygwin/bin/cygpath.exe",
23
+ "automake": "cygwin/bin/automake-1.18",
24
+ "pkg_config": pkg_config.relative_to(root).as_posix(),
25
+ }
26
+ if not names["pkg_config"].startswith("pkgconf-image/"):
27
+ raise ValueError("pkg-config must belong to the extracted image")
28
+ outputs = ("BUILD.bazel", "MODULE.bazel", "voice-tools.json")
29
+ if any((root / name).exists() for name in outputs):
30
+ raise ValueError("Bazel tool repository outputs must be fresh")
31
+ # A nested package boundary would silently remove files from Bazel's glob.
32
+ # Reparse points could escape the installation or hide support directories.
33
+ for directory in ("cygwin", "pkgconf-image"):
34
+ tree = root / directory
35
+ if not tree.is_dir():
36
+ raise ValueError(f"missing tool tree: {directory}")
37
+ for path in itertools.chain((tree,), tree.rglob("*")):
38
+ info = path.lstat()
39
+ if (
40
+ stat.S_ISLNK(info.st_mode)
41
+ or getattr(info, "st_file_attributes", 0)
42
+ & stat.FILE_ATTRIBUTE_REPARSE_POINT
43
+ or not (stat.S_ISDIR(info.st_mode) or stat.S_ISREG(info.st_mode))
44
+ or path.name.casefold() in ("build", "build.bazel")
45
+ ):
46
+ raise ValueError(f"unsupported tool-tree entry: {path}")
47
+ if any(not (root / name).is_file() for name in names.values()):
48
+ raise ValueError("required Windows build tool missing")
49
+ definitions = [
50
+ 'package(default_visibility = ["//visibility:public"])',
51
+ 'filegroup(name = "cygwin", srcs = glob(["cygwin/**"], allow_empty = False))',
52
+ 'filegroup(name = "pkgconf", srcs = glob(["pkgconf-image/**"], allow_empty = False))',
53
+ 'filegroup(name = "tools", srcs = [":cygwin", ":pkgconf", "voice-tools.json"])',
54
+ ]
55
+ definitions.extend(
56
+ f"filegroup(name = {json.dumps(name)}, srcs = [{json.dumps(path)}])"
57
+ for name, path in names.items()
58
+ )
59
+ metadata = {
60
+ "schemaVersion": 1,
61
+ "target": target,
62
+ "cygwinArchitecture": "x86_64",
63
+ "tools": names,
64
+ }
65
+ for name, contents in (
66
+ ("BUILD.bazel", "\n".join(definitions) + "\n"),
67
+ ("MODULE.bazel", 'module(name = "voice_windows_tools")\n'),
68
+ ("voice-tools.json", json.dumps(metadata, indent=2) + "\n"),
69
+ ):
70
+ with (root / name).open("x", encoding="utf-8") as output:
71
+ output.write(contents)
72
+
73
+
74
+ if __name__ == "__main__":
75
+ parser = argparse.ArgumentParser(description=__doc__)
76
+ parser.add_argument("--root", type=Path, required=True)
77
+ parser.add_argument("--target", required=True)
78
+ parser.add_argument("--pkg-config", type=Path, required=True)
79
+ args = parser.parse_args()
80
+ export_repository(args.root, args.target, args.pkg_config)
.github/scripts/watch_voice_bazel.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Capture one bounded diagnostic when the ARM64 voice build goes silent."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ import threading
9
+ import time
10
+
11
+
12
+ def diagnose():
13
+ root = Path(os.environ.get("BAZEL_OUTPUT_BASE", ""))
14
+ if not root.is_absolute():
15
+ print("Bazel output base unavailable; skipping diagnostics", flush=True)
16
+ return
17
+ for relative in ("command.log", "server/jvm.out"):
18
+ try:
19
+ with (root / relative).open("rb") as source:
20
+ source.seek(max(0, source.seek(0, 2) - 16384))
21
+ print(
22
+ f"Bazel {relative} tail:\n{source.read(16384).decode(errors='replace')}",
23
+ flush=True,
24
+ )
25
+ except OSError as error:
26
+ print(f"Cannot read {relative}: {error}", flush=True)
27
+ try:
28
+ pid = str(int((root / "server/server.pid.txt").read_text().strip()))
29
+ jstack = shutil.which("jstack")
30
+ if not jstack:
31
+ print("jstack unavailable; log tails retained", flush=True)
32
+ return
33
+ result = subprocess.run([jstack, pid], capture_output=True, timeout=20)
34
+ print(
35
+ f"jstack exit {result.returncode}:\n{(result.stdout + result.stderr)[-65536:].decode(errors='replace')}",
36
+ flush=True,
37
+ )
38
+ except (OSError, ValueError, subprocess.TimeoutExpired) as error:
39
+ print(f"Cannot capture JVM stacks: {error}", flush=True)
40
+
41
+
42
+ def main():
43
+ with subprocess.Popen(
44
+ sys.argv[1:], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
45
+ ) as process:
46
+ last_output = time.monotonic()
47
+
48
+ def forward():
49
+ nonlocal last_output
50
+ for line in process.stdout:
51
+ last_output = time.monotonic()
52
+ sys.stdout.buffer.write(line)
53
+ sys.stdout.buffer.flush()
54
+
55
+ reader = threading.Thread(target=forward)
56
+ reader.start()
57
+ captured = False
58
+ while process.poll() is None:
59
+ if (
60
+ not captured
61
+ and os.environ.get("VOICE_ARCH") == "aarch64"
62
+ and time.monotonic() - last_output >= 600
63
+ ):
64
+ captured = True
65
+ print(
66
+ "ARM64 Bazel silent for ten minutes; capturing diagnostics",
67
+ flush=True,
68
+ )
69
+ diagnose()
70
+ time.sleep(1)
71
+ reader.join()
72
+ if process.returncode and not captured:
73
+ print("Bazel command failed; capturing diagnostics", flush=True)
74
+ diagnose()
75
+ return process.returncode
76
+
77
+
78
+ if __name__ == "__main__":
79
+ sys.exit(main())
.github/workflows/Dockerfile.bazel ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM ubuntu:24.04
2
+
3
+ # TODO(mbolin): Published to docker.io/mbolin491/codex-bazel:latest for
4
+ # initial debugging, but we should publish to a more proper location.
5
+ #
6
+ # docker buildx create --use
7
+ # docker buildx build --platform linux/amd64,linux/arm64 -f .github/workflows/Dockerfile.bazel -t mbolin491/codex-bazel:latest --push .
8
+
9
+ RUN apt-get update && \
10
+ apt-get install -y --no-install-recommends \
11
+ curl git python3 ca-certificates && \
12
+ rm -rf /var/lib/apt/lists/*
13
+
14
+ # Install dotslash.
15
+ RUN curl -LSfs "https://github.com/facebook/dotslash/releases/download/v0.5.8/dotslash-ubuntu-22.04.$(uname -m).tar.gz" | tar fxz - -C /usr/local/bin
16
+
17
+ # Ubuntu 24.04 ships with user 'ubuntu' already created with UID 1000.
18
+ USER ubuntu
19
+
20
+ WORKDIR /workspace
.github/workflows/README.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Workflow Strategy
2
+
3
+ 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.
4
+
5
+ ## Pull Requests
6
+
7
+ - Required checks run against GitHub's synthetic merge commit, not the pull
8
+ request head alone. This includes changes already on `main` and catches
9
+ conflicts before they reach the branch.
10
+ - `bazel.yml` is the main pre-merge verification path for Rust code.
11
+ It runs Bazel `test` and Bazel `clippy` on the supported Bazel targets,
12
+ including the generated Rust test binaries needed to lint inline `#[cfg(test)]`
13
+ code.
14
+ - `rust-ci.yml` keeps the Cargo-native PR checks intentionally small:
15
+ - `cargo fmt --check`
16
+ - `cargo shear`
17
+ - `argument-comment-lint` on Linux, macOS, and Windows
18
+ - `tools/argument-comment-lint` package tests when the lint or its workflow wiring changes
19
+
20
+ ## Post-Merge On `main`
21
+
22
+ - `bazel.yml` also runs on pushes to `main`.
23
+ This re-verifies the merged Bazel path and helps keep the BuildBuddy caches warm.
24
+ - `rust-ci-full.yml` is the full Cargo-native verification workflow.
25
+ It keeps the heavier checks off the PR path while still validating them after merge:
26
+ - the full Cargo `clippy` matrix
27
+ - the full Cargo `nextest` matrix via per-platform archive-backed shards
28
+ - Windows ARM64 nextest archives cross-compiled on Windows x64, then replayed on native Windows ARM64 shards
29
+ - release-profile Cargo builds
30
+ - cross-platform `argument-comment-lint`
31
+ - Linux remote-env tests
32
+
33
+ ## Rule Of Thumb
34
+
35
+ - If a build/test/clippy check can be expressed in Bazel, prefer putting the PR-time version in `bazel.yml`.
36
+ - Keep `rust-ci.yml` fast enough that it usually does not dominate PR latency.
37
+ - Reserve `rust-ci-full.yml` for heavyweight Cargo-native coverage that Bazel does not replace yet.
.github/workflows/bazel.yml ADDED
@@ -0,0 +1,554 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Bazel
2
+
3
+ # Note this workflow was originally derived from:
4
+ # https://github.com/cerisier/toolchains_llvm_bootstrapped/blob/main/.github/workflows/ci.yaml
5
+
6
+ on:
7
+ workflow_call:
8
+ workflow_dispatch:
9
+
10
+ concurrency:
11
+ # Cancel previous actions from the same PR or branch except 'main' branch.
12
+ # See https://docs.github.com/en/actions/using-jobs/using-concurrency and https://docs.github.com/en/actions/learn-github-actions/contexts for more info.
13
+ group: concurrency-group::${{ github.workflow }}::${{ github.event.pull_request.number > 0 && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}${{ github.ref_name == 'main' && format('::{0}', github.run_id) || ''}}
14
+ cancel-in-progress: ${{ github.ref_name != 'main' }}
15
+
16
+ jobs:
17
+ test:
18
+ # PRs use the sharded Windows cross-compiled test jobs below. Post-merge
19
+ # pushes to main also run the native Windows test job for broader Windows
20
+ # signal without putting PR latency back on the critical path. When
21
+ # Code-mode unit tests run on every Bazel target. When authenticated RBE
22
+ # is available, the Windows-cross shards exercise the source-built V8 path.
23
+ timeout-minutes: 30
24
+ strategy:
25
+ fail-fast: false
26
+ matrix:
27
+ include:
28
+ # macOS
29
+ - os: macos-15-xlarge
30
+ target: aarch64-apple-darwin
31
+ - os: macos-15-xlarge
32
+ target: x86_64-apple-darwin
33
+
34
+ # Linux
35
+ - os: ubuntu-24.04
36
+ target: x86_64-unknown-linux-gnu
37
+ - os: ubuntu-24.04
38
+ target: x86_64-unknown-linux-musl
39
+ # 2026-02-27 Bazel tests have been flaky on arm in CI.
40
+ # Disable until we can investigate and stabilize them.
41
+ # - os: ubuntu-24.04-arm
42
+ # target: aarch64-unknown-linux-musl
43
+ # - os: ubuntu-24.04-arm
44
+ # target: aarch64-unknown-linux-gnu
45
+
46
+ runs-on: ${{ matrix.os }}
47
+
48
+ # Configure a human readable name for each job
49
+ name: Bazel test on ${{ matrix.os }} for ${{ matrix.target }}
50
+ environment:
51
+ name: bazel
52
+ deployment: false
53
+
54
+ steps:
55
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
56
+ with:
57
+ persist-credentials: false
58
+
59
+ - name: Prepare Bazel CI
60
+ id: prepare_bazel
61
+ uses: ./.github/actions/prepare-bazel-ci
62
+ with:
63
+ target: ${{ matrix.target }}
64
+ cache-scope: bazel-${{ github.job }}
65
+
66
+ - name: Check rusty_v8 MODULE.bazel checksums
67
+ if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
68
+ shell: bash
69
+ run: |
70
+ python3 .github/scripts/rusty_v8_bazel.py check-module-bazel
71
+ just test-github-scripts
72
+
73
+ - name: Check MODULE.bazel.lock is up to date
74
+ if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
75
+ shell: bash
76
+ run: ./scripts/check-module-bazel-lock.sh
77
+
78
+ - name: bazel test //...
79
+ env:
80
+ BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
81
+ shell: bash
82
+ run: |
83
+ bazel_targets=(
84
+ //...
85
+ # Keep standalone V8 library targets out of the ordinary Bazel CI
86
+ # path. V8 consumers under `//codex-rs/...` still participate
87
+ # transitively through `//...`.
88
+ -//third_party/v8:all
89
+ -//codex-rs/v8-poc:v8-poc-unit-tests
90
+ )
91
+
92
+ bazel_wrapper_args=(
93
+ --print-failed-action-summary
94
+ --print-failed-test-logs
95
+ )
96
+ bazel_test_args=(
97
+ test
98
+ --test_tag_filters=-argument-comment-lint
99
+ --test_verbose_timeout_warnings
100
+ --build_metadata=COMMIT_SHA=${GITHUB_SHA}
101
+ )
102
+ ./.github/scripts/run-bazel-ci.sh \
103
+ "${bazel_wrapper_args[@]}" \
104
+ -- \
105
+ "${bazel_test_args[@]}" \
106
+ -- \
107
+ "${bazel_targets[@]}"
108
+
109
+ - name: Upload Bazel execution logs
110
+ if: always() && !cancelled()
111
+ continue-on-error: true
112
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
113
+ with:
114
+ name: bazel-execution-logs-test-${{ matrix.target }}
115
+ path: ${{ runner.temp }}/bazel-execution-logs
116
+ if-no-files-found: ignore
117
+
118
+ # Save the job-scoped Bazel repository cache after cache misses. Keep the
119
+ # upload non-fatal so cache service issues never fail the job itself.
120
+ - name: Save bazel repository cache
121
+ if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
122
+ continue-on-error: true
123
+ uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
124
+ with:
125
+ path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
126
+ key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
127
+
128
+ - name: Check for a clean worktree
129
+ if: always() && !cancelled()
130
+ uses: ./.github/actions/check-clean-worktree
131
+
132
+ test-windows-shard:
133
+ # Split the Windows Bazel test leg across separate Windows hosts. Jobs with
134
+ # BuildBuddy credentials use Linux RBE for build actions; test execution
135
+ # remains on a Windows runner.
136
+ timeout-minutes: 30
137
+ strategy:
138
+ fail-fast: false
139
+ matrix:
140
+ shard:
141
+ - 1
142
+ - 2
143
+ - 3
144
+ - 4
145
+ runs-on:
146
+ group: ${{ github.event.repository.name }}-runners
147
+ labels: ${{ github.event.repository.name }}-windows-x64
148
+ name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm shard ${{ matrix.shard }}/4
149
+ environment:
150
+ name: bazel
151
+ deployment: false
152
+
153
+ steps:
154
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
155
+ with:
156
+ persist-credentials: false
157
+
158
+ - name: Test BuildBuddy Bazel wrapper
159
+ if: matrix.shard == 1
160
+ shell: pwsh
161
+ run: python .github/scripts/test_run_bazel_with_buildbuddy.py
162
+
163
+ - name: Prepare Bazel CI
164
+ id: prepare_bazel
165
+ uses: ./.github/actions/prepare-bazel-ci
166
+ with:
167
+ target: x86_64-pc-windows-gnullvm
168
+ # Reuse the former monolithic Windows test cache for restores. Do
169
+ # not save it from every shard below; duplicate uploads would sit on
170
+ # the PR-blocking critical path after the useful test work is done.
171
+ cache-scope: bazel-test
172
+
173
+ - name: bazel test shard
174
+ env:
175
+ BAZEL_TEST_SHARD: ${{ matrix.shard }}
176
+ BAZEL_TEST_SHARD_COUNT: 4
177
+ BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
178
+ shell: bash
179
+ run: |
180
+ set -euo pipefail
181
+
182
+ bazel_test_query='tests(//...) except tests(//third_party/v8:all) except attr(tags, "manual", tests(//...))'
183
+ mapfile -t bazel_targets < <(
184
+ ./.github/scripts/run-bazel-query-ci.sh --output=label -- "${bazel_test_query}" \
185
+ | LC_ALL=C sort
186
+ )
187
+
188
+ selected_targets=()
189
+ for bazel_target in "${bazel_targets[@]}"; do
190
+ target_bucket="$(
191
+ printf '%s\n' "${bazel_target}" \
192
+ | cksum \
193
+ | awk -v shard_count="${BAZEL_TEST_SHARD_COUNT}" '{ print ($1 % shard_count) + 1 }'
194
+ )"
195
+ if [[ "${target_bucket}" == "${BAZEL_TEST_SHARD}" ]]; then
196
+ selected_targets+=("${bazel_target}")
197
+ fi
198
+ done
199
+
200
+ if [[ ${#selected_targets[@]} -eq 0 ]]; then
201
+ echo "No Bazel test targets selected for Windows shard ${BAZEL_TEST_SHARD}/${BAZEL_TEST_SHARD_COUNT}." >&2
202
+ exit 1
203
+ fi
204
+
205
+ echo "Selected ${#selected_targets[@]} of ${#bazel_targets[@]} Bazel test targets for Windows shard ${BAZEL_TEST_SHARD}/${BAZEL_TEST_SHARD_COUNT}."
206
+
207
+ bazel_test_args=(
208
+ test
209
+ --skip_incompatible_explicit_targets
210
+ --test_tag_filters=-argument-comment-lint
211
+ --test_verbose_timeout_warnings
212
+ --build_metadata=COMMIT_SHA=${GITHUB_SHA}
213
+ --build_metadata=TAG_windows_test_shard=${BAZEL_TEST_SHARD}
214
+ )
215
+
216
+ ./.github/scripts/run-bazel-ci.sh \
217
+ --print-failed-action-summary \
218
+ --print-failed-test-logs \
219
+ --windows-cross-compile \
220
+ --remote-download-toplevel \
221
+ -- \
222
+ "${bazel_test_args[@]}" \
223
+ -- \
224
+ "${selected_targets[@]}"
225
+
226
+ - name: Upload Bazel execution logs
227
+ if: always() && !cancelled()
228
+ continue-on-error: true
229
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
230
+ with:
231
+ name: bazel-execution-logs-test-x86_64-pc-windows-gnullvm-shard-${{ matrix.shard }}
232
+ path: ${{ runner.temp }}/bazel-execution-logs
233
+ if-no-files-found: ignore
234
+
235
+ - name: Check for a clean worktree
236
+ if: always() && !cancelled()
237
+ uses: ./.github/actions/check-clean-worktree
238
+
239
+ test-windows:
240
+ # Preserve the existing required-check surface while the real work happens
241
+ # in the sharded Windows jobs above.
242
+ if: always()
243
+ needs: test-windows-shard
244
+ runs-on: ubuntu-24.04
245
+ name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm
246
+
247
+ steps:
248
+ - name: Confirm Windows Bazel test shards passed
249
+ shell: bash
250
+ run: |
251
+ if [[ "${{ needs.test-windows-shard.result }}" != "success" ]]; then
252
+ echo "Windows Bazel test shards finished with result: ${{ needs.test-windows-shard.result }}" >&2
253
+ exit 1
254
+ fi
255
+
256
+ test-windows-native-main:
257
+ # Native Windows Bazel tests are slower and frequently approach the
258
+ # 30-minute PR budget. Run this only for post-merge commits to main and give
259
+ # it a larger timeout.
260
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
261
+ timeout-minutes: 40
262
+ runs-on:
263
+ group: ${{ github.event.repository.name }}-runners
264
+ labels: ${{ github.event.repository.name }}-windows-x64
265
+ name: Bazel test on windows-latest for x86_64-pc-windows-msvc (native main)
266
+ environment:
267
+ name: bazel
268
+ deployment: false
269
+
270
+ steps:
271
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
272
+ with:
273
+ persist-credentials: false
274
+
275
+ - name: Prepare Bazel CI
276
+ id: prepare_bazel
277
+ uses: ./.github/actions/prepare-bazel-ci
278
+ with:
279
+ target: x86_64-pc-windows-msvc
280
+ cache-scope: bazel-${{ github.job }}
281
+
282
+ - name: bazel test //...
283
+ env:
284
+ BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
285
+ shell: bash
286
+ run: |
287
+ bazel_targets=(
288
+ //...
289
+ # Keep standalone V8 library targets out of the ordinary Bazel CI
290
+ # path. V8 consumers under `//codex-rs/...` still participate
291
+ # transitively through `//...`.
292
+ -//third_party/v8:all
293
+ -//codex-rs/v8-poc:v8-poc-unit-tests
294
+ )
295
+
296
+ bazel_test_args=(
297
+ test
298
+ --platforms=//:windows_x86_64_msvc
299
+ --test_tag_filters=-argument-comment-lint
300
+ --test_verbose_timeout_warnings
301
+ --build_metadata=COMMIT_SHA=${GITHUB_SHA}
302
+ --build_metadata=TAG_windows_native_main=true
303
+ )
304
+
305
+ ./.github/scripts/run-bazel-ci.sh \
306
+ --print-failed-action-summary \
307
+ --print-failed-test-logs \
308
+ --windows-msvc-host-platform \
309
+ -- \
310
+ "${bazel_test_args[@]}" \
311
+ -- \
312
+ "${bazel_targets[@]}"
313
+
314
+ - name: Upload Bazel execution logs
315
+ if: always() && !cancelled()
316
+ continue-on-error: true
317
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
318
+ with:
319
+ name: bazel-execution-logs-test-windows-native-x86_64-pc-windows-msvc
320
+ path: ${{ runner.temp }}/bazel-execution-logs
321
+ if-no-files-found: ignore
322
+
323
+ # Save the job-scoped Bazel repository cache after cache misses. Keep the
324
+ # upload non-fatal so cache service issues never fail the job itself.
325
+ - name: Save bazel repository cache
326
+ if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
327
+ continue-on-error: true
328
+ uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
329
+ with:
330
+ path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
331
+ key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
332
+
333
+ - name: Check for a clean worktree
334
+ if: always() && !cancelled()
335
+ uses: ./.github/actions/check-clean-worktree
336
+
337
+ clippy:
338
+ timeout-minutes: 30
339
+ strategy:
340
+ fail-fast: false
341
+ matrix:
342
+ include:
343
+ # Keep Linux lint coverage on x64 and add the arm64 macOS path that
344
+ # the Bazel test job already exercises. Add Windows gnullvm as well
345
+ # so PRs get Bazel-native lint signal on the same Windows toolchain
346
+ # that the Bazel test job uses.
347
+ - os: ubuntu-24.04
348
+ target: x86_64-unknown-linux-gnu
349
+ - os: macos-15-xlarge
350
+ target: aarch64-apple-darwin
351
+ - os: windows-latest
352
+ target: x86_64-pc-windows-gnullvm
353
+ runs_on:
354
+ group: ${{ github.event.repository.name }}-runners
355
+ labels: ${{ github.event.repository.name }}-windows-x64
356
+ runs-on: ${{ matrix.runs_on || matrix.os }}
357
+ name: Bazel clippy on ${{ matrix.os }} for ${{ matrix.target }}
358
+ environment:
359
+ name: bazel
360
+ deployment: false
361
+
362
+ steps:
363
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
364
+ with:
365
+ persist-credentials: false
366
+
367
+ - name: Prepare Bazel CI
368
+ id: prepare_bazel
369
+ uses: ./.github/actions/prepare-bazel-ci
370
+ with:
371
+ target: ${{ matrix.target }}
372
+ cache-scope: bazel-${{ github.job }}
373
+
374
+ - name: bazel build --config=clippy lint targets
375
+ env:
376
+ BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
377
+ shell: bash
378
+ run: |
379
+ bazel_clippy_args=(
380
+ --config=clippy
381
+ --build_metadata=COMMIT_SHA=${GITHUB_SHA}
382
+ --build_metadata=TAG_job=clippy
383
+ )
384
+ bazel_wrapper_args=()
385
+ bazel_target_list_args=()
386
+ if [[ "${RUNNER_OS}" == "Windows" ]]; then
387
+ # Keep this aligned with the fast Windows Bazel test job: use
388
+ # Linux RBE for clippy build actions while targeting Windows
389
+ # gnullvm. Fork/community PRs without the BuildBuddy secret fall
390
+ # back inside `run-bazel-ci.sh` to the previous local Windows MSVC
391
+ # host-platform shape.
392
+ bazel_wrapper_args+=(--windows-cross-compile)
393
+ bazel_target_list_args+=(--windows-cross-compile)
394
+ if [[ -z "${BUILDBUDDY_API_KEY:-}" ]]; then
395
+ # The fork fallback can see incompatible explicit Windows-cross
396
+ # internal test binaries in the generated target list. Preserve
397
+ # the old local-fallback behavior there.
398
+ bazel_clippy_args+=(--skip_incompatible_explicit_targets)
399
+ fi
400
+ fi
401
+
402
+ bazel_target_lines="$(./scripts/list-bazel-clippy-targets.sh "${bazel_target_list_args[@]}")"
403
+ bazel_targets=()
404
+ while IFS= read -r target; do
405
+ bazel_targets+=("${target}")
406
+ done <<< "${bazel_target_lines}"
407
+
408
+ ./.github/scripts/run-bazel-ci.sh \
409
+ --print-failed-action-summary \
410
+ "${bazel_wrapper_args[@]}" \
411
+ -- \
412
+ build \
413
+ "${bazel_clippy_args[@]}" \
414
+ -- \
415
+ "${bazel_targets[@]}"
416
+
417
+ - name: Upload Bazel execution logs
418
+ if: always() && !cancelled()
419
+ continue-on-error: true
420
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
421
+ with:
422
+ name: bazel-execution-logs-clippy-${{ matrix.target }}
423
+ path: ${{ runner.temp }}/bazel-execution-logs
424
+ if-no-files-found: ignore
425
+
426
+ # Save the job-scoped Bazel repository cache after cache misses. Keep the
427
+ # upload non-fatal so cache service issues never fail the job itself.
428
+ - name: Save bazel repository cache
429
+ if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
430
+ continue-on-error: true
431
+ uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
432
+ with:
433
+ path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
434
+ key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
435
+
436
+ - name: Check for a clean worktree
437
+ if: always() && !cancelled()
438
+ uses: ./.github/actions/check-clean-worktree
439
+
440
+ verify-release-build:
441
+ timeout-minutes: 30
442
+ strategy:
443
+ fail-fast: false
444
+ matrix:
445
+ include:
446
+ - os: ubuntu-24.04
447
+ target: x86_64-unknown-linux-gnu
448
+ - os: macos-15-xlarge
449
+ target: aarch64-apple-darwin
450
+ - os: windows-latest
451
+ target: x86_64-pc-windows-gnullvm
452
+ runs_on:
453
+ group: ${{ github.event.repository.name }}-runners
454
+ labels: ${{ github.event.repository.name }}-windows-x64
455
+ runs-on: ${{ matrix.runs_on || matrix.os }}
456
+ name: Verify release build on ${{ matrix.os }} for ${{ matrix.target }}
457
+ environment:
458
+ name: bazel
459
+ deployment: false
460
+
461
+ steps:
462
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
463
+ with:
464
+ persist-credentials: false
465
+
466
+ - name: Prepare Bazel CI
467
+ id: prepare_bazel
468
+ uses: ./.github/actions/prepare-bazel-ci
469
+ with:
470
+ target: ${{ matrix.target }}
471
+ cache-scope: bazel-${{ github.job }}
472
+
473
+ - name: bazel build verify-release-build targets
474
+ env:
475
+ BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
476
+ shell: bash
477
+ run: |
478
+ # This job exists to compile Rust code behind
479
+ # `cfg(not(debug_assertions))` so PR CI catches failures that would
480
+ # otherwise show up only in a release build. We do not need the full
481
+ # optimizer and debug-info work that normally comes with a release
482
+ # build to get that signal, so keep Bazel in `fastbuild` and disable
483
+ # Rust debug assertions explicitly.
484
+ bazel_wrapper_args=()
485
+ if [[ "${RUNNER_OS}" == "Windows" ]]; then
486
+ # This is build-only signal, so use the same Linux-RBE
487
+ # cross-compile path as the fast Windows test and clippy jobs.
488
+ # Fork/community PRs without the BuildBuddy secret fall back
489
+ # inside `run-bazel-ci.sh` to the previous local Windows MSVC
490
+ # host-platform shape.
491
+ bazel_wrapper_args+=(--windows-cross-compile)
492
+ fi
493
+
494
+ bazel_build_args=(
495
+ --compilation_mode=fastbuild
496
+ --@rules_rust//rust/settings:extra_rustc_flag=-Cdebug-assertions=no
497
+ --@rules_rust//rust/settings:extra_exec_rustc_flag=-Cdebug-assertions=no
498
+ --build_metadata=COMMIT_SHA=${GITHUB_SHA}
499
+ --build_metadata=TAG_job=verify-release-build
500
+ --build_metadata=TAG_rust_debug_assertions=off
501
+ )
502
+
503
+ bazel_target_lines="$(bash ./scripts/list-bazel-release-targets.sh)"
504
+ bazel_targets=()
505
+ while IFS= read -r target; do
506
+ bazel_targets+=("${target}")
507
+ done <<< "${bazel_target_lines}"
508
+
509
+ ./.github/scripts/run-bazel-ci.sh \
510
+ "${bazel_wrapper_args[@]}" \
511
+ -- \
512
+ build \
513
+ "${bazel_build_args[@]}" \
514
+ -- \
515
+ "${bazel_targets[@]}"
516
+
517
+ - name: Verify Bazel builds bwrap
518
+ if: runner.os == 'Linux'
519
+ env:
520
+ BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
521
+ shell: bash
522
+ run: |
523
+ ./.github/scripts/run-bazel-ci.sh \
524
+ --remote-download-toplevel \
525
+ --print-failed-action-summary \
526
+ -- \
527
+ build \
528
+ --build_metadata=COMMIT_SHA=${GITHUB_SHA} \
529
+ --build_metadata=TAG_job=verify-bwrap \
530
+ -- \
531
+ //codex-rs/bwrap:bwrap
532
+
533
+ - name: Upload Bazel execution logs
534
+ if: always() && !cancelled()
535
+ continue-on-error: true
536
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
537
+ with:
538
+ name: bazel-execution-logs-verify-release-build-${{ matrix.target }}
539
+ path: ${{ runner.temp }}/bazel-execution-logs
540
+ if-no-files-found: ignore
541
+
542
+ # Save the job-scoped Bazel repository cache after cache misses. Keep the
543
+ # upload non-fatal so cache service issues never fail the job itself.
544
+ - name: Save bazel repository cache
545
+ if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
546
+ continue-on-error: true
547
+ uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
548
+ with:
549
+ path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
550
+ key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
551
+
552
+ - name: Check for a clean worktree
553
+ if: always() && !cancelled()
554
+ uses: ./.github/actions/check-clean-worktree
.github/workflows/blob-size-policy.yml ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: blob-size-policy
2
+
3
+ on:
4
+ workflow_call:
5
+
6
+ jobs:
7
+ check:
8
+ name: Blob size policy
9
+ runs-on: ubuntu-24.04
10
+ permissions:
11
+ contents: read
12
+ steps:
13
+ - uses: openai/fence@e6b39c80c51cb2b2a39448ddeff74b3b886c0a63 # pin@v0.10.0
14
+ with:
15
+ mode: audit
16
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
17
+ with:
18
+ fetch-depth: 0
19
+ persist-credentials: false
20
+
21
+ - name: Determine comparison range
22
+ id: range
23
+ shell: bash
24
+ run: |
25
+ set -euo pipefail
26
+
27
+ # PRs inspect the proposed diff; main pushes inspect only the commit
28
+ # range that just landed. Both paths feed the same blob-size checker.
29
+ if [[ "${{ github.event_name }}" == "pull_request" ]]; then
30
+ base='${{ github.event.pull_request.base.sha }}'
31
+ head='${{ github.event.pull_request.head.sha }}'
32
+ else
33
+ base='${{ github.event.before }}'
34
+ head='${{ github.sha }}'
35
+ fi
36
+
37
+ echo "base=$base" >> "$GITHUB_OUTPUT"
38
+ echo "head=$head" >> "$GITHUB_OUTPUT"
39
+
40
+ - name: Check changed blob sizes
41
+ env:
42
+ BASE_SHA: ${{ steps.range.outputs.base }}
43
+ HEAD_SHA: ${{ steps.range.outputs.head }}
44
+ run: |
45
+ python3 scripts/check_blob_size.py \
46
+ --base "$BASE_SHA" \
47
+ --head "$HEAD_SHA" \
48
+ --max-bytes 512000 \
49
+ --allowlist .github/blob-size-allowlist.txt
50
+
51
+ - name: Check for a clean worktree
52
+ if: always() && !cancelled()
53
+ uses: ./.github/actions/check-clean-worktree
.github/workflows/blocking-ci.yml ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: blocking-ci
2
+
3
+ # This is the single entrypoint for checks that block a PR merge. It also runs
4
+ # after pushes to main so the same check family stays grouped in the Actions UI.
5
+ on:
6
+ pull_request: {}
7
+ push:
8
+ branches: [main]
9
+
10
+ jobs:
11
+ # Keep reusable workflow calls alphabetized. The `required` job below is the
12
+ # version-controlled list that the main-branch ruleset should require.
13
+ bazel:
14
+ name: Bazel
15
+ uses: ./.github/workflows/bazel.yml
16
+ secrets: inherit
17
+
18
+ blob-size-policy:
19
+ name: Blob size policy
20
+ uses: ./.github/workflows/blob-size-policy.yml
21
+ secrets: inherit
22
+
23
+ cargo-deny:
24
+ name: cargo-deny
25
+ uses: ./.github/workflows/cargo-deny.yml
26
+ secrets: inherit
27
+
28
+ codespell:
29
+ name: Codespell
30
+ uses: ./.github/workflows/codespell.yml
31
+ secrets: inherit
32
+
33
+ repo-checks:
34
+ name: repo-checks
35
+ uses: ./.github/workflows/repo-checks.yml
36
+ secrets: inherit
37
+
38
+ rust-ci:
39
+ name: rust-ci
40
+ uses: ./.github/workflows/rust-ci.yml
41
+ secrets: inherit
42
+
43
+ sdk:
44
+ name: sdk
45
+ uses: ./.github/workflows/sdk.yml
46
+ secrets: inherit
47
+
48
+ required:
49
+ name: CI required
50
+ # Without `always()`, GitHub skips this job after a failed dependency and a
51
+ # required check can appear successful instead of reporting the failure.
52
+ if: ${{ always() }}
53
+ needs:
54
+ - bazel
55
+ - blob-size-policy
56
+ - cargo-deny
57
+ - codespell
58
+ - repo-checks
59
+ - rust-ci
60
+ - sdk
61
+ runs-on: ubuntu-24.04
62
+ steps:
63
+ # Keep the helper on the same combined revision as the child workflows so
64
+ # the merge gate evaluates the exact candidate they validated.
65
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
66
+ with:
67
+ persist-credentials: false
68
+
69
+ - name: Require successful dependencies
70
+ env:
71
+ NEEDS: ${{ toJSON(needs) }}
72
+ run: python3 .github/scripts/check_ci_results.py
.github/workflows/cla.yml ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CLA Assistant
2
+ on:
3
+ issue_comment:
4
+ types: [created]
5
+ pull_request_target:
6
+ types: [opened, closed, synchronize]
7
+
8
+ permissions:
9
+ actions: write
10
+ contents: write
11
+ pull-requests: write
12
+ statuses: write
13
+
14
+ jobs:
15
+ cla:
16
+ # Only run the CLA assistant for the canonical openai repo so forks are not blocked
17
+ # and contributors who signed previously do not receive duplicate CLA notifications.
18
+ if: ${{ github.repository_owner == 'openai' }}
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
22
+ # Run on close only if the PR was merged. This will lock the PR to preserve
23
+ # the CLA agreement. We don't want to lock PRs that have been closed without
24
+ # merging because the contributor may want to respond with additional comments.
25
+ # This action has a "lock-pullrequest-aftermerge" option that can be set to false,
26
+ # but that would unconditionally skip locking even in cases where the PR was merged.
27
+ if: |
28
+ (
29
+ github.event_name == 'pull_request_target' &&
30
+ (
31
+ github.event.action == 'opened' ||
32
+ github.event.action == 'synchronize' ||
33
+ (github.event.action == 'closed' && github.event.pull_request.merged == true)
34
+ )
35
+ ) ||
36
+ (
37
+ github.event_name == 'issue_comment' &&
38
+ (
39
+ github.event.comment.body == 'recheck' ||
40
+ github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA'
41
+ )
42
+ )
43
+ env:
44
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45
+ with:
46
+ path-to-document: https://github.com/openai/codex/blob/main/docs/CLA.md
47
+ path-to-signatures: signatures/cla.json
48
+ branch: cla-signatures
49
+ allowlist: codex,dependabot,dependabot[bot],github-actions[bot]