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
189 changes: 189 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
name: CD

on:
push:
branches:
- main
tags:
- "v*"

concurrency:
group: cd-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
quality:
name: Quality gate
uses: ./.github/workflows/ci.yml

native:
name: Native SQLite+FTS binary (${{ matrix.target }})
if: startsWith(github.ref, 'refs/tags/v')
needs: quality
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
target: x86_64-unknown-linux-musl
- runner: macos-14
target: aarch64-apple-darwin
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Linux build dependencies
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y musl-tools

- name: Install Rust target
run: |
rustup toolchain install stable --profile minimal
rustup target add "${{ matrix.target }}"

- name: Build SQLite+FTS backend
run: |
cargo build \
--locked \
--release \
--package abyss-backend \
--no-default-features \
--features sqlite-fts \
--target "${{ matrix.target }}"

- name: Stage native binary
shell: bash
run: |
version="${GITHUB_REF_NAME#v}"
if [[ "v${version}" != "${GITHUB_REF_NAME}" ]]; then
echo "release tag must use the v<version> form" >&2
exit 1
fi
cargo_version="$(cargo metadata --no-deps --format-version 1 \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["packages"][0]["version"])')"
if [[ "${version}" != "${cargo_version}" ]]; then
echo "release tag ${GITHUB_REF_NAME} does not match Cargo version ${cargo_version}" >&2
exit 1
fi
asset="abyss-backend-${GITHUB_REF_NAME}-${{ matrix.target }}"
mkdir -p dist
cp "target/${{ matrix.target }}/release/abyss-backend" "dist/${asset}"
chmod 0755 "dist/${asset}"
file "dist/${asset}"

- name: Upload native binary
uses: actions/upload-artifact@v4
with:
name: abyss-backend-${{ matrix.target }}
path: dist/abyss-backend-*
if-no-files-found: error
retention-days: 7

publish:
name: Publish Docker Hub image
needs: quality
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Generate image metadata
id: metadata
uses: docker/metadata-action@v6
with:
images: lexmount/abyss-backend
flavor: latest=false
tags: |
type=sha,prefix=sha-,format=long
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
labels: |
org.opencontainers.image.title=abyss-backend
org.opencontainers.image.description=Open-source, self-hostable Agent event store for Abyss
org.opencontainers.image.licenses=GPL-3.0-only

- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: lexmount
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and publish image
id: image
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
annotations: ${{ steps.metadata.outputs.annotations }}
provenance: mode=max
sbom: true

- name: Record published image
env:
IMAGE_DIGEST: ${{ steps.image.outputs.digest }}
IMAGE_TAGS: ${{ steps.metadata.outputs.tags }}
run: |
{
echo "## Published Docker image"
echo
echo "Digest: \`lexmount/abyss-backend@${IMAGE_DIGEST}\`"
echo
echo "Tags:"
while IFS= read -r image_tag; do
echo "- \`${image_tag}\`"
done <<< "${IMAGE_TAGS}"
} >> "${GITHUB_STEP_SUMMARY}"

release-native:
name: Publish native GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
needs:
- native
- publish
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Download native binaries
uses: actions/download-artifact@v4
with:
pattern: abyss-backend-*
path: dist
merge-multiple: true

- name: Create checksums
run: |
cd dist
sha256sum abyss-backend-* > SHA256SUMS
cat SHA256SUMS

