Skip to content
This repository was archived by the owner on Aug 27, 2026. It is now read-only.

Commit b370789

Browse files
committed
Build release artifacts with cargo-dist
cargo-dist[^1] is a new tool that makes it easy to build and package release artifacts for Rust applications. It supports integration with GitHub, where it builds artifacts on GitHub Actions and publishes them to a matching GitHub release.
1 parent 715ab91 commit b370789

3 files changed

Lines changed: 300 additions & 15 deletions

File tree

‎.github/workflows/crates.yml‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
name: crates.io
3+
4+
"on":
5+
release:
6+
types:
7+
- published
8+
9+
jobs:
10+
crates:
11+
name: Publish
12+
runs-on: ubuntu-latest
13+
14+
container:
15+
image: ghcr.io/jdno/rust:main
16+
17+
steps:
18+
- name: Checkout code
19+
uses: actions/checkout@v4
20+
21+
- name: Cache build artifacts
22+
uses: swatinem/rust-cache@v2.7.1
23+
24+
- name: Publish to crates.io
25+
run: cargo publish --token ${{ secrets.CRATES_TOKEN }} -v --all-features

‎.github/workflows/release.yml‎

Lines changed: 256 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,266 @@
1-
---
1+
# Copyright 2022-2023, axodotdev
2+
# SPDX-License-Identifier: MIT or Apache-2.0
3+
#
4+
# CI that:
5+
#
6+
# * checks for a Git Tag that looks like a release
7+
# * builds artifacts with cargo-dist (archives, installers, hashes)
8+
# * uploads those artifacts to temporary workflow zip
9+
# * on success, uploads the artifacts to a Github Release
10+
#
11+
# Note that a Github Release with this tag is assumed to exist as a draft
12+
# with the appropriate title/body, and will be undrafted for you.
13+
214
name: Release
315

4-
"on":
5-
release:
6-
types:
7-
- published
16+
permissions:
17+
contents: write
18+
19+
# This task will run whenever you push a git tag that looks like a version
20+
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
21+
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
22+
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
23+
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
24+
#
25+
# If PACKAGE_NAME is specified, then the announcement will be for that
26+
# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
27+
#
28+
# If PACKAGE_NAME isn't specified, then the announcement will be for all
29+
# (cargo-dist-able) packages in the workspace with that version (this mode is
30+
# intended for workspaces with only one dist-able package, or with all dist-able
31+
# packages versioned/released in lockstep).
32+
#
33+
# If you push multiple tags at once, separate instances of this workflow will
34+
# spin up, creating an independent announcement for each one. However Github
35+
# will hard limit this to 3 tags per commit, as it will assume more tags is a
36+
# mistake.
37+
#
38+
# If there's a prerelease-style suffix to the version, then the release(s)
39+
# will be marked as a prerelease.
40+
on:
41+
push:
42+
tags:
43+
- "**[0-9]+.[0-9]+.[0-9]+*"
44+
pull_request:
845

