CI / 2247 #8478
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 | |
| run-name: ${{ github.event_name == 'pull_request' && format('CI / {0}', github.event.pull_request.number) || '' }} | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| types: [opened, synchronize, reopened, edited] | |
| workflow_dispatch: | |
| # PR metadata edits can retrigger full CI for the same head. Keep only the | |
| # newest run for a pull request; push and manual runs use a unique run ID. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| permissions: | |
| contents: read | |
| actions: read | |
| jobs: | |
| # ── Layer 1: Fast Gate ───────────────────────────────────────────── | |
| fast-gate: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: go.mod | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 | |
| with: | |
| python-version: '3.x' | |
| - name: Fetch meta data | |
| run: python3 scripts/fetch_meta.py | |
| - name: Build | |
| run: go build ./... | |
| - name: Vet | |
| run: go vet ./... | |
| - name: Check formatting | |
| run: | | |
| unformatted=$(gofmt -l .) | |
| if [ -n "$unformatted" ]; then | |
| echo "$unformatted" | |
| echo "::error::Unformatted Go files detected — run 'gofmt -w .' and commit" | |
| exit 1 | |
| fi | |
| - name: Check go.mod tidiness | |
| run: | | |
| go mod tidy | |
| if ! git diff --quiet go.mod go.sum; then | |
| echo "::error::go.mod or go.sum is not tidy. Run 'go mod tidy' and commit the changes." | |
| git diff go.mod go.sum | |
| exit 1 | |
| fi | |
| plugin-integration: | |
| needs: fast-gate | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: go.mod | |
| # No fetch_meta: the git-archive clean tree must embed only the | |
| # committed meta_data stub (reproduces the bare-module customer state). | |
| - name: Run plugin-integration L4 tests | |
| run: go test -count=1 -timeout=15m ./tests/plugin_e2e/... | |
| sidecar-integration: | |
| needs: fast-gate | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: go.mod | |
| - name: Run sidecar tag build + HMAC round-trip | |
| run: make sidecar-test | |
| # ── Layer 2: Quality Gate ────────────────────────────────────────── | |
| unit-test: | |
| needs: fast-gate | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: go.mod | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 | |
| with: | |
| python-version: '3.x' | |
| - name: Fetch meta data | |
| run: python3 scripts/fetch_meta.py | |
| - name: Run tests | |
| run: go test -v -race -count=1 -timeout=5m ./cmd/... ./internal/... ./shortcuts/... ./extension/... | |
| lint: | |
| needs: fast-gate | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: go.mod | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 | |
| with: | |
| python-version: '3.x' | |
| - name: Fetch meta data | |
| run: python3 scripts/fetch_meta.py | |
| - name: Resolve changed-from baseline | |
| env: | |
| QUALITY_GATE_CHANGED_FROM: ${{ github.event.pull_request.base.sha || github.event.before || 'origin/main' }} | |
| run: echo "QUALITY_GATE_CHANGED_FROM=$(bash scripts/resolve-changed-from.sh)" >> "$GITHUB_ENV" | |
| - name: Run golangci-lint | |
| run: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 run --new-from-rev="$QUALITY_GATE_CHANGED_FROM" | |
| - name: Run source-contract lint guards (lintcheck) | |
| run: go run -C lint . --changed-from "$QUALITY_GATE_CHANGED_FROM" .. | |
| - name: Run lint module tests | |
| run: go test -C lint ./... -count=1 | |
| script-test: | |
| needs: fast-gate | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: go.mod | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: '22' | |
| - name: Run script tests | |
| run: make script-test | |
| deterministic-gate: | |
| needs: fast-gate | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| actions: read | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: go.mod | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 | |
| with: | |
| python-version: '3.x' | |
| - name: Fetch meta data | |
| run: python3 scripts/fetch_meta.py | |
| - name: Resolve changed-from baseline | |
| env: | |
| QUALITY_GATE_CHANGED_FROM: ${{ github.event.pull_request.base.sha || github.event.before || 'origin/main' }} | |
| run: echo "QUALITY_GATE_CHANGED_FROM=$(bash scripts/resolve-changed-from.sh)" >> "$GITHUB_ENV" | |
| - name: Write public content metadata | |
| if: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| PR_TITLE: ${{ github.event.pull_request.title }} | |
| PR_BODY: ${{ github.event.pull_request.body }} | |
| PR_BRANCH: ${{ github.head_ref }} | |
| run: | | |
| mkdir -p .tmp/quality-gate | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| with open(".tmp/quality-gate/public-content-metadata.json", "w", encoding="utf-8") as f: | |
| json.dump({ | |
| "title": os.environ.get("PR_TITLE", ""), | |
| "body": os.environ.get("PR_BODY", ""), | |
| "branch": os.environ.get("PR_BRANCH", ""), | |
| }, f) | |
| f.write("\n") | |
| PY | |
| - name: Run CLI deterministic gate | |
| run: PUBLIC_CONTENT_METADATA=.tmp/quality-gate/public-content-metadata.json make quality-gate | |
| - name: Upload quality gate facts | |
| if: ${{ always() && github.event_name == 'pull_request' }} | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: quality-gate-facts-${{ github.event.pull_request.base.sha }}-${{ github.event.pull_request.head.sha }} | |
| path: .tmp/quality-gate/facts.json | |
| if-no-files-found: error | |
| retention-days: 7 | |
| coverage: | |
| needs: fast-gate | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: go.mod | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 | |
| with: | |
| python-version: '3.x' | |
| - name: Fetch meta data | |
| run: python3 scripts/fetch_meta.py | |
| - name: Run tests with coverage | |
| run: | | |
| # tests/ holds only L3/L4 suites (cli_e2e, plugin_e2e, sidecar_e2e) that | |
| # have dedicated jobs; exclude the whole subtree so none of them runs a | |
| # second time here — and, crucially, so an observe-only suite's failure | |
| # can never block merges through coverage's spot in the results loop. | |
| packages=$(go list ./... | grep -v '^github.com/larksuite/cli/tests/') | |
| go test -race -coverprofile=coverage.txt -covermode=atomic $packages | |
| - name: Upload coverage to Codecov | |
| if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }} | |
| uses: codecov/codecov-action@3f20e214133d0983f9a10f3d63b0faf9241a3daa # v6 | |
| with: | |
| files: coverage.txt | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| - name: Check coverage threshold | |
| run: | | |
| total=$(go tool cover -func=coverage.txt | grep total | awk '{print $3}' | tr -d '%') | |
| threshold=40 | |
| echo "Coverage: ${total}% (threshold: ${threshold}%)" | |
| if (( $(echo "$total < $threshold" | bc -l) )); then | |
| echo "::error::Coverage ${total}% is below threshold ${threshold}%" | |
| exit 1 | |
| fi | |
| - name: Coverage summary | |
| if: ${{ !cancelled() }} | |
| run: | | |
| if [ ! -f coverage.txt ]; then | |
| echo "No coverage data available" >> $GITHUB_STEP_SUMMARY | |
| exit 0 | |
| fi | |
| total=$(go tool cover -func=coverage.txt | grep total | awk '{print $3}') | |
| echo "## Coverage Report" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Total coverage: ${total}**" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "<details><summary>Details</summary>" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| go tool cover -func=coverage.txt >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| echo "</details>" >> $GITHUB_STEP_SUMMARY | |
| deadcode: | |
| needs: fast-gate | |
| if: ${{ github.event_name == 'pull_request' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: go.mod | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 | |
| with: | |
| python-version: '3.x' | |
| - name: Fetch meta data | |
| run: python3 scripts/fetch_meta.py | |
| - name: Dead code check (incremental) | |
| run: | | |
| # Analyze current HEAD (strip line:col for stable diff across line shifts) | |
| # Filter "go: downloading ..." lines to avoid false diffs from module cache state | |
| go run golang.org/x/tools/cmd/deadcode@v0.31.0 -test ./... 2>&1 | \ | |
| grep -v '^go: ' | \ | |
| sed 's/:[0-9][0-9]*:[0-9][0-9]*:/:/' | sort > /tmp/dc-head.txt | |
| # Analyze base branch via worktree | |
| git worktree add -q /tmp/dc-base "origin/${{ github.base_ref }}" | |
| (cd /tmp/dc-base && python3 scripts/fetch_meta.py && \ | |
| go run golang.org/x/tools/cmd/deadcode@v0.31.0 -test ./... 2>&1 | \ | |
| grep -v '^go: ' | \ | |
| sed 's/:[0-9][0-9]*:[0-9][0-9]*:/:/' | sort > /tmp/dc-base.txt) || { | |
| echo "::warning::Failed to analyze base branch — skipping incremental dead code check" | |
| git worktree remove -f /tmp/dc-base 2>/dev/null || true | |
| exit 0 | |
| } | |
| git worktree remove -f /tmp/dc-base | |
| # Only new dead code blocks the PR | |
| comm -23 /tmp/dc-head.txt /tmp/dc-base.txt > /tmp/dc-new.txt | |
| if [ -s /tmp/dc-new.txt ]; then | |
| echo "::group::New dead code" | |
| cat /tmp/dc-new.txt | |
| echo "::endgroup::" | |
| echo "::error::New dead code detected — remove unreachable functions before merging" | |
| exit 1 | |
| fi | |
| echo "No new dead code introduced" | |
| # ── Layer 3: E2E Gate ────────────────────────────────────────────── | |
| e2e-dry-run: | |
| needs: [unit-test, lint, script-test, deterministic-gate] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| outputs: | |
| mode: ${{ steps.e2e_domains.outputs.mode }} | |
| reason: ${{ steps.e2e_domains.outputs.reason }} | |
| live_packages: ${{ steps.e2e_domains.outputs.live_packages }} | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: go.mod | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 | |
| with: | |
| python-version: '3.x' | |
| - name: Resolve CLI E2E domains | |
| id: e2e_domains | |
| run: node scripts/e2e_domains.js | |
| - name: Validate CLI E2E domain outputs | |
| env: | |
| E2E_MODE: ${{ steps.e2e_domains.outputs.mode }} | |
| E2E_LIVE_PACKAGES: ${{ steps.e2e_domains.outputs.live_packages }} | |
| run: | | |
| case "$E2E_MODE" in | |
| skip) | |
| [ -z "$E2E_LIVE_PACKAGES" ] || { echo "::error::Skip mode must not resolve live packages"; exit 1; } | |
| ;; | |
| full|subset) | |
| [ -n "$E2E_LIVE_PACKAGES" ] || { echo "::error::No live packages resolved for mode $E2E_MODE"; exit 1; } | |
| ;; | |
| *) | |
| echo "::error::Invalid CLI E2E mode: $E2E_MODE" | |
| exit 1 | |
| ;; | |
| esac | |
| - name: Build lark-cli | |
| if: ${{ steps.e2e_domains.outputs.mode != 'skip' }} | |
| run: make build | |
| - name: Run dry-run E2E tests | |
| env: | |
| LARK_CLI_BIN: ${{ github.workspace }}/lark-cli | |
| LARKSUITE_CLI_APP_ID: dry-run | |
| LARKSUITE_CLI_APP_SECRET: dry-run | |
| LARKSUITE_CLI_BRAND: feishu | |
| E2E_MODE: ${{ steps.e2e_domains.outputs.mode }} | |
| E2E_REASON: ${{ steps.e2e_domains.outputs.reason }} | |
| E2E_DRY_ROOT_PACKAGE: ${{ steps.e2e_domains.outputs.dry_root_package }} | |
| E2E_DRY_PACKAGES: ${{ steps.e2e_domains.outputs.dry_packages }} | |
| run: | | |
| if [ "$E2E_MODE" = "skip" ]; then | |
| echo "No dry-run CLI E2E needed: $E2E_REASON" | |
| exit 0 | |
| fi | |
| if [ -z "$E2E_DRY_ROOT_PACKAGE" ] && [ -z "$E2E_DRY_PACKAGES" ]; then | |
| echo "::error::No dry-run CLI E2E packages resolved for mode $E2E_MODE" | |
| exit 1 | |
| fi | |
| echo "Dry-run CLI E2E domains: $E2E_MODE ($E2E_REASON)" | |
| if [ -n "$E2E_DRY_ROOT_PACKAGE" ]; then | |
| echo "Dry-run CLI E2E root package: $E2E_DRY_ROOT_PACKAGE" | |
| go test -v -count=1 -timeout=5m "$E2E_DRY_ROOT_PACKAGE" | |
| fi | |
| if [ -n "$E2E_DRY_PACKAGES" ]; then | |
| echo "Dry-run CLI E2E packages: $E2E_DRY_PACKAGES" | |
| go test -v -count=1 -timeout=5m $E2E_DRY_PACKAGES -run 'DryRun|Regression' | |
| fi | |
| e2e-live: | |
| needs: [unit-test, lint, script-test, deterministic-gate, e2e-dry-run] | |
| if: ${{ always() && (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) && needs.unit-test.result == 'success' && needs.lint.result == 'success' && needs.script-test.result == 'success' && needs.deterministic-gate.result == 'success' && needs.e2e-dry-run.result == 'success' && (needs.e2e-dry-run.outputs.mode == 'full' || needs.e2e-dry-run.outputs.mode == 'subset') && needs.e2e-dry-run.outputs.live_packages != '' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| actions: read | |
| contents: read | |
| checks: write | |
| env: | |
| TEST_BOT1_APP_ID: ${{ secrets.TEST_BOT1_APP_ID }} | |
| LARKSUITE_CLI_BRAND: feishu | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: go.mod | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 | |
| with: | |
| python-version: '3.x' | |
| - name: Build lark-cli | |
| id: build_cli | |
| run: make build | |
| - name: Prepare shared live E2E tenant token | |
| id: live_e2e_tat | |
| env: | |
| LARKSUITE_CLI_APP_ID: ${{ secrets.TEST_BOT1_APP_ID }} | |
| TEST_BOT1_APP_SECRET: ${{ secrets.TEST_BOT1_APP_SECRET }} | |
| run: node scripts/fetch_e2e_tat.js | |
| - name: Run CLI E2E tests | |
| # Keep an active Go test alive so t.Cleanup can finish. A queued stale | |
| # run is rejected below before it can start live E2E. | |
| if: ${{ always() && steps.build_cli.outcome == 'success' && steps.live_e2e_tat.outcome == 'success' }} | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPOSITORY: ${{ github.repository }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| RUN_ID: ${{ github.run_id }} | |
| RUN_NUMBER: ${{ github.run_number }} | |
| RUN_GENERATION: ${{ github.event_name == 'pull_request' && format('CI / {0}', github.event.pull_request.number) || '' }} | |
| LARK_CLI_BIN: ${{ github.workspace }}/lark-cli | |
| E2E_MODE: ${{ needs.e2e-dry-run.outputs.mode }} | |
| E2E_REASON: ${{ needs.e2e-dry-run.outputs.reason }} | |
| E2E_LIVE_PACKAGES: ${{ needs.e2e-dry-run.outputs.live_packages }} | |
| E2E_TENANT_AUTH_FILE: ${{ steps.live_e2e_tat.outputs.path }} | |
| TEST_USER_ACCESS_TOKEN: ${{ secrets.TEST_USER_ACCESS_TOKEN }} | |
| run: | | |
| if [ "$EVENT_NAME" = "pull_request" ]; then | |
| workflow_id="$(gh api "repos/$REPOSITORY/actions/runs/$RUN_ID" --jq '.workflow_id')" | |
| newer_runs="$( | |
| gh api --paginate -X GET "repos/$REPOSITORY/actions/workflows/$workflow_id/runs" \ | |
| -f event=pull_request -f branch="$GITHUB_HEAD_REF" -f per_page=100 | | |
| jq -r --arg repository "$REPOSITORY" --arg generation "$RUN_GENERATION" --argjson run_number "$RUN_NUMBER" \ | |
| '.workflow_runs[] | select(.head_repository.full_name == $repository and .display_title == $generation and .run_number > $run_number) | .id' | |
| )" | |
| if [ -n "$newer_runs" ]; then | |
| echo "::error::Superseded before live E2E started by newer workflow run(s): $newer_runs" | |
| exit 1 | |
| fi | |
| fi | |
| if [ -z "${E2E_TENANT_AUTH_FILE:-}" ] || [ ! -f "$E2E_TENANT_AUTH_FILE" ]; then | |
| echo "::error::Missing shared live E2E tenant token file" | |
| exit 1 | |
| fi | |
| export TEST_TENANT_ACCESS_TOKEN="$(cat "$E2E_TENANT_AUTH_FILE")" | |
| rm -f "$E2E_TENANT_AUTH_FILE" | |
| if ! LARKSUITE_CLI_APP_ID="$TEST_BOT1_APP_ID" \ | |
| LARKSUITE_CLI_TENANT_ACCESS_TOKEN="$TEST_TENANT_ACCESS_TOKEN" \ | |
| ./lark-cli whoami --as bot | node -e ' | |
| let input = ""; | |
| process.stdin.setEncoding("utf8"); | |
| process.stdin.on("data", (chunk) => { input += chunk; }); | |
| process.stdin.on("end", () => { | |
| const result = JSON.parse(input); | |
| if (result.identity !== "bot" || result.available !== true || result.tokenStatus !== "ready") process.exit(1); | |
| }); | |
| '; then | |
| echo "::error::Tenant credential preflight failed" | |
| exit 1 | |
| fi | |
| echo "Tenant credential preflight succeeded" | |
| packages="$E2E_LIVE_PACKAGES" | |
| if [ -z "$packages" ]; then | |
| echo "::error::No live CLI E2E packages resolved for mode $E2E_MODE" | |
| exit 1 | |
| fi | |
| echo "Live CLI E2E domains: $E2E_MODE ($E2E_REASON)" | |
| echo "Live CLI E2E packages: $packages" | |
| go run gotest.tools/gotestsum@v1.12.3 --rerun-fails=2 --rerun-fails-max-failures=20 --packages="$packages" --format testname --junitfile cli-e2e-report.xml -- -count=1 -v | |
| - name: Publish CLI E2E test report | |
| if: ${{ !cancelled() }} | |
| uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0 | |
| with: | |
| name: CLI E2E Tests | |
| path: cli-e2e-report.xml | |
| reporter: java-junit | |
| use-actions-summary: true | |
| list-suites: all | |
| list-tests: all | |
| # ── Layer 4: Security & Compliance (parallel with L2-L3) ────────── | |
| security: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: go.mod | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 | |
| with: | |
| python-version: '3.x' | |
| - name: Fetch meta data | |
| run: python3 scripts/fetch_meta.py | |
| - name: Gitleaks | |
| if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }} | |
| uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_KEY }} | |
| - name: govulncheck | |
| continue-on-error: true | |
| run: go run golang.org/x/vuln/cmd/govulncheck@v1.1.4 ./... | |
| - name: Check dependency licenses | |
| run: go run github.com/google/go-licenses/v2@v2.0.1 check ./... --disallowed_types=forbidden,restricted,reciprocal,unknown | |
| license-header: | |
| if: ${{ github.event_name == 'pull_request' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| - name: Check license headers | |
| uses: apache/skywalking-eyes/header@8c96ee223558797cdd9eba82c0919258e1cf2dad | |
| with: | |
| config: .licenserc.yaml | |
| # ── Results Gate (single required check for branch protection) ───── | |
| results: | |
| if: ${{ always() }} | |
| needs: [fast-gate, unit-test, lint, script-test, deterministic-gate, coverage, deadcode, e2e-dry-run, e2e-live, security, license-header, plugin-integration, sidecar-integration] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Evaluate results | |
| run: | | |
| echo "## CI Results" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Layer | Job | Status |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-------|-----|--------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| L1 | fast-gate | ${{ needs.fast-gate.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| L2 | unit-test | ${{ needs.unit-test.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| L2 | lint | ${{ needs.lint.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| L2 | script-test | ${{ needs.script-test.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| L2 | deterministic-gate | ${{ needs.deterministic-gate.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| L2 | coverage | ${{ needs.coverage.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| L2 | deadcode | ${{ needs.deadcode.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| L3 | e2e-dry-run | ${{ needs.e2e-dry-run.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| L3 | e2e-live | ${{ needs.e2e-live.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| L4 | security | ${{ needs.security.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| L4 | license-header | ${{ needs.license-header.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| L4 | plugin-integration (observe-only) | ${{ needs.plugin-integration.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| L4 | sidecar-integration (observe-only) | ${{ needs.sidecar-integration.result }} |" >> $GITHUB_STEP_SUMMARY | |
| # Any failure or cancellation in any job blocks the merge. | |
| # Legitimately skipped jobs (deadcode on push, e2e-live when not | |
| # needed or on a fork, license-header on push) are OK. | |
| # | |
| # plugin-integration and sidecar-integration are intentionally NOT | |
| # in this loop yet: they run on every PR and their status is shown | |
| # in the table above, but a failure is observe-only (non-blocking) | |
| # during the initial soak. Graduation to required is tracked in | |
| # https://github.com/larksuite/cli/issues/1894 (criteria: 4 | |
| # consecutive weeks with zero false positives). | |
| FAILED=0 | |
| for result in \ | |
| "${{ needs.fast-gate.result }}" \ | |
| "${{ needs.unit-test.result }}" \ | |
| "${{ needs.lint.result }}" \ | |
| "${{ needs.script-test.result }}" \ | |
| "${{ needs.deterministic-gate.result }}" \ | |
| "${{ needs.coverage.result }}" \ | |
| "${{ needs.deadcode.result }}" \ | |
| "${{ needs.e2e-dry-run.result }}" \ | |
| "${{ needs.e2e-live.result }}" \ | |
| "${{ needs.security.result }}" \ | |
| "${{ needs.license-header.result }}"; do | |
| if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then | |
| FAILED=1 | |
| fi | |
| done | |
| if [ "$FAILED" = "1" ]; then | |
| echo "" | |
| echo "::error::One or more CI jobs failed — see table above" | |
| exit 1 | |
| fi |