@ondeinference / ed / commits / 5646e61

Add rust crates dist

Seto Elkahfi committed Sep 10, 2026 at 22:16 UTC 5646e6151a04a80feb2123b08c4cbc9450ea197d
3 files changed +217 -2
.github/workflows/release-sdk-rust.yml new
+140
@@ -0,0 +1,140 @@
1 +name: Release SDK Rust (crates.io)
2 +
3 +on:
4 + push:
5 + tags:
6 + - "v[0-9]+.[0-9]+.[0-9]+"
7 + workflow_dispatch:
8 + inputs:
9 + publish:
10 + description: "Publish the selected existing release tag to crates.io"
11 + type: boolean
12 + default: false
13 + release_tag:
14 + description: "Existing vX.Y.Z tag to validate or publish on manual runs"
15 + type: string
16 + required: false
17 +
18 +permissions:
19 + contents: read
20 +
21 +jobs:
22 + preflight:
23 + runs-on: macos-26
24 + defaults:
25 + run:
26 + working-directory: ed
27 + steps:
28 + - uses: actions/checkout@v6
29 + with:
30 + path: ed
31 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref }}
32 + - name: Read Rust toolchain
33 + run: |
34 + rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
35 + test -n "$rust_toolchain"
36 + echo "RUST_TOOLCHAIN=$rust_toolchain" >> "$GITHUB_ENV"
37 + - uses: dtolnay/rust-toolchain@stable
38 + with:
39 + toolchain: ${{ env.RUST_TOOLCHAIN }}
40 + components: rustfmt, clippy
41 + - name: Validate release tag matches workspace version
42 + if: startsWith(github.ref, 'refs/tags/') || inputs.publish
43 + env:
44 + INPUT_RELEASE_TAG: ${{ inputs.release_tag }}
45 + run: |
46 + if [ "$GITHUB_REF_TYPE" = tag ]; then
47 + release_tag="$GITHUB_REF_NAME"
48 + else
49 + release_tag="$INPUT_RELEASE_TAG"
50 + fi
51 + test -n "$release_tag"
52 + [[ "$release_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
53 + git show-ref --verify --quiet "refs/tags/$release_tag"
54 + test "$(git rev-list -n 1 "$release_tag")" = "$(git rev-parse HEAD)"
55 + version="$(cargo metadata --no-deps --format-version 1 | python3 -c 'import json,sys; print(next(p["version"] for p in json.load(sys.stdin)["packages"] if p["name"] == "ed-agent"))')"
56 + test "${release_tag#v}" = "$version"
57 + - run: cargo fmt --all -- --check
58 + - run: cargo test --locked --workspace
59 + - run: cargo clippy --locked --workspace --all-targets -- -D warnings
60 + - run: cargo publish --dry-run --locked -p ed-agent
61 + - run: cargo publish --dry-run --locked -p ed-agent-tauri
62 +
63 + publish:
64 + needs: preflight
65 + if: startsWith(github.ref, 'refs/tags/') || inputs.publish
66 + runs-on: macos-26
67 + permissions:
68 + contents: read
69 + id-token: write
70 + defaults:
71 + run:
72 + working-directory: ed
73 + steps:
74 + - uses: actions/checkout@v6
75 + with:
76 + path: ed
77 + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref }}
78 + - name: Read Rust toolchain
79 + run: |
80 + rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
81 + test -n "$rust_toolchain"
82 + echo "RUST_TOOLCHAIN=$rust_toolchain" >> "$GITHUB_ENV"
83 + - uses: dtolnay/rust-toolchain@stable
84 + with:
85 + toolchain: ${{ env.RUST_TOOLCHAIN }}
86 + - id: version
87 + run: |
88 + echo "value=$(cargo metadata --no-deps --format-version 1 | python3 -c 'import json,sys; print(next(p["version"] for p in json.load(sys.stdin)["packages"] if p["name"] == "ed-agent"))')" >> "$GITHUB_OUTPUT"
89 + - name: Check whether ed-agent is already published
90 + id: core-published
91 + env:
92 + VERSION: ${{ steps.version.outputs.value }}
93 + run: |
94 + if python3 scripts/crates-io-published.py "$VERSION" --crate ed-agent; then
95 + echo "skip=true" >> "$GITHUB_OUTPUT"
96 + else
97 + status=$?
98 + test "$status" = 1 || exit "$status"
99 + echo "skip=false" >> "$GITHUB_OUTPUT"
100 + fi
101 + - name: Authenticate to publish ed-agent
102 + if: steps.core-published.outputs.skip != 'true'
103 + id: core-auth
104 + uses: rust-lang/crates-io-auth-action@v1
105 + - name: Publish ed-agent
106 + if: steps.core-published.outputs.skip != 'true'
107 + env:
108 + CARGO_REGISTRY_TOKEN: ${{ steps.core-auth.outputs.token }}
109 + run: cargo publish --locked -p ed-agent
110 + - name: Verify ed-agent reached crates.io
111 + if: steps.core-published.outputs.skip != 'true'
112 + env:
113 + VERSION: ${{ steps.version.outputs.value }}
114 + run: python3 scripts/crates-io-published.py "$VERSION" --crate ed-agent --wait
115 + - name: Check whether ed-agent-tauri is already published
116 + id: tauri-published
117 + env:
118 + VERSION: ${{ steps.version.outputs.value }}
119 + run: |
120 + if python3 scripts/crates-io-published.py "$VERSION" --crate ed-agent-tauri; then
121 + echo "skip=true" >> "$GITHUB_OUTPUT"
122 + else
123 + status=$?
124 + test "$status" = 1 || exit "$status"
125 + echo "skip=false" >> "$GITHUB_OUTPUT"
126 + fi
127 + - name: Authenticate to publish ed-agent-tauri
128 + if: steps.tauri-published.outputs.skip != 'true'
129 + id: tauri-auth
130 + uses: rust-lang/crates-io-auth-action@v1
131 + - name: Publish ed-agent-tauri
132 + if: steps.tauri-published.outputs.skip != 'true'
133 + env:
134 + CARGO_REGISTRY_TOKEN: ${{ steps.tauri-auth.outputs.token }}
135 + run: cargo publish --locked -p ed-agent-tauri
136 + - name: Verify ed-agent-tauri reached crates.io
137 + if: steps.tauri-published.outputs.skip != 'true'
138 + env:
139 + VERSION: ${{ steps.version.outputs.value }}
140 + run: python3 scripts/crates-io-published.py "$VERSION" --crate ed-agent-tauri --wait
.github/workflows/release-sdk-swift.yml
+2 -2
@@ -3,7 +3,7 @@ name: Release SDK Swift
3 on:
4 push:
5 tags:
6 - - "[0-9]+.[0-9]+.[0-9]+"
6 + - "v[0-9]+.[0-9]+.[0-9]+"
7 workflow_dispatch:
8 inputs:
9 publish:
@@ -58,7 +58,7 @@ jobs:
58 if: startsWith(github.ref, 'refs/tags/')
59 run: |
60 version="$(cargo metadata --no-deps --format-version 1 | python3 -c 'import json,sys; print(next(p["version"] for p in json.load(sys.stdin)["packages"] if p["name"] == "ed-agent"))')"
61 - test "$GITHUB_REF_NAME" = "$version"
61 + test "${GITHUB_REF_NAME#v}" = "$version"
62 - run: cargo fmt --all -- --check
63 - run: cargo test --locked --workspace
64 - run: cargo clippy --locked --workspace --all-targets -- -D warnings
scripts/crates-io-published.py new
+75
@@ -0,0 +1,75 @@
1 +#!/usr/bin/env python3
2 +"""Check whether a crate version is visible in the crates.io sparse index.
3 +
4 +Exit 0 when published, 1 when absent, and 2 when the registry could not be
5 +queried. Do not use `cargo info` for this: inside a workspace it can resolve
6 +the local package and report a version as published when it is not.
7 +"""
8 +
9 +import argparse
10 +import json
11 +import sys
12 +import time
13 +import urllib.error
14 +import urllib.request
15 +
16 +
17 +def index_path(crate: str) -> str:
18 + """Return the sparse-index path for a crate name."""
19 + name = crate.lower()
20 + if len(name) == 1:
21 + return f"1/{name}"
22 + if len(name) == 2:
23 + return f"2/{name}"
24 + if len(name) == 3:
25 + return f"3/{name[0]}/{name}"
26 + return f"{name[:2]}/{name[2:4]}/{name}"
27 +
28 +
29 +def versions(crate: str) -> list[str]:
30 + request = urllib.request.Request(
31 + f"https://index.crates.io/{index_path(crate)}",
32 + headers={"User-Agent": "ed-release (https://github.com/ondeinference/ed)"},
33 + )
34 + try:
35 + with urllib.request.urlopen(request, timeout=30) as response:
36 + body = response.read().decode("utf-8")
37 + except urllib.error.HTTPError as error:
38 + if error.code == 404:
39 + return []
40 + raise
41 + return [json.loads(line)["vers"] for line in body.splitlines() if line.strip()]
42 +
43 +
44 +def main() -> int:
45 + parser = argparse.ArgumentParser(description=__doc__)
46 + parser.add_argument("version")
47 + parser.add_argument("--crate", required=True)
48 + parser.add_argument("--wait", action="store_true")
49 + parser.add_argument("--attempts", type=int, default=10)
50 + parser.add_argument("--interval", type=int, default=15)
51 + args = parser.parse_args()
52 +
53 + total_attempts = args.attempts if args.wait else 1
54 + for attempt in range(1, total_attempts + 1):
55 + try:
56 + if args.version in versions(args.crate):
57 + print(f"{args.crate} {args.version} is on crates.io")
58 + return 0
59 + except (OSError, ValueError, urllib.error.URLError) as error:
60 + print(f"could not reach the crates.io index: {error}", file=sys.stderr)
61 + return 2
62 +
63 + if attempt < total_attempts:
64 + print(
65 + f"attempt {attempt}: {args.crate} {args.version} is not visible; "
66 + f"waiting {args.interval}s"
67 + )
68 + time.sleep(args.interval)
69 +
70 + print(f"{args.crate} {args.version} is not on crates.io")
71 + return 1
72 +
73 +
74 +if __name__ == "__main__":
75 + sys.exit(main())