1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
name: Release
# Publishes esp-emac to crates.io when a GitHub Release is published with a
# tag of the form `v<semver>`. Refuses to publish if Cargo.toml and the tag
# disagree, and refuses to re-publish a version already on crates.io
# (idempotent reruns).
#
# Pre-flight: the trait crate `eth-mdio-phy` (in the sibling eth-phy-rs
# repository) MUST already be on crates.io at a compatible version, because
# `cargo publish` emits a registry-only `version = "..."` dep once the local
# `path = "..."` is stripped, and verification will pull that registry copy.
#
# Pipeline (mirrors ci.yml — no green → no publish):
# 1. verify — host fmt + clippy + test + rustdoc on the tagged ref.
# 2. verify-xtensa — re-run the xtensa example build on the tagged ref.
# 3. verify-msrv — re-run the MSRV 1.88 host build on the tagged ref.
# 4. verify-version — confirm tag matches Cargo.toml.
# 5. publish — sparse-index skip-check, dry-run, then cargo publish.
#
# The draft Release itself is kept up-to-date by release-drafter.yml: every
# merged PR refreshes the draft. A maintainer reviews, edits, and publishes
# it manually — publishing creates the git tag, which fires this workflow.
on:
release:
types:
# Manual trigger for re-running the pipeline against an already-
# published release (e.g. after a workflow-only fix lands in `main`
# post-publish). The Release-event's `release.tag_name` is replaced
# by `inputs.tag` when this trigger fires.
workflow_dispatch:
inputs:
tag:
description: 'Release tag to re-publish (e.g. v0.2.0)'
required: true
type: string
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
# Tag we operate on. `release.tag_name` exists when the workflow is
# fired by a Release publish; for `workflow_dispatch` re-runs we fall
# back to the user-supplied input.
TAG_NAME: ${{ github.event.release.tag_name || inputs.tag }}
# See ci.yml — workflow-scope `RUSTFLAGS` would override `.cargo/config.toml`
# rustflags and break the xtensa link. `-D warnings` is enforced through
# clippy's `-- -D warnings` and per-job `RUSTDOCFLAGS`.
jobs:
verify:
name: Verify (re-run CI before publishing)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ env.TAG_NAME }}
path: esp-emac
- uses: actions/checkout@v6
with:
repository: jethub-iot/eth-phy-rs
path: eth-phy
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
targets: riscv32imc-unknown-none-elf
- uses: Swatinem/rust-cache@v2
with:
workspaces: esp-emac
- working-directory: esp-emac
run: cargo fmt --all -- --check
- working-directory: esp-emac
run: cargo clippy --lib --features 'mdio-phy embassy-net async defmt' -- -D warnings
- working-directory: esp-emac
run: cargo test --features 'mdio-phy embassy-net async defmt'
- working-directory: esp-emac
env:
RUSTDOCFLAGS: --cfg docsrs
run: cargo doc --no-deps --target riscv32imc-unknown-none-elf --features 'mdio-phy embassy-net async defmt'
verify-xtensa:
name: Verify (xtensa example builds)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ env.TAG_NAME }}
path: esp-emac
- uses: actions/checkout@v6
with:
repository: jethub-iot/eth-phy-rs
path: eth-phy
- uses: esp-rs/xtensa-toolchain@v1.7
with:
default: true
buildtargets: esp32
ldproxy: false
- uses: Swatinem/rust-cache@v2
with:
workspaces: esp-emac
- working-directory: esp-emac
run: cargo build --release --example embassy_net_lan8720a --target xtensa-esp32-none-elf --features 'esp-hal mdio-phy embassy-net'
verify-msrv:
name: Verify MSRV 1.88 still builds (host)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ env.TAG_NAME }}
path: esp-emac
- uses: actions/checkout@v6
with:
repository: jethub-iot/eth-phy-rs
path: eth-phy
- uses: dtolnay/rust-toolchain@1.88.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: esp-emac
- working-directory: esp-emac
run: cargo build --lib
- working-directory: esp-emac
run: cargo build --lib --features 'mdio-phy embassy-net async defmt'
verify-version:
name: Verify tag matches Cargo.toml
runs-on: ubuntu-latest
needs:
outputs:
version: ${{ steps.tag.outputs.version }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ env.TAG_NAME }}
- name: Resolve version from tag
id: tag
run: |
set -euo pipefail
TAG='${{ env.TAG_NAME }}'
# cargo / crates.io grammar: MAJOR.MINOR.PATCH with optional
# `-prerelease`. SemVer `+build` metadata is intentionally NOT
# accepted — cargo strips it on publish, so a `+build` tag
# would never match Cargo.toml or the registry, only opening
# a duplicate-publish footgun for no real benefit.
if [[ ! "$TAG" =~ ^v([0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?)$ ]]; then
echo "::error::Tag '$TAG' does not match 'v<MAJOR>.<MINOR>.<PATCH>[-<prerelease>]' (build metadata '+...' is not accepted, see release.yml comment)"
exit 1
fi
echo "version=${BASH_REMATCH[1]}" >>"$GITHUB_OUTPUT"
- name: Confirm Cargo.toml agrees
run: |
set -euo pipefail
ACTUAL=$(cargo metadata --no-deps --format-version 1 \
| jq -r '.packages[] | select(.name == "esp-emac") | .version')
if [ "$ACTUAL" != '${{ steps.tag.outputs.version }}' ]; then
echo "::error::Cargo.toml says $ACTUAL, tag says ${{ steps.tag.outputs.version }}"
exit 1
fi
publish:
name: Publish to crates.io
runs-on: ubuntu-latest
needs:
steps:
# Side-checkout the sibling jethub-iot/eth-phy-rs alongside this
# crate. Even though `eth-mdio-phy` is `optional = true` and not
# in the default feature set, cargo parses the *entire* manifest
# (including optional path-deps) before it can strip paths and
# publish. A missing `../eth-phy/...` directory makes
# `cargo publish` fail with "No such file or directory" at the
# manifest-load stage.
- uses: actions/checkout@v6
with:
ref: ${{ env.TAG_NAME }}
path: esp-emac
- uses: actions/checkout@v6
with:
repository: jethub-iot/eth-phy-rs
path: eth-phy
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: esp-emac
- name: Skip if already on crates.io
id: check
run: |
set -euo pipefail
VERSION='${{ needs.verify-version.outputs.version }}'
if curl -fsSL "https://index.crates.io/es/p-/esp-emac" 2>/dev/null \
| jq -e --arg v "$VERSION" 'select(.vers == $v)' >/dev/null; then
echo "esp-emac@$VERSION already published — skipping"
echo "skip=true" >>"$GITHUB_OUTPUT"
else
echo "skip=false" >>"$GITHUB_OUTPUT"
fi
# `cargo publish --dry-run` packages and verifies against the
# registry's read API but never authenticates, so we don't pass
# `CARGO_REGISTRY_TOKEN` here. Keeping the token off this step
# narrows its blast radius.
#
# The default `--verify` step compiles the packaged crate against
# the **default** feature set. esp-emac's default is empty
# (no `esp-hal`, no `eth-mdio-phy`), but the manifest-load itself
# still needs the sibling eth-phy-rs checkout above.
- name: Dry-run publish
if: steps.check.outputs.skip == 'false'
working-directory: esp-emac
run: cargo publish --dry-run
- name: Publish to crates.io
if: steps.check.outputs.skip == 'false'
working-directory: esp-emac
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.JETHUB_CRATES_TOKEN }}
run: cargo publish