946
jobs:
10-
crates:
11-
name: Publish to crates.io
47+
# Run 'cargo dist plan' (or host) to determine what tasks we need to do
48+
plan:
49+
name: Plan
1250
runs-on: ubuntu-latest
51+
outputs:
52+
val: ${{ steps.plan.outputs.manifest }}
53+
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
54+
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
55+
publishing: ${{ !github.event.pull_request }}
56+
env:
57+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58+
steps:
59+
- uses: actions/checkout@v4
60+
with:
61+
submodules: recursive
62+
- name: Install cargo-dist
63+
# we specify bash to get pipefail; it guards against the `curl` command
64+
# failing. otherwise `sh` won't catch that `curl` returned non-0
65+
shell: bash
66+
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.6.0/cargo-dist-installer.sh | sh"
67+
# sure would be cool if github gave us proper conditionals...
68+
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
69+
# functionality based on whether this is a pull_request, and whether it's from a fork.
70+
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
71+
# but also really annoying to build CI around when it needs secrets to work right.)
72+
- id: plan
73+
run: |
74+
cargo dist ${{ !github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name) || (github.event.pull_request.head.repo.fork && 'plan' || 'host --steps=check') }} --output-format=json > dist-manifest.json
75+
echo "cargo dist ran successfully"
76+
cat dist-manifest.json
77+
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
78+
- name: "Upload dist-manifest.json"
79+
uses: actions/upload-artifact@v3
80+
with:
81+
name: artifacts
82+
path: dist-manifest.json
83+
84+
# Build and packages all the platform-specific things
85+
build-local-artifacts:
86+
name: Build local artifacts
87+
# Let the initial task tell us to not run (currently very blunt)
88+
needs:
89+
- plan
90+
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
91+
strategy:
92+
fail-fast: false
93+
# Target platforms/runners are computed by cargo-dist in create-release.
94+
# Each member of the matrix has the following arguments:
95+
#
96+
# - runner: the github runner
97+
# - dist-args: cli flags to pass to cargo dist
98+
# - install-dist: expression to run to install cargo-dist on the runner
99+
#
100+
# Typically there will be:
101+
# - 1 "global" task that builds universal installers
102+
# - N "local" tasks that build each platform's binaries and platform-specific installers
103+
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
104+
runs-on: ${{ matrix.runner }}
105+
env:
106+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
107+
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
108+
steps:
109+
- uses: actions/checkout@v4
110+
with:
111+
submodules: recursive
112+
- uses: swatinem/rust-cache@v2
113+
- name: Install cargo-dist
114+
run: ${{ matrix.install_dist }}
115+
# Get the dist-manifest
116+
- name: Fetch local artifacts
117+
uses: actions/download-artifact@v3
118+
with:
119+
name: artifacts
120+
path: target/distrib/
121+
- name: Install dependencies
122+
run: |
123+
${{ matrix.packages_install }}
124+
- name: Build artifacts
125+
run: |
126+
# Actually do builds and make zips and whatnot
127+
cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
128+
echo "cargo dist ran successfully"
129+
- id: cargo-dist
130+
name: Post-build
131+
# We force bash here just because github makes it really hard to get values up
132+
# to "real" actions without writing to env-vars, and writing to env-vars has
133+
# inconsistent syntax between shell and powershell.
134+
shell: bash
135+
run: |
136+
# Parse out what we just built and upload it to scratch storage
137+
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
138+
jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
139+
echo "EOF" >> "$GITHUB_OUTPUT"
13140
14-
container:
15-
image: ghcr.io/jdno/rust:main
141+
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
142+
- name: "Upload artifacts"
143+
uses: actions/upload-artifact@v3
144+
with:
145+
name: artifacts
146+
path: |
147+
${{ steps.cargo-dist.outputs.paths }}
148+
${{ env.BUILD_MANIFEST_NAME }}
16149
150+
# Build and package all the platform-agnostic(ish) things
151+
build-global-artifacts:
152+
name: Build global artifacts
153+
needs:
154+
- plan
155+
- build-local-artifacts
156+
runs-on: "ubuntu-20.04"
157+
env:
158+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
159+
BUILD_MANIFEST_NAME: target/distrib/dist-manifest.json
17160
steps:
18-
- name: Checkout code
19-
uses: actions/checkout@v4
161+
- uses: actions/checkout@v4
162+
with:
163+
submodules: recursive
164+
- name: Install cargo-dist
165+
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.6.0/cargo-dist-installer.sh | sh"
166+
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
167+
- name: Fetch local artifacts
168+
uses: actions/download-artifact@v3
169+
with:
170+
name: artifacts
171+
path: target/distrib/
172+
- id: cargo-dist
173+
shell: bash
174+
run: |
175+
cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
176+
echo "cargo dist ran successfully"
177+
178+
# Parse out what we just built and upload it to scratch storage
179+
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
180+
jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
181+
echo "EOF" >> "$GITHUB_OUTPUT"
20182
21-
- name: Cache build artifacts
22-
uses: swatinem/rust-cache@v2.7.1
183+
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
184+
- name: "Upload artifacts"
185+
uses: actions/upload-artifact@v3
186+
with:
187+
name: artifacts
188+
path: |
189+
${{ steps.cargo-dist.outputs.paths }}
190+
${{ env.BUILD_MANIFEST_NAME }}
191+
# Determines if we should publish/announce
192+
host:
193+
name: Prepare release
194+
needs:
195+
- plan
196+
- build-local-artifacts
197+
- build-global-artifacts
198+
# Only run if we're "publishing", and only if local and global didn't fail (skipped is fine)
199+
if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
200+
env:
201+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
202+
runs-on: "ubuntu-20.04"
203+
outputs:
204+
val: ${{ steps.host.outputs.manifest }}
205+
steps:
206+
- uses: actions/checkout@v4
207+
with:
208+
submodules: recursive
209+
- name: Install cargo-dist
210+
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.6.0/cargo-dist-installer.sh | sh"
211+
# Fetch artifacts from scratch-storage
212+
- name: Fetch artifacts
213+
uses: actions/download-artifact@v3
214+
with:
215+
name: artifacts
216+
path: target/distrib/
217+
# This is a harmless no-op for Github Releases, hosting for that happens in "announce"
218+
- id: host
219+
shell: bash
220+
run: |
221+
cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
222+
echo "artifacts uploaded and released successfully"
223+
cat dist-manifest.json
224+
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
225+
- name: "Upload dist-manifest.json"
226+
uses: actions/upload-artifact@v3
227+
with:
228+
name: artifacts
229+
path: dist-manifest.json
23230

