name: Community Release on: push: tags: - "v*" workflow_dispatch: env: REGISTRY: git.itexp.me IMAGE_TAG: ${{ gitea.ref_name }} ADMIN_API_IMAGE: git.itexp.me/bsodfather/crank-community-admin-api MCP_SERVER_IMAGE: git.itexp.me/bsodfather/crank-community-mcp-server UI_IMAGE: git.itexp.me/bsodfather/crank-community-ui OPENBAO_ENV_FILE: .openbao-env jobs: release: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v5 - name: Use preinstalled Rust toolchain run: | set -eu toolchain_dir="${RUSTUP_HOME:-$HOME/.rustup}/toolchains/1.96.1-x86_64-unknown-linux-gnu" toolchain_bin="$toolchain_dir/bin" if [ ! -x "$toolchain_bin/rustc" ] || [ ! -x "$toolchain_bin/cargo" ]; then echo "Rust 1.96.1 is not preinstalled at $toolchain_dir." >&2 echo "Install it in the Gitea runner image/host before running CI:" >&2 echo "rustup toolchain install 1.96.1 --profile minimal --component clippy --component rustfmt" >&2 exit 1 fi printf '%s\n' "$toolchain_bin" >> "$GITHUB_PATH" "$toolchain_bin/rustc" --version "$toolchain_bin/cargo" --version "$toolchain_bin/rustfmt" --version "$toolchain_bin/cargo-clippy" --version - name: Verify runner toolchain run: | python3 --version rustc --version cargo --version node --version npm --version docker --version command -v bao bao version - name: Install dependency policy tool run: cargo install cargo-deny --version 0.20.2 --locked - name: Run tooling unit tests run: python3 -m unittest discover -s tests/unit - name: Check typed runtime configuration contract run: | cargo run -p crank-config --bin crank-config-contract -- --check python3 scripts/check-runtime-config.py --root . python3 scripts/check-config-boundaries.py --root . scripts/check-rust-boundaries.sh - name: Check canonical migration contract run: cargo run -p admin-api --bin crank-migrate -- plan --check - name: Check typed metrics contract run: | cargo run -p crank-metrics --bin crank-metrics-contract -- --check python3 scripts/check-metrics-boundaries.py --root . - name: Check Capability Inventory run: | required_args="" for number in $(seq 1 54); do required_args="$required_args --required-fr FR-$number" done python3 scripts/validate-capability-inventory.py \ --root . \ --inventory docs/capability-inventory.json \ --schema docs/schemas/capability-inventory.schema.json \ $required_args - name: Check Capability Baseline run: | python3 scripts/validate-capability-baseline.py \ --root . \ --manifest docs/capability-baseline/manifest.json \ --schema docs/schemas/capability-baseline.schema.json - name: Check Community scope run: scripts/check-community-scope.sh - name: Run release quality gates run: | cargo fmt --all --check cargo clippy --workspace --all-targets --all-features -- -D warnings cargo test --workspace --all-targets -- --test-threads=1 cargo deny --locked check advisories bans licenses sources - name: Build release binaries run: cargo build --release -p admin-api -p mcp-server - name: Install UI dependencies working-directory: apps/ui run: npm ci - name: Audit UI dependencies working-directory: apps/ui run: npm audit --audit-level=high - name: Build UI dist working-directory: apps/ui run: npm run build - name: Install Playwright browser working-directory: apps/ui run: npx playwright install --with-deps chromium - name: Run release end-to-end tests working-directory: apps/ui run: | mkdir -p ../../.tmp rm -f ../../.tmp/openapi-playwright.json ../../.tmp/openapi-ui-evidence.json PLAYWRIGHT_JSON_OUTPUT=../../.tmp/openapi-playwright.json npm run e2e - name: Collect sanitized OpenAPI UI release evidence working-directory: apps/ui run: | trap 'rm -f ../../.tmp/openapi-playwright.json' EXIT python3 ../../scripts/collect-capability-baseline.py playwright \ --report ../../.tmp/openapi-playwright.json \ --output ../../.tmp/openapi-ui-evidence.json \ --source-revision "$(git -C ../.. rev-parse HEAD)" \ --environment-class release \ --flow-id openapi-upload-ui \ --required-test 'operations page imports OpenAPI methods as drafts' \ --required-test 'OpenAPI upload rejects invalid files locally and restores focus after Escape' \ --required-test 'OpenAPI upload recovers from pagehide and a preview server error' \ --required-test 'OpenAPI upload invalidates active draft creation after language or workspace changes' \ --required-test 'OpenAPI upload only renders the latest selected file and clears reset or close races' \ --required-test 'OpenAPI upload ignores a stale failure and renders only correlation identifiers' python3 ../../scripts/validate-capability-run.py \ --schema ../../docs/schemas/capability-baseline.schema.json \ --candidate ../../.tmp/openapi-ui-evidence.json \ --require-accepted - name: Validate deployment manifests run: | docker compose -f docker-compose.yml --env-file .env.example config -q docker compose -f deploy/community/docker-compose.yml --env-file deploy/community/.env.example config -q docker compose -f deploy/community/docker-compose.images.yml --env-file deploy/community/.env.images.example --profile local-db config -q - name: Package release artifacts run: | mkdir -p dist/release source_revision="$(git rev-parse HEAD)" evidence_artifact="dist/crank-openapi-ui-evidence-${IMAGE_TAG}-${source_revision}.json" cp target/release/admin-api dist/release/admin-api cp target/release/crank-migrate dist/release/crank-migrate cp target/release/mcp-server dist/release/mcp-server cp .tmp/openapi-ui-evidence.json "$evidence_artifact" tar -C dist/release -czf dist/crank-community-admin-api-${IMAGE_TAG}.tar.gz admin-api crank-migrate tar -C dist/release -czf dist/crank-community-mcp-server-${IMAGE_TAG}.tar.gz mcp-server tar -C apps/ui/dist -czf dist/crank-community-ui-${IMAGE_TAG}.tar.gz . sha256sum \ dist/crank-community-admin-api-${IMAGE_TAG}.tar.gz \ dist/crank-community-mcp-server-${IMAGE_TAG}.tar.gz \ dist/crank-community-ui-${IMAGE_TAG}.tar.gz \ "$evidence_artifact" \ > dist/crank-community-${IMAGE_TAG}-checksums.txt - name: Generate SBOM run: | docker run --rm -v "$PWD:/workspace" anchore/syft:latest \ dir:/workspace -o spdx-json \ > dist/crank-community-${IMAGE_TAG}-sbom.spdx.json - name: Load deployment secrets from OpenBao env: BAO_ADDR: ${{ secrets.BAO_ADDR }} BAO_ROLE_ID: ${{ secrets.BAO_ROLE_ID }} BAO_SECRET_ID: ${{ secrets.BAO_SECRET_ID }} OPENBAO_APP: crank run: scripts/load-openbao-env.sh - name: Login to registry run: | . "$OPENBAO_ENV_FILE" printf '%s' "$DEPLOY_REGISTRY_TOKEN" | \ docker login '${{ env.REGISTRY }}' -u "$DEPLOY_REGISTRY_USER" --password-stdin - name: Build and push release images run: | docker build -f apps/admin-api/Dockerfile \ -t '${{ env.ADMIN_API_IMAGE }}:${{ env.IMAGE_TAG }}' \ -t '${{ env.ADMIN_API_IMAGE }}:latest' . docker build -f apps/mcp-server/Dockerfile \ -t '${{ env.MCP_SERVER_IMAGE }}:${{ env.IMAGE_TAG }}' \ -t '${{ env.MCP_SERVER_IMAGE }}:latest' . docker build -f apps/ui/Dockerfile \ -t '${{ env.UI_IMAGE }}:${{ env.IMAGE_TAG }}' \ -t '${{ env.UI_IMAGE }}:latest' . mkdir -p .tmp cat > .tmp/release-migration-smoke.env <