diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index bea9ac2d682..b982c1f942b 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -2,45 +2,101 @@ name: Build Image on: push: - branches: [ master ] + branches: [master, main] paths: - 'build-image/**' - '.github/workflows/build-image.yml' pull_request: - branches: [ master ] + branches: [master, main] paths: - 'build-image/**' - '.github/workflows/build-image.yml' + workflow_dispatch: -permissions: +permissions: contents: read + packages: write + +env: + REGISTRY: quay.io + IMAGE_NAME: cortexproject/build-image jobs: build: - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest + outputs: + image-digest: ${{ steps.build.outputs.digest }} + image-tag: ${{ steps.meta.outputs.tags }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Checkout + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@18ce135bb5112fa8ce4ed6c17ab05699d7f3a5e0 # v3.11.0 + uses: docker/setup-buildx-action@v3 - - name: Save image - run: make save-multiarch-build-image + - name: Login to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.QUAY_REGISTRY_USER }} + password: ${{ secrets.QUAY_REGISTRY_PASSWORD }} - - name: Upload Docker Images Artifacts - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 with: - name: build-image - path: | - ./build-image-amd64.tar - ./build-image-arm64.tar - if-no-files-found: error + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,enable={{is_default_branch}},prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push + id: build + uses: docker/build-push-action@v6 + with: + context: ./build-image + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + # Security features + provenance: true + sbom: true + + - name: Generate attestation + if: github.event_name != 'pull_request' + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true + + # Test the build image + test: + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + + - name: Test build image functionality + run: | + # Test that the build image contains expected tools + docker run --rm ${{ needs.build.outputs.image-tag }} which go + docker run --rm ${{ needs.build.outputs.image-tag }} which make + docker run --rm ${{ needs.build.outputs.image-tag }} which git + + # Test Go version + docker run --rm ${{ needs.build.outputs.image-tag }} go version push: needs: build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..88114560d98 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,566 @@ +name: CI + +on: + push: + branches: [master, main] + tags: + - v[0-9]+.[0-9]+.[0-9]+** + paths-ignore: + - 'build-image/**' + - '.github/workflows/build-image.yml' + - 'docs/**' + - '*.md' + pull_request: + paths-ignore: + - 'build-image/**' + - '.github/workflows/build-image.yml' + - 'docs/**' + - '*.md' + +env: + GO_VERSION: '1.24.0' + REGISTRY: quay.io + IMAGE_PREFIX: quay.io/cortexproject/ + +permissions: + contents: read + security-events: write + actions: read + +jobs: + # Detect changes to optimize what we build/test + changes: + runs-on: ubuntu-latest + outputs: + go: ${{ steps.changes.outputs.go }} + docker: ${{ steps.changes.outputs.docker }} + integration: ${{ steps.changes.outputs.integration }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + go: + - '**/*.go' + - 'go.mod' + - 'go.sum' + - 'Makefile' + - '.golangci.yml' + docker: + - '**/Dockerfile' + - 'build-image/**' + integration: + - 'integration/**' + - 'cmd/**' + + # Modern Go setup with caching + setup-go: + runs-on: ubuntu-latest + needs: changes + if: needs.changes.outputs.go == 'true' + outputs: + cache-key: ${{ steps.cache-key.outputs.key }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Generate cache key + id: cache-key + run: | + echo "key=go-mod-${{ hashFiles('go.sum') }}" >> $GITHUB_OUTPUT + + - name: Download dependencies + run: go mod download + + # Lint and static analysis + lint: + runs-on: ubuntu-latest + needs: [changes, setup-go] + if: needs.changes.outputs.go == 'true' + env: + GO111MODULE: on + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for better analysis + + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + # Modern linting with latest golangci-lint + - name: golangci-lint + uses: golangci/golangci-lint-action@v8 + with: + version: v2.1 + + # Additional security and quality checks + - name: Run gosec security scanner + uses: securego/gosec@master + with: + args: '-no-fail -fmt sarif -out results.sarif ./...' + + - name: Upload SARIF file + if: always() + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: results.sarif + + - name: Check dependencies + run: | + go mod verify + go mod tidy + git diff --exit-code -- go.sum go.mod + + - name: Check generated files + run: | + make check-protos + make check-doc + make check-white-noise + + # Unit tests with coverage - parallelized by package groups + test: + runs-on: ubuntu-latest + needs: [changes, setup-go] + if: needs.changes.outputs.go == 'true' + strategy: + fail-fast: false + matrix: + include: + # Core storage and ingestion (heavy tests) + - name: "storage-ingestion" + packages: "./pkg/storage/... ./pkg/ingester/... ./pkg/chunk/..." + race: true + - name: "storage-ingestion-no-race" + packages: "./pkg/storage/... ./pkg/ingester/... ./pkg/chunk/..." + race: false + + # Query path (heavy tests) + - name: "querier-frontend" + packages: "./pkg/querier/... ./pkg/frontend/... ./pkg/scheduler/..." + race: true + - name: "querier-frontend-no-race" + packages: "./pkg/querier/... ./pkg/frontend/... ./pkg/scheduler/..." + race: false + + # Ruler and alertmanager (moderate tests) + - name: "ruler-alertmanager" + packages: "./pkg/ruler/... ./pkg/alertmanager/..." + race: true + - name: "ruler-alertmanager-no-race" + packages: "./pkg/ruler/... ./pkg/alertmanager/..." + race: false + + # Distributor and ring (moderate tests) + - name: "distributor-ring" + packages: "./pkg/distributor/... ./pkg/ring/... ./pkg/ha/..." + race: true + - name: "distributor-ring-no-race" + packages: "./pkg/distributor/... ./pkg/ring/... ./pkg/ha/..." + race: false + + # Compactor and store gateway (moderate tests) + - name: "compactor-storegateway" + packages: "./pkg/compactor/... ./pkg/storegateway/..." + race: true + - name: "compactor-storegateway-no-race" + packages: "./pkg/compactor/... ./pkg/storegateway/..." + race: false + + # Utilities and smaller packages (lighter tests) + - name: "utilities" + packages: "./pkg/util/... ./pkg/tenant/... ./pkg/tracing/... ./pkg/api/... ./pkg/configs/... ./pkg/cortex/... ./pkg/flusher/... ./pkg/purger/..." + race: true + - name: "utilities-no-race" + packages: "./pkg/util/... ./pkg/tenant/... ./pkg/tracing/... ./pkg/api/... ./pkg/configs/... ./pkg/cortex/... ./pkg/flusher/... ./pkg/purger/..." + race: false + + # Command line tools and remaining packages + - name: "cmd-tools" + packages: "./cmd/... ./tools/... ./pkg/cortexpb/... ./pkg/engine/... ./pkg/parquetconverter/... ./pkg/querysharding/... ./pkg/testexporter/..." + race: true + - name: "cmd-tools-no-race" + packages: "./cmd/... ./tools/... ./pkg/cortexpb/... ./pkg/engine/... ./pkg/parquetconverter/... ./pkg/querysharding/... ./pkg/testexporter/..." + race: false + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Run tests + env: + RACE_FLAG: ${{ matrix.race && '-race' || '' }} + COVERAGE_FILE: ${{ matrix.race && 'coverage' || 'coverage-no-race' }}-${{ matrix.name }}.out + run: | + echo "Running tests for: ${{ matrix.name }}" + echo "Packages: ${{ matrix.packages }}" + echo "Race detection: ${{ matrix.race }}" + + # Create coverage directory + mkdir -p coverage + + # Run tests with appropriate flags + go test $RACE_FLAG \ + -coverprofile=coverage/$COVERAGE_FILE \ + -covermode=atomic \ + -timeout=30m \ + -v \ + ${{ matrix.packages }} + + - name: Upload coverage to Codecov + if: matrix.race == true + uses: codecov/codecov-action@v4 + with: + file: coverage/coverage-${{ matrix.name }}.out + flags: ${{ matrix.name }} + name: ${{ matrix.name }} + fail_ci_if_error: false + + - name: Upload coverage artifacts + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ matrix.name }}-${{ matrix.race && 'race' || 'no-race' }} + path: coverage/ + retention-days: 1 + + # Combine coverage reports + coverage: + runs-on: ubuntu-latest + needs: test + if: always() && needs.test.result == 'success' + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Download all coverage artifacts + uses: actions/download-artifact@v4 + with: + pattern: coverage-*-race + path: coverage-reports + merge-multiple: true + + - name: Install gocovmerge + run: go install github.com/wadey/gocovmerge@latest + + - name: Merge coverage reports + run: | + find coverage-reports -name "coverage-*.out" -exec echo "Found: {}" \; + gocovmerge coverage-reports/coverage-*.out > merged-coverage.out + + - name: Upload merged coverage + uses: codecov/codecov-action@v4 + with: + file: merged-coverage.out + name: merged-coverage + fail_ci_if_error: false + + - name: Generate coverage HTML + run: | + go tool cover -html=merged-coverage.out -o coverage.html + + - name: Upload coverage HTML + uses: actions/upload-artifact@v4 + with: + name: coverage-html + path: coverage.html + + # Build Docker images efficiently + build: + runs-on: ubuntu-latest + needs: [changes] + if: always() + outputs: + image-tag: ${{ steps.meta.outputs.tags }} + image-digest: ${{ steps.build.outputs.digest }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + # Modern Docker setup + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # Login for pushing (only on main/tags) + - name: Login to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.QUAY_REGISTRY_USER }} + password: ${{ secrets.QUAY_REGISTRY_PASSWORD }} + + # Generate metadata + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_PREFIX }}cortex + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + # Build Go binaries + - name: Build binaries + run: | + make BUILD_IN_CONTAINER=false + + # Build and push multi-arch images + - name: Build and push + id: build + uses: docker/build-push-action@v6 + with: + context: cmd/cortex + platforms: linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + # Modern build features + provenance: true + sbom: true + + # Upload binaries as artifacts + - name: Upload binaries + uses: actions/upload-artifact@v4 + with: + name: binaries + path: dist/ + retention-days: 7 + + # Integration tests with better parallelization + integration: + runs-on: ubuntu-latest + needs: [changes, build] + if: needs.changes.outputs.integration == 'true' || needs.changes.outputs.go == 'true' + strategy: + fail-fast: false + matrix: + suite: + - name: docker + tags: requires_docker + - name: alertmanager + tags: integration_alertmanager + - name: backward-compatibility + tags: integration_backward_compatibility + - name: memberlist + tags: integration_memberlist + - name: querier + tags: integration_querier + - name: ruler + tags: integration_ruler + - name: query-fuzz + tags: integration_query_fuzz + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Pre-pull common images to avoid timeouts + - name: Pre-pull Docker images + run: | + docker pull minio/minio:RELEASE.2024-05-28T17-19-04Z & + docker pull consul:1.8.4 & + docker pull gcr.io/etcd-development/etcd:v3.4.7 & + docker pull memcached:1.6.1 & + docker pull redis:7.0.4-alpine & + + # Suite-specific images + if [[ "${{ matrix.suite.name }}" == "backward-compatibility" ]]; then + for version in v1.13.1 v1.13.2 v1.14.0 v1.14.1 v1.15.0 v1.15.1 v1.15.2 v1.15.3 v1.16.0 v1.16.1 v1.17.0 v1.17.1 v1.18.0 v1.18.1; do + docker pull quay.io/cortexproject/cortex:$version & + done + elif [[ "${{ matrix.suite.name }}" == "query-fuzz" ]]; then + docker pull quay.io/cortexproject/cortex:v1.18.1 & + docker pull quay.io/prometheus/prometheus:v2.51.0 & + docker pull quay.io/prometheus/prometheus:v2.55.1 & + fi + + wait # Wait for all background pulls + + - name: Run integration tests + env: + CORTEX_IMAGE_PREFIX: ${{ env.IMAGE_PREFIX }} + IMAGE_TAG: ${{ needs.build.outputs.image-tag }} + run: | + export CORTEX_IMAGE="${CORTEX_IMAGE_PREFIX}cortex:${IMAGE_TAG}" + echo "Running integration tests with image: $CORTEX_IMAGE" + go test -tags=integration,${{ matrix.suite.tags }} -timeout 40m -v -count=1 ./integration/... + + # Database integration tests + integration-configs-db: + runs-on: ubuntu-latest + needs: [changes, build] + if: needs.changes.outputs.integration == 'true' || needs.changes.outputs.go == 'true' + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: configs_test + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Run database integration tests + env: + DB_ADDR: localhost:5432 + DB_USER: postgres + DB_PASSWORD: postgres + MIGRATIONS_DIR: ${{ github.workspace }}/cmd/cortex/migrations + run: | + go test -v -tags 'netgo integration' -timeout 10m ./pkg/configs/... ./pkg/ruler/... + + # Security scanning + security: + runs-on: ubuntu-latest + needs: changes + if: needs.changes.outputs.go == 'true' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # CodeQL analysis + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: go + queries: security-extended,security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:go" + + # Dependency scanning + dependency-review: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + - uses: actions/dependency-review-action@v4 + with: + fail-on-severity: moderate + + # Website deployment (only on main branch) + deploy-website: + runs-on: ubuntu-latest + needs: [build, coverage] + if: github.ref == 'refs/heads/master' && github.repository == 'cortexproject/cortex' + steps: + - uses: actions/checkout@v4 + with: + ssh-key: ${{ secrets.WEBSITE_DEPLOY_SSH_PRIVATE_KEY }} + submodules: recursive + + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Setup Hugo + uses: peaceiris/actions-hugo@v3 + with: + hugo-version: 'latest' + extended: true + + - name: Build website + run: | + cd website + HUGO_ENV=production hugo --config config.toml --minify -v + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + deploy_key: ${{ secrets.WEBSITE_DEPLOY_SSH_PRIVATE_KEY }} + publish_dir: ./website/public + external_repository: cortexproject/cortexproject.github.io + publish_branch: master + + # Container image deployment + # Container image deployment + deploy: + runs-on: ubuntu-latest + needs: [build, coverage, lint, integration, integration-configs-db] + if: (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')) && github.repository == 'cortexproject/cortex' + steps: + - uses: actions/checkout@v4 + + - name: Login to additional registries + if: env.DOCKER_REGISTRY_PASSWORD != '' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_REGISTRY_USER }} + password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }} + env: + DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }} + + - name: Multi-registry push + if: needs.build.outputs.image-digest != '' + run: | + # Images are already built and pushed to Quay in build job + # Additional registry pushes can be added here if needed + echo "Image successfully deployed: ${{ needs.build.outputs.image-tag }}" + echo "Digest: ${{ needs.build.outputs.image-digest }}" + + # Generate SLSA provenance +# provenance: +# needs: [build, deploy] +# if: startsWith(github.ref, 'refs/tags/') +# permissions: +# actions: read +# id-token: write +# contents: write +# uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0 +# with: +# image: ${{ needs.build.outputs.image-tag }} +# digest: ${{ needs.build.outputs.image-digest }} +# secrets: +# registry-username: ${{ secrets.QUAY_REGISTRY_USER }} +# registry-password: ${{ secrets.QUAY_REGISTRY_PASSWORD }} diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml new file mode 100644 index 00000000000..180712607aa --- /dev/null +++ b/.github/workflows/performance.yml @@ -0,0 +1,238 @@ +name: Performance & Benchmarks + +on: + push: + branches: [master] + paths: + - '**/*.go' + - 'go.mod' + - 'go.sum' + pull_request: + paths: + - '**/*.go' + - 'go.mod' + - 'go.sum' + schedule: + # Run weekly performance tests + - cron: '0 3 * * 1' + workflow_dispatch: + inputs: + benchmark_filter: + description: 'Benchmark filter pattern' + required: false + default: '.' + +permissions: + contents: read + pull-requests: write + +jobs: + # Go benchmarks + benchmarks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 # Need previous commit for comparison + + - uses: actions/setup-go@v5 + with: + go-version: '1.24.0' + cache: true + + - name: Run benchmarks + run: | + go test -bench="${{ github.event.inputs.benchmark_filter || '.' }}" \ + -benchmem -count=5 -timeout=30m ./... | tee benchmark-results.txt + + - name: Store benchmark result + uses: benchmark-action/github-action-benchmark@v1 + with: + tool: 'go' + output-file-path: benchmark-results.txt + github-token: ${{ secrets.GITHUB_TOKEN }} + auto-push: ${{ github.event_name != 'pull_request' }} + comment-on-alert: true + alert-threshold: '150%' + fail-on-alert: false + + - name: Upload benchmark results + uses: actions/upload-artifact@v4 + with: + name: benchmark-results + path: benchmark-results.txt + + # Memory profiling + memory-profile: + runs-on: ubuntu-latest + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.24.0' + cache: true + + - name: Install pprof tools + run: | + go install github.com/google/pprof@latest + + - name: Run memory profiling tests + run: | + # Run specific memory-intensive tests with profiling + go test -memprofile=mem.prof -run=TestMemoryIntensive ./... + + # Generate memory profile report + go tool pprof -text mem.prof > memory-profile.txt || true + + - name: Upload memory profile + uses: actions/upload-artifact@v4 + with: + name: memory-profile + path: memory-profile.txt + + # Load testing with k6 (if applicable) + load-test: + runs-on: ubuntu-latest + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + services: + cortex: + image: quay.io/cortexproject/cortex:latest + ports: + - 9009:9009 + options: >- + --health-cmd "wget --no-verbose --tries=1 --spider http://localhost:9009/ready || exit 1" + --health-interval 30s + --health-timeout 10s + --health-retries 5 + steps: + - uses: actions/checkout@v4 + + - name: Setup k6 + uses: grafana/setup-k6-action@v1 + + - name: Run load tests + if: hashFiles('tests/load/*.js') != '' + run: | + k6 run tests/load/basic-load-test.js || echo "Load test completed" + + - name: Upload load test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: load-test-results + path: | + summary.json + result.html + + # Binary size tracking + binary-size: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - uses: actions/setup-go@v5 + with: + go-version: '1.24.0' + cache: true + + - name: Build binaries + run: | + make dist + + - name: Check binary sizes + run: | + echo "Binary sizes:" > binary-sizes.txt + for binary in dist/cortex-*; do + size=$(stat --format="%s" "$binary") + echo "$(basename "$binary"): ${size} bytes" >> binary-sizes.txt + done + cat binary-sizes.txt + + - name: Compare with previous version + if: github.event_name == 'pull_request' + run: | + # Get previous commit binary sizes for comparison + git checkout HEAD~1 + make dist + echo "Previous binary sizes:" > prev-binary-sizes.txt + for binary in dist/cortex-*; do + size=$(stat --format="%s" "$binary") + echo "$(basename "$binary"): ${size} bytes" >> prev-binary-sizes.txt + done + + # Compare sizes + echo "Size comparison:" > size-comparison.txt + # Add comparison logic here + + - name: Upload size reports + uses: actions/upload-artifact@v4 + with: + name: binary-size-report + path: | + binary-sizes.txt + prev-binary-sizes.txt + size-comparison.txt + + # Performance regression detection + performance-regression: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version: '1.24.0' + cache: true + + - name: Install benchstat + run: go install golang.org/x/perf/cmd/benchstat@latest + + - name: Run benchmarks for base commit + run: | + git checkout ${{ github.event.pull_request.base.sha }} + go test -bench=. -count=10 -benchmem ./... > base-bench.txt + + - name: Run benchmarks for PR commit + run: | + git checkout ${{ github.event.pull_request.head.sha }} + go test -bench=. -count=10 -benchmem ./... > pr-bench.txt + + - name: Compare benchmarks + run: | + benchstat base-bench.txt pr-bench.txt > bench-comparison.txt + cat bench-comparison.txt + + - name: Comment PR with benchmark results + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + try { + const comparison = fs.readFileSync('bench-comparison.txt', 'utf8'); + const comment = ` + ## 📊 Benchmark Comparison + + \`\`\` + ${comparison} + \`\`\` + + > This compares the performance of the base branch with your changes. + > Significant regressions are highlighted above. + `; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + } catch (error) { + console.log('No benchmark comparison available'); + } \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000000..17e058a2e04 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,328 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g., v1.2.3)' + required: true + prerelease: + description: 'Mark as pre-release' + required: false + default: false + type: boolean + +permissions: + contents: write + packages: write + attestations: write + id-token: write + +env: + REGISTRY: quay.io + IMAGE_PREFIX: quay.io/cortexproject/ + +jobs: + # Validate release + validate: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + is-prerelease: ${{ steps.version.outputs.is-prerelease }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Determine version + id: version + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + VERSION="${{ github.event.inputs.version }}" + IS_PRERELEASE="${{ github.event.inputs.prerelease }}" + else + VERSION=${GITHUB_REF#refs/tags/} + # Check if it's a pre-release (contains alpha, beta, rc) + if [[ $VERSION =~ (alpha|beta|rc) ]]; then + IS_PRERELEASE=true + else + IS_PRERELEASE=false + fi + fi + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "is-prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT + echo "Releasing version: $VERSION (prerelease: $IS_PRERELEASE)" + + - name: Validate version format + run: | + if [[ ! "${{ steps.version.outputs.version }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "Invalid version format. Expected: vX.Y.Z" + exit 1 + fi + + # Build release artifacts + build: + runs-on: ubuntu-latest + needs: validate + strategy: + matrix: + include: + - os: linux + arch: amd64 + - os: linux + arch: arm64 + - os: darwin + arch: amd64 + - os: darwin + arch: arm64 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version: '1.24.0' + cache: true + + - name: Build binary + env: + VERSION: ${{ needs.validate.outputs.version }} + GOOS: ${{ matrix.os }} + GOARCH: ${{ matrix.arch }} + run: | + CGO_ENABLED=0 go build \ + -ldflags "-X main.Branch=${GITHUB_REF#refs/heads/} -X main.Revision=${GITHUB_SHA} -X main.Version=${VERSION} -extldflags '-static' -s -w" \ + -tags netgo \ + -o cortex-${GOOS}-${GOARCH} \ + ./cmd/cortex + + # Create archive + if [[ "${{ matrix.os }}" == "windows" ]]; then + zip cortex-${VERSION}-${GOOS}-${GOARCH}.zip cortex-${GOOS}-${GOARCH} + else + tar -czf cortex-${VERSION}-${GOOS}-${GOARCH}.tar.gz cortex-${GOOS}-${GOARCH} + fi + + # Generate checksums + if [[ "${{ matrix.os }}" == "windows" ]]; then + sha256sum cortex-${VERSION}-${GOOS}-${GOARCH}.zip > cortex-${VERSION}-${GOOS}-${GOARCH}.zip.sha256 + else + sha256sum cortex-${VERSION}-${GOOS}-${GOARCH}.tar.gz > cortex-${VERSION}-${GOOS}-${GOARCH}.tar.gz.sha256 + fi + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: cortex-${{ matrix.os }}-${{ matrix.arch }} + path: | + cortex-${{ needs.validate.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}.* + + # Build and push container images + container: + runs-on: ubuntu-latest + needs: validate + outputs: + image-digest: ${{ steps.build.outputs.digest }} + image-uri: ${{ steps.build.outputs.image-uri }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.QUAY_REGISTRY_USER }} + password: ${{ secrets.QUAY_REGISTRY_PASSWORD }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_PREFIX }}cortex + tags: | + type=semver,pattern={{version}},value=${{ needs.validate.outputs.version }} + type=semver,pattern={{major}}.{{minor}},value=${{ needs.validate.outputs.version }} + type=semver,pattern={{major}},value=${{ needs.validate.outputs.version }} + + - name: Build and push + id: build + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + # Enhanced security features + provenance: true + sbom: true + build-args: | + VERSION=${{ needs.validate.outputs.version }} + GIT_REVISION=${{ github.sha }} + + - name: Generate attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.IMAGE_PREFIX }}cortex + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true + + # Build packages (DEB/RPM) + packages: + runs-on: ubuntu-latest + needs: [validate, build] + strategy: + matrix: + arch: [amd64, arm64] + steps: + - uses: actions/checkout@v4 + + - name: Download binary artifacts + uses: actions/download-artifact@v4 + with: + name: cortex-linux-${{ matrix.arch }} + + - name: Install packaging tools + run: | + sudo apt-get update + sudo apt-get install -y rpm + gem install fpm + + - name: Create packages + env: + VERSION: ${{ needs.validate.outputs.version }} + ARCH: ${{ matrix.arch }} + run: | + # Prepare binary + chmod +x cortex-linux-${ARCH} + mkdir -p package-root/usr/local/bin + cp cortex-linux-${ARCH} package-root/usr/local/bin/cortex + + # Create DEB package + fpm -s dir -t deb -n cortex -v ${VERSION#v} \ + --architecture ${ARCH} \ + --description "Cortex: A horizontally scalable, highly available, multi-tenant, long term Prometheus." \ + --url "https://github.com/cortexproject/cortex" \ + --license "Apache-2.0" \ + --maintainer "Cortex Team " \ + --after-install packaging/deb/control/postinst \ + --before-remove packaging/deb/control/prerm \ + --config-files /etc/cortex/single-process-config.yaml \ + --config-files /etc/default/cortex \ + -C package-root \ + usr/local/bin/cortex=/usr/local/bin/cortex \ + docs/configuration/single-process-config-blocks.yaml=/etc/cortex/single-process-config.yaml \ + packaging/deb/default/cortex=/etc/default/cortex \ + packaging/deb/systemd/cortex.service=/etc/systemd/system/cortex.service + + # Create RPM package + fpm -s dir -t rpm -n cortex -v ${VERSION#v} \ + --architecture ${ARCH} \ + --description "Cortex: A horizontally scalable, highly available, multi-tenant, long term Prometheus." \ + --url "https://github.com/cortexproject/cortex" \ + --license "Apache-2.0" \ + --maintainer "Cortex Team " \ + --after-install packaging/rpm/control/post \ + --before-remove packaging/rpm/control/preun \ + --config-files /etc/cortex/single-process-config.yaml \ + --config-files /etc/sysconfig/cortex \ + -C package-root \ + usr/local/bin/cortex=/usr/local/bin/cortex \ + docs/configuration/single-process-config-blocks.yaml=/etc/cortex/single-process-config.yaml \ + packaging/rpm/sysconfig/cortex=/etc/sysconfig/cortex \ + packaging/rpm/systemd/cortex.service=/etc/systemd/system/cortex.service + + - name: Upload packages + uses: actions/upload-artifact@v4 + with: + name: packages-${{ matrix.arch }} + path: | + *.deb + *.rpm + + # Create GitHub release + release: + runs-on: ubuntu-latest + needs: [validate, build, container, packages] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Prepare release assets + run: | + mkdir -p release-assets + find artifacts -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.sha256" -o -name "*.deb" -o -name "*.rpm" \) \ + -exec cp {} release-assets/ \; + + - name: Generate release notes + id: release-notes + run: | + # Generate changelog since last tag + PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -v "${{ needs.validate.outputs.version }}" | head -n1) + if [[ -n "$PREVIOUS_TAG" ]]; then + echo "## Changes since $PREVIOUS_TAG" > release-notes.md + git log --pretty=format:"- %s (%h)" ${PREVIOUS_TAG}..${{ github.sha }} >> release-notes.md + else + echo "## Initial Release" > release-notes.md + fi + + # Add container image info + echo "" >> release-notes.md + echo "## Container Images" >> release-notes.md + echo "- \`${{ env.IMAGE_PREFIX }}cortex:${{ needs.validate.outputs.version }}\`" >> release-notes.md + echo "- \`${{ env.IMAGE_PREFIX }}cortex:latest\`" >> release-notes.md + echo "" >> release-notes.md + echo "**Digest:** \`${{ needs.container.outputs.image-digest }}\`" >> release-notes.md + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.validate.outputs.version }} + name: Release ${{ needs.validate.outputs.version }} + body_path: release-notes.md + draft: false + prerelease: ${{ needs.validate.outputs.is-prerelease == 'true' }} + files: release-assets/* + generate_release_notes: true + + - name: Update latest tag + if: needs.validate.outputs.is-prerelease == 'false' + run: | + git tag -f latest + git push -f origin latest + + # SLSA provenance + provenance: + needs: [validate, container] + permissions: + actions: read + id-token: write + contents: write + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0 + with: + image: ${{ needs.container.outputs.image-uri }} + digest: ${{ needs.container.outputs.image-digest }} + secrets: + registry-username: ${{ secrets.QUAY_REGISTRY_USER }} + registry-password: ${{ secrets.QUAY_REGISTRY_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000000..17297409cf4 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,163 @@ +name: Security & Dependencies + +on: + schedule: + # Run daily at 6 AM UTC + - cron: '0 6 * * *' + workflow_dispatch: + push: + branches: [master] + paths: + - 'go.mod' + - 'go.sum' + - '.github/workflows/security.yml' + +permissions: + contents: read + security-events: write + pull-requests: write + +jobs: + # Vulnerability scanning + trivy-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner in repo mode + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-results.sarif' + + # Container image scanning (if images exist) + container-scan: + runs-on: ubuntu-latest + if: github.event_name != 'schedule' # Skip for scheduled runs to avoid rate limits + steps: + - uses: actions/checkout@v4 + + - name: Build image for scanning + run: | + docker build -t cortex-scan:latest . + + - name: Run Trivy vulnerability scanner on image + uses: aquasecurity/trivy-action@master + with: + image-ref: 'cortex-scan:latest' + format: 'sarif' + output: 'trivy-image-results.sarif' + + - name: Upload image scan results + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-image-results.sarif' + + # Dependency updates (Dependabot alternative for more control) + dependency-update: + runs-on: ubuntu-latest + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/setup-go@v5 + with: + go-version: '1.24.0' + cache: true + + - name: Update Go dependencies + run: | + go get -u ./... + go mod tidy + + - name: Check for changes + id: changes + run: | + if git diff --quiet go.mod go.sum; then + echo "changed=false" >> $GITHUB_OUTPUT + else + echo "changed=true" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.changes.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore: update Go dependencies' + title: 'chore: update Go dependencies' + body: | + This PR updates Go dependencies to their latest versions. + + Changes: + - Updated Go module dependencies + - Ran `go mod tidy` to clean up go.sum + + Please review the changes and ensure all tests pass. + branch: chore/update-dependencies + delete-branch: true + + # License compliance check + license-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.24.0' + cache: true + + - name: Install go-licenses + run: go install github.com/google/go-licenses@latest + + - name: Check licenses + run: | + go-licenses check ./... + + - name: Generate license report + run: | + go-licenses report ./... > license-report.txt + + - name: Upload license report + uses: actions/upload-artifact@v4 + with: + name: license-report + path: license-report.txt + + # Supply chain security with OpenSSF Scorecard + scorecard: + runs-on: ubuntu-latest + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + permissions: + security-events: write + id-token: write + contents: read + actions: read + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Run analysis + uses: ossf/scorecard-action@v2.4.0 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + - name: Upload SARIF results + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif \ No newline at end of file diff --git a/Makefile b/Makefile index 705e005dac1..24580ed2f1d 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ VERSION=$(shell cat "./VERSION" 2> /dev/null) GOPROXY_VALUE=$(shell go env GOPROXY) # ARCHS -ARCHS = amd64 arm64 +ARCHS = arm64 # Boiler plate for building Docker containers. # All this must go at top of file I'm afraid. @@ -320,14 +320,14 @@ dist: dist/$(UPTODATE) dist/$(UPTODATE): rm -fr ./dist mkdir -p ./dist - for os in linux darwin; do \ - for arch in amd64 arm64; do \ + for os in linux ; do \ + for arch in arm64; do \ echo "Building Cortex for $$os/$$arch"; \ GOOS=$$os GOARCH=$$arch CGO_ENABLED=0 go build $(GO_FLAGS) -o ./dist/cortex-$$os-$$arch ./cmd/cortex; \ sha256sum ./dist/cortex-$$os-$$arch | cut -d ' ' -f 1 > ./dist/cortex-$$os-$$arch-sha-256; \ - echo "Building query-tee for $$os/$$arch"; \ - GOOS=$$os GOARCH=$$arch CGO_ENABLED=0 go build $(GO_FLAGS) -o ./dist/query-tee-$$os-$$arch ./cmd/query-tee; \ - sha256sum ./dist/query-tee-$$os-$$arch | cut -d ' ' -f 1 > ./dist/query-tee-$$os-$$arch-sha-256; \ +# echo "Building query-tee for $$os/$$arch"; \ +# GOOS=$$os GOARCH=$$arch CGO_ENABLED=0 go build $(GO_FLAGS) -o ./dist/query-tee-$$os-$$arch ./cmd/query-tee; \ +# sha256sum ./dist/query-tee-$$os-$$arch | cut -d ' ' -f 1 > ./dist/query-tee-$$os-$$arch-sha-256; \ done; \ done; \ touch $@ diff --git a/integration/e2ecortex/client.go b/integration/e2ecortex/client.go index 9067b60c078..81acd3fc450 100644 --- a/integration/e2ecortex/client.go +++ b/integration/e2ecortex/client.go @@ -40,7 +40,7 @@ var ErrNotFound = errors.New("not found") var DefaultFilter = RuleFilter{} -// Client is a client used to interact with Cortex in integration tests +// Client is a client used to interact with Cortex in integration tests. type Client struct { alertmanagerClient promapi.Client querierAddress string