feat(frontend): make the role grant's environment field a direct-execution switch #429
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: | |
| pull_request: | |
| branches: | |
| - 'main' | |
| - 'release/*.*.*' | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| detect-changed-files: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| go: ${{ steps.detect.outputs.go }} | |
| frontend: ${{ steps.detect.outputs.frontend }} | |
| proto: ${{ steps.detect.outputs.proto }} | |
| steps: | |
| # HEAD is the pull request's merge commit; its first parent is the base branch. | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 2 | |
| sparse-checkout: .github | |
| - name: Detect which areas changed | |
| id: detect | |
| run: | | |
| files=$(git diff --name-only --no-renames HEAD^1 HEAD) | |
| changed() { grep -qE "$1" <<<"$files" && echo true || echo false; } | |
| go=$(changed '^(backend/|action/|proto/.*\.go$|go\.(mod|sum)$|\.golangci\.yaml$|\.github/workflows/ci\.yml$)') | |
| # The frontend gate's `prepare` script also reads these backend files. | |
| frontend=$(changed '^(frontend/|backend/enterprise/plan\.yaml$|backend/common/permission/permission\.yaml$|backend/api/mcp/gen/openapi\.yaml$|scripts/check_link_test\.go$|\.github/workflows/ci\.yml$)') | |
| proto=$(changed '^(proto/|\.github/workflows/ci\.yml$)') | |
| printf 'go=%s\nfrontend=%s\nproto=%s\n' "$go" "$frontend" "$proto" >> "$GITHUB_OUTPUT" | |
| echo "Changed areas: go=$go, frontend=$frontend, proto=$proto. Checks for an unchanged area are skipped, and all-checks-passed counts a skipped check as passing." | tee -a "$GITHUB_STEP_SUMMARY" | |
| go-tests: | |
| needs: detect-changed-files | |
| if: needs.detect-changed-files.outputs.go == 'true' | |
| runs-on: self-hosted | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v7 | |
| with: | |
| go-version: '1.27.1' | |
| cache: false # Caching is slow. | |
| - name: Verify go.mod is tidy | |
| run: | | |
| go mod tidy | |
| git diff --exit-code | |
| - name: Run all tests | |
| # go test runs packages in command-line order. backend/tests is the | |
| # longest package by far, so it goes first instead of after the ~130 | |
| # packages that sort before it alphabetically. | |
| run: go test -p=8 -timeout 30m -ldflags "-w -s" -v ./backend/tests ./backend/api/v1 ./backend/... ./action/... | tee test.log; exit ${PIPESTATUS[0]} | |
| - name: Pretty print tests running time | |
| # grep: filter out lines like "--- PASS: Test (15.04s)" | |
| # sed: remove unnecessary characters | |
| # awk: re-format lines to "PASS: Test (15.04s)" | |
| # sort: cut into columns by delimiter ' ' (single space) and sort by column 3 (test time in seconds) as numeric type in reverse order (largest comes first) | |
| # awk: accumulate sum by test time in seconds | |
| run: grep --color=never -e '--- PASS:' -e '--- FAIL:' test.log | sed 's/[:()]//g' | awk '{print $2,$3,$4}' | sort -t' ' -nk3 -r | awk '{sum += $3; print $1,$2,$3,sum"s"}' | |
| golangci-lint: | |
| needs: detect-changed-files | |
| if: needs.detect-changed-files.outputs.go == 'true' | |
| runs-on: self-hosted | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-go@v7 | |
| with: | |
| go-version: '1.27.1' | |
| cache: false # Caching is slow. | |
| - name: Install golangci-lint | |
| uses: golangci/golangci-lint-action@v9 | |
| with: | |
| # renovate: datasource=github-releases depName=golangci/golangci-lint | |
| version: v2.13.2 | |
| install-only: true | |
| skip-cache: true | |
| - name: Verify Go version compatibility | |
| run: | | |
| GO_MAJOR_MINOR=$(go env GOVERSION | sed 's/go//' | cut -d. -f1-2) | |
| LINT_GO=$(golangci-lint version --json 2>/dev/null | jq -r '.go // empty' || true) | |
| if [ -z "$LINT_GO" ]; then | |
| echo "Skipping compatibility check: unable to read golangci-lint Go version" | |
| exit 0 | |
| fi | |
| LINT_MAJOR_MINOR=$(echo "$LINT_GO" | sed 's/go//' | cut -d. -f1-2) | |
| if [ "$GO_MAJOR_MINOR" != "$LINT_MAJOR_MINOR" ]; then | |
| echo "::error::Go version mismatch: setup-go provides go${GO_MAJOR_MINOR} but golangci-lint was built with go${LINT_MAJOR_MINOR}. Update golangci-lint to a version built with go${GO_MAJOR_MINOR}." | |
| exit 1 | |
| fi | |
| - name: golangci-lint | |
| env: | |
| # Go's default GOGC=100 collects every time a ~5 GB live heap doubles, which | |
| # is the wrong policy for a batch run. Collecting near a target instead is | |
| # cheaper on CPU, wall and peak RSS alike. GOMEMLIMIT is a soft limit over | |
| # Go-managed memory, not a guaranteed RSS ceiling, but it holds in practice | |
| # here: 10.17 GB peak measured, against 11.05 GB with the default. | |
| GOGC: "off" | |
| GOMEMLIMIT: 10GiB | |
| run: golangci-lint run --verbose -j 8 --timeout 30m --max-same-issues=30 --allow-parallel-runners | |
| frontend-tests: | |
| needs: detect-changed-files | |
| if: needs.detect-changed-files.outputs.frontend == 'true' | |
| runs-on: self-hosted | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: '24.18.1' | |
| # The runner's pnpm store persists between runs, so the Actions | |
| # cache adds nothing and downloading it is slow. | |
| cache: '' | |
| - uses: pnpm/setup@v2 | |
| with: | |
| version: '11.17.0' | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| working-directory: frontend | |
| # Single gate, identical to what contributors run locally. The stages | |
| # it runs are listed in frontend/scripts/run-gate.mjs. | |
| - name: Test | |
| run: pnpm test | |
| working-directory: frontend | |
| - uses: actions/setup-go@v7 | |
| with: | |
| go-version: '1.27.1' | |
| cache: false # Caching is slow. | |
| - name: Validate links | |
| run: go test -timeout 600s -v ./scripts/... | |
| lint-protos: | |
| needs: detect-changed-files | |
| if: needs.detect-changed-files.outputs.proto == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: buf | |
| uses: bufbuild/buf-action@v1 | |
| with: | |
| input: "proto" | |
| breaking: false | |
| token: ${{ secrets.BUF_TOKEN }} | |
| # The only check the main ruleset requires, so every job that must pass | |
| # belongs in `needs`. | |
| all-checks-passed: | |
| needs: [detect-changed-files, go-tests, golangci-lint, frontend-tests, lint-protos] | |
| # always() on the job and its step: a cancelled run must fail this check, not skip it. | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Fail if any job failed or was cancelled | |
| if: always() | |
| env: | |
| NEEDS: ${{ toJSON(needs) }} | |
| run: | | |
| jq -r 'to_entries[] | "- \(.key): \(.value.result)"' <<<"$NEEDS" | tee -a "$GITHUB_STEP_SUMMARY" | |
| failed=$(jq -r 'to_entries[] | select(.value.result == "failure" or .value.result == "cancelled") | .key' <<<"$NEEDS") | |
| for job in $failed; do echo "::error::$job did not pass"; done | |
| [ -z "$failed" ] || exit 1 |