fix(purchases): correct Savings History chart axis, tooltip decimals, y-axis ticks #189
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |