Overhaul CI, nightly, and Docker release workflows

This commit is contained in:
2026-03-25 11:35:36 -04:00
parent 0cd401e03c
commit 33e0cae5b6
13 changed files with 484 additions and 686 deletions

355
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,355 @@
name: Build
on:
workflow_call:
inputs:
version_suffix:
description: >
Suffix appended to the VERSION file content.
Empty string for stable releases.
For nightly: "-nightly+abc1234"
type: string
required: false
default: ""
push_docker:
description: Whether to push the Docker image
type: boolean
required: false
default: false
docker_tags:
description: Newline-separated list of Docker tags to push
type: string
required: false
default: ""
prerelease:
description: Whether to mark the GitHub release as prerelease
type: boolean
required: false
default: false
make_latest:
description: Whether to mark as latest release
type: boolean
required: false
default: false
release_tag:
description: The git tag to use for the GitHub release
type: string
required: false
default: ""
release_name:
description: The display name of the GitHub release
type: string
required: false
default: ""
publish_release:
description: Whether to publish a GitHub release
type: boolean
required: false
default: false
secrets:
GITHUB_TOKEN:
required: true
env:
IMAGE_NAME: ghcr.io/bybrooklyn/alchemist
jobs:
build-frontend:
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: web
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Restore Bun cache
uses: actions/cache@v4
with:
path: web/node_modules
key: bun-${{ github.workflow }}-${{ hashFiles('web/bun.lock') }}
restore-keys: bun-${{ github.workflow }}-
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Typecheck
run: bun run typecheck
- name: Astro diagnostics
run: bun run check
- name: Build
run: bun run build
- name: Upload frontend artifact
uses: actions/upload-artifact@v4
with:
name: web-dist-${{ github.run_id }}
path: web/dist
retention-days: 1
build-linux:
needs: [build-frontend]
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
artifact_name: alchemist-linux-x86_64
- target: aarch64-unknown-linux-gnu
artifact_name: alchemist-linux-aarch64
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Download frontend artifact
uses: actions/download-artifact@v4
with:
name: web-dist-${{ github.run_id }}
path: web/dist
- name: Read VERSION
id: ver
shell: bash
run: |
base=$(tr -d '\n\r' < VERSION)
echo "version=${base}${{ inputs.version_suffix }}" >> "$GITHUB_OUTPUT"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
key: build-linux-${{ matrix.target }}
- name: Install aarch64 cross-compiler
if: matrix.target == 'aarch64-unknown-linux-gnu'
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV"
- name: Build
env:
ALCHEMIST_VERSION: ${{ steps.ver.outputs.version }}
run: cargo build --release --target ${{ matrix.target }}
- name: Package
shell: bash
run: |
set -euo pipefail
BINARY="target/${{ matrix.target }}/release/alchemist"
ARCHIVE="${{ matrix.artifact_name }}.tar.gz"
tar -czf "$ARCHIVE" -C "$(dirname "$BINARY")" "$(basename "$BINARY")"
sha256sum "$ARCHIVE" > "$ARCHIVE.sha256"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: |
${{ matrix.artifact_name }}.tar.gz
${{ matrix.artifact_name }}.tar.gz.sha256
build-windows:
needs: [build-frontend]
runs-on: windows-latest
timeout-minutes: 45
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Download frontend artifact
uses: actions/download-artifact@v4
with:
name: web-dist-${{ github.run_id }}
path: web/dist
- name: Read VERSION
id: ver
shell: bash
run: |
base=$(tr -d '\n\r' < VERSION)
echo "version=${base}${{ inputs.version_suffix }}" >> "$GITHUB_OUTPUT"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
key: build-windows-x86_64
- name: Build
env:
ALCHEMIST_VERSION: ${{ steps.ver.outputs.version }}
run: cargo build --release --target x86_64-pc-windows-msvc
- name: Package
shell: bash
run: |
set -euo pipefail
cp "target/x86_64-pc-windows-msvc/release/alchemist.exe" \
"alchemist-windows-x86_64.exe"
sha256sum "alchemist-windows-x86_64.exe" \
> "alchemist-windows-x86_64.exe.sha256"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: alchemist-windows-x86_64
path: |
alchemist-windows-x86_64.exe
alchemist-windows-x86_64.exe.sha256
build-macos:
needs: [build-frontend]
runs-on: macos-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-apple-darwin
artifact_name: alchemist-macos-x86_64
- target: aarch64-apple-darwin
artifact_name: alchemist-macos-arm64
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Download frontend artifact
uses: actions/download-artifact@v4
with:
name: web-dist-${{ github.run_id }}
path: web/dist
- name: Read VERSION
id: ver
shell: bash
run: |
base=$(tr -d '\n\r' < VERSION)
echo "version=${base}${{ inputs.version_suffix }}" >> "$GITHUB_OUTPUT"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
key: build-macos-${{ matrix.target }}
- name: Build
env:
ALCHEMIST_VERSION: ${{ steps.ver.outputs.version }}
run: cargo build --release --target ${{ matrix.target }}
- name: Package
shell: bash
run: |
set -euo pipefail
BINARY="target/${{ matrix.target }}/release/alchemist"
ARCHIVE="${{ matrix.artifact_name }}.tar.gz"
tar -czf "$ARCHIVE" -C "$(dirname "$BINARY")" "$(basename "$BINARY")"
shasum -a 256 "$ARCHIVE" > "$ARCHIVE.sha256"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: |
${{ matrix.artifact_name }}.tar.gz
${{ matrix.artifact_name }}.tar.gz.sha256
build-docker:
needs: [build-frontend]
runs-on: ubuntu-latest
timeout-minutes: 90
if: inputs.push_docker
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ inputs.docker_tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
publish-release:
needs:
[build-linux, build-windows, build-macos, build-docker]
runs-on: ubuntu-latest
timeout-minutes: 20
if: inputs.publish_release
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download artifacts
uses: actions/download-artifact@v4
with:
pattern: alchemist-*
merge-multiple: true
path: release-assets
- name: Publish release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.release_tag }}
name: ${{ inputs.release_name }}
prerelease: ${{ inputs.prerelease }}
make_latest: ${{ inputs.make_latest }}
overwrite_files: true
generate_release_notes: false
files: release-assets/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -4,22 +4,19 @@ on:
push:
branches:
- main
- master
paths-ignore:
- '**.md'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
- 'deploy/helm/**'
pull_request:
paths-ignore:
- '**.md'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
- 'deploy/helm/**'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
@@ -48,10 +45,10 @@ jobs:
- name: Check formatting
run: cargo fmt --all -- --check
- name: Lint Rust
- name: Lint
run: cargo clippy --locked --all-targets --all-features -- -D warnings
- name: Check Rust
- name: Check
run: cargo check --locked --all-targets --all-features
rust-test:
@@ -78,10 +75,10 @@ jobs:
with:
prefix-key: ci-test
- name: Run Rust tests
- name: Run tests
run: cargo test --locked --all-targets -- --test-threads=4
- name: Upload test artifacts on failure
- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
@@ -106,24 +103,23 @@ jobs:
with:
bun-version: latest
- name: Restore Bun modules cache
- name: Restore Bun cache
uses: actions/cache@v4
with:
path: web/node_modules
key: bun-${{ hashFiles('web/bun.lock') }}
restore-keys: |
bun-
key: bun-ci-${{ hashFiles('web/bun.lock') }}
restore-keys: bun-ci-
- name: Install frontend dependencies
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Typecheck frontend
- name: Typecheck
run: bun run typecheck
- name: Astro diagnostics
run: bun run check
- name: Build frontend
- name: Build
run: bun run build
e2e-reliability:
@@ -156,7 +152,10 @@ jobs:
bun-version: latest
- name: Build frontend
run: cd ../web && bun install --frozen-lockfile && bun run build
run: |
cd ../web
bun install --frozen-lockfile
bun run build
- name: Install e2e dependencies
run: bun install --frozen-lockfile
@@ -167,7 +166,7 @@ jobs:
- name: Run reliability tests
run: bun run test:reliability
- name: Upload test artifacts on failure
- name: Upload artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:

