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
name: Release
on:
push:
tags:
- 'v*'
# Default to least privilege (I1). The `release` job overrides to
# `contents: write` locally to create the GitHub Release; the
# `build` and `publish-crate` jobs stay read-only.
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
archive: tar.gz
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
- target: x86_64-apple-darwin
os: macos-latest
archive: tar.gz
- target: aarch64-apple-darwin
os: macos-latest
archive: tar.gz
- target: x86_64-pc-windows-msvc
os: windows-latest
archive: zip
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: release-${{ matrix.target }}
- name: Install cross-compilation tools
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Install musl tools
if: matrix.target == 'x86_64-unknown-linux-musl'
run: |
sudo apt-get update
sudo apt-get install -y musl-tools
- name: Build
run: cargo build --release --target ${{ matrix.target }} --bin lob
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
- name: Package (Unix)
if: matrix.os != 'windows-latest'
run: |
cd target/${{ matrix.target }}/release
tar czf ../../../lob-${{ matrix.target }}.tar.gz lob
cd ../../..
- name: Package (Windows)
if: matrix.os == 'windows-latest'
run: |
cd target/${{ matrix.target }}/release
7z a ../../../lob-${{ matrix.target }}.zip lob.exe
cd ../../..
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: lob-${{ matrix.target }}
path: lob-${{ matrix.target }}.${{ matrix.archive }}
release:
name: Create Release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Extract changelog for this version
run: |
VERSION=${GITHUB_REF_NAME#v}
awk "/^## \[${VERSION}\]/{found=1; next} /^## \[/{if(found) exit} found" CHANGELOG.md > RELEASE_NOTES.md
if [ ! -s RELEASE_NOTES.md ]; then
echo "No CHANGELOG entry for ${VERSION}, falling back to auto-generated notes." > RELEASE_NOTES.md
fi
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: artifacts/**/*
body_path: RELEASE_NOTES.md
publish-crate:
name: Publish to crates.io
needs: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
# Version-gated publish (I1). Replaces `|| true`, which used
# to mask real publish failures (network, auth, malformed
# manifest) alongside the intended "already uploaded" case.
# Now: we only skip when crates.io reports the exact version
# we're about to publish; every other kind of failure
# surfaces and fails the job.
#
# Order matters: nanobook is the base crate, others depend
# on it. Publish in dependency order so the crates.io index
# resolves correctly.
- name: Publish workspace
run: |
set -euo pipefail
publish_if_new() {
local pkg="$1"
local current
current="$(cargo pkgid -p "$pkg" | sed -E 's/.*[@#]([0-9]+\.[0-9]+\.[0-9]+.*)/\1/')"
local published
published="$(cargo search "$pkg" --limit 1 | head -1 | sed -nE 's/.*"([0-9]+\.[0-9]+\.[0-9]+[^"]*)".*/\1/p')"
if [ "$current" = "$published" ]; then
echo "$pkg $current already published — skipping"
return 0
fi
echo "publishing $pkg $current (crates.io has $published)"
cargo publish -p "$pkg"
}
publish_if_new nanobook
publish_if_new nanobook-broker
publish_if_new nanobook-risk
publish_if_new nanobook-rebalancer
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}