Skip to content

fix(purchases): correct Savings History chart axis, tooltip decimals, y-axis ticks #189

fix(purchases): correct Savings History chart axis, tooltip decimals, y-axis ticks

fix(purchases): correct Savings History chart axis, tooltip decimals, y-axis ticks #189

Workflow file for this run

# CI Workflow - Build, Test, Lint, Security
#
# This workflow runs on every pull request and push to main/develop branches.
# It performs comprehensive quality checks before allowing code to be merged.
#
# Required GitHub Secrets: None (all checks run without cloud credentials)
#
# Required GitHub Variables:
# - GO_VERSION: Go version to use (default: 1.25)
#
# Triggered by:
# - Pull requests to main/develop
# - Pushes to main/develop
# - Manual workflow dispatch
name: CI - Build & Test
on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
workflow_dispatch:
env:
GO_VERSION: '1.25'
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
jobs:
# Go code linting
lint:
name: Lint Code
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Run golangci-lint
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
with:
version: v2.10.1
args: --timeout=5m
- name: Run go vet
run: go vet ./...
- name: Install gocyclo
run: go install github.com/fzipp/gocyclo/cmd/gocyclo@v0.6.0
- name: Check cyclomatic complexity
run: |
echo "Checking for functions with cyclomatic complexity over 10..."
COMPLEXITY_ISSUES=$(gocyclo -over 10 . 2>&1 || true)
if [ -n "$COMPLEXITY_ISSUES" ]; then
echo "❌ Found functions with cyclomatic complexity over 10:"
echo "$COMPLEXITY_ISSUES"
echo ""
echo "⚠️ Please refactor these functions to reduce complexity."
echo "📖 Tip: Extract helper functions, use early returns, or simplify logic."
exit 1
fi
echo "✅ All functions have acceptable cyclomatic complexity (≤10)"
# Unit tests with race detection
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Download dependencies
run: |
go mod download
go mod verify
- name: Run unit tests
run: |
go test -v -race -short -coverprofile=coverage.out -covermode=atomic ./...
- name: Upload coverage to Codecov
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
files: ./coverage.out
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
- name: Generate coverage report
run: |
go tool cover -html=coverage.out -o coverage.html
- name: Upload coverage artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: coverage-report
path: |
coverage.out
coverage.html
retention-days: 30
- name: Check coverage threshold
run: |
coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
echo "Total coverage: ${coverage}%"
if (( $(echo "$coverage < 80" | bc -l) )); then
echo "::warning::Coverage is below 80% (current: ${coverage}%)"
fi
# Integration tests with real PostgreSQL
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: cudly_test
POSTGRES_USER: cudly_test
POSTGRES_PASSWORD: test_password # CI-only throwaway password — not used in any real environment
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Install golang-migrate
run: |
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@v4.19.1
- name: Run database migrations
env:
DB_HOST: localhost
DB_PORT: 5432
DB_NAME: cudly_test
DB_USER: cudly_test
DB_PASSWORD: test_password # CI-only throwaway password — not used in any real environment
run: |
if [ -d "internal/database/postgres/migrations" ]; then
migrate -path internal/database/postgres/migrations \
-database "postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable" \
up
fi
- name: Run integration tests
env:
DB_HOST: localhost
DB_PORT: 5432
DB_NAME: cudly_test
DB_USER: cudly_test
DB_PASSWORD: test_password # CI-only throwaway password — not used in any real environment
DB_SSL_MODE: disable
run: |
go test -v -race -tags=integration -coverprofile=coverage-integration.out ./...
- name: Upload integration coverage
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: integration-coverage
path: coverage-integration.out
retention-days: 30
# Docker image build test
docker-build:
name: Build Docker Image
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build Docker image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
push: false
tags: cudly:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ github.sha }}
- name: Test Docker image
run: |
docker build -t cudly:test .
docker run --rm cudly:test /app/cudly --version || true
docker run --rm cudly:test /app/cudly --help || true
# Terraform validation for all environments
terraform-validate:
name: Validate Terraform (${{ matrix.cloud }})
runs-on: ubuntu-latest
strategy:
matrix:
cloud: [aws, gcp, azure]
fail-fast: false
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false
- name: Setup Terraform
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
# Must satisfy `required_version = ">= 1.10.0"` declared by every
# terraform/environments/*/main.tf -- pinning below 1.10 makes
# `terraform init`/`validate` abort with "Unsupported Terraform
# Core version". Matches the pin in pre-commit.yml so both
# workflows resolve to the same binary.
terraform_version: 1.10.5
- name: Terraform Format Check
run: terraform fmt -check -recursive terraform/
- name: Terraform Init
run: |
cd terraform/environments/${{ matrix.cloud }}
terraform init -backend=false
- name: Terraform Validate
run: |
cd terraform/environments/${{ matrix.cloud }}
terraform validate
- name: Validate environment tfvars files
run: |
cd terraform/environments/${{ matrix.cloud }}
for env in dev staging prod; do
# Check local tfvars if present
if [ -f "${env}.tfvars" ]; then
echo "Checking ${env}.tfvars syntax..."
terraform fmt -check "${env}.tfvars" || echo "Note: ${env}.tfvars may need formatting"
fi
# Check GitHub CI/CD tfvars
if [ -f "github-${env}.tfvars" ]; then
echo "Checking github-${env}.tfvars syntax..."
terraform fmt -check "github-${env}.tfvars" || echo "Note: github-${env}.tfvars may need formatting"
fi
done
# Security scanning
security-scan:
name: Security Scanning
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ env.GO_VERSION }}
- name: Run govulncheck CVE scanner (all modules)
run: |
# Pinned (not @latest): a govulncheck release with new
# detection logic could silently change the gate's verdict
# between PRs without an intentional bump in this repo.
# Bumping is a deliberate review item, not a drift.
go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
# Multi-module repo: each ./... only walks the current module,
# so scanning the root would silently miss pkg/ and providers/*.
# Walk every module independently and fail on any HIGH/CRITICAL.
set -e
for mod in . pkg providers/aws providers/azure providers/gcp tests/e2e; do
echo "==> govulncheck in $mod"
(cd "$mod" && govulncheck ./...)
done
- name: Run npm audit (frontend)
run: |
if [ -f frontend/package.json ]; then
cd frontend && npm audit --audit-level=high
fi
- name: Run gosec Security Scanner
uses: securego/gosec@4a3bd8af174872c778439083ded7adbf3747e770 # v2.26.1
with:
args: '-fmt sarif -out gosec-results.sarif ./...'
- name: Upload gosec results to GitHub Security
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
sarif_file: gosec-results.sarif
- name: Run Trivy vulnerability scanner (filesystem)
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
sarif_file: 'trivy-results.sarif'
- name: Run tfsec (Terraform Security)
uses: aquasecurity/tfsec-action@b466648d6e39e7c75324f25d83891162a721f2d6 # v1.0.3
with:
working_directory: terraform/
soft_fail: true
# Snyk security scanning
snyk-scan:
name: Snyk Security Scan
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ env.GO_VERSION }}
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/golang@b98d498629f1c368650224d6d212bf7dfa89e4bf # 0.4.0
continue-on-error: true
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
# Docker Compose E2E tests: build the app + test-runner images, bring up
# postgres + cudly-app, then run the black-box suite in tests/e2e against
# the app over HTTP. The test-runner service is profile-gated, so every
# compose invocation needs --profile test (issue #1180).
e2e-tests:
name: E2E Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build images
run: |
docker compose -f docker-compose.test.yml --profile test build
- name: Run E2E tests with docker compose
run: |
docker compose -f docker-compose.test.yml --profile test up --abort-on-container-exit --exit-code-from test-runner
env:
COMPOSE_INTERACTIVE_NO_CLI: 1
- name: Cleanup
if: always()
run: |
docker compose -f docker-compose.test.yml --profile test down -v
# Assert that the Azure custom-role actions list is identical in the TF module
# and the ARM onboarding template. Fast (shell + jq only), so it always runs.
# Path changes that trigger drift will be caught regardless of PR context.
azure-role-parity:
name: Azure role actions parity (ARM vs TF)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false
- name: Assert ARM/TF actions parity
run: bash scripts/check-azure-role-parity.sh
- name: Run parity script self-tests
run: bash scripts/test-azure-role-parity.sh
# Summary job - all checks must pass
ci-success:
name: CI Success
runs-on: ubuntu-latest
needs:
- lint
- unit-tests
- integration-tests
- docker-build
- terraform-validate
- security-scan
- e2e-tests
- azure-role-parity
if: always()
steps:
- name: Check all jobs
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
echo "One or more CI jobs failed"
exit 1
fi
if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more CI jobs were cancelled"
exit 1
fi
echo "All CI checks passed!"
- name: Post status
if: always()
run: |
echo "## CI Status" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ All CI checks completed successfully!" >> $GITHUB_STEP_SUMMARY