Skip to content

CI

CI #1022

Workflow file for this run

# PCMI CI: lint → unit/integration (race) → smoke → optional OpenAI E2E
# Coverage: sticky PR comment (refreshed after *every* push), job summary, artifacts
# (see scripts/ci_coverage_check.sh + the "Resolve PR for coverage comment" step).
# This design prevents stale coverage numbers on long-lived feature PRs.
#
# ── Job DAG (needs →) ─────────────────────────────────────────────────────────
# Wave 1 (parallel):
# golangci-lint | security | helm-lint | trivy-images | go
# Wave 2 (after Wave 1 subsets):
# integration-smoke → needs [go, golangci-lint, security, trivy-images]
# integration-e2e → needs [go, golangci-lint] (skips if no OPENAI_API_KEY; no retry)
# integration-graph → needs [go, golangci-lint] (complete cognitive graph matrix gate)
# CodeQL: separate workflow (.github/workflows/codeql.yml)
# Local parity: make ci-like-github → scripts/ci_like_github.sh (phases A–G)
# Version for smoke: scripts/ci/resolve_version.sh (internal/version/version.go)
name: CI
on:
push:
branches:
- main
- release/**
- develop
- feat/**
- feature/**
# Skipped only when every changed file matches (badge-only pushes on main).
paths-ignore:
- 'badges/**'
pull_request:
branches:
- main
- release/**
- develop
- feat/**
- feature/**
workflow_dispatch:
schedule:
# Daily bug-hunt on main — runs non-redundant phases (nilaway, gosec,
# staticcheck, fuzz, api_property, migrations, containers).
- cron: '37 4 * * *'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
jobs:
golangci-lint:
name: golangci-lint
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.12.2
- name: nilaway (nil-deref detection)
run: |
go install go.uber.org/nilaway/cmd/nilaway@latest
nilaway ./... || true
- name: gosec (security lint)
run: |
go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec -quiet -severity medium -confidence medium ./... || true
security:
name: Security scan (govulncheck)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: govulncheck
run: scripts/ci/phases/c_govulncheck.sh
helm-lint:
name: Helm chart lint
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- name: Helm + kubeconform validation
run: scripts/ci/phases/d_helm.sh
trivy-images:
name: Trivy image scan (API + Worker)
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Skip notice (act only)
if: ${{ env.ACT == 'true' }}
run: echo "::notice::trivy-images skipped under act; run 'make act-trivy' locally"
- uses: actions/checkout@v7
if: ${{ env.ACT != 'true' }}
- name: Set up Docker Buildx
if: ${{ env.ACT != 'true' }}
uses: docker/setup-buildx-action@v4
- name: Build and scan images (Trivy)
if: ${{ env.ACT != 'true' }}
run: |
scripts/ci/trivy_images.sh
go:
name: Go build, vet & test + coverage
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: write
pull-requests: write
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_DB: pcmi
POSTGRES_USER: pcmi
POSTGRES_PASSWORD: pcmi
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U pcmi -d pcmi"
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v7
with:
persist-credentials: true
token: ${{ secrets.BADGE_UPDATE_TOKEN || github.token }}
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: Static checks (build, vet, config audit)
run: scripts/ci/phases/a_go_static.sh
- name: Install postgresql-client
run: sudo apt-get update && sudo apt-get install -y postgresql-client
- uses: ./.github/actions/pcmi-postgres-migrate
- name: Test (race detector + integration + coverage)
env:
DATABASE_URL: postgres://pcmi:pcmi@127.0.0.1:5432/pcmi?sslmode=disable
PCMI_ENCRYPTION_KEY: "01234567890123456789012345678901"
SKIP_COVERAGE: "1"
run: scripts/ci/phases/f_go_integration.sh
- name: Enforce coverage thresholds
id: coverage
env:
# Build the workflow run URL so the script can embed it in the markdown comment.
COVERAGE_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# If a previous coverage % is known (e.g. from a prior comment or artifact), the
# script will show a delta line (point 8 — "change since last time").
COVERAGE_PREVIOUS_PCT: ${{ env.PREVIOUS_COVERAGE_PCT || '' }}
run: |
# shellcheck disable=SC1091
source scripts/ci/coverage_env.sh
scripts/ci_coverage_check.sh
- name: Publish coverage to job summary
if: ${{ always() && hashFiles('coverage-summary.md') != '' }}
run: cat coverage-summary.md >> "$GITHUB_STEP_SUMMARY"
# ─────────────────────────────────────────────────────────────────────────
# Coverage sticky comment — refreshed on EVERY push
#
# Goal: Prevent "visual staleness" on long-lived PRs.
# When a big feature PR (e.g. #112) is opened, the coverage numbers are
# captured at that moment. Later PRs (e.g. #115) can massively improve
# coverage on packages like webhook, handler, graph, etc.
#
# By re-running the coverage job + sticky comment on every push to a
# feat/** branch (and on pull_request events), the numbers shown in the
# PR are always from the *current* code on that branch, not from the
# moment the PR was first created.
#
# Key improvements (PR #119, points 1-8):
# 1. Fork support — uses pull_request.head.repo.owner for fork PRs
# 2. Workflow run link — included in the posted comment
# 3. Multiple PRs — logs warning, takes the most recently updated
# 4. Post on failed runs — posts with 🔴 + failure note (transparency)
# 5. Global row formatting — fixed markdown via ci_coverage_check.sh
# 6. Documentation — docs/ci-coverage-reporting.md
# 7. Skip on main pushes — explicit early exit for main/release/**
# 8. Change since last time — delta shown when COVERAGE_PREVIOUS_PCT is set
# ─────────────────────────────────────────────────────────────────────────
# Step A: Resolve which PR to comment on.
#
# Fork support (point 1): on pull_request events we read the head repo
# owner from the event payload (works for both same-repo and fork PRs).
# On push events we fall back to context.repo.owner and try to find an
# open PR whose head branch matches.
#
# Skip on main/release pushes (point 7): we short-circuit when the push
# target is main or release/** — no PR comment to update on those branches.
- name: Resolve PR for coverage comment
id: cov_pr
if: ${{ always() && hashFiles('coverage-summary.md') != '' }}
uses: actions/github-script@v9
with:
script: |
// ── Point 7: skip on main / release pushes ─────────────────
const ref = context.ref || '';
const isMainPush =
context.eventName === 'push' &&
(ref === 'refs/heads/main' || ref.startsWith('refs/heads/release/'));
if (isMainPush) {
core.info('Skipping coverage comment: push to main/release branch');
core.setOutput('number', '');
core.setOutput('skip', 'true');
return;
}
let n = '';
let forkWarning = false;
// ── Point 1: fork-aware PR resolution ──────────────────────
if (context.eventName === 'pull_request' && context.payload.pull_request) {
// On pull_request events we have the number directly — most reliable.
n = String(context.payload.pull_request.number);
// Detect fork PRs: head.repo.owner differs from base.repo.owner.
const headOwner = context.payload.pull_request.head?.repo?.owner?.login || '';
const baseOwner = context.payload.pull_request.base?.repo?.owner?.login || '';
if (headOwner && baseOwner && headOwner !== baseOwner) {
core.info(`Fork PR detected: head=${headOwner}, base=${baseOwner}`);
forkWarning = true;
}
} else if (context.eventName === 'push') {
// On push events: find open PR(s) whose head branch matches.
const branch = ref.replace('refs/heads/', '');
try {
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: `${context.repo.owner}:${branch}`,
state: 'open',
sort: 'updated',
direction: 'desc',
per_page: 10,
});
if (prs.length === 0) {
// Try also searching with the fork owner pattern
core.info(`No PR found for head=${context.repo.owner}:${branch}`);
} else {
// ── Point 3: multiple PRs → take most recent, log warning ──
if (prs.length > 1) {
core.warning(
`Found ${prs.length} open PRs for branch ${branch}. ` +
`Taking the most recently updated one (#${prs[0].number}). ` +
`Full list: ${prs.map(p => `#${p.number}`).join(', ')}`
);
}
n = String(prs[0].number);
}
} catch (e) {
core.warning(`Failed to resolve PR for branch ${branch}: ${e.message}`);
}
}
// Also accept explicit PR number via workflow_dispatch inputs.
if (!n && context.payload.inputs && context.payload.inputs.pr_number) {
n = String(context.payload.inputs.pr_number);
}
core.setOutput('number', n);
core.setOutput('skip', (!n) ? 'true' : 'false');
core.info(`Coverage comment target PR: ${n || '(none resolved)'}`);
# Step B: Post (or update) the sticky comment.
#
# Point 4: we always post when we have coverage data, even when thresholds
# failed. The script already sets 🔴 vs ✅ in the markdown header, so
# readers immediately see whether coverage passed or failed.
- name: Comment coverage on PR
if: ${{ always() && hashFiles('coverage-summary.md') != '' && steps.cov_pr.outputs.number != '' && steps.cov_pr.outputs.skip != 'true' }}
uses: marocchino/sticky-pull-request-comment@v3
with:
header: pcmi-coverage
number: ${{ steps.cov_pr.outputs.number }}
path: coverage-summary.md
- name: Upload coverage artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: coverage-${{ github.run_id }}
path: |
coverage.out
coverage-summary.md
coverage-badge.txt
badges/coverage.json
retention-days: 14
# Intentional one-file chore on main (not feature work). Requires Ruleset bypass
# for GitHub Actions — see docs/github-branch-protection.md.
# Loop prevention: on.push.paths-ignore badges/** + [skip ci] in commit message.
- name: Commit dynamic coverage badge (main only)
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && hashFiles('badges/coverage.json') != '' }}
run: |
if git diff --quiet -- badges/coverage.json; then
echo "badge unchanged — skipping commit"
exit 0
fi
git config user.name "pcmi-ci[bot]"
git config user.email "pcmi-ci@users.noreply.github.com"
git add badges/coverage.json
git commit -m "chore(ci): update coverage badge [skip ci]" \
-m "Generated by scripts/ci_coverage_check.sh on commit ${GITHUB_SHA::8}."
git push origin HEAD:main
test-massive:
name: Massive / realistic workload tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: Run massive realistic multi-agent workload test
run: |
go test -tags=massive -count=1 -timeout=5m ./internal/service -run TestMassiveRealisticMultiAgentWorkload -v
test-massive-live:
name: Massive workload via HTTP (docker compose)
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [go, golangci-lint]
steps:
- uses: actions/checkout@v7
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Create .env for compose
run: |
cat > .env <<'ENVEOF'
DATABASE_URL=postgres://pcmi:pcmi@postgres:5432/pcmi?sslmode=disable
REDIS_ADDR=redis:6379
API_PORT=8000
RATE_LIMIT_DISABLED=true
EMBEDDING_MODEL=text-embedding-3-small
ENVEOF
- name: Install tooling
run: sudo apt-get update && sudo apt-get install -y jq curl
- name: Start stack
run: docker compose up -d --build --wait
- name: Run massive curl smoke
env:
PCMI_BASE_URL: http://127.0.0.1:8000
PCMI_API_KEY: testkey123
run: ./scripts/ci_massive_curl_smoke.sh
- name: Show compose logs on failure
if: failure()
run: docker compose logs --tail=200 || true
- name: Cleanup
if: always()
run: docker compose down -v --remove-orphans
integration-smoke:
name: Integration smoke (Postgres + Redis)
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [go, golangci-lint, security, trivy-images]
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_DB: pcmi
POSTGRES_USER: pcmi
POSTGRES_PASSWORD: pcmi
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U pcmi -d pcmi"
--health-interval 5s
--health-timeout 5s
--health-retries 10
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v7
- name: Skip notice (act only)
if: ${{ env.ACT == 'true' }}
run: echo "::notice::integration-smoke skipped under act — run 'make act-integration-smoke' locally"
- uses: actions/setup-go@v7
if: ${{ env.ACT != 'true' }}
with:
go-version-file: go.mod
cache: true
- uses: ./.github/actions/pcmi-resolve-version
if: ${{ env.ACT != 'true' }}
id: api_version
- name: Install tooling
if: ${{ env.ACT != 'true' }}
run: sudo apt-get update && sudo apt-get install -y postgresql-client jq curl
- uses: ./.github/actions/pcmi-postgres-migrate
if: ${{ env.ACT != 'true' }}
- name: Build and start API + worker
if: ${{ env.ACT != 'true' }}
run: |
scripts/ci/start_api_worker.sh
- name: Wait for API health
if: ${{ env.ACT != 'true' }}
run: |
scripts/ci/wait_api_health.sh || {
cat api.log 2>/dev/null || true
cat worker.log 2>/dev/null || true
exit 1
}
- name: Integration smoke suite (bash)
if: ${{ env.ACT != 'true' }}
env:
PCMI_EXPECT_VERSION: ${{ steps.api_version.outputs.tag }}
EXPECT_API_VERSION: ${{ steps.api_version.outputs.tag }}
PGHOST: "127.0.0.1"
API: "http://127.0.0.1:8000"
run: ./scripts/ci_integration_smoke.sh
- name: gRPC integration tests (Go)
if: ${{ env.ACT != 'true' }}
env:
GRPC_HOST: 127.0.0.1:50051
GRPC_TEST_API_KEY: testkey123
DATABASE_URL: postgres://pcmi:pcmi@127.0.0.1:5432/pcmi?sslmode=disable
run: go test -tags=integration -count=1 ./internal/grpc/...
- name: SDK smoke (Python + TypeScript)
if: ${{ env.ACT != 'true' }}
env:
PCMI_BASE_URL: http://127.0.0.1:8000
PCMI_API_KEY: testkey123
run: ./scripts/ci_sdk_smoke.sh
- name: Upload smoke logs on failure
if: failure() && env.ACT != 'true'
uses: actions/upload-artifact@v7
with:
name: integration-smoke-logs-${{ github.run_id }}
path: |
api.log
worker.log
retention-days: 7
if-no-files-found: ignore
- name: Show logs on failure
if: failure() && env.ACT != 'true'
run: |
cat api.log 2>/dev/null || true
cat worker.log 2>/dev/null || true
- name: Stop processes
if: always() && env.ACT != 'true'
run: |
scripts/ci/stop_api_worker.sh
integration-e2e:
name: Integration E2E (docker compose + OpenAI)
runs-on: ubuntu-latest
timeout-minutes: 45
needs: [go, golangci-lint]
steps:
- uses: actions/checkout@v7
- id: openai
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
if [ -z "$OPENAI_API_KEY" ]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::notice::OPENAI_API_KEY secret not set — skipping embedding E2E"
else
echo "enabled=true" >> "$GITHUB_OUTPUT"
fi
- name: Set up Docker Buildx
if: steps.openai.outputs.enabled == 'true'
uses: docker/setup-buildx-action@v4
- name: Create .env for compose
if: steps.openai.outputs.enabled == 'true'
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
cat > .env <<EOF
OPENAI_API_KEY=${OPENAI_API_KEY}
DATABASE_URL=postgres://pcmi:pcmi@postgres:5432/pcmi
REDIS_ADDR=redis:6379
API_PORT=8000
RATE_LIMIT_DISABLED=true
EMBEDDING_MODEL=text-embedding-3-small
DISTILLATION_MODEL=gpt-4o-mini
EOF
- name: Install test tooling
if: steps.openai.outputs.enabled == 'true'
run: sudo apt-get update && sudo apt-get install -y jq curl
- name: Start stack and run E2E scripts
if: steps.openai.outputs.enabled == 'true'
run: |
./scripts/e2e/test_pcmi.sh
./scripts/ci_e2e_sse_dedup.sh
./scripts/ci_e2e_finale.sh
- name: Upload compose logs on failure
if: failure() && steps.openai.outputs.enabled == 'true'
run: docker compose logs --tail=200 > compose-failure.log || true
- name: Publish compose failure log
if: failure() && steps.openai.outputs.enabled == 'true' && hashFiles('compose-failure.log') != ''
uses: actions/upload-artifact@v7
with:
name: integration-e2e-logs-${{ github.run_id }}
path: compose-failure.log
retention-days: 7
- name: Show compose logs on failure
if: failure() && steps.openai.outputs.enabled == 'true'
run: docker compose logs --tail=100
- name: Cleanup compose
if: always() && steps.openai.outputs.enabled == 'true'
run: docker compose down -v --remove-orphans
integration-graph:
name: Integration graph (complete AGE matrix)
runs-on: ubuntu-latest
timeout-minutes: 60
needs: [go, golangci-lint]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: Install graph matrix tooling
run: sudo apt-get update && sudo apt-get install -y jq curl
- name: Complete cognitive graph matrix
env:
REALISTIC_SMOKE_LIMIT: "180"
run: |
make test-cognitive-graph-matrix
- name: Upload graph matrix logs on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: graph-matrix-logs-${{ github.run_id }}
path: |
/tmp/graph-matrix-*.log
/tmp/graph-realistic-load-smoke.log
retention-days: 7
if-no-files-found: ignore
# ───────────────────────────────────────────────────────────────────────────
# Bug-hunt — non-redundant phases only (daily + manual + commit tag)
#
# Triggers:
# • schedule (daily cron on main)
# • workflow_dispatch (gh workflow run CI --ref <branch>)
# • push to any tracked branch when the commit message contains [bug-hunt]
# • pull_request when the PR head commit contains [bug-hunt]
# (pull_request payloads lack head_commit.message in `if:` — see bug-hunt-tag-check)
#
# Add [bug-hunt] to a commit message to run bug-hunt on push without waiting
# for the daily cron. Example: git commit -m "fix: foo [bug-hunt]"
#
# Skipped on ordinary PR/push (no tag) because it needs Postgres + Redis and
# takes ~15-20 minutes.
#
# Phases skipped (already covered by other CI jobs):
# go vet, golangci-lint, govulncheck → golangci-lint + security jobs
# unit (test -race -cover) → go job
# integration → integration-smoke job
# trivy image → trivy-images job
# helm lint → helm-lint job
#
# Phases run here (NOT covered elsewhere):
# staticcheck, errcheck, gosec, nilaway, gitleaks, buf, hadolint
# fuzz, api_property, security (trivy fs+config), migrations, containers
# ───────────────────────────────────────────────────────────────────────────
#
# pull_request payloads do not expose head commit messages in job `if:` expressions,
# so PRs use this lightweight gate (checks out the head commit and greps for the tag).
# Push with [bug-hunt] uses github.event.head_commit.message directly in bug-hunt `if:`.
bug-hunt-tag-check:
name: Bug hunt tag check (PR)
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 3
outputs:
tagged: ${{ steps.tag.outputs.tagged }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 1
- id: tag
run: |
if git log -1 --format=%B | grep -qF '[bug-hunt]'; then
echo "tagged=true" >> "$GITHUB_OUTPUT"
echo "PR head commit contains [bug-hunt] — bug-hunt will run"
else
echo "tagged=false" >> "$GITHUB_OUTPUT"
echo "PR head commit has no [bug-hunt] tag — skipping bug-hunt"
fi
bug-hunt:
name: Bug hunt (daily)
needs: [bug-hunt-tag-check]
# Same commit can arrive via push + pull_request synchronize; dedupe by SHA.
concurrency:
group: bug-hunt-${{ github.sha }}
cancel-in-progress: true
if: >-
always() &&
(needs.bug-hunt-tag-check.result == 'success' || needs.bug-hunt-tag-check.result == 'skipped') &&
(
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'push' &&
github.event.head_commit != null &&
contains(github.event.head_commit.message, '[bug-hunt]')
) ||
(
github.event_name == 'pull_request' &&
needs.bug-hunt-tag-check.outputs.tagged == 'true'
)
)
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
security-events: write
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_DB: pcmi
POSTGRES_USER: pcmi
POSTGRES_PASSWORD: pcmi
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U pcmi -d pcmi"
--health-interval 5s
--health-timeout 5s
--health-retries 10
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
- name: Install tooling
run: |
# Static analysis tools
go install honnef.co/go/tools/cmd/staticcheck@latest
go install github.com/securego/gosec/v2/cmd/gosec@latest
go install github.com/kisielk/errcheck@latest
go install go.uber.org/nilaway/cmd/nilaway@latest
go install github.com/zricethezav/gitleaks/v8@latest
# Proto linting
go install github.com/bufbuild/buf/cmd/buf@latest
# Load testing
go install github.com/bojand/ghz/cmd/ghz@latest
go install github.com/tsenart/vegeta/v12@latest
# Dockerfile lint (Haskell binary — not go-installable)
curl -sSfL -o "$(go env GOPATH)/bin/hadolint" \
https://github.com/hadolint/hadolint/releases/download/v2.14.0/hadolint-linux-x86_64
chmod +x "$(go env GOPATH)/bin/hadolint"
# Migration testing
go install github.com/golang-migrate/migrate/v4/cmd/migrate@latest
# Security
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
curl -sfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Python tools
pip3 install --quiet --break-system-packages schemathesis 2>/dev/null || true
# Add Go bin to PATH
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
- name: Install postgresql-client
run: sudo apt-get update && sudo apt-get install -y postgresql-client
- uses: ./.github/actions/pcmi-postgres-migrate
- name: Create .env for compose validation
run: cp .env.example .env
- name: Run bug-hunt (non-redundant phases)
env:
BUG_HUNT_CI: "1"
DATABASE_URL: postgres://pcmi:pcmi@127.0.0.1:5432/pcmi?sslmode=disable
REDIS_ADDR: localhost:6379
PCMI_ENCRYPTION_KEY: "01234567890123456789012345678901"
RATE_LIMIT_DISABLED: "true"
COVERAGE_MIN: "39"
run: |
chmod +x scripts/bug_hunt.sh
# Phases run (all non-redundant with other CI jobs):
# static — staticcheck, gosec, errcheck, nilaway, gitleaks,
# buf, hadolint (skips go vet + golangci-lint in CI)
# fuzz — native Go fuzz tests (30s each)
# security — trivy fs + config scan (image scan is separate job)
# migrations — down→up→down→up reversibility check
# containers — kustomize + helm lint (helm-lint job covers too)
# report — summary markdown
# Skipped:
# api_property — needs running API server (not started here)
bash scripts/bug_hunt.sh \
--phase=static,fuzz,security,migrations,containers,report
- name: Upload bug-hunt report
if: always()
uses: actions/upload-artifact@v7
with:
name: bug-hunt-report
path: .bug_hunt/*/summary.md
retention-days: 30
if-no-files-found: ignore