Zyntem Fiscalization - CI/CD Pipeline
Company: Zyntem Product: Fiscalization by Zyntem Version: 1.0 Last Updated: 2025-10-30 Status: Active
Overview
This document describes the Continuous Integration and Continuous Deployment (CI/CD) pipeline for Zyntem's Fiscalization product. The pipeline automates testing, building, and deployment to ensure code quality and enable rapid, safe releases.
Pipeline Trigger: Every push to main and all pull requests
Pipeline Architecture
graph LR
A[Push to main / PR] --> B[Lint Job]
B --> C[Test Job]
C --> D[Build Job]
D --> D2[Contract Test Job]
D2 --> E{Branch?}
E -->|main| F[Deploy-Dev Job]
E -->|PR| G[No Deploy]
F --> H[Smoke Tests]
H --> I{Success?}
I -->|Yes| J[✓ Deployed]
I -->|No| K[Rollback & Alert]
Jobs Overview
| Job | Runtime | Triggers | Fail Conditions |
|---|---|---|---|
| Lint | < 2 min | All pushes | Linting errors, Spectral violations |
| Test | < 5 min | After lint passes | Test failures, coverage < 80% |
| Build | < 8 min | After tests pass | Build errors |
| Contract Test | < 3 min | After build passes | API/schema mismatch |
| Deploy-Dev | < 5 min | Push to main only | Deployment errors, smoke test failures |
Total Pipeline Time: ~20 minutes (main branch) Total Pipeline Time: ~15 minutes (PRs, no deployment)
Job Details
1. Lint Job
Purpose: Enforce code style, catch basic errors, and validate the OpenAPI spec
Steps:
-
Go Linting (golangci-lint)
golangci-lint run --config .golangci.yml- Checks: gofmt, govet, ineffassign, unused, staticcheck
- Config:
.golangci.yml
-
TypeScript Linting (ESLint + Prettier)
cd apps/dashboard && npm run lint
cd apps/docs && npm run lint- Checks: ESLint rules, Prettier formatting
- Config:
.eslintrc.js,.prettierrc
-
OpenAPI Linting (Spectral)
npx --yes @stoplight/spectral-cli@latest lint docs/openapi.yaml- Checks: OpenAPI spec against
spectral:oasruleset - Config:
.spectral.yaml
- Checks: OpenAPI spec against
-
Terraform Linting
terraform fmt -check -recursive infrastructure/- Checks: Terraform formatting
Failure Actions:
- Pipeline stops
- PR cannot be merged
- Notification sent (if configured)
2. Test Job
Purpose: Run all unit tests and verify code coverage
Dependencies: Lint job must pass
Services:
- PostgreSQL 15 (GitHub Actions service container)
Steps:
-
Setup Services
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: fiscalization_test
POSTGRES_USER: fiscalization
POSTGRES_PASSWORD: test_password
ports:
- 5433:5432 -
Go Tests
go test -v -cover -coverprofile=coverage.out ./...
go tool cover -func=coverage.out- Minimum coverage: 80%
-
TypeScript Tests
cd apps/dashboard && npm run test:coverage
cd apps/docs && npm test- Minimum coverage: 75%
-
Upload Coverage Reports
- uses: actions/upload-artifact@v3
with:
name: coverage-reports
path: |
coverage.out
apps/dashboard/coverage/
Failure Actions:
- Pipeline stops
- Coverage report available in artifacts
- PR comment added with coverage summary
3. Build Job
Purpose: Build Docker images and verify they start successfully
Dependencies: Test job must pass
Steps:
-
Build Go Services
# Core API
docker build -t ghcr.io/zyntem/fiscalization-api/core-api:sha-${GITHUB_SHA} apps/core-api/
# Adapters (Epic 2+)
docker build -t ghcr.io/zyntem/fiscalization-api/adapter-spain:sha-${GITHUB_SHA} apps/adapter-spain/ -
Build TypeScript Apps
# Dashboard
cd apps/dashboard
npm run build
docker build -t ghcr.io/zyntem/fiscalization-api/dashboard:sha-${GITHUB_SHA} .
# Docs
cd apps/docs
npm run build
docker build -t ghcr.io/zyntem/fiscalization-api/docs:sha-${GITHUB_SHA} . -
Push to GitHub Container Registry
docker push ghcr.io/zyntem/fiscalization-api/core-api:sha-${GITHUB_SHA}
docker push ghcr.io/zyntem/fiscalization-api/dashboard:sha-${GITHUB_SHA}
# ... (all images)
Failure Actions:
- Pipeline stops
- Build logs available
- PR comment added with build errors
4. Deploy-Dev Job
Purpose: Deploy to development environment and run smoke tests
Dependencies: Build job must pass
Trigger: Push to main branch only (not PRs)
Authentication: Workload Identity Federation (keyless)
Steps:
-
Authenticate to GCP
- uses: google-github-actions/auth@v1
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: github-actions@zyntem-dev.iam.gserviceaccount.com -
Deploy to Cloud Run
# Core API
gcloud run deploy core-api \
--image ghcr.io/zyntem/fiscalization-api/core-api:sha-${GITHUB_SHA} \
--region europe-west1 \
--platform managed
# Dashboard
gcloud run deploy dashboard \
--image ghcr.io/zyntem/fiscalization-api/dashboard:sha-${GITHUB_SHA} \
--region europe-west1 \
--platform managed -
Run Smoke Tests
# Health check
curl -f https://core-api-dev-zyntem.run.app/health || exit 1
# Basic API test
curl -f -H "Authorization: Bearer $TEST_API_KEY" \
https://core-api-dev-zyntem.run.app/v1/locations || exit 1 -
Send Notification (optional)
- uses: slackapi/slack-github-action@v1
if: failure()
with:
payload: |
{
"text": "Deployment to dev failed: ${{ github.event.head_commit.message }}"
}
Failure Actions:
- Automatic rollback triggered (Story 1.10)
- Slack notification sent
- Issue created (optional)
5. Contract Test Job
Purpose: Validate API responses against the OpenAPI specification
Dependencies: Build job must pass
Steps:
-
Install Schemathesis
pip install schemathesis -
Run Contract Tests
schemathesis run http://localhost:8080/docs/openapi.yaml \
--checks all \
--max-response-time 10000 \
--hypothesis-max-examples 50 \
--junit-xml schemathesis-report.xml- Phases: examples, coverage, fuzzing
- Tests paths:
/health,/version,/v1/accounts
-
Upload Report
- uses: actions/upload-artifact@v3
with:
name: schemathesis-report
path: schemathesis-report.xml
Failure Actions:
- Pipeline stops
- JUnit XML report available in artifacts
- Indicates API response/schema mismatch
Workload Identity Federation Setup
Purpose: Keyless authentication to GCP (no service account keys)
Setup Steps
-
Create Workload Identity Pool
gcloud iam workload-identity-pools create github-actions-pool \
--location="global" \
--display-name="GitHub Actions Pool"
gcloud iam workload-identity-pools providers create-oidc github-actions-provider \
--location="global" \
--workload-identity-pool="github-actions-pool" \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" \
--attribute-condition="assertion.repository_owner=='yourusername'" -
Create Service Account
gcloud iam service-accounts create github-actions \
--display-name="GitHub Actions Deployment"
# Grant roles
gcloud projects add-iam-policy-binding zyntem-dev \
--member="serviceAccount:github-actions@zyntem-dev.iam.gserviceaccount.com" \
--role="roles/run.admin"
gcloud projects add-iam-policy-binding zyntem-dev \
--member="serviceAccount:github-actions@zyntem-dev.iam.gserviceaccount.com" \
--role="roles/artifactregistry.writer" -
Allow GitHub Actions to Impersonate
gcloud iam service-accounts add-iam-policy-binding \
github-actions@zyntem-dev.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-actions-pool/attribute.repository/yourusername/fiscalization" -
Add GitHub Secrets
GCP_PROJECT_ID: zyntem-devGCP_WORKLOAD_IDENTITY_PROVIDER: projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider
Environment Secrets & Variables
GitHub Secrets
| Secret Name | Description | Example |
|---|---|---|
GCP_PROJECT_ID | GCP project ID | zyntem-dev |
GCP_WORKLOAD_IDENTITY_PROVIDER | Workload Identity Provider | projects/123.../providers/github-actions-provider |
TEST_API_KEY | API key for smoke tests | fsk_test_... |
GitHub Variables
| Variable Name | Description | Example |
|---|---|---|
DEV_CLOUD_RUN_REGION | Cloud Run region | europe-west1 |
Pipeline Status Badge
Add to main README:

