Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .github/actions/binding-tools/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Prepare binding tools
description: Select targets, install the exact compilers and prerequisites, and build the shared generator
inputs:
targets:
description: Comma-separated package-key:go, package-key:python, or package-key:typescript targets, or all
description: Comma-separated package-key:go, package-key:python, package-key:typescript, or package-key:rust targets, or all
required: true
publication:
description: Include the committed package dependency closure for publication
Expand All @@ -23,6 +23,12 @@ outputs:
has-typescript:
description: Whether a TypeScript target is selected
value: ${{ steps.configure.outputs.has-typescript }}
rust-version:
description: Exact catalog Rust version, empty when no Rust target is selected
value: ${{ steps.configure.outputs.rust-version }}
has-rust:
description: Whether a Rust target is selected
value: ${{ steps.configure.outputs.has-rust }}
source-commit:
description: Checked-out source commit
value: ${{ steps.configure.outputs.source-commit }}
Expand Down Expand Up @@ -60,6 +66,7 @@ runs:
python tooling/extension-bindings/ci/generate-go.py configure --targets "$INPUT_TARGETS"
python tooling/extension-bindings/ci/generate-python.py configure --targets "$INPUT_TARGETS"
python tooling/extension-bindings/ci/generate-typescript.py configure --targets "$INPUT_TARGETS"
python tooling/extension-bindings/ci/generate-rust.py configure --targets "$INPUT_TARGETS"
fi
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
Expand Down Expand Up @@ -129,6 +136,15 @@ runs:
shell: bash
working-directory: tooling/extension-bindings/emitters/typescript
run: npm ci --ignore-scripts && npm run build
- name: Install the pinned Rust toolchain
if: steps.configure.outputs.has-rust == 'true'
shell: bash
env:
RUST_VERSION: ${{ steps.configure.outputs.rust-version }}
run: |
set -euo pipefail
rustup toolchain install "$RUST_VERSION" --profile minimal --component rustfmt
rustup default "$RUST_VERSION"
- name: Build shared generator
shell: bash
run: go -C tooling/extension-bindings/orchestrator build -trimpath -buildvcs=false -o "$RUNNER_TEMP/binding-tools/rc" ./cmd/rc
14 changes: 11 additions & 3 deletions .github/workflows/binding-generate.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Generate and commit Go, Python and TypeScript bindings
name: Generate and commit Go, Python, TypeScript and Rust bindings

on:
push:
Expand All @@ -11,19 +11,21 @@ on:
- 'bindings/**/go/**'
- 'bindings/**/python/**'
- 'bindings/**/typescript/**'
- 'bindings/**/rust/**'
- 'tooling/extension-bindings/packages.yaml'
- 'tooling/extension-bindings/toolchain.lock.yaml'
- 'tooling/extension-bindings/model/**'
- 'tooling/extension-bindings/normalizer/**'
- 'tooling/extension-bindings/emitters/go/**'
- 'tooling/extension-bindings/emitters/python/**'
- 'tooling/extension-bindings/emitters/typescript/**'
- 'tooling/extension-bindings/emitters/rust/**'
- 'tooling/extension-bindings/orchestrator/**'
- 'tooling/extension-bindings/ci/**'
workflow_dispatch:
inputs:
targets:
description: 'Comma-separated package-key:go, package-key:python, or package-key:typescript targets, or all; empty uses automatic targets'
description: 'Comma-separated package-key:go, package-key:python, package-key:typescript, or package-key:rust targets, or all; empty uses automatic targets'
type: string
default: ''

Expand All @@ -42,7 +44,7 @@ jobs:
permissions:
contents: write
env:
INPUT_TARGETS: ${{ inputs.targets || vars.BINDING_GENERATION_TARGETS || 'common-integrations:go,env-configuration:go,common-integrations:python,env-configuration:python,common-integrations:typescript,env-configuration:typescript' }}
INPUT_TARGETS: ${{ inputs.targets || vars.BINDING_GENERATION_TARGETS || 'common-integrations:go,env-configuration:go,common-integrations:python,env-configuration:python,common-integrations:typescript,env-configuration:typescript,common-integrations:rust,env-configuration:rust' }}
GOTOOLCHAIN: local
steps:
- name: Check out current main
Expand All @@ -62,6 +64,7 @@ jobs:
env:
HAS_PYTHON: ${{ steps.tools.outputs.has-python }}
HAS_TYPESCRIPT: ${{ steps.tools.outputs.has-typescript }}
HAS_RUST: ${{ steps.tools.outputs.has-rust }}
run: |
set -euo pipefail
python -m unittest discover -s tooling/extension-bindings/ci -p 'test_*.py'
Expand All @@ -77,6 +80,9 @@ jobs:
if [[ "$HAS_TYPESCRIPT" == true ]]; then
npm --prefix tooling/extension-bindings/emitters/typescript test
fi
if [[ "$HAS_RUST" == true ]]; then
cargo test --locked --manifest-path tooling/extension-bindings/emitters/rust/Cargo.toml
fi

