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())