From 5920f6b82b686fbdfca108f3a307c00ba3772d86 Mon Sep 17 00:00:00 2001 From: Jacob Elias Date: Wed, 29 Jul 2026 12:00:24 -0400 Subject: [PATCH 1/5] feat(op-conductor-ops): build a single-file executable with PyInstaller --- op-conductor-ops/.gitignore | 3 + op-conductor-ops/entrypoint.py | 3 + op-conductor-ops/justfile | 6 ++ op-conductor-ops/poetry.lock | 133 +++++++++++++++++++++++++++++++- op-conductor-ops/pyproject.toml | 1 + 5 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 op-conductor-ops/entrypoint.py diff --git a/op-conductor-ops/.gitignore b/op-conductor-ops/.gitignore index c18dd8d83..147c64c24 100644 --- a/op-conductor-ops/.gitignore +++ b/op-conductor-ops/.gitignore @@ -1 +1,4 @@ __pycache__/ +build/ +dist/ +*.spec diff --git a/op-conductor-ops/entrypoint.py b/op-conductor-ops/entrypoint.py new file mode 100644 index 000000000..83aa3b682 --- /dev/null +++ b/op-conductor-ops/entrypoint.py @@ -0,0 +1,3 @@ +from op_conductor_ops.cli import app + +app() diff --git a/op-conductor-ops/justfile b/op-conductor-ops/justfile index 4d3c5849a..6a6a3e4d0 100644 --- a/op-conductor-ops/justfile +++ b/op-conductor-ops/justfile @@ -1,3 +1,9 @@ lint: poetry run black . +# Build a single-file executable. VERSION has no leading "v" (e.g. 0.2.0). +# PLATFORM is one of darwin-arm64, darwin-amd64, linux-amd64, linux-arm64. +build-binary VERSION PLATFORM: + poetry run pyinstaller --onefile --clean --noconfirm \ + --name "op-conductor-ops-{{VERSION}}-{{PLATFORM}}" \ + entrypoint.py diff --git a/op-conductor-ops/poetry.lock b/op-conductor-ops/poetry.lock index e58ee45cc..5205c2306 100644 --- a/op-conductor-ops/poetry.lock +++ b/op-conductor-ops/poetry.lock @@ -1,4 +1,17 @@ -# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand. + +[[package]] +name = "altgraph" +version = "0.17.5" +description = "Python graph (network) package" +optional = false +python-versions = "*" +groups = ["dev"] +markers = "python_version < \"3.14\"" +files = [ + {file = "altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597"}, + {file = "altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7"}, +] [[package]] name = "black" @@ -206,6 +219,22 @@ files = [ [package.extras] all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "macholib" +version = "1.16.4" +description = "Mach-O header analysis and editing" +optional = false +python-versions = "*" +groups = ["dev"] +markers = "python_version < \"3.14\" and sys_platform == \"darwin\"" +files = [ + {file = "macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea"}, + {file = "macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362"}, +] + +[package.dependencies] +altgraph = ">=0.17" + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -284,6 +313,19 @@ hyperscan = ["hyperscan (>=0.7)"] optional = ["typing-extensions (>=4)"] re2 = ["google-re2 (>=1.1)"] +[[package]] +name = "pefile" +version = "2024.8.26" +description = "Python PE parsing module" +optional = false +python-versions = ">=3.6.0" +groups = ["dev"] +markers = "python_version < \"3.14\" and sys_platform == \"win32\"" +files = [ + {file = "pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f"}, + {file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"}, +] + [[package]] name = "platformdirs" version = "4.3.7" @@ -316,6 +358,58 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyinstaller" +version = "6.21.0" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +optional = false +python-versions = "<3.16,>=3.8" +groups = ["dev"] +markers = "python_version < \"3.14\"" +files = [ + {file = "pyinstaller-6.21.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:327d132389f37912609e01be62810cf96b5aa95b613903e4b8692e0d12fb0eda"}, + {file = "pyinstaller-6.21.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7071d4b094d5b40deeef5fa3d3b98a1b846087f7562b49209663d5f9281fe251"}, + {file = "pyinstaller-6.21.0-py3-none-manylinux2014_i686.whl", hash = "sha256:6b6374d652107dd4a2eeece903ff82bb4045bb5e1006c5a158a6dcdbefe84bf2"}, + {file = "pyinstaller-6.21.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4e3108b3f02384560da70e39b8bf22b0ad597d02bd68a40d76ea91c1cfa00cad"}, + {file = "pyinstaller-6.21.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:697532279f535ad572bda613db4f821540e235c7854ca6da4d3bf0373f4415ee"}, + {file = "pyinstaller-6.21.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:605169523a6b5ace39f13dfbff21add9f2bc43df99c7daf9394fefb2c45e8b6f"}, + {file = "pyinstaller-6.21.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:5fa56746c1e76f93634d018502301378a2d0c382553d37d8c3c34ff436c12dd1"}, + {file = "pyinstaller-6.21.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:42395ec76df8e8120c36b13339d9db8cab83e316a12839ee303cc00fc941bb74"}, + {file = "pyinstaller-6.21.0-py3-none-win32.whl", hash = "sha256:c6b28d30d8fd99ce162ff3aab5013ed44dbfb747566b1f01b9bed7964d7c14e9"}, + {file = "pyinstaller-6.21.0-py3-none-win_amd64.whl", hash = "sha256:7fae06c494ce0ebfe6bd3055c0e409def884f63af2e3705d06bd431ad9237fc7"}, + {file = "pyinstaller-6.21.0-py3-none-win_arm64.whl", hash = "sha256:f13c95c9c03fb567217135919f93815c305813126780b0ed6e0123cb8acaf025"}, + {file = "pyinstaller-6.21.0.tar.gz", hash = "sha256:bb9fab705983e393a2d1cac77d6972513057ad800215fd861dc15ff5272e98fd"}, +] + +[package.dependencies] +altgraph = "*" +macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +packaging = ">=22.0" +pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} +pyinstaller-hooks-contrib = ">=2026.6" +pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} +setuptools = ">=42.0.0" + +[package.extras] +completion = ["argcomplete"] +hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2026.6" +description = "Community maintained hooks for PyInstaller" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.14\"" +files = [ + {file = "pyinstaller_hooks_contrib-2026.6-py3-none-any.whl", hash = "sha256:fd13b8ac126b35361175edacd41a0d97080b75dd5f4b594ecefefff969509dd3"}, + {file = "pyinstaller_hooks_contrib-2026.6.tar.gz", hash = "sha256:bef5002c32f4f50bd55b005da12cff64eca8783e7eaf86a06a62410164bab725"}, +] + +[package.dependencies] +packaging = ">=22.0" + [[package]] name = "pytokens" version = "0.4.1" @@ -371,6 +465,19 @@ files = [ [package.extras] dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "python_version < \"3.14\" and sys_platform == \"win32\"" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + [[package]] name = "requests" version = "2.34.2" @@ -412,6 +519,28 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "setuptools" +version = "83.0.0" +description = "Most extensible Python build backend with support for C/C++ extension modules" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +markers = "python_version < \"3.14\"" +files = [ + {file = "setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3"}, + {file = "setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] + [[package]] name = "shellingham" version = "1.5.4" @@ -487,4 +616,4 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "fd79c6be3949766df435dc0547a4dc14d355d7ea71406bf05f53579d251100f9" +content-hash = "1a1c65fe6838c1632b7767e9d50b7369f399473c5c4365eea89b6b3ff72d4414" diff --git a/op-conductor-ops/pyproject.toml b/op-conductor-ops/pyproject.toml index 0d99d602c..2cc16c5c5 100644 --- a/op-conductor-ops/pyproject.toml +++ b/op-conductor-ops/pyproject.toml @@ -19,6 +19,7 @@ op-conductor-ops = "op_conductor_ops.cli:app" [tool.poetry.group.dev.dependencies] black = ">=25.1.0" +pyinstaller = {version = "^6.11.0", python = ">=3.12,<3.14"} [build-system] requires = ["poetry-core"] From 12c857bc2fb1288056ecf5f6d0fac3dbd8d61b78 Mon Sep 17 00:00:00 2001 From: Jacob Elias Date: Wed, 29 Jul 2026 12:03:19 -0400 Subject: [PATCH 2/5] ci(op-conductor-ops): release single-file executables on tag --- .../workflows/release-op-conductor-ops.yaml | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 .github/workflows/release-op-conductor-ops.yaml diff --git a/.github/workflows/release-op-conductor-ops.yaml b/.github/workflows/release-op-conductor-ops.yaml new file mode 100644 index 000000000..2ab975aab --- /dev/null +++ b/.github/workflows/release-op-conductor-ops.yaml @@ -0,0 +1,139 @@ +name: release op-conductor-ops + +on: + push: + tags: + - 'op-conductor-ops/v*' + pull_request: + paths: + - 'op-conductor-ops/**' + - '.github/workflows/release-op-conductor-ops.yaml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - platform: darwin-arm64 + runner: macos-14 + - platform: darwin-amd64 + runner: macos-15-intel + - platform: linux-amd64 + runner: ubuntu-24.04 + - platform: linux-arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6 + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 + with: + python-version: '3.12' + + - name: Resolve version + id: version + run: | + set -euo pipefail + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + version="${GITHUB_REF_NAME#op-conductor-ops/v}" + else + version="0.0.0-dev" + fi + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "Building version ${version}" + + - name: Install dependencies + working-directory: op-conductor-ops + run: | + set -euo pipefail + pip install poetry + poetry check --lock + poetry install --no-interaction --with dev + + - name: Build executable + working-directory: op-conductor-ops + env: + ASSET: op-conductor-ops-${{ steps.version.outputs.version }}-${{ matrix.platform }} + run: | + set -euo pipefail + poetry run pyinstaller --onefile --clean --noconfirm \ + --name "${ASSET}" entrypoint.py + + - name: Smoke test executable + working-directory: op-conductor-ops + env: + ASSET: op-conductor-ops-${{ steps.version.outputs.version }}-${{ matrix.platform }} + run: | + set -euo pipefail + file "dist/${ASSET}" + "./dist/${ASSET}" --help + + - name: Compute checksum + working-directory: op-conductor-ops/dist + env: + ASSET: op-conductor-ops-${{ steps.version.outputs.version }}-${{ matrix.platform }} + run: | + set -euo pipefail + shasum -a 256 "${ASSET}" > "${ASSET}.sha256" + cat "${ASSET}.sha256" + + - name: Upload artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: op-conductor-ops-${{ matrix.platform }} + path: | + op-conductor-ops/dist/op-conductor-ops-*-${{ matrix.platform }} + op-conductor-ops/dist/op-conductor-ops-*-${{ matrix.platform }}.sha256 + if-no-files-found: error + + release: + needs: [build] + if: github.ref_type == 'tag' + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - name: Resolve version + id: version + run: | + set -euo pipefail + version="${GITHUB_REF_NAME#op-conductor-ops/v}" + echo "version=${version}" >> "$GITHUB_OUTPUT" + + - name: Download artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + path: dist + merge-multiple: true + + - name: Assemble checksums file + working-directory: dist + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + cat ./*.sha256 | sort -k2 > "op-conductor-ops_${VERSION}_checksums.txt" + rm -f ./*.sha256 + cat "op-conductor-ops_${VERSION}_checksums.txt" + test "$(grep -c . "op-conductor-ops_${VERSION}_checksums.txt")" -eq 4 + + - name: Create release + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + gh release create "${GITHUB_REF_NAME}" \ + --repo "${GITHUB_REPOSITORY}" \ + --title "op-conductor-ops ${VERSION}" \ + --generate-notes \ + dist/* From ee30a8d133d57dd55b30ad9c4f153b1bef26f6e6 Mon Sep 17 00:00:00 2001 From: Jacob Elias Date: Wed, 29 Jul 2026 12:08:36 -0400 Subject: [PATCH 3/5] docs(op-conductor-ops): document binary and op-toolbox install paths --- op-conductor-ops/README.md | 59 +++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/op-conductor-ops/README.md b/op-conductor-ops/README.md index 1bc280682..50d746142 100644 --- a/op-conductor-ops/README.md +++ b/op-conductor-ops/README.md @@ -4,29 +4,62 @@ op-conductor-ops is a CLI tool for managing op-conductor sequencer clusters. **WARNING!!! This tool can cause a network outage if used improperly. Please consult #pod-devinfra before using.** -## Setup +## Install + +### Via op-toolbox (recommended for operators) + +```sh +op-toolbox install op-conductor-ops +op-toolbox op-conductor-ops status +``` + +### Direct binary download + +Releases attach a self-contained executable per platform; no Python is required. + +```sh +version=0.2.0 +platform=darwin-arm64 # or darwin-amd64, linux-amd64, linux-arm64 +gh release download "op-conductor-ops/v${version}" \ + --repo ethereum-optimism/infra \ + --pattern "op-conductor-ops-${version}-${platform}" +chmod +x "op-conductor-ops-${version}-${platform}" +./op-conductor-ops-${version}-${platform} --help +``` + +Verify the download against `op-conductor-ops_${version}_checksums.txt` from the +same release. + +### From source (development) Requires [poetry](https://github.com/python-poetry/poetry). -Install the package and its dependencies with `poetry install`. This registers the -`op-conductor-ops` console script (see `[tool.poetry.scripts]` in `pyproject.toml`). +```sh +poetry install +poetry run op-conductor-ops --help +``` -Recommended update to your .bashrc/zshrc: +Build the executable locally with `just build-binary 0.2.0 darwin-arm64`. -1. `export CONDUCTOR_CONFIG=""` +## Configuration -## Usage +Recommended addition to your `.bashrc`/`.zshrc`: -After installing with `poetry install`, the tool is invoked with `poetry run op-conductor-ops` -(or just `op-conductor-ops` from within `poetry shell`), passing on any arguments. +```sh +export CONDUCTOR_CONFIG="" +``` + +## Usage -### Example Usage +```sh +# Implicit config lookup at ./config.toml or $CONDUCTOR_CONFIG +op-conductor-ops status -* Example usage with implicit config file with lookup at ./config.toml -```poetry run op-conductor-ops status ``` +# Explicit config and certificate paths +op-conductor-ops -c .//config.toml --cert .//cacert.pem +``` -* Usage with explicit path to config and certificate -```poetry run op-conductor-ops -c .//config.toml --cert .//cacert.pem ``` +From a source checkout, prefix the above with `poetry run`. ## Example Configuration File: example.config.toml From 11126653dd4fa181dc87363005680edb058240e4 Mon Sep 17 00:00:00 2001 From: Jacob Elias Date: Wed, 29 Jul 2026 12:21:45 -0400 Subject: [PATCH 4/5] fix(op-conductor-ops): pin poetry, harden release workflow, keep pyinstaller out of the image --- .github/workflows/release-op-conductor-ops.yaml | 13 ++++++++++--- op-conductor-ops/Dockerfile | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-op-conductor-ops.yaml b/.github/workflows/release-op-conductor-ops.yaml index 2ab975aab..ee65558e7 100644 --- a/.github/workflows/release-op-conductor-ops.yaml +++ b/.github/workflows/release-op-conductor-ops.yaml @@ -5,6 +5,7 @@ on: tags: - 'op-conductor-ops/v*' pull_request: + branches: [main] paths: - 'op-conductor-ops/**' - '.github/workflows/release-op-conductor-ops.yaml' @@ -13,6 +14,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: build: strategy: @@ -24,15 +29,17 @@ jobs: - platform: darwin-amd64 runner: macos-15-intel - platform: linux-amd64 - runner: ubuntu-24.04 + runner: ubuntu-22.04 - platform: linux-arm64 - runner: ubuntu-24.04-arm + runner: ubuntu-22.04-arm runs-on: ${{ matrix.runner }} permissions: contents: read steps: - name: Checkout uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 @@ -55,7 +62,7 @@ jobs: working-directory: op-conductor-ops run: | set -euo pipefail - pip install poetry + pip install "poetry==2.3.4" poetry check --lock poetry install --no-interaction --with dev diff --git a/op-conductor-ops/Dockerfile b/op-conductor-ops/Dockerfile index 5be56f310..e3324baff 100644 --- a/op-conductor-ops/Dockerfile +++ b/op-conductor-ops/Dockerfile @@ -10,6 +10,6 @@ COPY /op-conductor-ops/pyproject.toml /op-conductor-ops/poetry.lock* /op-conduct COPY /op-conductor-ops/op_conductor_ops/ ./op_conductor_ops/ RUN python3.12 -m poetry config virtualenvs.create false \ - && python3.12 -m poetry install --no-interaction --no-ansi + && python3.12 -m poetry install --no-interaction --no-ansi --only main ENTRYPOINT ["op-conductor-ops"] From bb4be8ea2fcf0777e929a7975aa2668b009f01a0 Mon Sep 17 00:00:00 2001 From: Jacob Elias Date: Thu, 30 Jul 2026 10:04:06 -0400 Subject: [PATCH 5/5] ci(op-conductor-ops): attest build provenance and publish via draft upload --- .../workflows/release-op-conductor-ops.yaml | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-op-conductor-ops.yaml b/.github/workflows/release-op-conductor-ops.yaml index ee65558e7..b5a1dd6c1 100644 --- a/.github/workflows/release-op-conductor-ops.yaml +++ b/.github/workflows/release-op-conductor-ops.yaml @@ -35,6 +35,8 @@ jobs: runs-on: ${{ matrix.runner }} permissions: contents: read + id-token: write + attestations: write steps: - name: Checkout uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6 @@ -84,6 +86,13 @@ jobs: file "dist/${ASSET}" "./dist/${ASSET}" --help + # Only released binaries get an attestation; PR and dispatch builds are throwaway. + - name: Attest build provenance + if: github.ref_type == 'tag' + uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4 + with: + subject-path: op-conductor-ops/dist/op-conductor-ops-${{ steps.version.outputs.version }}-${{ matrix.platform }} + - name: Compute checksum working-directory: op-conductor-ops/dist env: @@ -108,6 +117,7 @@ jobs: runs-on: ubuntu-24.04 permissions: contents: write + attestations: read steps: - name: Resolve version id: version @@ -133,14 +143,40 @@ jobs: cat "op-conductor-ops_${VERSION}_checksums.txt" test "$(grep -c . "op-conductor-ops_${VERSION}_checksums.txt")" -eq 4 - - name: Create release + # The checksums file is generated here and is deliberately not attested. + - name: Verify build provenance + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + for f in dist/op-conductor-ops-*-*; do + gh attestation verify "$f" --repo "${GITHUB_REPOSITORY}" + done + + - name: Publish release env: GH_TOKEN: ${{ github.token }} VERSION: ${{ steps.version.outputs.version }} run: | set -euo pipefail - gh release create "${GITHUB_REF_NAME}" \ - --repo "${GITHUB_REPOSITORY}" \ - --title "op-conductor-ops ${VERSION}" \ - --generate-notes \ - dist/* + # Reuse an existing draft so a rerun repairs a partial upload instead of + # failing on create, then publish only after all uploads succeed. Refuse to + # touch an already-published release: PyInstaller output is not reproducible, + # so clobbering assets would invalidate the SHA-256 digests op-toolbox pins. + draft="$(gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" \ + --json isDraft --jq .isDraft 2>/dev/null || echo missing)" + case "${draft}" in + false) + echo "::error::${GITHUB_REF_NAME} is already published; delete the release or cut a new version" >&2 + exit 1 + ;; + missing) + gh release create "${GITHUB_REF_NAME}" \ + --repo "${GITHUB_REPOSITORY}" \ + --draft \ + --title "op-conductor-ops ${VERSION}" \ + --generate-notes + ;; + esac + gh release upload "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" dist/* --clobber + gh release edit "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" --draft=false