| 1 | name: Release |
| 2 | |
| 3 | on: |
| 4 | push: |
| 5 | tags: ['v*'] |
| 6 | workflow_dispatch: |
| 7 | inputs: |
| 8 | version: |
| 9 | description: 'Package/release version to publish to npm, without the leading v' |
| 10 | required: true |
| 11 | type: string |
| 12 | |
| 13 | env: |
| 14 | CARGO_TERM_COLOR: always |
| 15 | RUSTFLAGS: -Dwarnings |
| 16 | |
| 17 | jobs: |
| 18 | parity: |
| 19 | if: github.event_name == 'push' |
| 20 | runs-on: ubuntu-latest |
| 21 | steps: |
| 22 | - uses: actions/checkout@v4 |
| 23 | - uses: dtolnay/rust-toolchain@stable |
| 24 | with: |
| 25 | components: clippy, rustfmt |
| 26 | - name: Install Linux system dependencies |
| 27 | if: runner.os == 'Linux' |
| 28 | run: | |
| 29 | for i in 1 2 3 4 5; do |
| 30 | sudo apt-get update && break |
| 31 | echo "apt-get update failed (attempt $i); retrying in 15s" |
| 32 | sleep 15 |
| 33 | done |
| 34 | sudo apt-get install -y libdbus-1-dev pkg-config |
| 35 | - uses: Swatinem/rust-cache@v2 |
| 36 | - name: Format check |
| 37 | run: cargo fmt --all -- --check |
| 38 | - name: Compile check |
| 39 | run: cargo check --workspace --all-targets --locked |
| 40 | - name: Clippy |
| 41 | run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings |
| 42 | - name: Workspace tests |
| 43 | run: cargo test --workspace --all-features --locked |
| 44 | - name: TUI snapshot parity |
| 45 | run: cargo test -p deepseek-tui-core --test snapshot --locked |
| 46 | - name: Protocol schema parity |
| 47 | run: cargo test -p deepseek-protocol --test parity_protocol --locked |
| 48 | - name: State persistence parity |
| 49 | run: cargo test -p deepseek-state --test parity_state --locked |
| 50 | - name: Lockfile drift guard |
| 51 | run: git diff --exit-code -- Cargo.lock |
| 52 | |
| 53 | build: |
| 54 | needs: parity |
| 55 | # `parity` is gated to tag-push events. On manual `workflow_dispatch`, |
| 56 | # parity is skipped, so let `build` proceed when parity either succeeded |
| 57 | # or was skipped — but never when it actually failed or the run was |
| 58 | # cancelled. Operators using dispatch are expected to have already run |
| 59 | # the same gates locally / via ci.yml on `main`. |
| 60 | if: ${{ !cancelled() && (needs.parity.result == 'success' || needs.parity.result == 'skipped') }} |
| 61 | strategy: |
| 62 | matrix: |
| 63 | include: |
| 64 | # --- deepseek (cli) --- |
| 65 | - os: ubuntu-latest |
| 66 | target: x86_64-unknown-linux-gnu |
| 67 | target_zig: x86_64-unknown-linux-gnu.2.28 |
| 68 | binary: deepseek |
| 69 | artifact_name: deepseek-linux-x64 |
| 70 | - os: ubuntu-24.04-arm |
| 71 | target: aarch64-unknown-linux-gnu |
| 72 | target_zig: aarch64-unknown-linux-gnu.2.28 |
| 73 | binary: deepseek |
| 74 | artifact_name: deepseek-linux-arm64 |
| 75 | - os: macos-latest |
| 76 | target: x86_64-apple-darwin |
| 77 | binary: deepseek |
| 78 | artifact_name: deepseek-macos-x64 |
| 79 | - os: macos-latest |
| 80 | target: aarch64-apple-darwin |
| 81 | binary: deepseek |
| 82 | artifact_name: deepseek-macos-arm64 |
| 83 | - os: windows-latest |
| 84 | target: x86_64-pc-windows-msvc |
| 85 | binary: deepseek.exe |
| 86 | artifact_name: deepseek-windows-x64.exe |
| 87 | # --- deepseek-tui (TUI) --- |
| 88 | - os: ubuntu-latest |
| 89 | target: x86_64-unknown-linux-gnu |
| 90 | target_zig: x86_64-unknown-linux-gnu.2.28 |
| 91 | binary: deepseek-tui |
| 92 | artifact_name: deepseek-tui-linux-x64 |
| 93 | - os: ubuntu-24.04-arm |
| 94 | target: aarch64-unknown-linux-gnu |
| 95 | target_zig: aarch64-unknown-linux-gnu.2.28 |
| 96 | binary: deepseek-tui |
| 97 | artifact_name: deepseek-tui-linux-arm64 |
| 98 | - os: macos-latest |
| 99 | target: x86_64-apple-darwin |
| 100 | binary: deepseek-tui |
| 101 | artifact_name: deepseek-tui-macos-x64 |
| 102 | - os: macos-latest |
| 103 | target: aarch64-apple-darwin |
| 104 | binary: deepseek-tui |
| 105 | artifact_name: deepseek-tui-macos-arm64 |
| 106 | - os: windows-latest |
| 107 | target: x86_64-pc-windows-msvc |
| 108 | binary: deepseek-tui.exe |
| 109 | artifact_name: deepseek-tui-windows-x64.exe |
| 110 | runs-on: ${{ matrix.os }} |
| 111 | steps: |
| 112 | - uses: actions/checkout@v4 |
| 113 | - uses: dtolnay/rust-toolchain@stable |
| 114 | with: |
| 115 | targets: ${{ matrix.target }} |
| 116 | - uses: Swatinem/rust-cache@v2 |
| 117 | - name: Install Linux system dependencies |
| 118 | if: runner.os == 'Linux' |
| 119 | run: | |
| 120 | for i in 1 2 3 4 5; do |
| 121 | sudo apt-get update && break |
| 122 | echo "apt-get update failed (attempt $i); retrying in 15s" |
| 123 | sleep 15 |
| 124 | done |
| 125 | sudo apt-get install -y libdbus-1-dev pkg-config |
| 126 | - name: Install zig |
| 127 | if: runner.os == 'Linux' |
| 128 | uses: goto-bus-stop/setup-zig@v2 |
| 129 | with: |
| 130 | version: '0.13.0' |
| 131 | - name: Install cargo-zigbuild |
| 132 | if: runner.os == 'Linux' |
| 133 | run: cargo install cargo-zigbuild --locked |
| 134 | - name: Build |
| 135 | shell: bash |
| 136 | run: | |
| 137 | if [ -n "${{ matrix.target_zig }}" ]; then |
| 138 | cargo zigbuild --release --locked --target ${{ matrix.target_zig }} |
| 139 | else |
| 140 | cargo build --release --locked --target ${{ matrix.target }} |
| 141 | fi |
| 142 | - name: Rename binary |
| 143 | shell: bash |
| 144 | run: | |
| 145 | # cargo zigbuild writes binaries to target/<rust-target>/release/ — |
| 146 | # the .glibc suffix in matrix.target_zig is consumed by zig as a CC |
| 147 | # flag, not by cargo as a target dir. Always use the rust target. |
| 148 | BIN_PATH="target/${{ matrix.target }}/release/${{ matrix.binary }}" |
| 149 | if [ ! -f "${BIN_PATH}" ]; then |
| 150 | echo "Binary not at ${BIN_PATH}; searching target/ for ${{ matrix.binary }}:" |
| 151 | find target -name "${{ matrix.binary }}" -type f |
| 152 | exit 1 |
| 153 | fi |
| 154 | cp "${BIN_PATH}" "${{ matrix.artifact_name }}" |
| 155 | - uses: actions/upload-artifact@v4 |
| 156 | with: |
| 157 | name: ${{ matrix.artifact_name }} |
| 158 | path: ${{ matrix.artifact_name }} |
| 159 | docker: |
| 160 | needs: build |
| 161 | if: ${{ !cancelled() && needs.build.result == 'success' }} |
| 162 | runs-on: ubuntu-latest |
| 163 | permissions: |
| 164 | contents: read |
| 165 | packages: write |
| 166 | steps: |
| 167 | - uses: actions/checkout@v4 |
| 168 | - name: Set up QEMU |
| 169 | uses: docker/setup-qemu-action@v3 |
| 170 | - name: Set up Docker Buildx |
| 171 | uses: docker/setup-buildx-action@v3 |
| 172 | - name: Log in to GitHub Container Registry |
| 173 | uses: docker/login-action@v3 |
| 174 | with: |
| 175 | registry: ghcr.io |
| 176 | username: ${{ github.repository_owner }} |
| 177 | password: ${{ secrets.GITHUB_TOKEN }} |
| 178 | - name: Extract metadata |
| 179 | id: meta |
| 180 | uses: docker/metadata-action@v5 |
| 181 | with: |
| 182 | images: | |
| 183 | ghcr.io/${{ github.repository }} |
| 184 | tags: | |
| 185 | type=semver,pattern={{version}} |
| 186 | type=semver,pattern={{major}}.{{minor}} |
| 187 | type=semver,pattern=v{{major}} |
| 188 | type=ref,event=tag |
| 189 | - name: Build and push |
| 190 | uses: docker/build-push-action@v6 |
| 191 | with: |
| 192 | context: . |
| 193 | platforms: linux/amd64,linux/arm64 |
| 194 | push: true |
| 195 | tags: ${{ steps.meta.outputs.tags }} |
| 196 | labels: ${{ steps.meta.outputs.labels }} |
| 197 | cache-from: type=gha |
| 198 | cache-to: type=gha,mode=max |
| 199 | |
| 200 | release: |
| 201 | needs: build |
| 202 | if: ${{ !cancelled() && needs.build.result == 'success' }} |
| 203 | runs-on: ubuntu-latest |
| 204 | permissions: |
| 205 | contents: write |
| 206 | steps: |
| 207 | - uses: actions/download-artifact@v4 |
| 208 | with: |
| 209 | path: artifacts |
| 210 | - name: List artifacts |
| 211 | run: find artifacts -type f |
| 212 | - name: Generate checksum manifest |
| 213 | shell: bash |
| 214 | run: | |
| 215 | mkdir -p artifacts/checksums |
| 216 | manifest="artifacts/checksums/deepseek-artifacts-sha256.txt" |
| 217 | : > "${manifest}" |
| 218 | while IFS= read -r -d '' file; do |
| 219 | hash="$(sha256sum "${file}" | awk '{print $1}')" |
| 220 | base="$(basename "${file}")" |
| 221 | printf '%s %s\n' "${hash}" "${base}" >> "${manifest}" |
| 222 | done < <(find artifacts -type f ! -path 'artifacts/checksums/*' -print0 | sort -z) |
| 223 | cat "${manifest}" |
| 224 | - uses: softprops/action-gh-release@v1 |
| 225 | with: |
| 226 | files: artifacts/*/* |
| 227 | prerelease: false |
| 228 | body: | |
| 229 | ## Install |
| 230 | |
| 231 | ### Recommended — npm (one command, both binaries) |
| 232 | |
| 233 | ```bash |
| 234 | npm install -g deepseek-tui |
| 235 | ``` |
| 236 | |
| 237 | The wrapper downloads both binaries from this Release and places them in the same directory. |
| 238 | |
| 239 | ### Cargo (Linux / macOS) |
| 240 | |
| 241 | ```bash |
| 242 | cargo install deepseek-tui-cli deepseek-tui --locked |
| 243 | ``` |
| 244 | |
| 245 | Both crates are required — `deepseek-tui-cli` produces the `deepseek` dispatcher and `deepseek-tui` produces the interactive runtime that the dispatcher delegates to. Installing only one binary will fail at runtime with a `MISSING_COMPANION_BINARY` error. |
| 246 | |
| 247 | ### Manual download |
| 248 | |
| 249 | **Both** binaries below must be downloaded for your platform and dropped into the same directory (e.g. `~/.local/bin/`): |
| 250 | |
| 251 | | Platform | Dispatcher | TUI runtime | |
| 252 | |---|---|---| |
| 253 | | Linux x64 | `deepseek-linux-x64` | `deepseek-tui-linux-x64` | |
| 254 | | Linux ARM64 | `deepseek-linux-arm64` | `deepseek-tui-linux-arm64` | |
| 255 | | macOS x64 | `deepseek-macos-x64` | `deepseek-tui-macos-x64` | |
| 256 | | macOS ARM | `deepseek-macos-arm64` | `deepseek-tui-macos-arm64` | |
| 257 | | Windows x64 | `deepseek-windows-x64.exe` | `deepseek-tui-windows-x64.exe` | |
| 258 | |
| 259 | Then `chmod +x` both (Unix) and run `./deepseek`. |
| 260 | |
| 261 | ### Verify (recommended) |
| 262 | |
| 263 | Download `deepseek-artifacts-sha256.txt` from this Release and verify: |
| 264 | |
| 265 | ```bash |
| 266 | # Linux |
| 267 | sha256sum -c deepseek-artifacts-sha256.txt |
| 268 | |
| 269 | # macOS |
| 270 | shasum -a 256 -c deepseek-artifacts-sha256.txt |
| 271 | ``` |
| 272 | |
| 273 | ## Changelog |
| 274 | |
| 275 | See [CHANGELOG.md](https://github.com/Hmbown/DeepSeek-TUI/blob/main/CHANGELOG.md) for the full notes for this release. |
| 276 | |
| 277 | # npm publish is intentionally not automated. The npm account requires 2FA OTP |
| 278 | # on every publish, and a granular automation token that bypasses 2FA has not |
| 279 | # been provisioned. Release the npm wrapper manually from a developer machine |
| 280 | # after the GitHub Release has been created — see CLAUDE.md "Releases" for the |
| 281 | # exact commands. |
| 282 |