Skip to content
Closed
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
14 changes: 14 additions & 0 deletions .agent/tasks/gh-63.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# GH-63

**Created:** 2026-04-02

## Problem

GitHub Issue #63: PR validation workflow with CI services

Parent: GH-36

Create `.github/workflows/ci.yml` and `.env.ci`. This is the most complex piece: 4 parallel jobs (Lint with golangci-lint, Test with race detector + coverage using PostgreSQL 16 and Redis 7 service containers, Security with gosec + govulncheck in report-only mode, Build verifying both `go build` and Docker build). Include Go module caching, coverage report as PR comment, and the `.env.ci` file with CI-specific database/Redis connection strings and test configuration. Triggered on PRs to `main`.

## Acceptance Criteria

48 changes: 48 additions & 0 deletions .env.ci
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Auth Service — CI Environment
# Used by GitHub Actions test job with PostgreSQL 16 and Redis 7 service containers.

# ── Server ──────────────────────────────────────────
APP_ENV=development
PUBLIC_PORT=4000
ADMIN_PORT=4001
LOG_LEVEL=warn

# ── PostgreSQL ──────────────────────────────────────
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=auth_test
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_SSLMODE=disable
POSTGRES_MAX_CONNS=5

# ── Redis ───────────────────────────────────────────
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0

# ── JWT / Token Signing ─────────────────────────────
JWT_PRIVATE_KEY_PATH=testdata/ec256_private.pem
JWT_ALGORITHM=ES256
ACCESS_TOKEN_TTL=15m
REFRESH_TOKEN_TTL=1d

# ── System Secrets ──────────────────────────────────
SYSTEM_SECRETS=ci-test-secret-do-not-use-in-production

# ── Password Hashing (Argon2id) — reduced for CI speed
ARGON2_MEMORY=4096
ARGON2_TIME=1
ARGON2_PARALLELISM=1
PASSWORD_PEPPER=ci-test-pepper-do-not-use-in-production

# ── Rate Limiting ───────────────────────────────────
RATE_LIMIT_RPS=100
RATE_LIMIT_BURST=200

# ── TLS ─────────────────────────────────────────────
TLS_ENABLED=false

# ── CORS ────────────────────────────────────────────
CORS_ALLOWED_ORIGINS=http://localhost:3000
161 changes: 161 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
name: CI

on:
pull_request:
branches: [main]

permissions:
contents: read
pull-requests: write

env:
GO_VERSION: "1.25.6"

jobs:
# ── Lint ────────────────────────────────────────────
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest

# ── Test ────────────────────────────────────────────
test:
name: Test
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: auth_test
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U postgres"
--health-interval=10s
--health-timeout=5s
--health-retries=5
redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}

- name: Load CI environment
run: |
grep -v '^#' .env.ci | grep -v '^$' >> "$GITHUB_ENV"

- name: Run tests with race detector and coverage
run: go test -race -coverprofile=coverage.out -covermode=atomic ./...

- name: Generate coverage report
if: always()
run: go tool cover -func=coverage.out -o=coverage.txt

- name: Comment coverage on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const coverage = fs.readFileSync('coverage.txt', 'utf8');
const totalLine = coverage.split('\n').find(l => l.includes('total:'));
const total = totalLine ? totalLine.trim() : 'Could not parse total coverage';

const body = `## Test Coverage\n\n\`\`\`\n${total}\n\`\`\`\n\n<details>\n<summary>Full coverage report</summary>\n\n\`\`\`\n${coverage}\n\`\`\`\n</details>`;

// Find and update existing comment or create new one
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const marker = '## Test Coverage';
const existing = comments.find(c => c.body.startsWith(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}

# ── Security ────────────────────────────────────────
security:
name: Security
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}

- name: Run gosec
uses: securego/gosec@master
with:
args: -fmt=json -stdout -severity=medium ./...
continue-on-error: true

- name: Run govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./... || true

# ── Build ───────────────────────────────────────────
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}

- name: Build binary
run: go build -o bin/auth-service cmd/server/main.go

- name: Verify binary exists
run: test -f bin/auth-service

- name: Build Docker image
if: hashFiles('docker/Dockerfile') != ''
run: docker build -f docker/Dockerfile -t auth-service:ci .
Loading