View File

@@ -13,108 +13,31 @@ on:
- 'migrations/**'
- 'build.rs'
- '.github/workflows/docker.yml'
workflow_run:
workflows:
- CI
types:
- completed
branches:
- main
- master
push:
tags:
- 'v*'
workflow_dispatch:
concurrency:
group: >-
${{ github.workflow }}-${{
github.event.pull_request.number ||
github.event.workflow_run.head_branch ||
github.ref
}}
group: docker-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
permissions:
contents: read
packages: write
jobs:
docker:
if: >-
${{
github.event_name == 'pull_request' ||
github.event_name == 'workflow_dispatch' ||
github.event_name == 'push' ||
(
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push'
)
}}
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Plan build mode
id: plan
shell: bash
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
{
echo "checkout_ref=${{ github.event.workflow_run.head_sha }}"
echo "push=true"
echo "platforms=linux/amd64,linux/arm64"
} >> "$GITHUB_OUTPUT"
elif [[ "${{ github.event_name }}" == "push" ]]; then
{
echo "checkout_ref=${{ github.sha }}"
echo "push=true"
echo "platforms=linux/amd64,linux/arm64"
} >> "$GITHUB_OUTPUT"
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
{
echo "checkout_ref=${{ github.sha }}"
echo "push=false"
echo "platforms=linux/amd64"
} >> "$GITHUB_OUTPUT"
else
{
echo "checkout_ref=${{ github.sha }}"
echo "push=false"
echo "platforms=linux/amd64"
} >> "$GITHUB_OUTPUT"
fi
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: ${{ steps.plan.outputs.checkout_ref }}
- name: Read VERSION file
id: version
run: echo "version=$(tr -d '\n\r' < VERSION)" >> "$GITHUB_OUTPUT"
- name: Lowercase image name
id: image
run: echo "name=$(echo '${{ env.REGISTRY }}/${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
- name: Compute tags
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ steps.image.outputs.name }}
tags: |
type=raw,value=pr-${{ github.event.pull_request.number }},enable=${{ github.event_name == 'pull_request' }}
type=raw,value=${{ steps.version.outputs.version }},enable=${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=latest,enable=${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && !contains(steps.version.outputs.version, '-') }}
type=raw,value=edge,enable=${{ github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch' }}
type=sha,prefix=sha-,format=short,enable=${{ github.event_name != 'pull_request' }}
run: |
echo "name=$(echo 'ghcr.io/bybrooklyn/alchemist' \
| tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -123,36 +46,32 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
if: steps.plan.outputs.push == 'true'
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
id: build
- name: Build (PR preview — no push)
if: github.event_name == 'pull_request'
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ steps.plan.outputs.platforms }}
push: ${{ steps.plan.outputs.push == 'true' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
push: false
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
- name: Write job summary
shell: bash
run: |
set -euo pipefail
{
echo "### Docker Build"
echo "**Image:** ${{ steps.image.outputs.name }}"
echo "**Event:** ${{ github.event_name }}"
echo "**Pushed:** ${{ steps.plan.outputs.push }}"
echo "**Version:** ${{ steps.version.outputs.version }}"
echo "**Platforms:** ${{ steps.plan.outputs.platforms }}"
echo "**Digest:** ${{ steps.build.outputs.digest }}"
} >> "$GITHUB_STEP_SUMMARY"
- name: Build and push (manual dispatch)
if: github.event_name == 'workflow_dispatch'
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.image.outputs.name }}:dev
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false

