Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .github/ISSUE_TEMPLATE/1-codex-app.yml +54 -0
- .github/ISSUE_TEMPLATE/2-extension.yml +61 -0
- .github/ISSUE_TEMPLATE/3-cli.yml +81 -0
- .github/ISSUE_TEMPLATE/4-bug-report.yml +37 -0
- .github/ISSUE_TEMPLATE/5-feature-request.yml +32 -0
- .github/ISSUE_TEMPLATE/6-docs-issue.yml +27 -0
- .github/actions/check-clean-worktree/action.yml +19 -0
- .github/actions/linux-code-sign/action.yml +49 -0
- .github/actions/prepare-bazel-ci/action.yml +59 -0
- .github/actions/setup-bazel-ci/action.yml +94 -0
- .github/scripts/archive-release-symbols-and-strip-binaries.sh +119 -0
- .github/scripts/build-codex-package-archive.sh +243 -0
- .github/scripts/build-zsh-release-artifact.sh +61 -0
- .github/scripts/check_ci_results.py +34 -0
- .github/scripts/compute-bazel-windows-path.ps1 +113 -0
- .github/scripts/install-musl-build-tools.sh +290 -0
- .github/scripts/install-musl-openssl.sh +51 -0
- .github/scripts/publish_r2_release.py +532 -0
- .github/scripts/resolve_python_cli_release.py +123 -0
- .github/scripts/run-argument-comment-lint-bazel.sh +57 -0
- .github/scripts/run-bazel-ci.sh +420 -0
- .github/scripts/run-bazel-query-ci.sh +42 -0
- .github/scripts/run_bazel_with_buildbuddy.py +217 -0
- .github/scripts/rusty_v8_bazel.py +411 -0
- .github/scripts/rusty_v8_module_bazel.py +243 -0
- .github/scripts/setup-dev-drive.ps1 +63 -0
- .github/scripts/setup-voice-windows.ps1 +96 -0
- .github/scripts/test_macos_signing_entitlements.py +37 -0
- .github/scripts/test_resolve_python_cli_release.py +250 -0
- .github/scripts/test_run_bazel_with_buildbuddy.py +290 -0
- .github/scripts/test_rusty_v8_bazel.py +439 -0
- .github/scripts/test_v8_canary_changes.py +111 -0
- .github/scripts/test_verify_pypi_release.py +96 -0
- .github/scripts/test_voice_cygwin_inputs.py +83 -0
- .github/scripts/v8_canary_changes.py +193 -0
- .github/scripts/verify-zsh-manifest.sh +12 -0
- .github/scripts/verify_bazel_clippy_lints.py +234 -0
- .github/scripts/verify_cargo_workspace_manifests.py +395 -0
- .github/scripts/verify_pypi_release.py +76 -0
- .github/scripts/verify_tui_core_boundary.py +91 -0
- .github/scripts/voice-cygwin-inputs.py +98 -0
- .github/scripts/voice-cygwin-snapshot.json +145 -0
- .github/scripts/voice_windows_tools.py +80 -0
- .github/scripts/watch_voice_bazel.py +79 -0
- .github/workflows/Dockerfile.bazel +20 -0
- .github/workflows/README.md +37 -0
- .github/workflows/bazel.yml +554 -0
- .github/workflows/blob-size-policy.yml +53 -0
- .github/workflows/blocking-ci.yml +72 -0
- .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]
|