- name: Publish release assets
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then
gh release upload "${GITHUB_REF_NAME}" dist/* --clobber
else
gh release create \
"${GITHUB_REF_NAME}" \
dist/* \
--verify-tag \
--generate-notes \
--title "abyss-backend ${GITHUB_REF_NAME}"
fi
76 changes: 2 additions & 74 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
name: CI

on:
workflow_call:
workflow_dispatch:
pull_request:
push:
branches:
- main
tags:
- "v*"

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
Expand Down Expand Up @@ -91,71 +87,3 @@ jobs:

- name: Render Kubernetes manifests
run: kubectl kustomize k8s > /tmp/abyss-backend.yaml

publish:
name: Publish Docker Hub image
if: github.event_name == 'push'
needs:
- rust
- blackbox
- packaging
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Generate image metadata
id: metadata
uses: docker/metadata-action@v6
with:
images: lexmount/abyss-backend
flavor: latest=false
tags: |
type=sha,prefix=sha-,format=long
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
labels: |
org.opencontainers.image.title=abyss-backend
org.opencontainers.image.description=Open-source, self-hostable Agent event store for Abyss
org.opencontainers.image.licenses=GPL-3.0-only

- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: lexmount
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and publish image
id: image
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
annotations: ${{ steps.metadata.outputs.annotations }}
provenance: mode=max
sbom: true

- name: Record published image
env:
IMAGE_DIGEST: ${{ steps.image.outputs.digest }}
IMAGE_TAGS: ${{ steps.metadata.outputs.tags }}
run: |
{
echo "## Published Docker image"
echo
echo "Digest: \`lexmount/abyss-backend@${IMAGE_DIGEST}\`"
echo
echo "Tags:"
while IFS= read -r image_tag; do
echo "- \`${image_tag}\`"
done <<< "${IMAGE_TAGS}"
} >> "${GITHUB_STEP_SUMMARY}"
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ clippy:
test:
cargo test --locked --workspace
cargo test --locked --no-default-features --features sqlite-fts --workspace
python3 scripts/tests/test_native_release_contract.py

test-blackbox: test-blackbox-postgres test-blackbox-sqlite

Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ Exactly one storage profile is compiled into a binary:
- `postgres-es` uses PostgreSQL as the source of truth and optionally projects
search documents to Elasticsearch. It is the default profile.
- `sqlite-fts` stores both authoritative data and its transactional FTS5 index
in one local SQLite file. It does not compile Diesel, PostgreSQL, reqwest, or
Elasticsearch worker code into the binary.
in one local SQLite file through Diesel, with FTS5 operations expressed as
SQL. It does not compile PostgreSQL, reqwest, or Elasticsearch worker code
into the binary.

## Authentication

Expand Down Expand Up @@ -119,6 +120,20 @@ password must be provided together. Search remains disabled in that profile
when no URL is configured; event storage and queries continue to work. The
`sqlite-fts` profile always provides search through the local FTS5 index.

## Native releases

Version tags publish checksummed `sqlite-fts` executables on the repository's
GitHub Release. The current native targets are:

- `aarch64-apple-darwin` for macOS ARM64.
- `x86_64-unknown-linux-musl` for Linux x86_64.

Each filename contains the tag and Rust target, for example
`abyss-backend-v1.0.0-aarch64-apple-darwin`. `SHA256SUMS` in the same release
authenticates the downloaded bytes. These artifacts are consumed by
`abyss deploy-local`; they do not require Docker, PostgreSQL, Elasticsearch, or
a Rust toolchain on the destination machine.

## Containers and Kubernetes

### Docker
Expand Down
51 changes: 51 additions & 0 deletions scripts/tests/test_native_release_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Contracts for tag-published SQLite+FTS native binaries."""

from __future__ import annotations

import unittest
from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parents[2]
CI_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "ci.yml"
CD_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "cd.yml"


class NativeReleaseContractTests(unittest.TestCase):
def test_ci_is_a_reusable_validation_only_workflow(self) -> None:
source = CI_WORKFLOW.read_text(encoding="utf-8")

self.assertIn("workflow_call:", source)
self.assertNotIn("docker/login-action", source)
self.assertNotIn("gh release", source)

def test_cd_runs_the_quality_gate_before_publishing_docker(self) -> None:
source = CD_WORKFLOW.read_text(encoding="utf-8")

self.assertIn("uses: ./.github/workflows/ci.yml", source)
self.assertIn("docker/login-action", source)
self.assertIn("docker/build-push-action", source)
self.assertIn("needs: quality", source)

def test_release_builds_only_the_local_storage_profile(self) -> None:
source = CD_WORKFLOW.read_text(encoding="utf-8")

self.assertIn("Native SQLite+FTS binary", source)
self.assertIn("--no-default-features", source)
self.assertIn("--features sqlite-fts", source)
self.assertIn("x86_64-unknown-linux-musl", source)
self.assertIn("aarch64-apple-darwin", source)

def test_release_publishes_versioned_checksummed_assets(self) -> None:
source = CD_WORKFLOW.read_text(encoding="utf-8")

self.assertIn('asset="abyss-backend-${GITHUB_REF_NAME}-${{ matrix.target }}"', source)
self.assertIn("sha256sum abyss-backend-* > SHA256SUMS", source)
self.assertIn("gh release create", source)
self.assertIn('"${GITHUB_REF_NAME}"', source)
self.assertIn("release tag ${GITHUB_REF_NAME} does not match Cargo version", source)


if __name__ == "__main__":
unittest.main()
Loading