View File

@@ -7,326 +7,97 @@ on:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: nightly-${{ github.ref }}
cancel-in-progress: true
env:
IMAGE_NAME: ghcr.io/brooklynloveszelda/alchemist
permissions:
contents: write
packages: write
jobs:
build-frontend:
ci-gate:
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: web
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Restore Bun modules cache
uses: actions/cache@v4
with:
path: web/node_modules
key: bun-nightly-${{ hashFiles('web/bun.lock') }}
restore-keys: |
bun-nightly-
- name: Install frontend dependencies
run: bun install --frozen-lockfile
- name: Typecheck frontend
run: bun run typecheck
- name: Astro diagnostics
run: bun run check
- name: Build frontend
run: bun run build
- name: Upload frontend artifact
uses: actions/upload-artifact@v4
with:
name: web-dist
path: web/dist
retention-days: 1
build-linux:
needs: [build-frontend]
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
artifact_name: alchemist-linux-x86_64
- target: aarch64-unknown-linux-gnu
artifact_name: alchemist-linux-aarch64
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Download frontend artifact
uses: actions/download-artifact@v4
with:
name: web-dist
path: web/dist
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
components: rustfmt, clippy
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
key: nightly-linux-${{ matrix.target }}
prefix-key: nightly-gate
- name: Install aarch64 cross-compilation dependencies
if: matrix.target == 'aarch64-unknown-linux-gnu'
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV"
- name: Check formatting
run: cargo fmt --all -- --check
- name: Build release binary
run: cargo build --release --target ${{ matrix.target }}
- name: Lint
run: cargo clippy --locked --all-targets --all-features -- -D warnings
- name: Package release artifact
shell: bash
run: |
set -euo pipefail
BINARY="target/${{ matrix.target }}/release/alchemist"
ARCHIVE="${{ matrix.artifact_name }}.tar.gz"
tar -czf "$ARCHIVE" -C "$(dirname "$BINARY")" "$(basename "$BINARY")"
sha256sum "$ARCHIVE" > "$ARCHIVE.sha256"
- name: Check
run: cargo check --locked --all-targets --all-features
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: |
${{ matrix.artifact_name }}.tar.gz
${{ matrix.artifact_name }}.tar.gz.sha256
build-windows:
needs: [build-frontend]
runs-on: windows-latest
timeout-minutes: 45
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Download frontend artifact
uses: actions/download-artifact@v4
with:
name: web-dist
path: web/dist
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
key: nightly-windows-x86_64
- name: Build release binary
run: cargo build --release --target x86_64-pc-windows-msvc
- name: Package release artifact
shell: bash
run: |
set -euo pipefail
cp "target/x86_64-pc-windows-msvc/release/alchemist.exe" "alchemist-windows-x86_64.exe"
sha256sum "alchemist-windows-x86_64.exe" > "alchemist-windows-x86_64.exe.sha256"
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: alchemist-windows-x86_64
path: |
alchemist-windows-x86_64.exe
alchemist-windows-x86_64.exe.sha256
build-macos:
needs: [build-frontend]
runs-on: macos-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-apple-darwin
artifact_name: alchemist-macos-x86_64
- target: aarch64-apple-darwin
artifact_name: alchemist-macos-arm64
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Download frontend artifact
uses: actions/download-artifact@v4
with:
name: web-dist
path: web/dist
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
key: nightly-macos-${{ matrix.target }}
- name: Build release binary
run: cargo build --release --target ${{ matrix.target }}
- name: Package release artifact
shell: bash
run: |
set -euo pipefail
BINARY="target/${{ matrix.target }}/release/alchemist"
ARCHIVE="${{ matrix.artifact_name }}.tar.gz"
tar -czf "$ARCHIVE" -C "$(dirname "$BINARY")" "$(basename "$BINARY")"
shasum -a 256 "$ARCHIVE" > "$ARCHIVE.sha256"
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: |
${{ matrix.artifact_name }}.tar.gz
${{ matrix.artifact_name }}.tar.gz.sha256
docker-nightly:
needs: [build-frontend]
compute-meta:
runs-on: ubuntu-latest
timeout-minutes: 90
timeout-minutes: 5
outputs:
short_sha: ${{ steps.meta.outputs.short_sha }}
version_suffix: ${{ steps.meta.outputs.version_suffix }}
release_name: ${{ steps.meta.outputs.release_name }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Compute metadata
id: meta
shell: bash
run: |
set -euo pipefail
SHORT_SHA=$(git rev-parse --short=7 "${GITHUB_SHA}")
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "version_suffix=-nightly+${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "release_name=Nightly — ${SHORT_SHA} ($(date -u +%Y-%m-%d))" \
>> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push nightly image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.IMAGE_NAME }}:nightly
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
publish-nightly:
needs: [build-linux, build-windows, build-macos, docker-nightly]
force-nightly-tag:
needs: [ci-gate, compute-meta]
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download release artifacts
uses: actions/download-artifact@v4
with:
pattern: alchemist-*
merge-multiple: true
path: release-assets
- name: Compute nightly metadata
id: meta
shell: bash
run: |
set -euo pipefail
echo "short_sha=$(git rev-parse --short=7 "${GITHUB_SHA}")" >> "$GITHUB_OUTPUT"
echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT"
- name: Force-update nightly tag
shell: bash
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config user.email \
"41898282+github-actions[bot]@users.noreply.github.com"
git tag -f nightly "${GITHUB_SHA}"
git push origin refs/tags/nightly --force
- name: Publish nightly release
uses: softprops/action-gh-release@v2
with:
tag_name: nightly
name: Nightly — ${{ steps.meta.outputs.short_sha }} (${{ steps.meta.outputs.date }})
prerelease: true
make_latest: false
overwrite_files: true
generate_release_notes: false
files: release-assets/*
body: |
## Nightly Build
Commit: `${{ github.sha }}`
Date: `${{ steps.meta.outputs.date }}`
Docker:
```bash
docker pull ${{ env.IMAGE_NAME }}:nightly
```
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Write job summary
shell: bash
run: |
set -euo pipefail
{
echo "## Nightly Published"
echo "**Tag:** nightly"
echo "**Commit:** ${{ github.sha }}"
echo "**Docker:** ${{ env.IMAGE_NAME }}:nightly"
echo
echo "### Artifacts"
for file in release-assets/*; do
echo "- $(basename "$file")"
done
} >> "$GITHUB_STEP_SUMMARY"
build:
needs: [ci-gate, compute-meta]
uses: ./.github/workflows/build.yml
with:
version_suffix: ${{ needs.compute-meta.outputs.version_suffix }}
push_docker: true
docker_tags: ghcr.io/bybrooklyn/alchemist:nightly
publish_release: true
release_tag: nightly
release_name: ${{ needs.compute-meta.outputs.release_name }}
prerelease: true
make_latest: false
secrets:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -7,27 +7,29 @@ on:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: release-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
packages: write
jobs:
release-meta:
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
version: ${{ steps.version.outputs.version }}
prerelease: ${{ steps.channel.outputs.prerelease }}
make_latest: ${{ steps.channel.outputs.make_latest }}
version: ${{ steps.version.outputs.version }}
docker_tags: ${{ steps.docker.outputs.tags }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Read version
- name: Read VERSION
id: version
run: echo "version=$(tr -d '\n\r' < VERSION)" >> "$GITHUB_OUTPUT"
@@ -36,9 +38,9 @@ jobs:
shell: bash
run: |
set -euo pipefail
expected_tag="v${{ steps.version.outputs.version }}"
if [[ "${GITHUB_REF_NAME}" != "${expected_tag}" ]]; then
echo "Tag ${GITHUB_REF_NAME} does not match VERSION ${expected_tag}" >&2
expected="v${{ steps.version.outputs.version }}"
if [[ "${GITHUB_REF_NAME}" != "${expected}" ]]; then
echo "Tag ${GITHUB_REF_NAME} does not match VERSION ${expected}" >&2
exit 1
fi
@@ -47,280 +49,44 @@ jobs:
shell: bash
run: |
set -euo pipefail
if [[ "${GITHUB_REF_NAME}" == *-rc.* || "${GITHUB_REF_NAME}" == *-beta.* || "${GITHUB_REF_NAME}" == *-alpha.* ]]; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
if [[ "${GITHUB_REF_NAME}" == *-rc.* ||
"${GITHUB_REF_NAME}" == *-beta.* ||
"${GITHUB_REF_NAME}" == *-alpha.* ]]; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
echo "make_latest=false" >> "$GITHUB_OUTPUT"
else
echo "prerelease=false" >> "$GITHUB_OUTPUT"
echo "make_latest=true" >> "$GITHUB_OUTPUT"
echo "make_latest=true" >> "$GITHUB_OUTPUT"
fi
build-frontend:
needs: [release-meta]
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: web
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Restore Bun modules cache
uses: actions/cache@v4
with:
path: web/node_modules
key: bun-release-${{ hashFiles('web/bun.lock') }}
restore-keys: |
bun-release-
- name: Install frontend dependencies
run: bun install --frozen-lockfile
- name: Typecheck frontend
run: bun run typecheck
- name: Astro diagnostics
run: bun run check
- name: Build frontend
run: bun run build
- name: Upload frontend artifact
uses: actions/upload-artifact@v4
with:
name: web-dist
path: web/dist
retention-days: 1
build-linux:
needs: [release-meta, build-frontend]
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
artifact_name: alchemist-linux-x86_64
- target: aarch64-unknown-linux-gnu
artifact_name: alchemist-linux-aarch64
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Download frontend artifact
uses: actions/download-artifact@v4
with:
name: web-dist
path: web/dist
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
key: release-linux-${{ matrix.target }}
- name: Install aarch64 cross-compilation dependencies
if: matrix.target == 'aarch64-unknown-linux-gnu'
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV"
- name: Build release binary
run: cargo build --release --target ${{ matrix.target }}
- name: Package release artifact
shell: bash
run: |
set -euo pipefail
BINARY="target/${{ matrix.target }}/release/alchemist"
ARCHIVE="${{ matrix.artifact_name }}.tar.gz"
tar -czf "$ARCHIVE" -C "$(dirname "$BINARY")" "$(basename "$BINARY")"
sha256sum "$ARCHIVE" > "$ARCHIVE.sha256"
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: |
${{ matrix.artifact_name }}.tar.gz
${{ matrix.artifact_name }}.tar.gz.sha256
build-windows:
needs: [release-meta, build-frontend]
runs-on: windows-latest
timeout-minutes: 45
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Download frontend artifact
uses: actions/download-artifact@v4
with:
name: web-dist
path: web/dist
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
key: release-windows-x86_64
- name: Build release binary
run: cargo build --release --target x86_64-pc-windows-msvc
- name: Package release artifact
shell: bash
run: |
set -euo pipefail
cp "target/x86_64-pc-windows-msvc/release/alchemist.exe" "alchemist-windows-x86_64.exe"
sha256sum "alchemist-windows-x86_64.exe" > "alchemist-windows-x86_64.exe.sha256"
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: alchemist-windows-x86_64
path: |
alchemist-windows-x86_64.exe
alchemist-windows-x86_64.exe.sha256
build-macos:
needs: [release-meta, build-frontend]
runs-on: macos-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-apple-darwin
artifact_name: alchemist-macos-x86_64
- target: aarch64-apple-darwin
artifact_name: alchemist-macos-arm64
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Download frontend artifact
uses: actions/download-artifact@v4
with:
name: web-dist
path: web/dist
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
key: release-macos-${{ matrix.target }}
- name: Build release binary
run: cargo build --release --target ${{ matrix.target }}
- name: Package release artifact
shell: bash
run: |
set -euo pipefail
BINARY="target/${{ matrix.target }}/release/alchemist"
ARCHIVE="${{ matrix.artifact_name }}.tar.gz"
tar -czf "$ARCHIVE" -C "$(dirname "$BINARY")" "$(basename "$BINARY")"
shasum -a 256 "$ARCHIVE" > "$ARCHIVE.sha256"
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: |
${{ matrix.artifact_name }}.tar.gz
${{ matrix.artifact_name }}.tar.gz.sha256
publish-release:
needs: [release-meta, build-linux, build-windows, build-macos]
runs-on: ubuntu-latest
timeout-minutes: 20
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Download release artifacts
uses: actions/download-artifact@v4
with:
pattern: alchemist-*
merge-multiple: true
path: release-assets
- name: List release assets
run: ls -lh release-assets/
- name: Publish release
uses: softprops/action-gh-release@v2
with:
files: release-assets/*
prerelease: ${{ needs.release-meta.outputs.prerelease }}
make_latest: ${{ needs.release-meta.outputs.make_latest }}
generate_release_notes: true
tag_name: ${{ github.ref_name }}
name: Alchemist ${{ needs.release-meta.outputs.version }}
body: |
## Alchemist ${{ needs.release-meta.outputs.version }}
### Downloads
| Platform | File |
|----------|------|
| Linux x86_64 | `alchemist-linux-x86_64.tar.gz` |
| Linux ARM64 (Pi, NAS) | `alchemist-linux-aarch64.tar.gz` |
| Windows x86_64 | `alchemist-windows-x86_64.exe` |
| macOS Apple Silicon | `alchemist-macos-arm64.tar.gz` |
| macOS Intel | `alchemist-macos-x86_64.tar.gz` |
SHA256 checksums are provided as `.sha256` files alongside each download.
Verify with: `sha256sum -c alchemist-linux-x86_64.tar.gz.sha256`
### Docker
```bash
docker pull ghcr.io/${{ github.repository }}:${{ needs.release-meta.outputs.version }}
```
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Write job summary
- name: Compute Docker tags
id: docker
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
TAGS="ghcr.io/bybrooklyn/alchemist:${VERSION}"
if [[ "${{ steps.channel.outputs.prerelease }}" == "false" ]]; then
TAGS="${TAGS}"$'\n'"ghcr.io/bybrooklyn/alchemist:latest"
fi
{
echo "## Release Published"
echo "**Version:** ${{ needs.release-meta.outputs.version }}"
echo "**Prerelease:** ${{ needs.release-meta.outputs.prerelease }}"
echo
echo "### Artifacts"
for file in release-assets/*; do
echo "- $(basename "$file")"
done
} >> "$GITHUB_STEP_SUMMARY"
echo "tags<<EOF"
echo "${TAGS}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
build:
needs: [release-meta]
uses: ./.github/workflows/build.yml
with:
version_suffix: ""
push_docker: ${{ startsWith(github.ref, 'refs/tags/') }}
docker_tags: ${{ needs.release-meta.outputs.docker_tags }}
publish_release: ${{ startsWith(github.ref, 'refs/tags/') }}
release_tag: ${{ github.ref_name }}
release_name: >-
Alchemist ${{ needs.release-meta.outputs.version }}
prerelease: ${{ needs.release-meta.outputs.prerelease == 'true' }}
make_latest: ${{ needs.release-meta.outputs.make_latest == 'true' }}
secrets:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
Cargo.lock generated
View File

@@ -13,7 +13,7 @@ dependencies = [
[[package]]
name = "alchemist"
version = "0.3.0-dev.1"
version = "0.3.0-dev.2"
dependencies = [
"anyhow",
"argon2",

View File

@@ -1,6 +1,6 @@
[package]
name = "alchemist"
version = "0.3.0-dev.1"
version = "0.3.0-dev.2"
edition = "2024"
rust-version = "1.85"
license = "GPL-3.0"

View File

@@ -1 +1 @@
0.3.0-dev.1
0.3.0-dev.2

View File

@@ -1,6 +1,6 @@
{
"name": "alchemist-docs",
"version": "0.3.0-dev.1",
"version": "0.3.0-dev.2",
"private": true,
"scripts": {
"dev": "astro dev",

View File

@@ -1,6 +1,6 @@
{
"name": "alchemist-web-e2e",
"version": "0.3.0-dev.1",
"version": "0.3.0-dev.2",
"private": true,
"packageManager": "bun@1",
"type": "module",

View File

@@ -73,7 +73,7 @@ test("setup shows a persistent inline alert and disables telemetry", async ({ pa
await expect(page.getByRole("heading", { name: "Library Selection" })).toBeVisible();
await page.getByRole("button", { name: "Next" }).click();
const alert = page.getByRole("alert");
const alert = page.getByRole("alert").first();
await expect(alert).toBeVisible();
await expect(alert).toContainText("Select at least one server folder before continuing.");
});

View File

@@ -1,6 +1,6 @@
{
"name": "alchemist-web",
"version": "0.3.0-dev.1",
"version": "0.3.0-dev.2",
"private": true,
"packageManager": "bun@1",
"type": "module",

View File

@@ -56,18 +56,6 @@ export default function SetupFrame({ step, configMutable, error, submitting, onB
</div>
</div>
{error && (
<div className="mx-6 pb-4">
<div
role="alert"
aria-live="polite"
className="mx-auto max-w-4xl rounded-lg border border-status-error/30 bg-status-error/10 px-4 py-3 text-sm text-status-error"
>
{error}
</div>
</div>
)}
{/* Navigation footer */}
{step < 6 && (
<div className="shrink-0 border-t border-helios-line/20