- name: Generate and verify bindings
env:
Expand All @@ -89,6 +95,8 @@ jobs:
--rc "$RC" --core-schema "$CORE_SCHEMA"
python tooling/extension-bindings/ci/generate-typescript.py update --targets "$INPUT_TARGETS" \
--rc "$RC" --core-schema "$CORE_SCHEMA"
python tooling/extension-bindings/ci/generate-rust.py update --targets "$INPUT_TARGETS" \
--rc "$RC" --core-schema "$CORE_SCHEMA"

- name: Commit verified generated source to main
env:
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/binding-publish-cargo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Publish Rust bindings

on:
workflow_dispatch:
inputs:
targets:
description: 'Comma-separated package-key:rust targets, or all'
type: string
default: ''
dry_run:
description: 'Verify publication without pushing to crates.io'
type: boolean
default: true

permissions:
contents: read

concurrency:
group: binding-publish-cargo
cancel-in-progress: false

jobs:
publish:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
timeout-minutes: 15

steps:
- name: Publish to crates.io (placeholder)
env:
TARGETS: ${{ inputs.targets }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
set -euo pipefail
echo "crates.io publication is not implemented; no package was published."
echo "targets=${TARGETS:-<none>} dry_run=${DRY_RUN}"
30 changes: 30 additions & 0 deletions .github/workflows/extension-binding-normalizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
- "tooling/extension-bindings/README.md"
- "tooling/extension-bindings/emitters/go/**"
- "tooling/extension-bindings/emitters/typescript/**"
- "tooling/extension-bindings/emitters/rust/**"
- "tooling/extension-bindings/model/**"
- "tooling/extension-bindings/normalizer/**"
push:
Expand All @@ -20,6 +21,7 @@ on:
- "tooling/extension-bindings/README.md"
- "tooling/extension-bindings/emitters/go/**"
- "tooling/extension-bindings/emitters/typescript/**"
- "tooling/extension-bindings/emitters/rust/**"
- "tooling/extension-bindings/model/**"
- "tooling/extension-bindings/normalizer/**"
workflow_dispatch:
Expand Down Expand Up @@ -111,3 +113,31 @@ jobs:

- name: Test the TypeScript emitter and generated conformance packages
run: npm test

verify-rust-emitter:
name: Verify Rust emitter on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]

defaults:
run:
working-directory: tooling/extension-bindings/emitters/rust

steps:
- name: Check out repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false

- name: Set up Rust
run: rustup toolchain install 1.92.0 --profile minimal --component clippy --component rustfmt && rustup default 1.92.0

- name: Test the Rust emitter and generated conformance packages
run: cargo test --locked