Local Testing
Run the same checks locally before pushing:
1. Lint
# Go
golangci-lint run
# TypeScript
npm run lint
2. Test
# All tests
make test
# With coverage
make test-coverage
3. Build
# Build all services
make build
# Or individual services
docker build -t core-api apps/core-api/
Manual Deployment (Fallback)
If CI/CD fails, use manual deployment script:
# Deploy specific service
./scripts/deploy-dev.sh core-api
# Deploy all services
./scripts/deploy-dev.sh all
Script: scripts/deploy-dev.sh
#!/bin/bash
set -e
SERVICE=$1
REGION="europe-west1"
PROJECT="zyntem-dev"
if [ "$SERVICE" == "all" ]; then
SERVICES=("core-api" "dashboard")
else
SERVICES=("$SERVICE")
fi
for svc in "${SERVICES[@]}"; do
echo "Deploying $svc..."
gcloud run deploy $svc \
--source apps/$svc \
--region $REGION \
--project $PROJECT \
--platform managed
done
echo "✓ Deployment complete"
Troubleshooting
Pipeline Fails on Lint
Symptom: golangci-lint: command not found
Solution:
# Install locally
brew install golangci-lint
# Or use Docker
docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:latest golangci-lint run
Tests Pass Locally But Fail in CI
Symptom: Tests fail due to environment differences
Solution:
# Match CI environment with Docker
docker-compose -f docker-compose.test.yml up
Deployment Authentication Fails
Symptom: ERROR: (gcloud.run.deploy) User does not have permission to access project
Solution:
# Verify Workload Identity setup
gcloud iam service-accounts get-iam-policy github-actions@zyntem-dev.iam.gserviceaccount.com
# Check GitHub secrets are set correctly
# Settings → Secrets and variables → Actions
Smoke Tests Fail
Symptom: Health check returns 503
Solution:
# Check Cloud Run logs
gcloud run services logs read core-api --region europe-west1 --limit 50
# Verify service is running
gcloud run services describe core-api --region europe-west1
Future Enhancements (Epic 3+)
- SDK Publishing: Automate SDK releases to NPM, PyPI, etc. (Story 3.3a)
- Performance Testing: Load testing in staging
- Security Scanning: Trivy/Snyk integration
- Deploy to Staging: Separate staging environment