24-
- name: Publish to crates.io
25-
run: cargo publish --token ${{ secrets.CRATES_TOKEN }} -v --all-features
231+
# Create a Github Release while uploading all files to it
232+
announce:
233+
name: Publish release
234+
needs:
235+
- plan
236+
- host
237+
# use "always() && ..." to allow us to wait for all publish jobs while
238+
# still allowing individual publish jobs to skip themselves (for prereleases).
239+
# "host" however must run to completion, no skipping allowed!
240+
if: ${{ always() && needs.host.result == 'success' }}
241+
runs-on: "ubuntu-20.04"
242+
env:
243+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
244+
steps:
245+
- uses: actions/checkout@v4
246+
with:
247+
submodules: recursive
248+
- name: "Download Github Artifacts"
249+
uses: actions/download-artifact@v3
250+
with:
251+
name: artifacts
252+
path: artifacts
253+
- name: Cleanup
254+
run: |
255+
# Remove the granular manifests
256+
rm -f artifacts/*-dist-manifest.json
257+
- name: Create Github Release
258+
uses: ncipollo/release-action@v1
259+
with:
260+
tag: ${{ needs.plan.outputs.tag }}
261+
allowUpdates: true
262+
updateOnlyUnreleased: true
263+
omitBodyDuringUpdate: true
264+
omitNameDuringUpdate: true
265+
prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }}
266+
artifacts: "artifacts/*"

‎Cargo.toml‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,22 @@ mockito = "1.2.0"
4444
serde_json = "1.0.108"
4545
tempfile = "3.8.1"
4646
serde_yaml = { version = "0.9.27" }
47+
48+
[profile.dist]
49+
inherits = "release"
50+
lto = "thin"
51+
52+
# Config for 'cargo dist'
53+
[workspace.metadata.dist]
54+
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
55+
cargo-dist-version = "0.6.0"
56+
# Whether cargo-dist should create a Github Release or use an existing draft
57+
create-release = false
58+
# The installers to generate for each app
59+
installers = ["shell"]
60+
# Target platforms to build apps for (Rust target-triple syntax)
61+
targets = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin", "x86_64-apple-darwin"]
62+
# CI backends to support
63+
ci = ["github"]
64+
# Publish jobs to run in CI
65+
pr-run-mode = "upload"

0 commit comments

Comments
 (0)