- name: Lint Rust emitter
run: cargo clippy --locked -- -D warnings
3 changes: 2 additions & 1 deletion tooling/extension-bindings/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
`IMPLEMENTATION.md` is historical design guidance; the repository owner's
standing overrides govern generation and publication.
The shared Go resolver and normalizer feed native Go and Python emitters. A native TypeScript
emitter lives under `emitters/typescript/`.
emitter lives under `emitters/typescript/`, and a native Rust emitter lives under
`emitters/rust/`; its contract is in `CONTRACT.md`.
The Go orchestrator exposes eight commands through Go's standard `flag` package.
GitHub Actions generates and commits Go and Python bindings to `main`, then a
separate manual workflow publishes verified Go module tags and release assets.
Expand Down
81 changes: 81 additions & 0 deletions tooling/extension-bindings/ci/generate-rust.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Configure and update verified Rust bindings for the Actions workflow."""

from __future__ import annotations

import argparse
import shutil
import subprocess
from pathlib import Path

from generation import (
Target,
exact_version,
of_language,
select_targets,
summarize,
write_outputs,
)

LANGUAGE = "rust"
TOOLS = ("cargo", "rustc", "rustfmt")


def require_tools() -> None:
missing = [name for name in TOOLS if shutil.which(name) is None]
if missing:
raise ValueError(
"Rust targets require the provisioned toolchain on PATH: missing "
+ ", ".join(missing)
)


def update(root: Path, targets: list[Target], rc: Path, core: Path) -> None:
selected = of_language(targets, LANGUAGE)
if not selected:
summarize("No Rust targets selected; nothing to update.")
return
require_tools()
tooling = root / "tooling" / "extension-bindings"
args = [
str(rc.resolve()),
"bindings",
"update",
"--packages",
str(tooling / "packages.yaml"),
"--toolchain-lock",
str(tooling / "toolchain.lock.yaml"),
"--core-schema",
str(core.resolve()),
]
for key, _, _ in selected:
args.extend(["--target", key])
subprocess.run(args, cwd=root, check=True)
summarize(f"Verified and synchronized {len(selected)} Rust binding targets.")


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("command", choices=["configure", "update"])
parser.add_argument("--root", type=Path, default=Path.cwd())
parser.add_argument("--targets", default="")
parser.add_argument("--rc", type=Path)
parser.add_argument("--core-schema", type=Path)
args = parser.parse_args()
root = args.root.resolve()
try:
targets = select_targets(
root / "tooling/extension-bindings/packages.yaml", args.targets
)
if args.command == "configure":
version = exact_version(targets, LANGUAGE)
write_outputs(rust_version=version, has_rust="true" if version else "false")
else:
if args.rc is None or args.core_schema is None:
raise ValueError("update requires --rc and --core-schema")
update(root, targets, args.rc, args.core_schema)
except (ValueError, KeyError, OSError, subprocess.CalledProcessError) as error:
parser.exit(1, f"binding generation failed: {error}\n")


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion tooling/extension-bindings/ci/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import yaml

LANGUAGES = {"go": "Go", "python": "Python", "typescript": "TypeScript"}
LANGUAGES = {"go": "Go", "python": "Python", "typescript": "TypeScript", "rust": "Rust"}

Target = tuple[str, str, str]

Expand Down
72 changes: 72 additions & 0 deletions tooling/extension-bindings/ci/test_generate_rust.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Exercise the Rust generation script."""

from __future__ import annotations

from pathlib import Path
from unittest.mock import patch

from generation import select_targets
from testsupport import RepoTestCase, load_script, run_script

script = load_script("generate-rust.py")


class RustGenerationTests(RepoTestCase):
def setUp(self) -> None:
super().setUp()
self.packages["owner"]["languages"]["rust"] = {
"sourceDirectory": "bindings/owner/rust",
"languageVersion": "1.92.0",
}
self.write_catalog()

def test_configure_reports_the_version_and_whether_rust_is_selected(self) -> None:
output = self.root.parent / "github-output"
for targets, version, has_rust in [
("owner:rust", "1.92.0", "true"),
("owner:go", "", "false"),
("owner:go,owner:rust", "1.92.0", "true"),
]:
with self.subTest(targets=targets):
values = run_script(
"generate-rust.py",
self.root,
output,
"configure",
"--targets",
targets,
)
self.assertEqual(values, {"rust-version": version, "has-rust": has_rust})

def test_update_refuses_rust_targets_without_the_toolchain(self) -> None:
targets = select_targets(self.catalog, "owner:rust")
with patch.object(script.shutil, "which", return_value=None), self.assertRaisesRegex(
ValueError, "cargo, rustc, rustfmt"
):
script.update(self.root, targets, Path("rc"), Path("core.yaml"))

def test_update_passes_only_rust_targets_to_the_generator(self) -> None:
targets = select_targets(self.catalog, "")
with patch.object(script.shutil, "which", return_value="/bin/tool"), patch.object(
script.subprocess, "run"
) as run:
script.update(self.root, targets, Path("rc"), Path("core.yaml"))
arguments = run.call_args.args[0]
self.assertEqual(
[arguments[i + 1] for i, arg in enumerate(arguments) if arg == "--target"],
["owner:rust"],
)

def test_update_needs_no_tools_without_rust_targets(self) -> None:
targets = select_targets(self.catalog, "owner:go")
with patch.object(script.shutil, "which", return_value=None), patch.object(
script.subprocess, "run"
) as run:
script.update(self.root, targets, Path("rc"), Path("core.yaml"))
run.assert_not_called()


if __name__ == "__main__":
import unittest

unittest.main()
1 change: 1 addition & 0 deletions tooling/extension-bindings/emitters/rust/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
Loading
Loading