V2 django #85
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
| name: CI | |
| on: | |
| push: | |
| branches: [main, develop] | |
| tags: ['v*'] | |
| pull_request: | |
| branches: [main, develop] | |
| workflow_dispatch: | |
| inputs: | |
| create_release: | |
| description: 'Create a release' | |
| type: boolean | |
| default: false | |
| version: | |
| description: 'Release version (e.g., 1.2.3)' | |
| type: string | |
| required: false | |
| prerelease: | |
| description: 'Mark as pre-release' | |
| type: boolean | |
| default: false | |
| draft: | |
| description: 'Create as draft release' | |
| type: boolean | |
| default: true | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| GO_VERSION: '1.24' | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # =========================================================================== | |
| # Stage 1: Path Detection (runs in parallel with linting) | |
| # =========================================================================== | |
| detect-changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| python: ${{ steps.filter.outputs.python }} | |
| frontend: ${{ steps.filter.outputs.frontend }} | |
| go: ${{ steps.filter.outputs.go }} | |
| docker: ${{ steps.filter.outputs.docker }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dorny/paths-filter@v3 | |
| id: filter | |
| with: | |
| filters: | | |
| python: | |
| - 'skyspy_django/**' | |
| - 'skyspy_common/**' | |
| - 'requirements*.txt' | |
| - 'pyproject.toml' | |
| frontend: | |
| - 'web/**' | |
| - '!web/e2e/**' | |
| - '!web/playwright-report/**' | |
| - '!web/test-results/**' | |
| go: | |
| - 'skyspy-go/**' | |
| docker: | |
| - 'Dockerfile' | |
| - 'docker-compose*.yaml' | |
| - 'docker-compose*.yml' | |
| # =========================================================================== | |
| # Python: Lint & Tests | |
| # =========================================================================== | |
| lint-python: | |
| name: Python Lint | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.python == 'true' | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: astral-sh/setup-uv@v5 | |
| - run: uv tool run ruff check --output-format=github . | |
| - run: uv tool run ruff format --check . | |
| test-python-unit: | |
| name: Python Tests | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, lint-python] | |
| if: needs.detect-changes.outputs.python == 'true' | |
| timeout-minutes: 10 | |
| services: | |
| postgres: | |
| image: postgres:16-alpine | |
| env: | |
| POSTGRES_USER: adsb | |
| POSTGRES_PASSWORD: adsb | |
| POSTGRES_DB: adsb_test | |
| options: --health-cmd pg_isready --health-interval 5s --health-timeout 3s --health-retries 5 | |
| ports: ['5432:5432'] | |
| redis: | |
| image: redis:7-alpine | |
| options: --health-cmd "redis-cli ping" --health-interval 5s --health-timeout 3s --health-retries 5 | |
| ports: ['6379:6379'] | |
| env: | |
| DJANGO_SETTINGS_MODULE: skyspy.tests.test_settings | |
| DJANGO_SECRET_KEY: test-secret-key-for-ci | |
| DATABASE_URL: postgresql://adsb:adsb@localhost:5432/adsb_test | |
| REDIS_URL: redis://localhost:6379/0 | |
| FEEDER_LAT: '48.6969' | |
| FEEDER_LON: '-122.6767' | |
| SAFETY_MONITORING_ENABLED: 'true' | |
| PHOTO_CACHE_ENABLED: 'false' | |
| S3_ENABLED: 'false' | |
| WHISPER_ENABLED: 'false' | |
| TRANSCRIPTION_ENABLED: 'false' | |
| OPENSKY_DB_ENABLED: 'false' | |
| LLM_ENABLED: 'false' | |
| PROMETHEUS_ENABLED: 'false' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - uses: astral-sh/setup-uv@v5 | |
| - uses: actions/cache@v4 | |
| id: venv-cache | |
| with: | |
| path: skyspy_django/.venv | |
| key: venv-${{ runner.os }}-${{ hashFiles('skyspy_django/requirements.txt', 'skyspy_common/pyproject.toml') }} | |
| - name: Install dependencies | |
| working-directory: skyspy_django | |
| if: steps.venv-cache.outputs.cache-hit != 'true' | |
| run: | | |
| uv venv | |
| uv pip install -r requirements.txt | |
| uv pip install pytest pytest-cov pytest-django pytest-asyncio | |
| uv pip install -e ../skyspy_common | |
| - name: Run tests | |
| working-directory: skyspy_django | |
| run: | | |
| source .venv/bin/activate | |
| python manage.py migrate --noinput | |
| pytest -v --tb=short \ | |
| --ignore=skyspy/tests/e2e \ | |
| --ignore=skyspy/tests/integration \ | |
| --cov=skyspy --cov-report=xml --cov-fail-under=40 # TODO: Investigate coverage drop and restore to 80 | |
| - uses: codecov/codecov-action@v5 | |
| with: | |
| files: skyspy_django/coverage.xml | |
| flags: python-unit | |
| fail_ci_if_error: false | |
| env: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| # =========================================================================== | |
| # Go: Lint & Tests | |
| # =========================================================================== | |
| lint-go: | |
| name: Go Lint | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.go == 'true' | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: '1.24' | |
| cache-dependency-path: skyspy-go/go.sum | |
| - uses: golangci/golangci-lint-action@v6 | |
| with: | |
| version: latest | |
| working-directory: skyspy-go | |
| - name: Check gofmt | |
| working-directory: skyspy-go | |
| run: test -z "$(gofmt -l .)" | |
| test-go: | |
| name: Go Tests | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, lint-go] | |
| if: needs.detect-changes.outputs.go == 'true' | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: '1.24' | |
| cache-dependency-path: skyspy-go/go.sum | |
| - name: Run tests | |
| working-directory: skyspy-go | |
| run: | | |
| go test -v -race -timeout 5m -coverprofile=coverage.out -covermode=atomic ./... | |
| COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') | |
| echo "Coverage: ${COVERAGE}%" | |
| (( $(echo "$COVERAGE >= 70" | bc -l) )) || exit 1 | |
| - uses: codecov/codecov-action@v5 | |
| with: | |
| files: skyspy-go/coverage.out | |
| flags: go | |
| fail_ci_if_error: false | |
| env: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| # =========================================================================== | |
| # Frontend: Lint & Tests | |
| # =========================================================================== | |
| lint-frontend: | |
| name: Frontend Lint | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.frontend == 'true' | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| cache-dependency-path: web/package-lock.json | |
| - run: npm ci | |
| working-directory: web | |
| - run: npm run lint && npm run format:check | |
| working-directory: web | |
| test-frontend-unit: | |
| name: Frontend Tests | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, lint-frontend] | |
| if: needs.detect-changes.outputs.frontend == 'true' | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| cache-dependency-path: web/package-lock.json | |
| - run: npm ci | |
| working-directory: web | |
| - name: Run tests | |
| run: npm run test:unit:coverage | |
| working-directory: web | |
| - uses: codecov/codecov-action@v5 | |
| with: | |
| files: web/coverage/coverage-final.json | |
| flags: frontend | |
| fail_ci_if_error: false | |
| env: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| # =========================================================================== | |
| # Security | |
| # =========================================================================== | |
| security-python: | |
| name: Python Security | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.python == 'true' | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: astral-sh/setup-uv@v5 | |
| - name: Run security scans | |
| run: | | |
| uv tool run bandit -r skyspy_django skyspy_common -x "*/tests/*,*/test_*" -ll -ii | |
| grep -v '^-e ' skyspy_django/requirements.txt > /tmp/requirements-audit.txt | |
| uv tool run pip-audit --requirement /tmp/requirements-audit.txt --ignore-vuln PYSEC-2024-48 | |
| security-frontend: | |
| name: Frontend Security | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.frontend == 'true' | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| cache-dependency-path: web/package-lock.json | |
| - run: npm ci && npm audit --audit-level=high | |
| working-directory: web | |
| security-go: | |
| name: Go Security | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.go == 'true' | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: '1.24' | |
| cache-dependency-path: skyspy-go/go.sum | |
| - name: Install govulncheck | |
| run: go install golang.org/x/vuln/cmd/govulncheck@latest | |
| - name: Run govulncheck | |
| working-directory: skyspy-go | |
| run: govulncheck ./... | |
| snyk-python: | |
| name: Snyk Python | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.python == 'true' | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - uses: snyk/actions/setup@master | |
| - name: Snyk test | |
| working-directory: skyspy_django | |
| env: | |
| SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} | |
| run: | | |
| if [[ -n "$SNYK_TOKEN" ]]; then | |
| snyk test --file=requirements.txt --severity-threshold=high || true | |
| else | |
| echo "SNYK_TOKEN not set, skipping Snyk scan" | |
| fi | |
| continue-on-error: true | |
| snyk-frontend: | |
| name: Snyk Frontend | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.frontend == 'true' | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| cache-dependency-path: web/package-lock.json | |
| - uses: snyk/actions/setup@master | |
| - name: Install dependencies | |
| working-directory: web | |
| run: npm ci | |
| - name: Snyk test | |
| working-directory: web | |
| env: | |
| SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} | |
| run: | | |
| if [[ -n "$SNYK_TOKEN" ]]; then | |
| snyk test --severity-threshold=high || true | |
| else | |
| echo "SNYK_TOKEN not set, skipping Snyk scan" | |
| fi | |
| continue-on-error: true | |
| snyk-go: | |
| name: Snyk Go | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.go == 'true' | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache-dependency-path: skyspy-go/go.sum | |
| - uses: snyk/actions/setup@master | |
| - name: Snyk test | |
| working-directory: skyspy-go | |
| env: | |
| SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} | |
| run: | | |
| if [[ -n "$SNYK_TOKEN" ]]; then | |
| snyk test --severity-threshold=high || true | |
| else | |
| echo "SNYK_TOKEN not set, skipping Snyk scan" | |
| fi | |
| continue-on-error: true | |
| snyk-docker: | |
| name: Snyk Docker | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, docker-build-push] | |
| if: | | |
| always() && | |
| needs.docker-build-push.result == 'success' | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: snyk/actions/setup@master | |
| - name: Login to registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Snyk container test | |
| env: | |
| SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} | |
| run: | | |
| if [[ -n "$SNYK_TOKEN" ]]; then | |
| # Get tag based on ref | |
| if [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| TAG="${{ github.ref_name }}" | |
| elif [[ "${{ github.ref }}" == refs/heads/* ]]; then | |
| TAG="${{ github.ref_name }}" | |
| else | |
| TAG="sha-$(echo ${{ github.sha }} | cut -c1-7)" | |
| fi | |
| IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG}" | |
| echo "Scanning ${IMAGE}..." | |
| snyk container test "$IMAGE" --severity-threshold=high || true | |
| else | |
| echo "SNYK_TOKEN not set, skipping Snyk container scan" | |
| fi | |
| continue-on-error: true | |
| codeql: | |
| name: CodeQL Analysis | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.python == 'true' || needs.detect-changes.outputs.go == 'true' || needs.detect-changes.outputs.frontend == 'true' | |
| timeout-minutes: 15 | |
| permissions: | |
| security-events: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| language: [python, javascript-typescript] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: github/codeql-action/init@v3 | |
| with: | |
| languages: ${{ matrix.language }} | |
| - uses: github/codeql-action/autobuild@v3 | |
| - uses: github/codeql-action/analyze@v3 | |
| with: | |
| category: "/language:${{ matrix.language }}" | |
| # =========================================================================== | |
| # Build | |
| # =========================================================================== | |
| build-frontend: | |
| name: Build Frontend | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, lint-frontend, test-frontend-unit, security-frontend] | |
| if: | | |
| needs.detect-changes.outputs.frontend == 'true' && | |
| needs.lint-frontend.result == 'success' && | |
| needs.test-frontend-unit.result == 'success' && | |
| needs.security-frontend.result == 'success' | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| cache-dependency-path: web/package-lock.json | |
| - run: npm ci && npm run build | |
| working-directory: web | |
| # =========================================================================== | |
| # Stage 3: Integration Tests | |
| # =========================================================================== | |
| test-python-integration: | |
| name: Python Integration Tests | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, test-python-unit] | |
| if: needs.detect-changes.outputs.python == 'true' | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/setup-buildx-action@v3 | |
| - name: Cache Docker layers | |
| uses: actions/cache@v4 | |
| with: | |
| path: /tmp/.buildx-cache | |
| key: buildx-integration-${{ runner.os }}-${{ hashFiles('Dockerfile', 'skyspy_django/requirements.txt', 'skyspy_common/pyproject.toml') }} | |
| restore-keys: | | |
| buildx-integration-${{ runner.os }}- | |
| - name: Cache service images | |
| uses: actions/cache@v4 | |
| with: | |
| path: /tmp/docker-images | |
| key: docker-images-integration-${{ runner.os }}-v1 | |
| restore-keys: | | |
| docker-images-integration-${{ runner.os }}- | |
| - name: Load cached images | |
| run: | | |
| if [[ -d /tmp/docker-images ]]; then | |
| for img in /tmp/docker-images/*.tar; do | |
| if [[ -f "$img" ]]; then | |
| echo "Loading $(basename $img)..." | |
| docker load -i "$img" || true | |
| fi | |
| done | |
| fi | |
| - name: Pull and cache service images | |
| run: | | |
| mkdir -p /tmp/docker-images | |
| IMAGES=( | |
| "postgres:16-alpine" | |
| "redis:7-alpine" | |
| "efrecon/pgbouncer:1.23.1" | |
| ) | |
| for img in "${IMAGES[@]}"; do | |
| FILENAME=$(echo "$img" | tr '/:' '_').tar | |
| if [[ ! -f "/tmp/docker-images/$FILENAME" ]]; then | |
| echo "Pulling and saving $img..." | |
| docker pull "$img" | |
| docker save "$img" -o "/tmp/docker-images/$FILENAME" | |
| else | |
| echo "Using cached $img" | |
| fi | |
| done | |
| - name: Create test env | |
| run: | | |
| cat > .env.test << 'EOF' | |
| POSTGRES_USER=adsb | |
| POSTGRES_PASSWORD=adsb | |
| POSTGRES_DB=adsb_test | |
| DJANGO_SECRET_KEY=test-secret-key-for-ci | |
| DJANGO_SETTINGS_MODULE=skyspy.tests.test_settings | |
| DEBUG=False | |
| ALLOWED_HOSTS=* | |
| DATABASE_URL=postgresql://adsb:adsb@pgbouncer:5432/adsb_test | |
| REDIS_URL=redis://redis:6379/0 | |
| ULTRAFEEDER_HOST=ultrafeeder | |
| ULTRAFEEDER_PORT=80 | |
| DUMP978_HOST=dump978 | |
| DUMP978_PORT=80 | |
| FEEDER_LAT=48.6969 | |
| FEEDER_LON=-122.6767 | |
| POLLING_INTERVAL=2 | |
| DB_STORE_INTERVAL=5 | |
| SESSION_TIMEOUT_MINUTES=30 | |
| SAFETY_MONITORING_ENABLED=true | |
| ACARS_ENABLED=true | |
| ACARS_PORT=5555 | |
| VDLM2_PORT=5556 | |
| OPENSKY_DB_ENABLED=false | |
| PHOTO_CACHE_ENABLED=false | |
| S3_ENABLED=false | |
| WHISPER_ENABLED=false | |
| TRANSCRIPTION_ENABLED=false | |
| LLM_ENABLED=false | |
| PROMETHEUS_ENABLED=false | |
| PYTHONDONTWRITEBYTECODE=1 | |
| PYTHONUNBUFFERED=1 | |
| MOCK_ENABLED=true | |
| MOCK_INTERVAL_MIN=1.0 | |
| MOCK_INTERVAL_MAX=2.0 | |
| STATION_ID=CI-TEST-STATION | |
| EOF | |
| - name: Build and run tests | |
| run: | | |
| mkdir -p test-results | |
| chmod 777 test-results | |
| rm -f test-results/.coverage test-results/coverage.xml | |
| # Build with layer caching | |
| docker buildx bake -f docker-compose.test.yaml \ | |
| --set "*.cache-from=type=gha" \ | |
| --set "*.cache-from=type=local,src=/tmp/.buildx-cache" \ | |
| --set "*.cache-to=type=gha,mode=max" \ | |
| --set "*.cache-to=type=local,dest=/tmp/.buildx-cache-new,mode=max" \ | |
| --load | |
| docker compose -f docker-compose.test.yaml --env-file .env.test up -d --wait postgres pgbouncer redis ultrafeeder dump978 | |
| docker compose -f docker-compose.test.yaml --env-file .env.test --profile test run --rm --user root api-test | |
| env: | |
| DOCKER_BUILDKIT: 1 | |
| - name: Update cache | |
| if: always() | |
| run: | | |
| rm -rf /tmp/.buildx-cache | |
| if [[ -d /tmp/.buildx-cache-new ]]; then | |
| mv /tmp/.buildx-cache-new /tmp/.buildx-cache | |
| fi | |
| - uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: integration-test-results | |
| path: test-results/ | |
| retention-days: 7 | |
| - uses: codecov/codecov-action@v5 | |
| if: success() | |
| with: | |
| files: test-results/coverage.xml | |
| flags: python-integration | |
| fail_ci_if_error: false | |
| env: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| - if: always() | |
| run: docker compose -f docker-compose.test.yaml --env-file .env.test down -v | |
| # =========================================================================== | |
| # Stage 4: E2E Tests | |
| # =========================================================================== | |
| test-e2e: | |
| name: E2E Tests | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, test-python-integration, build-frontend] | |
| if: needs.detect-changes.outputs.python == 'true' || needs.detect-changes.outputs.frontend == 'true' | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/setup-buildx-action@v3 | |
| - name: Create test env | |
| run: | | |
| cat > .env.test << 'EOF' | |
| POSTGRES_USER=adsb | |
| POSTGRES_PASSWORD=adsb | |
| POSTGRES_DB=adsb_test | |
| DJANGO_SECRET_KEY=test-secret-key-for-ci | |
| DJANGO_SETTINGS_MODULE=skyspy.settings | |
| DEBUG=False | |
| ALLOWED_HOSTS=* | |
| DATABASE_URL=postgresql://adsb:adsb@pgbouncer:5432/adsb_test | |
| REDIS_URL=redis://redis:6379/0 | |
| ULTRAFEEDER_HOST=ultrafeeder | |
| ULTRAFEEDER_PORT=80 | |
| DUMP978_HOST=dump978 | |
| DUMP978_PORT=80 | |
| FEEDER_LAT=48.6969 | |
| FEEDER_LON=-122.6767 | |
| POLLING_INTERVAL=2 | |
| DB_STORE_INTERVAL=5 | |
| SESSION_TIMEOUT_MINUTES=30 | |
| SAFETY_MONITORING_ENABLED=true | |
| ACARS_ENABLED=true | |
| ACARS_PORT=5555 | |
| VDLM2_PORT=5556 | |
| OPENSKY_DB_ENABLED=false | |
| PHOTO_CACHE_ENABLED=false | |
| S3_ENABLED=false | |
| WHISPER_ENABLED=false | |
| TRANSCRIPTION_ENABLED=false | |
| LLM_ENABLED=false | |
| PROMETHEUS_ENABLED=false | |
| PYTHONDONTWRITEBYTECODE=1 | |
| PYTHONUNBUFFERED=1 | |
| MOCK_ENABLED=true | |
| MOCK_INTERVAL_MIN=1.0 | |
| MOCK_INTERVAL_MAX=2.0 | |
| STATION_ID=CI-TEST-STATION | |
| EOF | |
| - name: Start backend | |
| run: | | |
| docker buildx bake -f docker-compose.test.yaml \ | |
| --set "*.cache-from=type=gha" \ | |
| --set "*.cache-to=type=gha,mode=max" \ | |
| --load | |
| docker compose -f docker-compose.test.yaml --env-file .env.test --profile dev up -d --wait | |
| env: | |
| DOCKER_BUILDKIT: 1 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| cache-dependency-path: web/package-lock.json | |
| - run: npm ci | |
| working-directory: web | |
| - uses: actions/cache@v4 | |
| id: playwright-cache | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-${{ runner.os }}-${{ hashFiles('web/package-lock.json') }} | |
| - name: Install Playwright | |
| working-directory: web | |
| run: npx playwright install --with-deps chromium | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| - name: Install Playwright deps | |
| working-directory: web | |
| run: npx playwright install-deps chromium | |
| if: steps.playwright-cache.outputs.cache-hit == 'true' | |
| - name: Run E2E tests | |
| uses: nick-fields/retry@v3 | |
| with: | |
| timeout_minutes: 10 | |
| max_attempts: 2 | |
| command: cd web && npm run test:e2e | |
| env: | |
| CI: true | |
| - uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: playwright-report | |
| path: web/playwright-report/ | |
| retention-days: 7 | |
| - uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: e2e-test-results | |
| path: web/test-results/ | |
| retention-days: 7 | |
| - if: always() | |
| run: docker compose -f docker-compose.test.yaml --env-file .env.test down -v | |
| # =========================================================================== | |
| # Stage 5: ReadMe Sync (each job has its own concurrency group) | |
| # =========================================================================== | |
| export-openapi: | |
| name: Sync API to ReadMe | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, docker-build-push] | |
| if: | | |
| always() && | |
| github.event_name == 'push' && | |
| needs.docker-build-push.result == 'success' && | |
| (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) | |
| timeout-minutes: 10 | |
| concurrency: | |
| group: readme-api-sync | |
| cancel-in-progress: false | |
| services: | |
| postgres: | |
| image: postgres:16-alpine | |
| env: | |
| POSTGRES_USER: adsb | |
| POSTGRES_PASSWORD: adsb | |
| POSTGRES_DB: adsb_test | |
| options: --health-cmd pg_isready --health-interval 5s --health-timeout 3s --health-retries 5 | |
| ports: ['5432:5432'] | |
| redis: | |
| image: redis:7-alpine | |
| options: --health-cmd "redis-cli ping" --health-interval 5s --health-timeout 3s --health-retries 5 | |
| ports: ['6379:6379'] | |
| env: | |
| DJANGO_SETTINGS_MODULE: skyspy.settings | |
| DJANGO_SECRET_KEY: openapi-export-key | |
| DATABASE_URL: postgresql://adsb:adsb@localhost:5432/adsb_test | |
| REDIS_URL: redis://localhost:6379/0 | |
| FEEDER_LAT: '48.6969' | |
| FEEDER_LON: '-122.6767' | |
| SAFETY_MONITORING_ENABLED: 'false' | |
| PHOTO_CACHE_ENABLED: 'false' | |
| S3_ENABLED: 'false' | |
| WHISPER_ENABLED: 'false' | |
| OPENSKY_DB_ENABLED: 'false' | |
| LLM_ENABLED: 'false' | |
| PROMETHEUS_ENABLED: 'false' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - uses: astral-sh/setup-uv@v5 | |
| - name: Install dependencies | |
| working-directory: skyspy_django | |
| run: | | |
| uv venv | |
| uv pip install -r requirements.txt | |
| uv pip install -e ../skyspy_common | |
| - name: Export OpenAPI schema | |
| working-directory: skyspy_django | |
| run: | | |
| source .venv/bin/activate | |
| python manage.py migrate --noinput | |
| python manage.py spectacular --file openapi.json --validate | |
| - name: Get version | |
| id: version | |
| run: | | |
| if [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| VERSION="${{ github.ref_name }}" | |
| VERSION="${VERSION#v}" | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "is_release=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "version=" >> $GITHUB_OUTPUT | |
| echo "is_release=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Upload to ReadMe (release) | |
| if: steps.version.outputs.is_release == 'true' | |
| uses: readmeio/rdme@v8 | |
| with: | |
| rdme: openapi upload skyspy_django/openapi.json --key=${{ secrets.README_API_KEY }} --slug=skyspy-api --version=${{ steps.version.outputs.version }} --create | |
| - name: Upload to ReadMe (main) | |
| if: steps.version.outputs.is_release != 'true' | |
| uses: readmeio/rdme@v8 | |
| with: | |
| rdme: openapi upload skyspy_django/openapi.json --key=${{ secrets.README_API_KEY }} --slug=skyspy-api --confirm-overwrite | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: openapi-schema | |
| path: skyspy_django/openapi.json | |
| retention-days: 30 | |
| sync-docs: | |
| name: Sync Docs to ReadMe | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, docker-build-push] | |
| if: | | |
| always() && | |
| github.event_name == 'push' && | |
| needs.docker-build-push.result == 'success' && | |
| (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) | |
| timeout-minutes: 5 | |
| concurrency: | |
| group: readme-docs-sync | |
| cancel-in-progress: false | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Get version | |
| id: version | |
| run: | | |
| if [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| VERSION="${{ github.ref_name }}" | |
| VERSION="${VERSION#v}" | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "is_release=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "version=" >> $GITHUB_OUTPUT | |
| echo "is_release=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Sync documentation to ReadMe (release) | |
| if: steps.version.outputs.is_release == 'true' | |
| uses: readmeio/rdme@v8 | |
| with: | |
| rdme: docs upload docs --key=${{ secrets.README_API_KEY }} --version=${{ steps.version.outputs.version }} | |
| - name: Sync documentation to ReadMe (main) | |
| if: steps.version.outputs.is_release != 'true' | |
| uses: readmeio/rdme@v8 | |
| with: | |
| rdme: docs upload docs --key=${{ secrets.README_API_KEY }} | |
| # =========================================================================== | |
| # Stage 6: Docker Build & Push | |
| # =========================================================================== | |
| docker-build-push: | |
| name: Build ${{ matrix.name }} | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, lint-python, lint-go, lint-frontend, security-python, security-frontend, security-go, test-python-unit, test-go, test-python-integration, test-e2e] | |
| if: | | |
| always() && | |
| (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && | |
| (needs.lint-python.result == 'success' || needs.lint-python.result == 'skipped') && | |
| (needs.lint-go.result == 'success' || needs.lint-go.result == 'skipped') && | |
| (needs.lint-frontend.result == 'success' || needs.lint-frontend.result == 'skipped') && | |
| (needs.security-python.result == 'success' || needs.security-python.result == 'skipped') && | |
| (needs.security-frontend.result == 'success' || needs.security-frontend.result == 'skipped') && | |
| (needs.security-go.result == 'success' || needs.security-go.result == 'skipped') && | |
| (needs.test-python-unit.result == 'success' || needs.test-python-unit.result == 'skipped') && | |
| (needs.test-go.result == 'success' || needs.test-go.result == 'skipped') && | |
| (needs.test-python-integration.result == 'success' || needs.test-python-integration.result == 'skipped') && | |
| (needs.test-e2e.result == 'success' || needs.test-e2e.result == 'skipped') | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| attestations: write | |
| strategy: | |
| fail-fast: true | |
| matrix: | |
| include: | |
| - name: API | |
| dockerfile: Dockerfile | |
| image-suffix: "" | |
| context: . | |
| - name: rtl-airband-uploader | |
| dockerfile: rtl-airband-uploader/Dockerfile | |
| image-suffix: -rtl-airband-uploader | |
| context: rtl-airband-uploader | |
| - name: 1090 Mock | |
| dockerfile: test/mock-1090/Dockerfile | |
| image-suffix: -1090-mock | |
| context: test/mock-1090 | |
| - name: ACARS Mock | |
| dockerfile: test/acars-mock/Dockerfile | |
| image-suffix: -acarshub-mock | |
| context: test/acars-mock | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/setup-qemu-action@v3 | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - uses: docker/metadata-action@v5 | |
| id: meta | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.image-suffix }} | |
| tags: | | |
| type=ref,event=branch | |
| type=ref,event=pr | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=sha,prefix={{branch}}- | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| - uses: docker/build-push-action@v6 | |
| id: build | |
| with: | |
| context: ${{ matrix.context }} | |
| file: ${{ matrix.dockerfile }} | |
| 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 | |
| sbom: true | |
| provenance: true | |
| - uses: actions/attest-build-provenance@v2 | |
| with: | |
| subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.image-suffix }} | |
| subject-digest: ${{ steps.build.outputs.digest }} | |
| push-to-registry: true | |
| # =========================================================================== | |
| # Stage 7: Sign Docker Images | |
| # =========================================================================== | |
| sign-docker-images: | |
| name: Sign Docker Images | |
| runs-on: ubuntu-latest | |
| needs: [docker-build-push] | |
| if: | | |
| always() && | |
| needs.docker-build-push.result == 'success' && | |
| (github.event_name == 'push' || github.event_name == 'workflow_dispatch') | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: API | |
| image-suffix: "" | |
| - name: rtl-airband-uploader | |
| image-suffix: -rtl-airband-uploader | |
| - name: 1090 Mock | |
| image-suffix: -1090-mock | |
| - name: ACARS Mock | |
| image-suffix: -acarshub-mock | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install signing tools | |
| run: | | |
| # Install step-cli | |
| wget -q https://dl.smallstep.com/gh-release/cli/gh-release-header/v0.27.2/step-cli_0.27.2_amd64.deb | |
| sudo dpkg -i step-cli_0.27.2_amd64.deb | |
| rm step-cli_0.27.2_amd64.deb | |
| # Install notation | |
| NOTATION_VERSION="1.2.0" | |
| wget -q "https://github.com/notaryproject/notation/releases/download/v${NOTATION_VERSION}/notation_${NOTATION_VERSION}_linux_amd64.tar.gz" | |
| tar -xzf "notation_${NOTATION_VERSION}_linux_amd64.tar.gz" | |
| sudo mv notation /usr/local/bin/ | |
| rm "notation_${NOTATION_VERSION}_linux_amd64.tar.gz" | |
| - name: Login to registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate signing certificate | |
| id: cert | |
| env: | |
| STEP_CA_JWK: ${{ secrets.STEP_CA_JWK }} | |
| run: | | |
| mkdir -p $HOME/.config/notation/signing-keys | |
| CERT_DIR=$(mktemp -d) | |
| echo "cert_dir=$CERT_DIR" >> $GITHUB_OUTPUT | |
| # Read CA config from repo | |
| CA_CONFIG=".github/signing/step-ca.json" | |
| STEP_CA_URL=$(jq -r '."ca-url"' "$CA_CONFIG") | |
| STEP_CA_FINGERPRINT=$(jq -r '.fingerprint' "$CA_CONFIG") | |
| PROVISIONER_NAME=$(jq -r '.provisioner.name' "$CA_CONFIG") | |
| # Check if CA is configured (not placeholder values) and JWK secret exists | |
| if [[ -n "$STEP_CA_JWK" && "$STEP_CA_URL" != "https://ca.example.com" && "$STEP_CA_FINGERPRINT" != "REPLACE_WITH_YOUR_CA_FINGERPRINT" ]]; then | |
| echo "Using Step CA for certificate generation" | |
| echo "signing_method=ca" >> $GITHUB_OUTPUT | |
| # Write JWK to temp file | |
| echo "$STEP_CA_JWK" > "$CERT_DIR/provisioner.jwk" | |
| # Get certificate from Step CA | |
| step ca certificate \ | |
| "skyspy-ci@github.com" \ | |
| "$CERT_DIR/signing.crt" \ | |
| "$CERT_DIR/signing.key" \ | |
| --ca-url "$STEP_CA_URL" \ | |
| --fingerprint "$STEP_CA_FINGERPRINT" \ | |
| --provisioner "$PROVISIONER_NAME" \ | |
| --provisioner-password-file "$CERT_DIR/provisioner.jwk" \ | |
| --not-after 24h \ | |
| --force | |
| else | |
| echo "Using ephemeral self-signed certificate" | |
| echo "signing_method=ephemeral" >> $GITHUB_OUTPUT | |
| # Create ephemeral self-signed certificate | |
| step certificate create \ | |
| "skyspy-ci@github.com" \ | |
| "$CERT_DIR/signing.crt" \ | |
| "$CERT_DIR/signing.key" \ | |
| --profile self-signed \ | |
| --subtle \ | |
| --not-after 24h \ | |
| --no-password \ | |
| --insecure \ | |
| --kty EC \ | |
| --curve P-256 \ | |
| --force | |
| fi | |
| # Set restrictive permissions | |
| chmod 600 "$CERT_DIR/signing.key" | |
| chmod 644 "$CERT_DIR/signing.crt" | |
| - name: Configure notation | |
| env: | |
| CERT_DIR: ${{ steps.cert.outputs.cert_dir }} | |
| run: | | |
| # Add signing key to notation | |
| notation key add skyspy-signing \ | |
| --plugin-config "certificatePath=$CERT_DIR/signing.crt" \ | |
| --id "$CERT_DIR/signing.key" \ | |
| --default | |
| # Configure trust policy for verification | |
| mkdir -p $HOME/.config/notation/trustpolicy | |
| cat > $HOME/.config/notation/trustpolicy/trustpolicy.json << 'EOF' | |
| { | |
| "version": "1.0", | |
| "trustPolicies": [ | |
| { | |
| "name": "skyspy-images", | |
| "registryScopes": ["ghcr.io/${{ github.repository }}*"], | |
| "signatureVerification": { | |
| "level": "strict" | |
| }, | |
| "trustStores": ["ca:skyspy"], | |
| "trustedIdentities": ["*"] | |
| } | |
| ] | |
| } | |
| EOF | |
| - name: Get image digest | |
| id: digest | |
| run: | | |
| IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.image-suffix }}" | |
| # Get tag based on ref | |
| if [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| TAG="${{ github.ref_name }}" | |
| elif [[ "${{ github.ref }}" == refs/heads/* ]]; then | |
| TAG="${{ github.ref_name }}" | |
| else | |
| TAG="sha-$(echo ${{ github.sha }} | cut -c1-7)" | |
| fi | |
| # Get the digest for the image | |
| DIGEST=$(docker buildx imagetools inspect "${IMAGE}:${TAG}" --format '{{.Manifest.Digest}}' 2>/dev/null || true) | |
| if [[ -z "$DIGEST" ]]; then | |
| echo "Could not get digest for ${IMAGE}:${TAG}, trying latest" | |
| DIGEST=$(docker buildx imagetools inspect "${IMAGE}:latest" --format '{{.Manifest.Digest}}' 2>/dev/null || true) | |
| TAG="latest" | |
| fi | |
| echo "image=${IMAGE}" >> $GITHUB_OUTPUT | |
| echo "tag=${TAG}" >> $GITHUB_OUTPUT | |
| echo "digest=${DIGEST}" >> $GITHUB_OUTPUT | |
| echo "full_ref=${IMAGE}@${DIGEST}" >> $GITHUB_OUTPUT | |
| - name: Sign Docker image | |
| if: steps.digest.outputs.digest != '' | |
| env: | |
| CERT_DIR: ${{ steps.cert.outputs.cert_dir }} | |
| run: | | |
| echo "Signing ${{ steps.digest.outputs.full_ref }}" | |
| # Sign using notation with the certificate | |
| notation sign \ | |
| "${{ steps.digest.outputs.full_ref }}" \ | |
| --key "$CERT_DIR/signing.key" \ | |
| --signature-format cose \ | |
| --timestamp-url "http://timestamp.digicert.com" || \ | |
| notation sign \ | |
| "${{ steps.digest.outputs.full_ref }}" \ | |
| --key "$CERT_DIR/signing.key" \ | |
| --signature-format cose | |
| echo "Successfully signed ${{ matrix.name }} image" | |
| - name: Verify signature | |
| if: steps.digest.outputs.digest != '' | |
| env: | |
| CERT_DIR: ${{ steps.cert.outputs.cert_dir }} | |
| run: | | |
| # Add certificate to trust store for verification | |
| mkdir -p $HOME/.config/notation/truststore/x509/ca/skyspy | |
| cp "$CERT_DIR/signing.crt" $HOME/.config/notation/truststore/x509/ca/skyspy/ | |
| notation verify "${{ steps.digest.outputs.full_ref }}" || echo "Verification skipped (self-signed cert)" | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| rm -rf "${{ steps.cert.outputs.cert_dir }}" | |
| # =========================================================================== | |
| # Stage 8: Build Go Binaries (Matrix) | |
| # =========================================================================== | |
| go-version: | |
| name: Go Version | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, test-go, security-go] | |
| if: | | |
| always() && | |
| (needs.test-go.result == 'success' || needs.test-go.result == 'skipped') && | |
| (needs.security-go.result == 'success' || needs.security-go.result == 'skipped') | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| steps: | |
| - name: Get version | |
| id: version | |
| run: | | |
| if [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| VERSION="${{ github.ref_name }}" | |
| else | |
| VERSION="v0.0.0-$(date +%Y%m%d)-$(echo ${{ github.sha }} | cut -c1-7)" | |
| fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| build-go-binaries: | |
| name: Build Go (${{ matrix.goos }}/${{ matrix.goarch }}) | |
| runs-on: ubuntu-latest | |
| needs: [go-version] | |
| if: needs.go-version.result == 'success' | |
| timeout-minutes: 10 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - goos: linux | |
| goarch: amd64 | |
| - goos: linux | |
| goarch: arm64 | |
| - goos: darwin | |
| goarch: amd64 | |
| - goos: darwin | |
| goarch: arm64 | |
| - goos: windows | |
| goarch: amd64 | |
| ext: .exe | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache-dependency-path: skyspy-go/go.sum | |
| - name: Build binary | |
| working-directory: skyspy-go | |
| env: | |
| VERSION: ${{ needs.go-version.outputs.version }} | |
| CGO_ENABLED: "0" | |
| GOOS: ${{ matrix.goos }} | |
| GOARCH: ${{ matrix.goarch }} | |
| run: | | |
| BINARY_NAME="skyspy-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }}" | |
| LDFLAGS="-s -w -X main.version=${VERSION} -X main.commit=${{ github.sha }} -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| echo "Building ${BINARY_NAME}..." | |
| go build \ | |
| -ldflags "$LDFLAGS" \ | |
| -trimpath \ | |
| -o "$BINARY_NAME" \ | |
| ./cmd/skyspy | |
| ls -la "$BINARY_NAME" | |
| - name: Upload binary | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: go-binary-${{ matrix.goos }}-${{ matrix.goarch }} | |
| path: skyspy-go/skyspy-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }} | |
| retention-days: 7 | |
| # =========================================================================== | |
| # Stage 9: Sign Go Binaries (Matrix) | |
| # =========================================================================== | |
| sign-go-binaries: | |
| name: Sign Go (${{ matrix.goos }}/${{ matrix.goarch }}) | |
| runs-on: ubuntu-latest | |
| needs: [go-version, build-go-binaries] | |
| if: needs.build-go-binaries.result == 'success' | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - goos: linux | |
| goarch: amd64 | |
| - goos: linux | |
| goarch: arm64 | |
| - goos: darwin | |
| goarch: amd64 | |
| - goos: darwin | |
| goarch: arm64 | |
| - goos: windows | |
| goarch: amd64 | |
| ext: .exe | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download binary | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: go-binary-${{ matrix.goos }}-${{ matrix.goarch }} | |
| path: dist/ | |
| - name: Install step-cli | |
| run: | | |
| wget -q https://dl.smallstep.com/gh-release/cli/gh-release-header/v0.27.2/step-cli_0.27.2_amd64.deb | |
| sudo dpkg -i step-cli_0.27.2_amd64.deb | |
| rm step-cli_0.27.2_amd64.deb | |
| - name: Generate signing certificate | |
| id: cert | |
| env: | |
| STEP_CA_JWK: ${{ secrets.STEP_CA_JWK }} | |
| run: | | |
| CERT_DIR=$(mktemp -d) | |
| echo "cert_dir=$CERT_DIR" >> $GITHUB_OUTPUT | |
| # Read CA config from repo | |
| CA_CONFIG=".github/signing/step-ca.json" | |
| STEP_CA_URL=$(jq -r '."ca-url"' "$CA_CONFIG") | |
| STEP_CA_FINGERPRINT=$(jq -r '.fingerprint' "$CA_CONFIG") | |
| PROVISIONER_NAME=$(jq -r '.provisioner.name' "$CA_CONFIG") | |
| # Check if CA is configured (not placeholder values) and JWK secret exists | |
| if [[ -n "$STEP_CA_JWK" && "$STEP_CA_URL" != "https://ca.example.com" && "$STEP_CA_FINGERPRINT" != "REPLACE_WITH_YOUR_CA_FINGERPRINT" ]]; then | |
| echo "Using Step CA for certificate generation" | |
| echo "signing_method=ca" >> $GITHUB_OUTPUT | |
| echo "$STEP_CA_JWK" > "$CERT_DIR/provisioner.jwk" | |
| step ca certificate \ | |
| "skyspy-ci@github.com" \ | |
| "$CERT_DIR/signing.crt" \ | |
| "$CERT_DIR/signing.key" \ | |
| --ca-url "$STEP_CA_URL" \ | |
| --fingerprint "$STEP_CA_FINGERPRINT" \ | |
| --provisioner "$PROVISIONER_NAME" \ | |
| --provisioner-password-file "$CERT_DIR/provisioner.jwk" \ | |
| --not-after 24h \ | |
| --force | |
| else | |
| echo "Using ephemeral self-signed certificate" | |
| echo "signing_method=ephemeral" >> $GITHUB_OUTPUT | |
| step certificate create \ | |
| "skyspy-ci@github.com" \ | |
| "$CERT_DIR/signing.crt" \ | |
| "$CERT_DIR/signing.key" \ | |
| --profile self-signed \ | |
| --subtle \ | |
| --not-after 24h \ | |
| --no-password \ | |
| --insecure \ | |
| --kty EC \ | |
| --curve P-256 \ | |
| --force | |
| fi | |
| chmod 600 "$CERT_DIR/signing.key" | |
| chmod 644 "$CERT_DIR/signing.crt" | |
| - name: Sign binary | |
| env: | |
| CERT_DIR: ${{ steps.cert.outputs.cert_dir }} | |
| run: | | |
| cd dist | |
| BINARY="skyspy-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }}" | |
| echo "Signing ${BINARY}..." | |
| openssl dgst -sha256 -sign "$CERT_DIR/signing.key" -out "${BINARY}.sig" "$BINARY" | |
| # Export certificate | |
| cp "$CERT_DIR/signing.crt" signing-cert.pem | |
| ls -la | |
| - name: Upload signed binary | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: go-signed-${{ matrix.goos }}-${{ matrix.goarch }} | |
| path: | | |
| dist/skyspy-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }} | |
| dist/skyspy-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }}.sig | |
| dist/signing-cert.pem | |
| retention-days: 7 | |
| - name: Cleanup | |
| if: always() | |
| run: rm -rf "${{ steps.cert.outputs.cert_dir }}" | |
| # =========================================================================== | |
| # Stage 10: Package Go Binaries | |
| # =========================================================================== | |
| package-go-binaries: | |
| name: Package Go Binaries | |
| runs-on: ubuntu-latest | |
| needs: [go-version, sign-go-binaries] | |
| if: needs.sign-go-binaries.result == 'success' | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download all signed binaries | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: go-signed-* | |
| path: dist/ | |
| merge-multiple: true | |
| - name: Create checksums and archives | |
| env: | |
| VERSION: ${{ needs.go-version.outputs.version }} | |
| run: | | |
| cd dist | |
| ls -la | |
| # Create verification instructions | |
| cat > VERIFY.md << 'EOF' | |
| # Verifying Binary Signatures | |
| ## Prerequisites | |
| - OpenSSL installed | |
| ## Verification Steps | |
| 1. Verify checksums: | |
| ```bash | |
| sha256sum -c checksums.txt | |
| ``` | |
| 2. Verify binary signature: | |
| ```bash | |
| openssl dgst -sha256 -verify signing-cert.pem -signature <binary>.sig <binary> | |
| ``` | |
| 3. Verify checksums signature: | |
| ```bash | |
| openssl dgst -sha256 -verify signing-cert.pem -signature checksums.txt.sig checksums.txt | |
| ``` | |
| ## Certificate Info | |
| View certificate details: | |
| ```bash | |
| openssl x509 -in signing-cert.pem -text -noout | |
| ``` | |
| EOF | |
| # Generate checksums for binaries | |
| sha256sum skyspy-* | grep -v '\.sig$' > checksums.txt | |
| echo "Checksums:" | |
| cat checksums.txt | |
| # Create tar.gz for Unix binaries | |
| for binary in skyspy-linux-* skyspy-darwin-*; do | |
| if [[ -f "$binary" && ! "$binary" =~ \.sig$ ]]; then | |
| ARCHIVE_NAME="${binary}-${VERSION}.tar.gz" | |
| tar -czvf "$ARCHIVE_NAME" "$binary" "${binary}.sig" signing-cert.pem VERIFY.md | |
| echo "Created $ARCHIVE_NAME" | |
| fi | |
| done | |
| # Create zip for Windows | |
| for binary in skyspy-windows-*.exe; do | |
| if [[ -f "$binary" ]]; then | |
| ARCHIVE_NAME="${binary%.exe}-${VERSION}.zip" | |
| zip "$ARCHIVE_NAME" "$binary" "${binary}.sig" signing-cert.pem VERIFY.md | |
| echo "Created $ARCHIVE_NAME" | |
| fi | |
| done | |
| # Add archive checksums | |
| sha256sum *.tar.gz *.zip 2>/dev/null >> checksums.txt || true | |
| # Sign checksums (use first available cert - they should all be the same for ephemeral) | |
| if command -v step &> /dev/null; then | |
| # Generate ephemeral cert for signing checksums | |
| step certificate create \ | |
| "skyspy-ci@github.com" \ | |
| /tmp/signing.crt \ | |
| /tmp/signing.key \ | |
| --profile self-signed \ | |
| --subtle \ | |
| --not-after 24h \ | |
| --no-password \ | |
| --insecure \ | |
| --kty EC \ | |
| --curve P-256 \ | |
| --force | |
| openssl dgst -sha256 -sign /tmp/signing.key -out checksums.txt.sig checksums.txt | |
| rm -f /tmp/signing.key /tmp/signing.crt | |
| else | |
| # Fallback: sign with openssl self-signed | |
| openssl ecparam -genkey -name prime256v1 -out /tmp/key.pem | |
| openssl dgst -sha256 -sign /tmp/key.pem -out checksums.txt.sig checksums.txt | |
| rm -f /tmp/key.pem | |
| fi | |
| ls -la | |
| - name: Upload packaged artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: go-binaries-signed | |
| path: | | |
| dist/*.tar.gz | |
| dist/*.zip | |
| dist/checksums.txt | |
| dist/checksums.txt.sig | |
| dist/signing-cert.pem | |
| dist/VERIFY.md | |
| retention-days: 30 | |
| - name: Upload to release | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: | | |
| dist/*.tar.gz | |
| dist/*.zip | |
| dist/checksums.txt | |
| dist/checksums.txt.sig | |
| dist/signing-cert.pem | |
| dist/VERIFY.md | |
| fail_on_unmatched_files: false | |
| - name: Summary | |
| env: | |
| VERSION: ${{ needs.go-version.outputs.version }} | |
| run: | | |
| echo "## Go Binaries Packaged" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Version**: ${VERSION}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Archives" >> $GITHUB_STEP_SUMMARY | |
| echo "| Platform | Archive |" >> $GITHUB_STEP_SUMMARY | |
| echo "|----------|---------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Linux amd64 | \`skyspy-linux-amd64-${VERSION}.tar.gz\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Linux arm64 | \`skyspy-linux-arm64-${VERSION}.tar.gz\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| macOS amd64 | \`skyspy-darwin-amd64-${VERSION}.tar.gz\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| macOS arm64 | \`skyspy-darwin-arm64-${VERSION}.tar.gz\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Windows amd64 | \`skyspy-windows-amd64-${VERSION}.zip\` |" >> $GITHUB_STEP_SUMMARY | |
| # =========================================================================== | |
| # CI Gate | |
| # =========================================================================== | |
| ci-success: | |
| name: CI Success | |
| runs-on: ubuntu-latest | |
| needs: [lint-python, lint-go, lint-frontend, security-python, security-frontend, security-go, snyk-python, snyk-frontend, snyk-go, snyk-docker, test-python-unit, test-go, test-frontend-unit, build-frontend, test-python-integration, test-e2e, docker-build-push, sign-docker-images, go-version, build-go-binaries, sign-go-binaries, package-go-binaries, export-openapi, sync-docs, codeql] | |
| if: always() | |
| steps: | |
| - name: Check results | |
| run: | | |
| RESULTS='${{ toJSON(needs.*.result) }}' | |
| echo "Job results: $RESULTS" | |
| if echo "$RESULTS" | grep -qE '"failure"|"cancelled"'; then | |
| echo "CI failed" | |
| exit 1 | |
| fi | |
| echo "CI passed" | |
| - name: Generate summary | |
| if: always() | |
| run: | | |
| echo "## CI Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Status | |
| if echo '${{ toJSON(needs.*.result) }}' | grep -qE '"failure"|"cancelled"'; then | |
| echo "### Status: :x: Failed" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "### Status: :white_check_mark: Passed" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Job Results Table | |
| echo "### Job Results" >> $GITHUB_STEP_SUMMARY | |
| echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Python Lint | ${{ needs.lint-python.result == 'success' && ':white_check_mark:' || (needs.lint-python.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.lint-python.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Go Lint | ${{ needs.lint-go.result == 'success' && ':white_check_mark:' || (needs.lint-go.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.lint-go.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Frontend Lint | ${{ needs.lint-frontend.result == 'success' && ':white_check_mark:' || (needs.lint-frontend.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.lint-frontend.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Python Security | ${{ needs.security-python.result == 'success' && ':white_check_mark:' || (needs.security-python.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.security-python.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Frontend Security | ${{ needs.security-frontend.result == 'success' && ':white_check_mark:' || (needs.security-frontend.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.security-frontend.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Go Security | ${{ needs.security-go.result == 'success' && ':white_check_mark:' || (needs.security-go.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.security-go.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Snyk Python | ${{ needs.snyk-python.result == 'success' && ':white_check_mark:' || (needs.snyk-python.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.snyk-python.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Snyk Frontend | ${{ needs.snyk-frontend.result == 'success' && ':white_check_mark:' || (needs.snyk-frontend.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.snyk-frontend.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Snyk Go | ${{ needs.snyk-go.result == 'success' && ':white_check_mark:' || (needs.snyk-go.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.snyk-go.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Snyk Docker | ${{ needs.snyk-docker.result == 'success' && ':white_check_mark:' || (needs.snyk-docker.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.snyk-docker.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| CodeQL | ${{ needs.codeql.result == 'success' && ':white_check_mark:' || (needs.codeql.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.codeql.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Python Tests | ${{ needs.test-python-unit.result == 'success' && ':white_check_mark:' || (needs.test-python-unit.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.test-python-unit.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Go Tests | ${{ needs.test-go.result == 'success' && ':white_check_mark:' || (needs.test-go.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.test-go.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Frontend Tests | ${{ needs.test-frontend-unit.result == 'success' && ':white_check_mark:' || (needs.test-frontend-unit.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.test-frontend-unit.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Integration Tests | ${{ needs.test-python-integration.result == 'success' && ':white_check_mark:' || (needs.test-python-integration.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.test-python-integration.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| E2E Tests | ${{ needs.test-e2e.result == 'success' && ':white_check_mark:' || (needs.test-e2e.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.test-e2e.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Docker Build | ${{ needs.docker-build-push.result == 'success' && ':white_check_mark:' || (needs.docker-build-push.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.docker-build-push.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Docker Sign | ${{ needs.sign-docker-images.result == 'success' && ':white_check_mark:' || (needs.sign-docker-images.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.sign-docker-images.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Go Build | ${{ needs.build-go-binaries.result == 'success' && ':white_check_mark:' || (needs.build-go-binaries.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.build-go-binaries.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Go Sign | ${{ needs.sign-go-binaries.result == 'success' && ':white_check_mark:' || (needs.sign-go-binaries.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.sign-go-binaries.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Go Package | ${{ needs.package-go-binaries.result == 'success' && ':white_check_mark:' || (needs.package-go-binaries.result == 'skipped' && ':fast_forward:' || ':x:') }} ${{ needs.package-go-binaries.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Links Section | |
| echo "### Links" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Vercel Preview (for PRs) | |
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| echo "- :rocket: **Vercel Preview**: [View Deployment](https://skyspy-git-${{ github.head_ref }}-${{ github.repository_owner }}.vercel.app)" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Artifacts | |
| echo "- :package: **Artifacts**: [View All Artifacts](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts)" >> $GITHUB_STEP_SUMMARY | |
| # Conditional artifact links | |
| if [[ "${{ needs.test-e2e.result }}" != "skipped" ]]; then | |
| echo " - :test_tube: Playwright Report" >> $GITHUB_STEP_SUMMARY | |
| echo " - :clipboard: E2E Test Results" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [[ "${{ needs.test-python-integration.result }}" != "skipped" ]]; then | |
| echo " - :clipboard: Integration Test Results" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [[ "${{ needs.export-openapi.result }}" == "success" ]]; then | |
| echo " - :page_facing_up: OpenAPI Schema" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Docker Images (if built) | |
| if [[ "${{ needs.docker-build-push.result }}" == "success" ]]; then | |
| echo "### Docker Images" >> $GITHUB_STEP_SUMMARY | |
| echo "- \`ghcr.io/${{ github.repository }}:${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- \`ghcr.io/${{ github.repository }}-rtl-airband-uploader:${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Coverage (link to Codecov) | |
| echo "### Coverage" >> $GITHUB_STEP_SUMMARY | |
| echo "- :bar_chart: [View on Codecov](https://codecov.io/gh/${{ github.repository }}/pull/${{ github.event.pull_request.number || github.sha }})" >> $GITHUB_STEP_SUMMARY | |
| # Cache Stats | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Cache" >> $GITHUB_STEP_SUMMARY | |
| echo "- Python venv, npm, Playwright, Go modules, and Docker layers are cached" >> $GITHUB_STEP_SUMMARY | |
| echo "- [View Cache Usage](${{ github.server_url }}/${{ github.repository }}/actions/caches)" >> $GITHUB_STEP_SUMMARY | |
| # =========================================================================== | |
| # Release (manual trigger only) | |
| # =========================================================================== | |
| release: | |
| name: Create Release | |
| runs-on: ubuntu-latest | |
| needs: [ci-success, docker-build-push, sign-docker-images, package-go-binaries] | |
| if: | | |
| github.event_name == 'workflow_dispatch' && | |
| inputs.create_release == true && | |
| needs.ci-success.result == 'success' && | |
| needs.docker-build-push.result == 'success' && | |
| (needs.sign-docker-images.result == 'success' || needs.sign-docker-images.result == 'skipped') && | |
| (needs.package-go-binaries.result == 'success' || needs.package-go-binaries.result == 'skipped') | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Validate version | |
| id: version | |
| run: | | |
| VERSION="${{ inputs.version }}" | |
| if [[ -z "$VERSION" ]]; then | |
| # Auto-generate version from date if not provided | |
| VERSION="$(date +%Y.%m.%d)-$(git rev-parse --short HEAD)" | |
| echo "Auto-generated version: $VERSION" | |
| fi | |
| # Validate semver-ish format | |
| if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]] && [[ ! "$VERSION" =~ ^[0-9]{4}\.[0-9]{2}\.[0-9]{2}- ]]; then | |
| echo "Warning: Version '$VERSION' doesn't follow semver format" | |
| fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "tag=v$VERSION" >> $GITHUB_OUTPUT | |
| - name: Check if tag exists | |
| run: | | |
| if git rev-parse "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then | |
| echo "Error: Tag v${{ steps.version.outputs.version }} already exists" | |
| exit 1 | |
| fi | |
| - name: Generate changelog | |
| id: changelog | |
| run: | | |
| # Get the previous tag | |
| PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") | |
| echo "## What's Changed" > changelog.md | |
| echo "" >> changelog.md | |
| if [[ -n "$PREV_TAG" ]]; then | |
| echo "Changes since $PREV_TAG:" >> changelog.md | |
| echo "" >> changelog.md | |
| # Group commits by type | |
| echo "### Features" >> changelog.md | |
| git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --grep="^feat" >> changelog.md || true | |
| echo "" >> changelog.md | |
| echo "### Bug Fixes" >> changelog.md | |
| git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --grep="^fix" >> changelog.md || true | |
| echo "" >> changelog.md | |
| echo "### Other Changes" >> changelog.md | |
| git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --invert-grep --grep="^feat" --grep="^fix" | head -20 >> changelog.md || true | |
| echo "" >> changelog.md | |
| else | |
| echo "Initial release" >> changelog.md | |
| fi | |
| echo "" >> changelog.md | |
| echo "### Docker Images (Signed)" >> changelog.md | |
| echo "\`\`\`" >> changelog.md | |
| echo "ghcr.io/${{ github.repository }}:v${{ steps.version.outputs.version }}" >> changelog.md | |
| echo "ghcr.io/${{ github.repository }}-rtl-airband-uploader:v${{ steps.version.outputs.version }}" >> changelog.md | |
| echo "\`\`\`" >> changelog.md | |
| echo "" >> changelog.md | |
| echo "### CLI Binaries (Signed)" >> changelog.md | |
| echo "All binaries are signed with ephemeral certificates. See \`VERIFY.md\` for verification instructions." >> changelog.md | |
| echo "| Platform | Archive |" >> changelog.md | |
| echo "|----------|---------|" >> changelog.md | |
| echo "| Linux amd64 | \`skyspy-linux-amd64-v${{ steps.version.outputs.version }}.tar.gz\` |" >> changelog.md | |
| echo "| Linux arm64 | \`skyspy-linux-arm64-v${{ steps.version.outputs.version }}.tar.gz\` |" >> changelog.md | |
| echo "| macOS amd64 | \`skyspy-darwin-amd64-v${{ steps.version.outputs.version }}.tar.gz\` |" >> changelog.md | |
| echo "| macOS arm64 | \`skyspy-darwin-arm64-v${{ steps.version.outputs.version }}.tar.gz\` |" >> changelog.md | |
| echo "| Windows amd64 | \`skyspy-windows-amd64-v${{ steps.version.outputs.version }}.zip\` |" >> changelog.md | |
| echo "" >> changelog.md | |
| echo "**Full Changelog**: ${{ github.server_url }}/${{ github.repository }}/compare/${PREV_TAG:-$(git rev-list --max-parents=0 HEAD)}..v${{ steps.version.outputs.version }}" >> changelog.md | |
| cat changelog.md | |
| - name: Download OpenAPI artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: openapi-schema | |
| path: release-assets/ | |
| continue-on-error: true | |
| - name: Download signed Go binaries | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: go-binaries-signed | |
| path: release-assets/go/ | |
| continue-on-error: true | |
| - name: Create tag | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git tag -a "v${{ steps.version.outputs.version }}" -m "Release v${{ steps.version.outputs.version }}" | |
| git push origin "v${{ steps.version.outputs.version }}" | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ steps.version.outputs.version }} | |
| name: v${{ steps.version.outputs.version }} | |
| body_path: changelog.md | |
| draft: ${{ inputs.draft }} | |
| prerelease: ${{ inputs.prerelease }} | |
| files: | | |
| release-assets/* | |
| release-assets/go/*.tar.gz | |
| release-assets/go/*.zip | |
| release-assets/go/checksums.txt | |
| release-assets/go/checksums.txt.sig | |
| release-assets/go/signing-cert.pem | |
| release-assets/go/VERIFY.md | |
| generate_release_notes: false | |
| - name: Release summary | |
| run: | | |
| echo "## Release Created" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Version**: v${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Tag**: [v${{ steps.version.outputs.version }}](${{ github.server_url }}/${{ github.repository }}/releases/tag/v${{ steps.version.outputs.version }})" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Draft**: ${{ inputs.draft }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Pre-release**: ${{ inputs.prerelease }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Artifacts Signed**: Yes (ephemeral or CA certificates)" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Docker Images (Signed)" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| echo "docker pull ghcr.io/${{ github.repository }}:v${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### CLI Binaries (Signed)" >> $GITHUB_STEP_SUMMARY | |
| echo "Download from the release assets. Verify with \`VERIFY.md\` instructions." >> $GITHUB_STEP_SUMMARY |