From 4a3a9822cf5ff6b654407d5dd2cedc8707674c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E8=88=8E=E3=81=AE=E3=83=8D=E3=82=BA=E3=83=9F?= Date: Sat, 29 Aug 2026 11:54:33 +0800 Subject: [PATCH 1/2] ci: publish native backend releases Detailed changes: - Build SQLite+FTS binaries for macOS ARM64 and Linux x86_64 musl on version tags. - Publish versioned GitHub Release assets with SHA-256 checksums while retaining Docker publishing. - Document native releases and test the release workflow contract. Signed-off-by: chunchi.che@lexmount.com Co-Authored-By: GPT-5 --- .github/workflows/ci.yml | 104 ++++++++++++++++++ Makefile | 1 + README.md | 19 +++- scripts/tests/test_native_release_contract.py | 35 ++++++ 4 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 scripts/tests/test_native_release_contract.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6d1a00..fa453f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,6 +92,69 @@ jobs: - name: Render Kubernetes manifests run: kubectl kustomize k8s > /tmp/abyss-backend.yaml + native: + name: Native SQLite+FTS binary (${{ matrix.target }}) + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + 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 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 if: github.event_name == 'push' @@ -159,3 +222,44 @@ jobs: echo "- \`${image_tag}\`" done <<< "${IMAGE_TAGS}" } >> "${GITHUB_STEP_SUMMARY}" + + release-native: + name: Publish native GitHub Release + if: github.event_name == 'push' && 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 diff --git a/Makefile b/Makefile index 05d69f0..8c40852 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 29000f6..0e37e07 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/scripts/tests/test_native_release_contract.py b/scripts/tests/test_native_release_contract.py new file mode 100644 index 0000000..d1fc3e3 --- /dev/null +++ b/scripts/tests/test_native_release_contract.py @@ -0,0 +1,35 @@ +#!/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] +WORKFLOW = REPO_ROOT / ".github" / "workflows" / "ci.yml" + + +class NativeReleaseContractTests(unittest.TestCase): + def test_release_builds_only_the_local_storage_profile(self) -> None: + source = 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 = 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() From 7aa4b0609535611268a941336c214305f7002044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E8=88=8E=E3=81=AE=E3=83=8D=E3=82=BA=E3=83=9F?= Date: Sat, 29 Aug 2026 12:21:50 +0800 Subject: [PATCH 2/2] ci: separate delivery workflow Detailed changes: - Keep CI focused on reusable Rust, storage, container, and Kubernetes validation. - Gate CD publishing on the reusable quality workflow before pushing Docker images. - Move native binary builds and GitHub Release publishing to cd.yml and update workflow contract coverage. Signed-off-by: chunchi.che@lexmount.com Co-Authored-By: GPT-5 --- .github/workflows/cd.yml | 189 ++++++++++++++++++ .github/workflows/ci.yml | 180 +---------------- scripts/tests/test_native_release_contract.py | 22 +- 3 files changed, 210 insertions(+), 181 deletions(-) create mode 100644 .github/workflows/cd.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..ef234ff --- /dev/null +++ b/.github/workflows/cd.yml @@ -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 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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa453f1..1979ad0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: @@ -91,175 +87,3 @@ jobs: - name: Render Kubernetes manifests run: kubectl kustomize k8s > /tmp/abyss-backend.yaml - - native: - name: Native SQLite+FTS binary (${{ matrix.target }}) - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') - 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 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 - 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}" - - release-native: - name: Publish native GitHub Release - if: github.event_name == 'push' && 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 diff --git a/scripts/tests/test_native_release_contract.py b/scripts/tests/test_native_release_contract.py index d1fc3e3..7ec38ef 100644 --- a/scripts/tests/test_native_release_contract.py +++ b/scripts/tests/test_native_release_contract.py @@ -8,12 +8,28 @@ REPO_ROOT = Path(__file__).resolve().parents[2] -WORKFLOW = REPO_ROOT / ".github" / "workflows" / "ci.yml" +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 = WORKFLOW.read_text(encoding="utf-8") + source = CD_WORKFLOW.read_text(encoding="utf-8") self.assertIn("Native SQLite+FTS binary", source) self.assertIn("--no-default-features", source) @@ -22,7 +38,7 @@ def test_release_builds_only_the_local_storage_profile(self) -> None: self.assertIn("aarch64-apple-darwin", source) def test_release_publishes_versioned_checksummed_assets(self) -> None: - source = WORKFLOW.read_text(encoding="utf-8") + 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)