From 10ff785bce9c27bcf80da6ca3fa4e46053fe14e7 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 25 Jun 2026 13:05:22 -0700 Subject: [PATCH 1/2] release: publish standalone zsh artifacts --- .github/dotslash-zsh-config.json | 4 ++ .github/workflows/rust-release-zsh.yml | 95 +++++++++++++++++++++++++- codex-rs/shell-escalation/README.md | 5 ++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/.github/dotslash-zsh-config.json b/.github/dotslash-zsh-config.json index 37285f19e2..2c2b11eec5 100644 --- a/.github/dotslash-zsh-config.json +++ b/.github/dotslash-zsh-config.json @@ -5,21 +5,25 @@ "macos-aarch64": { "name": "codex-zsh-aarch64-apple-darwin.tar.gz", "format": "tar.gz", + "hash": "sha256", "path": "codex-zsh/bin/zsh" }, "macos-x86_64": { "name": "codex-zsh-x86_64-apple-darwin.tar.gz", "format": "tar.gz", + "hash": "sha256", "path": "codex-zsh/bin/zsh" }, "linux-x86_64": { "name": "codex-zsh-x86_64-unknown-linux-musl.tar.gz", "format": "tar.gz", + "hash": "sha256", "path": "codex-zsh/bin/zsh" }, "linux-aarch64": { "name": "codex-zsh-aarch64-unknown-linux-musl.tar.gz", "format": "tar.gz", + "hash": "sha256", "path": "codex-zsh/bin/zsh" } } diff --git a/.github/workflows/rust-release-zsh.yml b/.github/workflows/rust-release-zsh.yml index b55d2e714b..6f73fb1a15 100644 --- a/.github/workflows/rust-release-zsh.yml +++ b/.github/workflows/rust-release-zsh.yml @@ -1,15 +1,43 @@ name: rust-release-zsh on: - workflow_call: + push: + tags: + - "codex-zsh-v*.*.*" env: ZSH_COMMIT: 77045ef899e53b9598bebc5a41db93a548a40ca6 ZSH_PATCH: codex-rs/shell-escalation/patches/zsh-exec-wrapper.patch +concurrency: + group: ${{ github.workflow }}::${{ github.ref_name }} + cancel-in-progress: false + jobs: + metadata: + runs-on: ubuntu-latest + outputs: + release_tag: ${{ steps.release_tag.outputs.release_tag }} + + steps: + - name: Validate release tag + id: release_tag + env: + RELEASE_TAG: ${{ github.ref_name }} + shell: bash + run: | + set -euo pipefail + + if [[ ! "${RELEASE_TAG}" =~ ^codex-zsh-v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Tag ${RELEASE_TAG} does not match codex-zsh-vX.Y.Z." >&2 + exit 1 + fi + + echo "release_tag=${RELEASE_TAG}" >> "${GITHUB_OUTPUT}" + linux: name: Build zsh (Linux) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: metadata runs-on: ${{ matrix.runner }} timeout-minutes: 30 container: @@ -62,6 +90,7 @@ jobs: darwin: name: Build zsh (macOS) - ${{ matrix.variant }} - ${{ matrix.target }} + needs: metadata runs-on: ${{ matrix.runner }} timeout-minutes: 30 @@ -101,3 +130,67 @@ jobs: with: name: codex-zsh-${{ matrix.target }} path: dist/zsh/${{ matrix.target }}/* + + publish-release: + needs: + - metadata + - linux + - darwin + runs-on: ubuntu-latest + permissions: + contents: write + actions: read + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Check whether release already exists + id: release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ needs.metadata.outputs.release_tag }} + shell: bash + run: | + set -euo pipefail + + if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" > /dev/null 2>&1; then + echo "exists=true" >> "${GITHUB_OUTPUT}" + else + echo "exists=false" >> "${GITHUB_OUTPUT}" + fi + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: dist + + - name: Create GitHub Release + if: ${{ steps.release.outputs.exists != 'true' }} + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + with: + tag_name: ${{ needs.metadata.outputs.release_tag }} + name: ${{ needs.metadata.outputs.release_tag }} + files: dist/** + # Keep zsh artifact releases out of Codex's normal "latest release" channel. + prerelease: true + + - name: Amend existing GitHub Release + if: ${{ steps.release.outputs.exists == 'true' }} + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + with: + tag_name: ${{ needs.metadata.outputs.release_tag }} + name: ${{ needs.metadata.outputs.release_tag }} + files: dist/** + overwrite_files: true + # Keep zsh artifact releases out of Codex's normal "latest release" channel. + prerelease: true + + - name: Publish DotSlash manifest + uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag: ${{ needs.metadata.outputs.release_tag }} + config: .github/dotslash-zsh-config.json diff --git a/codex-rs/shell-escalation/README.md b/codex-rs/shell-escalation/README.md index e4d3fecd66..69cd038220 100644 --- a/codex-rs/shell-escalation/README.md +++ b/codex-rs/shell-escalation/README.md @@ -27,3 +27,8 @@ git apply /path/to/patches/zsh-exec-wrapper.patch ./configure make -j"$(nproc)" ``` + +Release artifacts are built by `.github/workflows/rust-release-zsh.yml` when a +`codex-zsh-vX.Y.Z` tag is pushed. When the zsh commit or patch changes, publish +the next version tag and update the checked-in DotSlash manifests to use the new +release. From b9c83719c0570177f94b95c48f918d1d20a02453 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 25 Jun 2026 13:28:14 -0700 Subject: [PATCH 2/2] release: consume standalone zsh artifacts --- .../scripts/build-codex-package-archive.sh | 5 ++ .github/workflows/rust-release.yml | 44 +++++++------ scripts/codex_package/README.md | 4 +- scripts/codex_package/cli.py | 10 ++- scripts/codex_package/test_zsh.py | 63 +++++++++++++++++++ scripts/codex_package/zsh.py | 7 ++- 6 files changed, 111 insertions(+), 22 deletions(-) create mode 100644 scripts/codex_package/test_zsh.py diff --git a/.github/scripts/build-codex-package-archive.sh b/.github/scripts/build-codex-package-archive.sh index 80da4cf20c..229213cc53 100644 --- a/.github/scripts/build-codex-package-archive.sh +++ b/.github/scripts/build-codex-package-archive.sh @@ -9,6 +9,7 @@ Usage: build-codex-package-archive.sh \ --entrypoint-dir \ --archive-dir \ [--bwrap-bin ] \ + [--zsh-manifest ] \ [--codex-command-runner-bin ] \ [--codex-windows-sandbox-setup-bin ] \ [--target-suffixed-entrypoint] @@ -48,6 +49,10 @@ while [[ $# -gt 0 ]]; do bwrap_bin_provided="true" shift 2 ;; + --zsh-manifest) + resource_args+=(--zsh-manifest "${2:?--zsh-manifest requires a value}") + shift 2 + ;; --codex-command-runner-bin) resource_args+=( --codex-command-runner-bin diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index a2646e3eae..49d24abc18 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -14,6 +14,9 @@ on: tags: - "rust-v*.*.*" +env: + CODEX_ZSH_RELEASE_TAG: codex-zsh-v0.1.0 + concurrency: group: ${{ github.workflow }} cancel-in-progress: true @@ -354,6 +357,15 @@ jobs: cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" fi + - name: Download packaged zsh manifest + if: ${{ runner.os != 'macOS' }} + shell: bash + run: | + set -euo pipefail + curl -fsSL \ + "https://github.com/${GITHUB_REPOSITORY}/releases/download/${CODEX_ZSH_RELEASE_TAG}/codex-zsh" \ + -o "${RUNNER_TEMP}/codex-zsh" + - name: Build Codex package archive if: ${{ runner.os != 'macOS' }} shell: bash @@ -366,7 +378,8 @@ jobs: --target "$TARGET" \ --bundle "$BUNDLE" \ --entrypoint-dir "target/${TARGET}/release" \ - --archive-dir "dist/${TARGET}" + --archive-dir "dist/${TARGET}" \ + --zsh-manifest "${RUNNER_TEMP}/codex-zsh" - name: Build Python runtime wheel if: ${{ matrix.bundle == 'primary' && runner.os != 'macOS' }} @@ -697,6 +710,14 @@ jobs: cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" done + - name: Download packaged zsh manifest + shell: bash + run: | + set -euo pipefail + curl -fsSL \ + "https://github.com/${GITHUB_REPOSITORY}/releases/download/${CODEX_ZSH_RELEASE_TAG}/codex-zsh" \ + -o "${RUNNER_TEMP}/codex-zsh" + - name: Build Codex package archive shell: bash env: @@ -708,7 +729,8 @@ jobs: --target "$TARGET" \ --bundle "$BUNDLE" \ --entrypoint-dir "target/${TARGET}/release" \ - --archive-dir "dist/${TARGET}" + --archive-dir "dist/${TARGET}" \ + --zsh-manifest "${RUNNER_TEMP}/codex-zsh" - name: Build Python runtime wheel if: ${{ matrix.bundle == 'primary' }} @@ -1043,11 +1065,6 @@ jobs: with: publish: true - zsh-release-assets: - name: zsh release assets - needs: tag-check - uses: ./.github/workflows/rust-release-zsh.yml - release: needs: - tag-check @@ -1055,7 +1072,6 @@ jobs: - finalize-macos - build-windows - argument-comment-lint-release-assets - - zsh-release-assets if: >- ${{ always() && @@ -1063,8 +1079,7 @@ jobs: needs.build.result == 'success' && needs.finalize-macos.result == 'success' && needs.build-windows.result == 'success' && - needs.argument-comment-lint-release-assets.result == 'success' && - needs.zsh-release-assets.result == 'success' + needs.argument-comment-lint-release-assets.result == 'success' }} name: release runs-on: ubuntu-latest @@ -1113,7 +1128,7 @@ jobs: uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: dist - pattern: "{*-symbols,argument-comment-lint-*,codex-zsh-*,python-runtime-wheel-*}" + pattern: "{*-symbols,argument-comment-lint-*,python-runtime-wheel-*}" - name: List run: ls -R dist/ @@ -1236,13 +1251,6 @@ jobs: tag: ${{ github.ref_name }} config: .github/dotslash-config.json - - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ github.ref_name }} - config: .github/dotslash-zsh-config.json - - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/scripts/codex_package/README.md b/scripts/codex_package/README.md index 323a3ce5ba..b76c18c3c2 100644 --- a/scripts/codex_package/README.md +++ b/scripts/codex_package/README.md @@ -73,4 +73,6 @@ The patched zsh fork used by `shell_zsh_fork` is fetched from the DotSlash manifest at `scripts/codex_package/codex-zsh` when the selected target has a matching prebuilt artifact. Downloaded archives are cached under `$TMPDIR/codex-package/-zsh` and installed at -`codex-resources/zsh/bin/zsh`. +`codex-resources/zsh/bin/zsh`. Pass `--zsh-manifest` to use a different +DotSlash manifest, such as the manifest published with a standalone zsh +artifact release. diff --git a/scripts/codex_package/cli.py b/scripts/codex_package/cli.py index b7d919e4ff..1dd276827e 100644 --- a/scripts/codex_package/cli.py +++ b/scripts/codex_package/cli.py @@ -90,6 +90,14 @@ def parse_args() -> argparse.Namespace: "targets, bwrap is built with Cargo." ), ) + parser.add_argument( + "--zsh-manifest", + type=Path, + help=( + "Optional DotSlash manifest for the patched zsh fork instead of " + "scripts/codex_package/codex-zsh." + ), + ) parser.add_argument( "--codex-command-runner-bin", type=Path, @@ -160,7 +168,7 @@ def main() -> int: inputs = PackageInputs( entrypoint_bin=source_outputs.entrypoint_bin, rg_bin=resolve_rg_bin(spec, args.rg_bin), - zsh_bin=resolve_zsh_bin(spec), + zsh_bin=resolve_zsh_bin(spec, args.zsh_manifest), bwrap_bin=source_outputs.bwrap_bin, codex_command_runner_bin=source_outputs.codex_command_runner_bin, codex_windows_sandbox_setup_bin=source_outputs.codex_windows_sandbox_setup_bin, diff --git a/scripts/codex_package/test_zsh.py b/scripts/codex_package/test_zsh.py new file mode 100644 index 0000000000..b4f536cfc2 --- /dev/null +++ b/scripts/codex_package/test_zsh.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 + +import hashlib +import json +from pathlib import Path +import sys +import tarfile +import tempfile +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from codex_package.targets import TARGET_SPECS +from codex_package.zsh import resolve_zsh_bin + + +class ResolveZshBinTest(unittest.TestCase): + def test_uses_manifest_override(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + archive = root / "codex-zsh.tar.gz" + source = root / "zsh" + source.write_bytes(b"standalone zsh") + with tarfile.open(archive, "w:gz") as tar: + tar.add(source, arcname="codex-zsh/bin/zsh") + + manifest = root / "codex-zsh" + manifest.write_text( + json.dumps( + { + "platforms": { + "linux-x86_64": { + "size": archive.stat().st_size, + "hash": "sha256", + "digest": hashlib.sha256( + archive.read_bytes() + ).hexdigest(), + "format": "tar.gz", + "path": "codex-zsh/bin/zsh", + "providers": [{"url": archive.as_uri()}], + } + } + } + ), + encoding="utf-8", + ) + + with patch( + "codex_package.dotslash.default_cache_root", + return_value=root / "cache", + ): + zsh_bin = resolve_zsh_bin( + TARGET_SPECS["x86_64-unknown-linux-musl"], manifest + ) + + self.assertIsNotNone(zsh_bin) + assert zsh_bin is not None + self.assertEqual(zsh_bin.read_bytes(), b"standalone zsh") + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/codex_package/zsh.py b/scripts/codex_package/zsh.py index f4ee7e19dd..4ec2db502c 100644 --- a/scripts/codex_package/zsh.py +++ b/scripts/codex_package/zsh.py @@ -11,10 +11,13 @@ ZSH_MANIFEST = REPO_ROOT / "scripts" / "codex_package" / "codex-zsh" ZSH_RESOURCE_PATH = Path("zsh") / "bin" / "zsh" -def resolve_zsh_bin(spec: TargetSpec) -> Path | None: +def resolve_zsh_bin( + spec: TargetSpec, + manifest_path: Path | None = None, +) -> Path | None: return fetch_dotslash_executable( spec, - manifest_path=ZSH_MANIFEST, + manifest_path=manifest_path or ZSH_MANIFEST, artifact_label="codex-zsh", cache_key=f"{spec.target}-zsh", dest_name="zsh",