name: Release # Releases are cut from the Actions UI: "Run workflow" → enter the version # (e.g. 0.4.0). The `prepare` job stamps that version into every hardcoded # spot (scripts/set-version.sh), commits "chore(release): X.Y.Z", and pushes # the vX.Y.Z tag. The build matrix + release job then run off that tag. No # more hand-editing versions across Cargo.toml and individual crate manifests. on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+*' workflow_dispatch: inputs: version: description: 'Release version, no leading v (e.g. 0.4.0)' required: true type: string permissions: contents: write env: CARGO_TERM_COLOR: always RUST_BACKTRACE: short RUSTFLAGS: -D warnings jobs: prepare: name: bump + tag runs-on: ubuntu-latest outputs: version: ${{ steps.tag.outputs.tag_prefix }} tag: ${{ steps.tag.outputs.tag_full }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: dtolnay/rust-toolchain@1.83 - name: Cache cargo state uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-prepare-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-prepare- - name: Extract version from tag (push trigger) if: github.event_name == 'push' id: tag run: | echo "tag_prefix=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" - name: Validate version + stamp if: github.event_name == 'workflow_dispatch' id: stamp env: VERSION: ${{ inputs.version }} run: | set -euo pipefail ver="${VERSION#v}" # tolerate a leading v if ! printf '%s' "$ver" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$'; then echo "::error::'$ver' is not a semver version (expected e.g. 0.4.0)" exit 1 fi tag="v$ver" if git rev-parse -q --verify "refs/tags/$tag" >/dev/null \ || git ls-remote --exit-code --tags origin "$tag" >/dev/null 2>&1; then echo "::error::tag $tag already exists" exit 1 fi bash scripts/set-version.sh "$ver" echo "version=$ver" >> "$GITHUB_OUTPUT" echo "tag=$tag" >> "$GITHUB_OUTPUT" # Gate: never tag code that doesn't build/test. If this fails nothing # is committed or pushed, so the release simply doesn't happen. - name: Test run: cargo test --workspace - name: Commit, tag, push if: github.event_name == 'workflow_dispatch' env: VER: ${{ steps.stamp.outputs.version }} TAG: ${{ steps.stamp.outputs.tag }} run: | set -euo pipefail git config user.name "gitea-actions" git config user.email "gitea-actions@users.noreply.code.gfran.co" git add Cargo.toml Cargo.lock \ crates/nuwiki-lsp/Cargo.toml crates/nuwiki-ls/Cargo.toml git commit -m "chore(release): $VER" git tag -a "$TAG" -m "nuwiki $VER" git push origin "HEAD:${GITHUB_REF_NAME}" git push origin "$TAG" build: name: build ${{ matrix.target }} needs: prepare runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - target: x86_64-unknown-linux-gnu apt: "" rustflags: "-D warnings" - target: aarch64-unknown-linux-gnu # gcc-aarch64-linux-gnu ships the cross compiler/binutils # but no target libc; libc6-dev-arm64-cross adds Scrt1.o, # crti.o, and friends needed at link time. apt: "gcc-aarch64-linux-gnu libc6-dev-arm64-cross" rustflags: "-D warnings" - target: x86_64-unknown-linux-musl apt: "musl-tools" rustflags: "-D warnings" - target: aarch64-unknown-linux-musl apt: "" rustflags: "-D warnings -C linker=rust-lld -C link-self-contained=yes" steps: - uses: actions/checkout@v4 with: # Build the just-tagged commit, not the branch tip. ref: ${{ needs.prepare.outputs.tag }} - uses: dtolnay/rust-toolchain@1.83 with: targets: ${{ matrix.target }} - name: Install cross toolchain if: matrix.apt != '' run: | sudo apt-get update -qq sudo apt-get install -y --no-install-recommends ${{ matrix.apt }} - name: Cache cargo state uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-release-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-release-${{ matrix.target }}- - name: Build nuwiki-ls env: # Per-target linker overrides. Cargo ignores the entries that # don't match the current target, so setting all of them here # keeps the matrix declarative. CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER: musl-gcc RUSTFLAGS: ${{ matrix.rustflags }} run: cargo build --release --target ${{ matrix.target }} -p nuwiki-ls - name: Package archive id: package env: VERSION: ${{ needs.prepare.outputs.version }} run: | set -euo pipefail archive="nuwiki-ls-${VERSION}-${{ matrix.target }}.tar.gz" tar -czf "$archive" -C "target/${{ matrix.target }}/release" nuwiki-ls # Use a stable name without version so /releases/latest/download/nuwiki-ls-{target}.tar.gz always resolves. stable="nuwiki-ls-${{ matrix.target }}.tar.gz" mv "$archive" "$stable" echo "archive=$stable" >> "$GITHUB_OUTPUT" - name: Upload build artifact uses: actions/upload-artifact@v3 with: name: nuwiki-ls-${{ matrix.target }} path: ${{ steps.package.outputs.archive }} if-no-files-found: error release: name: gitea release needs: [prepare, build] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ needs.prepare.outputs.tag }} fetch-depth: 0 - name: Download all build artifacts # download-artifact@v3 nests each artifact under its own dir # (artifacts//), so recurse. uses: actions/download-artifact@v3 with: path: ./artifacts - name: Ensure jq + curl run: | if ! command -v jq >/dev/null 2>&1; then apt-get update && apt-get install -y --no-install-recommends jq fi if ! command -v curl >/dev/null 2>&1; then apt-get update && apt-get install -y --no-install-recommends curl fi - name: Generate release notes id: notes env: TAG: ${{ needs.prepare.outputs.tag }} run: | set -euo pipefail git fetch --tags --force origin >/dev/null 2>&1 || true # Previous tag = newest tag that isn't the one we just cut. prev_tag=$(git tag --sort=-version:refname | grep -vxF "$TAG" | head -n1 || echo "") if [ -n "$prev_tag" ]; then log=$(git log --oneline "$prev_tag..$TAG") else log=$(git log --oneline "$TAG") fi cat > release-notes.txt </dev/null || true) if [ "$existing" = "200" ]; then echo "Release for $TAG already exists — fetching existing id" release_id=$(curl --fail --silent --show-error \ -H "Authorization: token $RELEASE_TOKEN" \ "$GITEA_SERVER/api/v1/repos/$REPO/releases/tags/$TAG" \ | jq -r '.id') else notes=$(cat release-notes.txt 2>/dev/null || echo "") payload=$(jq -n --arg tag "$TAG" --arg name "$TAG" --arg body "$notes" \ '{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}') release_id=$(curl --fail --silent --show-error \ -H "Authorization: token $RELEASE_TOKEN" \ -H "Content-Type: application/json" \ -d "$payload" \ "$GITEA_SERVER/api/v1/repos/$REPO/releases" \ | jq -r '.id') if [ -z "$release_id" ] || [ "$release_id" = "null" ]; then echo "::error::failed to create release" exit 1 fi echo "Created release id=$release_id" fi # download-artifact@v3 nests each artifact under its own dir # (artifacts//), so recurse. for archive in $(find artifacts -name '*.tar.gz' -type f); do name="$(basename "$archive")" echo "Uploading $name …" curl --fail --silent --show-error \ -H "Authorization: token $RELEASE_TOKEN" \ -H "Content-Type: application/gzip" \ --data-binary "@$archive" \ "$GITEA_SERVER/api/v1/repos/$REPO/releases/$release_id/assets?name=$name" \ >/dev/null done echo "All assets uploaded."