Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
365 changes: 365 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
name: Release

on:
push:
branches:
- main
workflow_dispatch: {}

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: write
packages: write

env:
CARGO_TERM_COLOR: always
IMAGE_NAME: ${{ github.repository }}

jobs:
# ------------------------------------------------------------------
# 0. Detect whether this is a release run (workflow_dispatch only)
# ------------------------------------------------------------------
check-release:
name: Check release
runs-on: ubuntu-latest
outputs:
is_release: ${{ steps.check.outputs.is_release }}
version: ${{ steps.check.outputs.version }}
version_major: ${{ steps.check.outputs.version_major }}
version_minor: ${{ steps.check.outputs.version_minor }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6.0.2
with:
fetch-depth: 1
fetch-tags: true

- name: Detect release
id: check
shell: bash
run: |
if [ "${{ github.event_name }}" != "workflow_dispatch" ]; then
echo "is_release=false" >> "$GITHUB_OUTPUT"
echo "version=" >> "$GITHUB_OUTPUT"
echo "Not a release run"
exit 0
fi

# `grep '^version = '` (with the trailing space) so we don't also
# match `rust-version = "..."`.
RAW=$(grep '^version = ' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
VERSION="v${RAW}"
echo "is_release=true" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

# Only emit short aliases (e.g. `0` and `0.12` Docker tags, `latest`)
# for plain `X.Y.Z` releases — pre-releases like `0.12.2-rc1` must
# not clobber stable tag moving-targets.
if [[ "$RAW" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
MAJOR=$(echo "$RAW" | cut -d. -f1)
MINOR="${MAJOR}.$(echo "$RAW" | cut -d. -f2)"
echo "version_major=$MAJOR" >> "$GITHUB_OUTPUT"
echo "version_minor=$MINOR" >> "$GITHUB_OUTPUT"
else
echo "version_major=" >> "$GITHUB_OUTPUT"
echo "version_minor=" >> "$GITHUB_OUTPUT"
fi

if git tag -l "$VERSION" | grep -q .; then
echo "::error::Tag $VERSION already exists"
exit 1
fi

LOCK_VERSION=$(awk '/^name = "fastqc-rust"/{found=1} found && /^version =/{print; exit}' Cargo.lock | sed 's/.*"\(.*\)"/\1/')
if [ "$LOCK_VERSION" != "$RAW" ]; then
echo "::error::Cargo.lock version ($LOCK_VERSION) does not match Cargo.toml version ($RAW)"
exit 1
fi

echo "Detected release: $VERSION"

# ------------------------------------------------------------------
# 1. Build binaries for each target (always, to catch breakage)
# ------------------------------------------------------------------
build-binaries:
name: Build ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: linux-x86_64
os: ubuntu-latest
target: x86_64-unknown-linux-gnu

- name: linux-aarch64
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu

- name: macos-x86_64
os: macos-latest
target: x86_64-apple-darwin

- name: macos-aarch64
os: macos-latest
target: aarch64-apple-darwin

- name: windows-x86_64
os: windows-latest
target: x86_64-pc-windows-msvc
ext: .exe

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6.0.2

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # ratchet:dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}

- name: Build
shell: bash
run: cargo build --release --target ${{ matrix.target }}

- name: Package
id: package
shell: bash
run: |
BIN="fastqc"
EXT="${{ matrix.ext }}"
ARCHIVE="${BIN}-${{ matrix.name }}"

mkdir -p "staging/${ARCHIVE}"
cp "target/${{ matrix.target }}/release/${BIN}${EXT}" "staging/${ARCHIVE}/"
cp README.md LICENSE "staging/${ARCHIVE}/" 2>/dev/null || true

cd staging
tar czf "../${ARCHIVE}.tar.gz" "${ARCHIVE}"
cd ..

if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${ARCHIVE}.tar.gz" > "${ARCHIVE}.tar.gz.sha256"
else
shasum -a 256 "${ARCHIVE}.tar.gz" > "${ARCHIVE}.tar.gz.sha256"
fi

echo "archive=${ARCHIVE}.tar.gz" >> "$GITHUB_OUTPUT"
echo "checksum=${ARCHIVE}.tar.gz.sha256" >> "$GITHUB_OUTPUT"

- name: Upload build artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # ratchet:actions/upload-artifact@v7.0.0
with:
name: binary-${{ matrix.name }}
path: |
${{ steps.package.outputs.archive }}
${{ steps.package.outputs.checksum }}
retention-days: 1

# ------------------------------------------------------------------
# 2. Build per-platform Docker images on native runners
# ------------------------------------------------------------------
docker-build:
name: Docker ${{ matrix.platform }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6.0.2

- name: Lowercase image name
run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> "$GITHUB_ENV"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # ratchet:docker/setup-buildx-action@v4.0.0

- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # ratchet:docker/login-action@v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push by digest
id: build
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # ratchet:docker/build-push-action@v7.0.0
with:
context: .
platforms: ${{ matrix.platform }}
outputs: type=image,name=ghcr.io/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,scope=${{ matrix.platform }},mode=max

- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"

- name: Upload digest
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # ratchet:actions/upload-artifact@v7.0.0
with:
name: digests-${{ matrix.runner }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1

# ------------------------------------------------------------------
# 2b. Merge per-platform images into a multi-arch manifest
# ------------------------------------------------------------------
docker-merge:
name: Docker merge
needs: [check-release, docker-build]
runs-on: ubuntu-latest
steps:
- name: Lowercase image name
run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> "$GITHUB_ENV"

- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # ratchet:actions/download-artifact@v8.0.1
with:
pattern: digests-*
path: /tmp/digests
merge-multiple: true

- name: Docker meta
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # ratchet:docker/metadata-action@v6.0.0
with:
images: ghcr.io/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ needs.check-release.outputs.version }},enable=${{ needs.check-release.outputs.is_release == 'true' }}
type=raw,value=${{ needs.check-release.outputs.version_minor }},enable=${{ needs.check-release.outputs.version_minor != '' }}
type=raw,value=${{ needs.check-release.outputs.version_major }},enable=${{ needs.check-release.outputs.version_major != '' }}
type=raw,value=latest,enable=${{ needs.check-release.outputs.version_minor != '' }}
type=raw,value=dev

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # ratchet:docker/setup-buildx-action@v4.0.0

- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # ratchet:docker/login-action@v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'ghcr.io/${{ env.IMAGE_NAME }}@sha256:%s ' *)

# ------------------------------------------------------------------
# 3. Create tag and draft release (release only, after all builds)
# ------------------------------------------------------------------
create-tag-and-release:
name: Create tag and release
if: needs.check-release.outputs.is_release == 'true'
needs: [check-release, build-binaries, docker-merge]
runs-on: ubuntu-latest
outputs:
tag: ${{ needs.check-release.outputs.version }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6.0.2

- name: Create tag
run: |
git tag "${{ needs.check-release.outputs.version }}"
git push origin "${{ needs.check-release.outputs.version }}"

- name: Extract changelog section
id: notes
run: |
RAW="${{ needs.check-release.outputs.version }}"
RAW="${RAW#v}"
if [ -f CHANGELOG.md ] && awk '/^## \[Version '"$RAW"'\]/{found=1; next} /^## \[/{if(found) exit} found' CHANGELOG.md > /tmp/release-notes.md && [ -s /tmp/release-notes.md ]; then
echo "has_notes=true" >> "$GITHUB_OUTPUT"
else
echo "has_notes=false" >> "$GITHUB_OUTPUT"
fi

- name: Create release (with changelog notes)
if: steps.notes.outputs.has_notes == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${{ needs.check-release.outputs.version }}" \
--target "${{ github.sha }}" \
--title "FastQC-Rust ${{ needs.check-release.outputs.version }}" \
--notes-file /tmp/release-notes.md

- name: Create release (auto-generated notes)
if: steps.notes.outputs.has_notes == 'false'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${{ needs.check-release.outputs.version }}" \
--target "${{ github.sha }}" \
--title "FastQC-Rust ${{ needs.check-release.outputs.version }}" \
--generate-notes

# ------------------------------------------------------------------
# 4. Upload binaries to the release
# ------------------------------------------------------------------
upload-binaries:
name: Upload binaries
if: needs.check-release.outputs.is_release == 'true'
needs: [check-release, create-tag-and-release, build-binaries]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6.0.2

- name: Download all binary artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # ratchet:actions/download-artifact@v8.0.1
with:
pattern: binary-*
path: /tmp/binaries
merge-multiple: true

- name: Upload to release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release upload "${{ needs.create-tag-and-release.outputs.tag }}" \
/tmp/binaries/*.tar.gz \
/tmp/binaries/*.sha256

# ------------------------------------------------------------------
# 5. Publish to crates.io (release only, last step)
#
# TODO: `fastqc-rust` has not been published to crates.io yet. The
# crates-io-auth-action uses OIDC trusted publishers, which requires the
# crate to already exist with a trusted-publisher config. Bootstrap path:
# 1. First publish manually from a local checkout: `cargo publish`
# 2. Configure crates.io trusted publisher for this repo + workflow
# 3. Flip `if: false` below to `needs.check-release.outputs.is_release
# == 'true'`
# ------------------------------------------------------------------
publish-crate:
name: Publish to crates.io
if: false
needs: [check-release, upload-binaries]
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6.0.2

- name: Authenticate with crates.io
id: auth
uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # ratchet:rust-lang/crates-io-auth-action@v1.0.4

- name: Publish to crates.io
run: cargo publish --no-verify
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
33 changes: 33 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# ---- Build stage ----
FROM rust:1-bookworm AS builder

RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config \
zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*

ARG CPU_TARGET=""

WORKDIR /build
COPY Cargo.toml Cargo.lock ./
COPY src/ src/
COPY assets/ assets/

RUN HOST_TRIPLE=$(rustc -vV | awk '/^host:/ {print $2}') && \
cargo build --release --target "$HOST_TRIPLE" \
${CPU_TARGET:+--config "target.'$HOST_TRIPLE'.rustflags=['-C', 'target-cpu=$CPU_TARGET']"} \
&& strip "target/$HOST_TRIPLE/release/fastqc" \
&& cp "target/$HOST_TRIPLE/release/fastqc" /fastqc

# ---- Runtime stage ----
FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
procps \
zlib1g \
&& rm -rf /var/lib/apt/lists/*

COPY --from=builder /fastqc /usr/local/bin/fastqc

CMD ["fastqc"]
Loading