Skip to content

feat(groups): GAR-890 — DELETE /v1/groups/{id} — owner-only group soft-deletion #2284

feat(groups): GAR-890 — DELETE /v1/groups/{id} — owner-only group soft-deletion

feat(groups): GAR-890 — DELETE /v1/groups/{id} — owner-only group soft-deletion #2284

Workflow file for this run

name: CI
on:
push:
branches: [main, develop, master]
pull_request:
branches: [main, develop, master]
# Cancel superseded in-flight runs on the same PR to avoid empilhar FAILUREs
# em Dependabot rebases e PRs que serão fechados (ex.: #289/#286/#291 hoje
# 2026-05-13 geraram 33 jobs vermelhos cada um substituindo o anterior).
# Pushes em main/develop/master NÃO são cancelados — merges + post-merge
# runs precisam completar. Padrão alinhado a codeql.yml (que comentava
# "mirrors ci.yml convention" antes deste bloco existir).
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
# Workflow-level default: read-only GITHUB_TOKEN. The `coverage` and
# `dependency-review` jobs override with `pull-requests: write` to post their
# PR comments. Closes CodeQL rule `actions/missing-workflow-permissions`
# (Medium).
permissions:
contents: read
jobs:
# Check formatting
fmt:
name: Format Check
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: Check formatting
run: cargo fmt --check --all
# Plan 0127 (PR-B) — shellcheck `install.sh` and the bash test harness.
# `install.sh` is the public `curl | sh` entry point and has been
# historically linted by hand; promoting that to a blocking CI step
# closes the regression window. The bash test runner gets `bash -n`
# syntax-only checks (shellcheck of bash-flavored test scripts is too
# noisy — bash assertion DSL trips style rules that don't apply).
installer-shellcheck:
name: Install.sh shellcheck
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Run shellcheck on install.sh
run: shellcheck install.sh
- name: Run shellcheck on installer fixtures
run: shellcheck tests/install_sh/fixtures/garraia-stub.sh
- name: Run shellcheck on installer unit tests
run: shellcheck tests/install_sh/checksum_format.sh
- name: Syntax-check the bash test runner
run: bash -n tests/install_sh/bootstrap_phase.sh
- name: Execute installer bootstrap_phase tests
run: bash tests/install_sh/bootstrap_phase.sh
- name: Execute installer checksum-format tests
run: bash tests/install_sh/checksum_format.sh
# Lint with clippy (strict — bloqueante desde plan 0049 / GAR-429)
clippy:
name: Clippy Linting
runs-on: ubuntu-latest
permissions:
contents: read
# Plan 0049 (GAR-429 Lote Q.1, 2026-04-22) limpou os 26 warnings
# pré-existentes (too_many_arguments, collapsible_if, explicit_auto_deref,
# assertions_on_constants, new_without_default, items_after_test_module,
# needless_as_bytes, useless_conversion, if_same_then_else) e introduziu
# `clippy.toml` + `rustfmt.toml`. O step é bloqueante a partir deste PR —
# qualquer warning novo em crates ativos falha CI.
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Run clippy
# garraia-desktop (Tauri) is excluded because its build.rs depends on
# the GTK/glib system libraries and a Windows sidecar binary that are
# not available on GitHub-hosted runners. Desktop is built locally via
# scripts/build-installer.ps1.
run: cargo clippy --workspace --exclude garraia-desktop --all-targets -- -D warnings
# Supply-chain gate: license allow-list + crate bans + registry source bans
# + RustSec advisories. Uses deny.toml at the repo root. Runs in parallel
# with clippy. Now invokes the `cargo-deny-action` default `command: check`,
# which runs all four categories — `advisories` was added in plan 0053
# PR-7 followup / GAR-458 after the `audit.toml` train (PR-1..PR-7) closed
# the Q7 RUSTSEC triage. `deny.toml` ignore list = 47 RUSTSEC IDs total
# (15 wasmtime under GAR-454, 4 rustls-webpki residuals under GAR-455,
# 5 audit.toml mirrors with deny-stricter severity mapping under GAR-437/
# GAR-456/GAR-457, 23 unmaintained-only spread across GAR-430 ownership).
# Second-layer gate paired with the now-blocking PR-time `Security Audit`
# job (`cargo audit --deny unsound`, GAR-462 dropped `--no-fetch`).
cargo-deny:
name: cargo-deny
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Run cargo-deny (advisories + bans + licenses + sources)
uses: EmbarkStudios/cargo-deny-action@v2
# Dependency review — catches new vulnerable/deprecated transitive deps
# before they land on main. Runs only on pull_request (the action requires
# a base SHA for the diff and has no useful meaning on push).
# Plan 0050 Lote 1 / GAR-432. Re-enabled in follow-up after the repo owner
# turned on Dependency Graph (GAR-442 closed).
dependency-review:
name: Dependency Review
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions:
contents: read
# `comment-summary-in-pr: on-failure` below posts a PR comment via the
# GITHUB_TOKEN — requires pull-requests write. Without this, the comment
# silently 403s on first regression and the team loses signal.
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Dependency Review
uses: actions/dependency-review-action@v5
with:
# Block PR when any new dependency (direct or transitive) carries
# a High/Critical advisory. Existing advisories already tracked in
# .cargo/audit.toml are NOT double-flagged — this action only looks
# at the diff.
fail-on-severity: high
comment-summary-in-pr: on-failure
# Secret scanning — catches accidental commit of JWT signing secrets,
# API keys, refresh-HMAC secrets, metrics bearer tokens. Project handles
# sensitive material flagged in CLAUDE.md rule #6.
# Plan 0050 Lote 1 / GAR-433. Uses .gitleaks.toml at repo root for
# allow-list (.env.example placeholders, doc examples, test fixtures).
gitleaks:
name: Secret Scan (gitleaks)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
# Gitleaks needs the full history to scan commits reachable from
# the PR tip — shallow clones miss the diff baseline on push.
fetch-depth: 0
- name: Run gitleaks
uses: gitleaks/gitleaks-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Build and test on multiple platforms
test:
name: Test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo registry
uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
# utoipa-swagger-ui 9.0.2 build.rs uses reqwest to download the swagger-ui
# zip, which intermittently returns a corrupt file on cache miss ("InvalidArchive:
# Could not find EOCD"). Pre-download with curl + cache cross-platform. (GAR-822)
- name: Set SWAGGER_UI_DOWNLOAD_URL (cross-platform file:// URI)
shell: bash
run: |
mkdir -p "$RUNNER_TEMP/swagger-ui-cache"
URI=$(python3 -c "import pathlib,os; print(pathlib.Path(os.environ['RUNNER_TEMP'],'swagger-ui-cache','v5.17.14.zip').as_uri())")
echo "SWAGGER_UI_DOWNLOAD_URL=$URI" >> "$GITHUB_ENV"
- name: Cache Swagger UI zip
id: swagger-cache-test
uses: actions/cache@v5
with:
path: ${{ runner.temp }}/swagger-ui-cache
key: swagger-ui-v5.17.14-${{ runner.os }}
- name: Download Swagger UI zip
if: steps.swagger-cache-test.outputs.cache-hit != 'true'
shell: bash
run: |
curl -sSLf --retry 5 --retry-delay 5 \
-o "$RUNNER_TEMP/swagger-ui-cache/v5.17.14.zip" \
https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.17.14.zip
- name: Build workspace
# See clippy job comment: garraia-desktop excluded on CI.
run: cargo build --workspace --exclude garraia-desktop
- name: Run tests
# On Linux runners Docker is preinstalled, so testcontainers-based
# integration tests in garraia-auth / garraia-workspace /
# garraia-gateway run normally. GitHub-hosted macOS and Windows
# runners do NOT ship Docker, so attempting to `Pg::default()` a
# pgvector container fails with:
# Error: failed to initialize a docker client:
# Socket not found: /var/run/docker.sock
# On those platforms we compile the entire test matrix (`--no-run`)
# so we still catch cross-platform compile regressions, but do not
# execute the Docker-dependent binaries. Pure unit tests on Linux
# provide the runtime coverage.
run: |
if [ "${{ runner.os }}" = "Linux" ]; then
cargo test --workspace --exclude garraia-desktop
else
cargo test --workspace --exclude garraia-desktop --no-run
fi
shell: bash
# Coverage instrumentation (GAR-435 / Q5 of EPIC GAR-430). Linux-only.
#
# Generates lcov.info via cargo-llvm-cov, uploads as artifact (14d retention),
# and posts a textual summary as PR comment via `gh pr comment` using the
# default GITHUB_TOKEN. NO external service (per Linear GAR-435 / plan
# foamy-origami ajuste #2 — sem Codecov/Coveralls).
#
# Soft-gate semantics (per plan sprightly-raven §11.3):
# - NO threshold — coverage % does not block PRs.
# - Job runs on every PR and emits artifact + comment.
# - NO `continue-on-error: true` — if the job is broken, fix it; do not
# accumulate silent failures (the repo just migrated all soft-gates to
# blocking via GAR-453).
#
# Excludes (matching test/build/clippy/msrv):
# - garraia-desktop — Tauri sidecar + GTK absent on GHA runners.
# - garraia-auth — integration tests need testcontainers + secrets
# (this job intentionally has no GARRAIA_JWT_SECRET
# or DATABASE_URL provisioned). Coverage of this
# crate is a follow-up task once we wire the env
# and a dedicated Postgres service into this job.
# - garraia-workspace — same reason as garraia-auth (testcontainers +
# Postgres + JWT).
#
# Security review (PR #87): pre-merge audit by security-auditor agent
# flagged 4 findings; all addressed below:
# - HIGH: `gh pr comment` step gated by
# `github.event.pull_request.head.repo.full_name == github.repository`
# (fork PRs receive read-only token by GitHub default; without
# the gate, the step would 403 on every fork PR, regressing
# the §11.3 contract).
# - MEDIUM: cache restore-keys isolated to `*-coverage-llvm-` prefix to
# avoid restoring target/ from the `test` job (compiled
# without `llvm-tools-preview` instrumentation).
# - MEDIUM: `garraia-auth` and `garraia-workspace` excluded (see above).
# - LOW: `timeout-minutes: 30` to bound runaway test hangs.
coverage:
name: Coverage (cargo-llvm-cov)
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
pull-requests: write # required for gh pr comment
steps:
- name: Checkout repository
uses: actions/checkout@v6
# Plan abundant-thimble (GAR-439 slice 9.a, 2026-04-28): the default
# GHA `ubuntu-latest` runner ships with ~14 GB free disk. The combo of
# (cache restore ~3.2 GB target + cargo install --force rebuild +
# `cargo llvm-cov` instrumented rebuild of target/) crosses 14 GB and
# the runner crashes with `No space left on device` mid-job (verified
# empirically on PR #90 retry — same failure twice). Pruning the
# default runner image saves ~12 GB before any rust step touches disk.
# Inline `rm -rf` instead of an external action to keep supply-chain
# surface zero (same posture as `cargo-deny`/`cargo-audit` jobs).
- name: Free up disk space (GHA runner default ~14 GB → ~26 GB)
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force || true
df -h
- name: Install Rust + llvm-tools-preview
uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- name: Cache cargo registry + target
uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-coverage-llvm-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-coverage-llvm-
- name: Install cargo-llvm-cov
# `--locked` matches the cargo-audit / cargo-deny installation pattern
# for supply-chain hygiene. `^0.6` allows minor bumps within the 0.6.x
# line (current 0.6.16 as of 2026-04-28); breaking-change bumps require
# a deliberate edit.
#
# `--force` is required because the cache step above restores
# `~/.cargo/bin/` whenever the same Cargo.lock hash hits (which is
# always for refactor PRs that don't touch the lockfile). On a cache
# hit, the cargo-llvm-cov binary is already present and `cargo
# install` refuses to overwrite without `--force`. Discovered on
# PR-1 of plan abundant-thimble (GAR-440 slice 10.a, 2026-04-28):
# 1st CI run installed clean, 2nd run (after fmt fixup commit) hit
# cache and failed with `error: binary cargo-llvm-cov already
# exists in destination`. `--force` makes the step idempotent
# without sacrificing the supply-chain pin.
run: cargo install --locked --force --version '^0.6' cargo-llvm-cov
- name: Generate lcov coverage
run: |
cargo llvm-cov \
--workspace \
--exclude garraia-desktop \
--exclude garraia-auth \
--exclude garraia-workspace \
--lcov --output-path lcov.info
- name: Generate textual summary
run: cargo llvm-cov report --summary-only > coverage-summary.txt
- name: Upload lcov artifact
uses: actions/upload-artifact@v7
with:
name: coverage-lcov-${{ github.run_id }}
path: lcov.info
retention-days: 14
if-no-files-found: error
- name: Upload textual summary artifact
uses: actions/upload-artifact@v7
with:
name: coverage-summary-${{ github.run_id }}
path: coverage-summary.txt
retention-days: 14
if-no-files-found: error
- name: Post coverage summary as PR comment
# Skip on push events AND on PRs from forks. GitHub's default policy
# gives fork PRs a read-only GITHUB_TOKEN regardless of the workflow's
# `permissions:` declaration; without this guard, the `gh pr comment`
# step would 403 on every fork PR, breaking the §11.3 contract that
# the job runs on every PR.
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
{
echo '<!-- coverage-comment -->'
echo '## Coverage Report (cargo-llvm-cov)'
echo
echo '_Generated by `coverage` job in this PR run. Soft-gate: no threshold; artifact + summary only._'
echo
echo 'Excluded crates: `garraia-desktop`, `garraia-auth`, `garraia-workspace` (see ci.yml comment).'
echo
echo '```'
cat coverage-summary.txt
echo '```'
} > pr-comment.md
gh pr comment "$PR_NUMBER" --repo "${{ github.repository }}" --body-file pr-comment.md
# Build only (no test) for faster feedback on other platforms if needed
build:
name: Build Check
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Build workspace
# See clippy job comment: garraia-desktop excluded on CI.
run: cargo build --workspace --exclude garraia-desktop --release
# MSRV gate (GAR-441) — bloqueante. Drift descoberto em smoke test do
# plan 0050 (`cargo +1.85 check` falhava porque darling 0.23, home 0.5.12,
# process-wrap 9, time 0.3.47, time-macros 0.2.27 exigem ≥ 1.87/1.88) E
# depois confirmado pelo primeiro CI run desta PR: o bump `wasmtime 28
# → 44` (GAR-454, sessão `purrfect-lantern`) elevou o piso real para
# **1.92** porque ~30 sub-crates `wasmtime-*` declaram
# `rust-version = "1.92"` em seus manifests publicados. O piso real
# (1.92) é o gate, não o piso parcial (1.88) das deps não-wasmtime.
# GAR-708: wasmtime 45.0.0 declara rust-version = "1.93.0" — piso
# elevado de 1.92 → 1.93 junto com o bump de segurança (health run 32).
# `--locked` garante que o lockfile não é regerado durante o check
# (qualquer drift falha o build). `garraia-desktop` excluído pelos
# mesmos motivos do clippy/build (GTK + sidecar Windows ausentes em
# runners GHA).
msrv:
name: MSRV check (1.93)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Rust 1.93
uses: dtolnay/rust-toolchain@1.93
# utoipa-swagger-ui 9.0.2 build.rs uses reqwest to download the swagger-ui
# zip, which intermittently returns a corrupt file ("InvalidArchive: Could not
# find EOCD"). Pre-download with curl (reliable, retries) and point the build
# script to the local file via SWAGGER_UI_DOWNLOAD_URL. (GAR-822)
- name: Cache Swagger UI zip
id: swagger-cache-msrv
uses: actions/cache@v5
with:
path: /tmp/swagger-ui-cache
key: swagger-ui-v5.17.14
- name: Download Swagger UI zip
if: steps.swagger-cache-msrv.outputs.cache-hit != 'true'
run: |
mkdir -p /tmp/swagger-ui-cache
curl -sSLf --retry 5 --retry-delay 5 \
-o /tmp/swagger-ui-cache/v5.17.14.zip \
https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.17.14.zip
- name: cargo +1.93 check
env:
SWAGGER_UI_DOWNLOAD_URL: file:///tmp/swagger-ui-cache/v5.17.14.zip
run: cargo +1.93 check --workspace --exclude garraia-desktop --locked
# E2E integration test (GAR-216, GAR-438 / plan 0050 Lote 2)
e2e:
name: E2E Tests
runs-on: ubuntu-latest
permissions:
contents: read
# Plan 0050 Lote 2 (GAR-438, plan happy-finch 2026-04-24):
# The previous "gateway exits immediately" hypothesis was wrong. Root cause
# was that `garraia-gateway` is a library crate (no production [[bin]]);
# the real binary is `garra` from `garraia-cli`. The job used to
# `cargo build -p garraia-gateway --release` + `./target/release/garraia-gateway`
# — that binary never existed, the `&` backgrounded a non-existent
# process with exit 0, and `continue-on-error: true` masked the failure.
# Fix: build `--bin garra`, start via `garra start`, add Postgres
# service + minimal auth envs so `AuthConfig::from_env` does not bail.
services:
# Pin to 16.8-alpine (minor+patch) for deterministic CI runs —
# full SHA pin would be overkill for an ephemeral test service.
# `POSTGRES_USER: postgres` declared explicitly so the connection
# strings below remain auditable even if the image default changes.
postgres:
image: postgres:16.8-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: garraia_workspace_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
# Use a mock/echo LLM — no real API key required for E2E structure tests
ANTHROPIC_API_KEY: "sk-ant-test-key-for-ci"
GARRAIA_API_KEY: "ci-test-bearer-token"
# Plan 0050 Lote 2: minimal auth envs for `AuthConfig::from_env` (fail-soft
# to 503 on /v1/auth/* if any pool role is missing, but `/health` stays
# green and the E2E script only hits /health + /api/sessions + /v1/chat).
GARRAIA_JWT_SECRET: "ci-e2e-jwt-secret-minimum-32-bytes-XXXXXXXX"
GARRAIA_REFRESH_HMAC_SECRET: "ci-e2e-refresh-hmac-secret-min-32-bytes-XXXXXXXX"
GARRAIA_LOGIN_DATABASE_URL: "postgres://postgres:postgres@localhost:5432/garraia_workspace_test"
GARRAIA_SIGNUP_DATABASE_URL: "postgres://postgres:postgres@localhost:5432/garraia_workspace_test"
steps:
- name: Checkout repository
uses: actions/checkout@v6
# Fake CI-only secrets are still printed in logs by default. Mask them
# so the pattern doesn't become a precedent for real secrets later.
- name: Mask CI-only fake secrets
run: |
echo "::add-mask::ci-e2e-jwt-secret-minimum-32-bytes-XXXXXXXX"
echo "::add-mask::ci-e2e-refresh-hmac-secret-min-32-bytes-XXXXXXXX"
echo "::add-mask::postgres://postgres:postgres@localhost:5432/garraia_workspace_test"
# Plan 0051 (GAR-444): the `sk-ant-*` prefix above follows the real
# Anthropic secret format even though the suffix is fictional.
# Mask it so gitleaks-style scanners never treat this CI workflow
# as a "leaked key" reference point.
echo "::add-mask::sk-ant-test-key-for-ci"
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo registry
uses: actions/cache@v5
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-e2e-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-e2e-
# Plan 0051 (GAR-444): build with `dev-echo-provider` feature so
# `AgentRuntime::build_agent_runtime` wires the deterministic echo
# LLM provider when the e2e config declares `[llm.echo] provider = "echo"`.
# Default OFF in the workspace — never enabled by a release binary.
- name: Build garra binary (release, dev-echo-provider)
run: cargo build --bin garra --release --features dev-echo-provider
# Plan 0051 (GAR-444): seed a minimal config with one echo provider so
# the AgentRuntime has a registered LlmProvider when the gateway boots.
# `GARRAIA_CONFIG_DIR` (read by `ConfigLoader::default_config_dir`) makes
# the gateway pick this file over the user-home default. No secrets, no
# network, no external service.
- name: Seed echo LLM config for e2e
run: |
mkdir -p /tmp/garraia-e2e
cat > /tmp/garraia-e2e/config.toml <<'TOML'
[llm.echo]
provider = "echo"
model = "echo-stub"
[agent]
default_provider = "echo"
TOML
- name: Start gateway in background
env:
GARRAIA_CONFIG_DIR: /tmp/garraia-e2e
run: |
./target/release/garra start --host 0.0.0.0 --port 3888 &
echo $! > /tmp/garraia.pid
# Wait up to 30s for health endpoint to respond
for i in $(seq 1 30); do
if curl -sf http://localhost:3888/health; then
echo ""
echo "gateway responded to /health after ${i}s"
exit 0
fi
sleep 1
done
echo "gateway failed to respond to /health within 30s" >&2
exit 1
# Plan 0051 (GAR-444): the `continue-on-error: true` that previously
# masked this step is gone — the echo provider wired above makes the
# 6/6 [PASS] contract of `tests/e2e_telegram_api.sh` a real blocking
# gate. `Build / Seed / Start / Health probe` above remain genuinely
# blocking too.
- name: Run E2E tests
run: |
chmod +x tests/e2e_telegram_api.sh
GARRAIA_BASE_URL=http://localhost:3888 \
GARRAIA_API_KEY=ci-test-bearer-token \
bash tests/e2e_telegram_api.sh
- name: Stop gateway
if: always()
run: |
if [ -f /tmp/garraia.pid ]; then
kill $(cat /tmp/garraia.pid) 2>/dev/null || true
fi
# Playwright E2E Web UI tests (GAR-300, GAR-438 / plan 0050 Lote 2)
playwright:
name: Playwright E2E (MCP UI)
runs-on: ubuntu-latest
permissions:
contents: read
# Plan 0050 Lote 2 (GAR-438, plan happy-finch 2026-04-24): same root cause
# as the `e2e` job — the previous workflow ran a binary that never existed
# (`./target/release/garraia-gateway`, but the real binary is `garra`
# from `garraia-cli`). See the `e2e` job comment for the full causal
# chain.
services:
# Pin to 16.8-alpine (minor+patch) for deterministic CI runs —
# full SHA pin would be overkill for an ephemeral test service.
# `POSTGRES_USER: postgres` declared explicitly so the connection
# strings below remain auditable even if the image default changes.
postgres:
image: postgres:16.8-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: garraia_workspace_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
ANTHROPIC_API_KEY: "sk-ant-test-key-for-ci"
GARRAIA_ADMIN_USER: "admin"
GARRAIA_ADMIN_PASS: "admin123ci"
GARRAIA_JWT_SECRET: "ci-playwright-jwt-secret-minimum-32-bytes-XXXXXXXX"
GARRAIA_REFRESH_HMAC_SECRET: "ci-playwright-refresh-hmac-min-32-bytes-XXXXXXXX"
GARRAIA_LOGIN_DATABASE_URL: "postgres://postgres:postgres@localhost:5432/garraia_workspace_test"
GARRAIA_SIGNUP_DATABASE_URL: "postgres://postgres:postgres@localhost:5432/garraia_workspace_test"
defaults:
run:
working-directory: tests
steps:
- name: Checkout repository
uses: actions/checkout@v6
# See `e2e` job for rationale — mask CI-only fake secrets.
- name: Mask CI-only fake secrets
working-directory: .
run: |
echo "::add-mask::ci-playwright-jwt-secret-minimum-32-bytes-XXXXXXXX"
echo "::add-mask::ci-playwright-refresh-hmac-min-32-bytes-XXXXXXXX"
echo "::add-mask::postgres://postgres:postgres@localhost:5432/garraia_workspace_test"
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo registry
uses: actions/cache@v5
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-playwright-${{ hashFiles('**/Cargo.lock') }}
- name: Build garra binary (release)
working-directory: .
run: cargo build --bin garra --release
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
cache-dependency-path: tests/package-lock.json
- name: Install Playwright
run: npm ci && npx playwright install chromium --with-deps
- name: Write Playwright gateway config
run: |
# The default per-IP rate limit (1 req/s, burst 60) is exceeded by
# 9 sequential Playwright tests + the admin UI's 10s auto-refresh
# poll. Bump it for the test gateway only — production defaults
# in `garraia-config` remain untouched.
mkdir -p /tmp/garraia-test-config
cat > /tmp/garraia-test-config/config.yml <<'EOF'
gateway:
host: "127.0.0.1"
port: 3888
rate_limit:
per_second: 1000
burst_size: 5000
EOF
- name: Start gateway
working-directory: .
env:
GARRAIA_CONFIG_DIR: /tmp/garraia-test-config
run: |
./target/release/garra start --host 0.0.0.0 --port 3888 &
echo $! > /tmp/garraia.pid
for i in $(seq 1 30); do
if curl -sf http://localhost:3888/health; then
echo ""
echo "gateway responded to /health after ${i}s"
exit 0
fi
sleep 1
done
echo "gateway failed to respond to /health within 30s" >&2
exit 1
- name: Run Playwright tests
run: |
GARRAIA_BASE_URL=http://localhost:3888 \
GARRAIA_SKIP_SERVER=1 \
npx playwright test
- name: Stop gateway
if: always()
working-directory: .
run: |
if [ -f /tmp/garraia.pid ]; then
kill $(cat /tmp/garraia.pid) 2>/dev/null || true
fi
- name: Upload test report
if: always()
uses: actions/upload-artifact@v7
with:
name: playwright-report
path: tests/playwright-report/
retention-days: 7
# Security audit — bloqueante desde plan 0053 PR-7 (GAR-453, fecha GAR-437).
# Espelha o setup do nightly `.github/workflows/cargo-audit.yml` para evitar
# drift de decisão entre PR-time e schedule. Histórico: o `continue-on-error:
# true` que sobreviveu até plan 0053 PR-1..PR-6 cobria 6 advisories;
# PR-1..PR-6 fecharam ou documentaram cada uma em `.cargo/audit.toml` com
# Linear owner explícito, então o gate agora é blocking de fato.
#
# GAR-462 (2026-04-28): bump cargo-audit pin de `^0.21` para `^0.22` e
# remove o strip CVSS v4 do plan 0043. cargo-audit 0.22.1 ships rustsec
# >= 0.31 que (a) tolera CVSS v4 nativamente e (b) tolera filenames
# não-canônicos como `RUSTSEC-0000-0000.<N>.md` (placeholders pre-ID).
# Mudanças aqui devem replicar em cargo-audit.yml.
security:
name: Security Audit
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install stable Rust
uses: dtolnay/rust-toolchain@stable
- name: Install cargo-audit
# `--locked` pina as transitive deps de cargo-audit; range `^0.22`
# tracks a linha 0.22.x igual ao nightly. Bump deliberado quando
# upstream quebrar contrato CLI.
run: cargo install --locked --version '^0.22' cargo-audit
- name: Prime crates.io index for workspace deps
# Pre-popular o sparse index para que metadata lookups (yanked,
# existência) de cargo-audit funcionem rápido. Plan 0043 §5.6.
run: cargo fetch --locked
- name: Run security audit
# `--deny unsound` widens failure beyond the default vuln set.
# GAR-462 dropped `--no-fetch`: cargo-audit 0.22 gerencia o
# advisory-db sozinho. Mesmo flag-for-flag que o nightly
# cargo-audit.yml.
run: cargo audit --deny unsound