release #9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: release | |
| # Builds the release artefacts on native runners and uploads them to the release | |
| # for the tag that triggered this run. | |
| # | |
| # Why native runners rather than cross-compiling: the server carries twenty | |
| # tree-sitter C grammars, a bundled SQLite and protobuf codegen, and macOS cannot | |
| # be cross-targeted from Linux at all without an SDK. Native runners are the only | |
| # way to produce a macOS binary anyone should install, and they exercise each | |
| # target's own linker. | |
| # | |
| # What is built where, and why not everything everywhere: | |
| # - mindex-index / mindex-watch are pure Rust over reqwest+rustls with no C | |
| # grammars and no protoc. They cross platforms cleanly, and they are what a | |
| # user actually runs on their own machine. | |
| # - the mindex server is Linux x86-64 only as a *binary*. It is developed, run | |
| # and released on Linux; a server binary nobody has exercised on a platform | |
| # is worse on the release page than an absent one. Docker is what the README | |
| # tells everyone else to use, so the image is built and pushed here — saying | |
| # "use Docker" while publishing no image was the gap this closes. | |
| # - deploy/embedder/{server.py,requirements.txt} ship as assets too. mindex no | |
| # longer contains an embedder and cannot start without one, so the reference | |
| # implementation of the contract belongs beside the binary that needs it. | |
| # | |
| # The release itself is created by hand (`gh release create`) so its notes are | |
| # written rather than generated; these jobs only upload into it. `--clobber` makes | |
| # a re-run idempotent. | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Existing tag to build and upload to" | |
| required: true | |
| permissions: | |
| contents: write | |
| env: | |
| CARGO_TERM_COLOR: always | |
| # A release build is not the place to discover a warning — but note the blast | |
| # radius: RUSTFLAGS is process-global, so this denies warnings in every | |
| # DEPENDENCY crate too, and one upstream deprecation in a transitive fails the | |
| # release. Scoping it to this workspace means `[lints]` in Cargo.toml, which is | |
| # the right place and a change to the build rather than to CI; until then, a | |
| # release that fails here and nowhere else is almost always somebody else's | |
| # crate. | |
| RUSTFLAGS: "-D warnings" | |
| jobs: | |
| cli: | |
| name: CLI tools (${{ matrix.target }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| # BOTH macOS targets build on macos-14 (Apple silicon), and the Intel | |
| # one is a CROSS-COMPILE. It used to have its own `macos-13` runner, | |
| # and that runner is retired: the job was never picked up, sat queued | |
| # for GitHub's 24-hour maximum and was auto-cancelled — on v1.1.0 and | |
| # again on v1.2.0. Two consequences, and the second is why this went | |
| # unnoticed for two releases. `mindex-cli-x86_64-apple-darwin.tar.gz` | |
| # was silently absent from both release pages while README.md promised | |
| # "macOS (Intel and Apple silicon)". And because the other five jobs | |
| # succeeded and uploaded, the only symptom was the run's overall | |
| # conclusion reading `cancelled` a day after anyone stopped looking. | |
| # | |
| # Cross-compiling is safe for these two binaries specifically: they are | |
| # pure Rust over reqwest+rustls with no tree-sitter C grammars and no | |
| # protoc (the reason the server is Linux-only and these are not), and | |
| # Xcode ships both architectures' SDK. If a C dependency ever does | |
| # refuse, it fails the job in minutes rather than queueing for a day — | |
| # which is itself the improvement. | |
| - os: macos-14 | |
| target: x86_64-apple-darwin | |
| - os: macos-14 | |
| target: aarch64-apple-darwin | |
| steps: | |
| - uses: actions/checkout@v5 | |
| # rust-toolchain.toml pins the channel; `rustup show` materialises it. | |
| # | |
| # `shell: bash` here is prophylactic rather than load-bearing — two bare | |
| # commands parse under PowerShell too — but this matrix contains Windows, | |
| # and the step below shows what one continuation costs when it does not say | |
| # which shell it meant. | |
| - name: Install the pinned toolchain | |
| shell: bash | |
| run: | | |
| rustup show | |
| rustup target add ${{ matrix.target }} | |
| - uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| tools/indexer/target | |
| tools/watcher/target | |
| key: cli-${{ matrix.target }}-${{ hashFiles('tools/*/Cargo.lock') }} | |
| # tools/indexer and tools/watcher are SEPARATE workspaces with their own | |
| # Cargo.lock — they are not members of the root workspace, so they must be | |
| # built by manifest path rather than with -p from the root. | |
| # `--target` is passed on EVERY target, not only the cross one. Without it | |
| # cargo builds for the host and writes to `target/release/`, so the archive | |
| # named `mindex-cli-<triple>` carried whatever the runner happened to be — | |
| # a label rather than a fact, and correct only by coincidence. With it the | |
| # output is always `target/<triple>/release/`, which the packaging steps | |
| # below now read, so the name and the binary cannot disagree. | |
| # `shell: bash` is load-bearing on Windows and nowhere else. Without it the | |
| # runner's default there is PowerShell, which does not read `\` as a line | |
| # continuation — so the command below is a ParserError and the job dies | |
| # before cargo is invoked. The continuations arrived with `--target` in the | |
| # commit that fixed the macOS gap, which therefore broke the Windows archive | |
| # in the same breath: shipped in v1.1.0 and v1.2.0, absent from the v2.0.0 | |
| # rebuild. Every other multi-line step in this file already says `shell: | |
| # bash` for the same reason, including `Package (windows)` directly below. | |
| - name: Build | |
| shell: bash | |
| run: | | |
| cargo build --release --locked --target ${{ matrix.target }} \ | |
| --manifest-path tools/indexer/Cargo.toml | |
| cargo build --release --locked --target ${{ matrix.target }} \ | |
| --manifest-path tools/watcher/Cargo.toml | |
| # Staging and dist are separate directories on purpose: the upload step globs | |
| # `dist/*`, so anything in there must be an artefact. Globbing by extension | |
| # instead means naming both .tar.gz and .zip on every platform, and gh fails | |
| # the job on the one that cannot match. | |
| - name: Package (unix) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| name="mindex-cli-${{ matrix.target }}" | |
| mkdir -p "staging/$name" dist | |
| cp tools/indexer/target/${{ matrix.target }}/release/mindex-index "staging/$name/" | |
| cp tools/watcher/target/${{ matrix.target }}/release/mindex-watch "staging/$name/" | |
| cp LICENSE README.md "staging/$name/" | |
| tar -czf "dist/$name.tar.gz" -C staging "$name" | |
| (cd dist && shasum -a 256 "$name.tar.gz" > "$name.tar.gz.sha256") | |
| - name: Package (windows) | |
| if: runner.os == 'Windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| name="mindex-cli-${{ matrix.target }}" | |
| mkdir -p "staging/$name" dist | |
| cp tools/indexer/target/${{ matrix.target }}/release/mindex-index.exe "staging/$name/" | |
| cp tools/watcher/target/${{ matrix.target }}/release/mindex-watch.exe "staging/$name/" | |
| cp LICENSE README.md "staging/$name/" | |
| (cd staging && 7z a "../dist/$name.zip" "$name" > /dev/null) | |
| (cd dist && sha256sum "$name.zip" > "$name.zip.sha256") | |
| - name: Upload to the release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TAG: ${{ github.event.inputs.tag || github.ref_name }} | |
| run: gh release upload "$TAG" dist/* --clobber | |
| server: | |
| name: Server (linux x86-64) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| # protoc is qdrant-client's build-time dependency. rusqlite is `bundled` | |
| # and utoipa-swagger-ui is `vendored`, so nothing else reaches the network. | |
| - name: Install native build dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y protobuf-compiler cmake pkg-config | |
| - name: Install the pinned toolchain | |
| run: rustup show | |
| - uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: server-${{ hashFiles('Cargo.lock') }} | |
| - name: Build | |
| run: cargo build --release --locked --bin mindex | |
| - name: Package | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| name="mindex-server-x86_64-unknown-linux-gnu" | |
| mkdir -p "staging/$name" dist | |
| cp target/release/mindex "staging/$name/" | |
| cp LICENSE README.md config.example.toml "staging/$name/" | |
| tar -czf "dist/$name.tar.gz" -C staging "$name" | |
| (cd dist && shasum -a 256 "$name.tar.gz" > "$name.tar.gz.sha256") | |
| - name: Upload to the release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TAG: ${{ github.event.inputs.tag || github.ref_name }} | |
| run: gh release upload "$TAG" dist/* --clobber | |
| vsix: | |
| name: VS Code extension | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: tools/vscode | |
| steps: | |
| - uses: actions/checkout@v5 | |
| # 22, not 20, and both halves of the toolchain insist on it: `npm run test` | |
| # is `node --test "out/**/*.test.js"`, and glob expansion by `--test` landed | |
| # in Node 21 — under 20 it reports the pattern as a missing file and exits 1. | |
| # vsce's own Azure dependencies declare `node: >=22.0.0` besides. | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: tools/vscode/package-lock.json | |
| - run: npm ci | |
| # `npm run package` runs the full check suite (prettier, eslint, tsc, the | |
| # node:test suite) before vsce, so a broken extension fails the release | |
| # rather than shipping. vsce runs vscode:prepublish, which rebuilds dist/ — | |
| # the extension runs dist/, so a stale one would ship yesterday's code. | |
| - run: npm run package | |
| - name: Upload to the release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TAG: ${{ github.event.inputs.tag || github.ref_name }} | |
| run: gh release upload "$TAG" ./*.vsix --clobber | |
| embedder: | |
| name: Reference embedder | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| # No build: it is one Python file and its dependency list. It is published | |
| # because mindex ships no embedder and refuses to start without one, so | |
| # "download the server, then go and find something that speaks | |
| # /v1/embeddings" is not a complete release. Staged into its own directory | |
| # so the upload glob cannot catch anything else. | |
| - name: Package | |
| shell: bash | |
| run: | | |
| mkdir -p dist | |
| cp deploy/embedder/server.py deploy/embedder/requirements.txt \ | |
| deploy/embedder/embedder.env.example \ | |
| deploy/embedder/mindex-embedder.service \ | |
| deploy/embedder/README.md dist/ | |
| (cd dist && sha256sum server.py > server.py.sha256) | |
| - name: Upload to the release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TAG: ${{ github.event.inputs.tag || github.ref_name }} | |
| run: gh release upload "$TAG" dist/* --clobber | |
| image: | |
| name: Docker image | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ github.token }} | |
| # linux/amd64 only, matching the binary job and for the same reason: the | |
| # server carries twenty tree-sitter C grammars and protobuf codegen, and an | |
| # arm64 image built under emulation would take an hour to produce something | |
| # nobody has run. Add a platform when someone runs it. | |
| # | |
| # Two tags: the version, and `latest`. `latest` moves only on a real | |
| # release, because this workflow fires on `v*` tags and nothing else. | |
| - name: Build and push | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| platforms: linux/amd64 | |
| push: true | |
| tags: | | |
| ghcr.io/${{ github.repository }}:${{ github.event.inputs.tag || github.ref_name }} | |
| ghcr.io/${{ github.repository }}:latest | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| # The release page is the deliverable, and until now nothing checked it. Every | |
| # upload job is independent, so a release could be — and twice was — published | |
| # short of an artefact while the run's own conclusion said nothing useful: on | |
| # v1.1.0 and v1.2.0 the retired macos-13 runner never picked its job up, five | |
| # siblings succeeded, and `mindex-cli-x86_64-apple-darwin.tar.gz` was silently | |
| # absent from both pages while README.md promised it. v2.0.0 then published with | |
| # ZERO assets. | |
| # | |
| # Two properties make this catch that class rather than describe it: | |
| # | |
| # `if: always()` — a job whose `needs` were CANCELLED is SKIPPED, not failed, | |
| # and queued-then-cancelled is exactly the observed failure (on the v2.0.0 | |
| # rebuild five jobs were evicted after 15 minutes without ever being assigned | |
| # a runner, `runner_name` empty). Without this the guard would go quiet in the | |
| # one case it exists for. | |
| # | |
| # It reads the RELEASE, not the jobs. What matters is whether the artefact is | |
| # downloadable now — which also covers an upload that succeeded into the wrong | |
| # tag, and a re-run that was meant to backfill one missing file. | |
| # | |
| # The Docker image is deliberately not checked here: it is not a release asset, | |
| # it lives in ghcr, and `docker/build-push-action` fails its own job on a failed | |
| # push. Keep EXPECTED in step with the packaging steps above — it names files, | |
| # so a renamed archive fails here rather than on somebody's download. | |
| verify: | |
| name: Verify the release page | |
| runs-on: ubuntu-latest | |
| needs: [cli, server, vsix, embedder, image] | |
| if: always() | |
| steps: | |
| - name: Every promised asset is on the release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TAG: ${{ github.event.inputs.tag || github.ref_name }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| EXPECTED=( | |
| mindex-cli-x86_64-unknown-linux-gnu.tar.gz | |
| mindex-cli-x86_64-apple-darwin.tar.gz | |
| mindex-cli-aarch64-apple-darwin.tar.gz | |
| mindex-cli-x86_64-pc-windows-msvc.zip | |
| mindex-server-x86_64-unknown-linux-gnu.tar.gz | |
| server.py | |
| requirements.txt | |
| ) | |
| echo "Assets on $TAG:" | |
| gh release view "$TAG" --repo "$REPO" --json assets \ | |
| --jq '.assets[].name' | sort | tee assets.txt | |
| missing=() | |
| for want in "${EXPECTED[@]}"; do | |
| grep -qxF "$want" assets.txt || missing+=("$want") | |
| done | |
| # The .vsix carries the version in its name, so it is matched by shape | |
| # rather than spelled out — pinning the version here would mean editing | |
| # this file every release, which is how a guard rots into a lie. | |
| grep -qE '^mindex-vscode-.*\.vsix$' assets.txt || missing+=("mindex-vscode-<version>.vsix") | |
| if [ ${#missing[@]} -ne 0 ]; then | |
| echo | |
| echo "::error::The release is short of ${#missing[@]} artefact(s). A release page missing what the notes promise is the failure this job exists to refuse; re-run the jobs that produce them." | |
| printf ' missing: %s\n' "${missing[@]}" | |
| exit 1 | |
| fi | |
| echo | |
| echo "All expected assets are present." |