Skip to content

fix(ci): publish docs alongside the package #9

fix(ci): publish docs alongside the package

fix(ci): publish docs alongside the package #9

Workflow file for this run

name: Release
on:
push:
branches: [main]
workflow_dispatch:
inputs:
version:
description: "Force a specific version (no v prefix). Leave empty to derive from commits."
required: false
permissions:
contents: write
jobs:
decide:
name: Decide next version
runs-on: ubuntu-22.04
outputs:
release: ${{ steps.next.outputs.release }}
version: ${{ steps.next.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install git-cliff
shell: bash
run: |
set -euo pipefail
GC_VERSION="2.6.1"
URL="https://github.com/orhun/git-cliff/releases/download/v${GC_VERSION}/git-cliff-${GC_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
curl -sSL "$URL" -o /tmp/git-cliff.tar.gz
mkdir -p /tmp/git-cliff
tar -xzf /tmp/git-cliff.tar.gz -C /tmp/git-cliff --strip-components 1
sudo install /tmp/git-cliff/git-cliff /usr/local/bin/git-cliff
- name: Decide next version
id: next
shell: bash
run: |
set -euo pipefail
if [[ -n "${{ inputs.version }}" ]]; then
VERSION="${{ inputs.version }}"
echo "Forced version via workflow_dispatch: ${VERSION}"
echo "release=true" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
exit 0
fi
# Capture both stdout (the version string) and stderr (where
# git-cliff says "There is nothing to bump") in one go.
GC_OUT="$(git-cliff --config cliff.toml --bumped-version 2>&1 || true)"
NEXT="$(printf '%s\n' "$GC_OUT" | tail -n1)"
LATEST="$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true)"
echo "Latest tag: ${LATEST:-(none)}"
echo "git-cliff bumped-version: ${NEXT:-(none)}"
# Three reasons to skip:
# (a) git-cliff explicitly said nothing bumps;
# (b) git-cliff returned the same string as the latest tag;
# (c) git-cliff returned a version <= the latest tag (it
# can do this when the "latest" tag points at a commit
# whose only contents are filtered, e.g. our own
# chore(release): bump commit).
if printf '%s\n' "$GC_OUT" | grep -q "nothing to bump"; then
echo "git-cliff: nothing to bump; skipping release."
echo "release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ -z "$NEXT" || "$NEXT" == "$LATEST" ]]; then
echo "No new version proposed; skipping release."
echo "release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ -n "$LATEST" ]]; then
HIGHEST="$(printf '%s\n%s\n' "$LATEST" "$NEXT" | sort -V | tail -n1)"
if [[ "$HIGHEST" == "$LATEST" ]]; then
echo "Proposed ${NEXT} is not greater than ${LATEST}; skipping release."
echo "release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
fi
VERSION="${NEXT#v}"
echo "Will release ${NEXT}"
echo "release=true" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
build_nif:
name: NIF ${{ matrix.job.target }}
needs: decide
if: needs.decide.outputs.release == 'true'
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
nif: ["2.16"]
job:
- { target: aarch64-apple-darwin, os: macos-14, lib-ext: dylib }
- { target: x86_64-apple-darwin, os: macos-14, lib-ext: dylib }
- { target: x86_64-unknown-linux-gnu, os: ubuntu-22.04, lib-ext: so }
- { target: aarch64-unknown-linux-gnu, os: ubuntu-22.04-arm, lib-ext: so }
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
shell: bash
run: |
rustup toolchain install stable --profile minimal
rustup default stable
rustup target add ${{ matrix.job.target }}
- name: Compute NIF feature flag
id: feature
shell: bash
run: echo "flag=nif_version_$(echo ${{ matrix.nif }} | tr . _)" >> "$GITHUB_OUTPUT"
- name: Build NIF
shell: bash
working-directory: native/javex_nif
run: |
cargo build --release \
--target ${{ matrix.job.target }} \
--no-default-features \
--features ${{ steps.feature.outputs.flag }}
- name: Package artifact
id: package
shell: bash
run: |
set -euo pipefail
VERSION="${{ needs.decide.outputs.version }}"
NIF="${{ matrix.nif }}"
TARGET="${{ matrix.job.target }}"
SRC_EXT="${{ matrix.job.lib-ext }}"
# rustler_precompiled expects the artifact to be named with
# a `.so` suffix in the tarball regardless of the host
# platform, because Rustler renames all NIF shared libraries
# to .so inside priv/native at load time.
NAME="libjavex_nif-v${VERSION}-nif-${NIF}-${TARGET}"
ARCHIVE="${NAME}.so.tar.gz"
SRC="native/javex_nif/target/${TARGET}/release/libjavex_nif.${SRC_EXT}"
STAGE="$(mktemp -d)"
cp "${SRC}" "${STAGE}/${NAME}.so"
tar -C "${STAGE}" -czf "${ARCHIVE}" "${NAME}.so"
echo "archive=${ARCHIVE}" >> "$GITHUB_OUTPUT"
echo "name=${ARCHIVE}" >> "$GITHUB_OUTPUT"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ steps.package.outputs.name }}
path: ${{ steps.package.outputs.archive }}
if-no-files-found: error
release:
# Atomic by design: every reversible step happens first, then the
# three irreversible ones run back-to-back at the end:
# 1. mix hex.publish (cannot unpublish after 24h)
# 2. git push --atomic (commit + tag together)
# 3. GH release create (cosmetic, easiest to recreate manually)
# If any step before #1 fails, nothing leaks.
name: Atomic publish (hex + commit + tag + GH release)
needs: [decide, build_nif]
if: needs.decide.outputs.release == 'true'
runs-on: ubuntu-22.04
env:
VERSION: ${{ needs.decide.outputs.version }}
MIX_ENV: prod
HEX_API_KEY: ${{ secrets.HEX_API_KEY }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: jdx/mise-action@v2
with:
experimental: true
- name: Install hex / rebar
run: |
mix local.hex --force
mix local.rebar --force
- name: Install git-cliff
shell: bash
run: |
set -euo pipefail
GC_VERSION="2.6.1"
URL="https://github.com/orhun/git-cliff/releases/download/v${GC_VERSION}/git-cliff-${GC_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
curl -sSL "$URL" -o /tmp/git-cliff.tar.gz
mkdir -p /tmp/git-cliff
tar -xzf /tmp/git-cliff.tar.gz -C /tmp/git-cliff --strip-components 1
sudo install /tmp/git-cliff/git-cliff /usr/local/bin/git-cliff
- name: Download NIF artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Bump @version in mix.exs
shell: bash
run: |
set -euo pipefail
sed -i.bak -E "s/^( @version )\".*\"/\\1\"${VERSION}\"/" mix.exs
rm mix.exs.bak
grep '@version' mix.exs
- name: Generate checksum file from real artifacts
shell: bash
run: |
set -euo pipefail
OUT="checksum-Elixir.Javex.Native.exs"
{
echo "%{"
for f in artifacts/*.tar.gz; do
name="$(basename "$f")"
sum="$(sha256sum "$f" | cut -d' ' -f1)"
echo " \"${name}\" => \"sha256:${sum}\","
done
echo "}"
} > "$OUT"
- name: Pre-seed rustler_precompiled cache for host
# Stops the upcoming `mix hex.publish` compile step from trying
# to download the linux-gnu NIF from a GitHub release that does
# not exist yet (we create it after publishing to hex).
shell: bash
run: |
set -euo pipefail
mkdir -p "$HOME/.cache/rustler_precompiled/precompiled_nifs"
cp "artifacts/libjavex_nif-v${VERSION}-nif-2.16-x86_64-unknown-linux-gnu.so.tar.gz" \
"$HOME/.cache/rustler_precompiled/precompiled_nifs/"
- name: Generate release notes
shell: bash
run: |
git-cliff --config cliff.toml --bump --latest --strip header > CHANGES.md
echo "--- CHANGES.md ---"
cat CHANGES.md
- name: Fetch deps
run: mix deps.get --only prod
##
# Irreversible block — order is load-bearing.
##
- name: Publish to hex.pm (package + docs)
run: mix hex.publish --yes
- name: Configure git
run: |
git config user.name "tuistbot"
git config user.email "noreply@tuist.dev"
- name: Commit version bump and tag
shell: bash
run: |
set -euo pipefail
git add mix.exs checksum-Elixir.Javex.Native.exs
git commit -m "chore(release): v${VERSION}"
git tag "v${VERSION}"
- name: Push commit and tag atomically
shell: bash
run: |
set -euo pipefail
for attempt in 1 2 3; do
if git push --atomic origin main "v${VERSION}"; then
exit 0
fi
echo "Push attempt ${attempt} failed; rebasing and retrying"
git pull --rebase origin main
done
echo "git push --atomic failed after 3 attempts" >&2
exit 1
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
body_path: CHANGES.md
files: artifacts/*
fail_on_unmatched_files: true
tag_name: v${{ env.VERSION }}
name: v${{ env.VERSION }}
draft: false
prerelease: false