fix(release): sign and attest the image outside the job that builds it #2275
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # CI pipeline for gitlab-mcp-server | |
| # | |
| # Runs on: pull requests to main, pushes to main | |
| # | |
| # Shape. A job depends on another only when it consumes something the other | |
| # produced, and everything converges on one final job, "CI", which is the one | |
| # check branch protection requires. That is what makes the graph read as a | |
| # flow rather than as a box of unconnected names, and it is also what lets the | |
| # job list change without touching a repository setting: a required check | |
| # named after a job is a setting that has to move with the job. | |
| # | |
| # compile ─┬─ typecheck (windows/amd64 · darwin/arm64 · linux/arm64) | |
| # ├─ e2e-http | |
| # └─ e2e-stdio | |
| # coverage ─── sonarcloud | |
| # docker (×2) ─── docker-build | |
| # cross_platform (×3) ─── cross_platform_summary | |
| # generated · golangci-lint · govulncheck · markdown · site-lint · hadolint | |
| # server-json-schema · supply-chain · docs-audit ─── checks | |
| # └──► ci | |
| # | |
| # Every edge joins neighbouring columns, and that is deliberate. GitHub draws | |
| # a job with no parent and no child at the end of the first column, in one box | |
| # with every other such job, and draws that box's single edge straight to | |
| # whatever needs it. With "ci" two columns away, that edge crossed every node | |
| # of the second column on its way there, and so did the direct edges from | |
| # compile and coverage. So the standalone jobs fan into "checks" first, and | |
| # "ci" needs only second-column nodes; compile and coverage are still decided | |
| # there, because when either fails its children are skipped, and a skip is a | |
| # failure to the verdict script. | |
| # | |
| # Speed. The critical path used to be Test then Build in series, about twenty | |
| # minutes, and Build consumed nothing Test produced. Now the longest path is | |
| # the coverage run followed by the SonarCloud scan, around eleven, level with | |
| # the macOS leg of the platform matrix. Everything that runs a command by name | |
| # runs a pinned version, and nothing resolves a package from a registry at run | |
| # time except the audit whose job is to ask the registry. | |
| name: CI | |
| on: | |
| # No branches filter on pull_request: a stacked PR whose base is another | |
| # feature branch must run the same suite as one targeting main, or a | |
| # green check mark means "CI did not run here" instead of "CI passed". | |
| pull_request: | |
| push: | |
| branches: [main] | |
| # One in-flight run per ref: with pull_request no longer filtered by base | |
| # branch, a stacked PR's pushes would otherwise pile up obsolete runs. | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| # The Go version is not stated here: every setup-go step reads it from go.mod, | |
| # which is the one place a Go bump has to touch. The same goes for Node | |
| # (site/.node-version) and pnpm (the packageManager field in site/package.json). | |
| env: | |
| COVERAGE_MIN: "90" | |
| # Freshness gates compare a committed, generated artifact (README stats, | |
| # llms*.txt, the tool snapshots, the token footprint, the testing reference, | |
| # the request inventory, the manifests) with what the tree would generate | |
| # now. In a stack every layer below the top would fail on drift the top | |
| # overwrites, and refreshing each layer costs a suite run per layer for | |
| # artifacts nobody keeps. So they are deferred there and run where the | |
| # artifacts land: at the top of a stack, on a pull request to main that is | |
| # not in a stack, and on every push to main. The unit suite reads the same | |
| # answer through GITLAB_MCP_TEST_SNAPSHOT_PARITY. | |
| # | |
| # A pull request in a GitHub stack is tested as the whole stack up to it | |
| # merged into the stack's base, and github.base_ref is that base (main) for | |
| # every layer, so the layer is told from its position in the stack rather | |
| # than from its own base branch, which stays in the event payload. A pull | |
| # request outside a stack whose base is another branch is deferred as before. | |
| FRESHNESS: ${{ github.event_name == 'pull_request' && ((github.event.pull_request.stack != null && github.event.pull_request.stack.position != github.event.pull_request.stack.size) || (github.event.pull_request.stack == null && github.event.pull_request.base.ref != 'main')) && 'deferred' || 'checked' }} | |
| jobs: | |
| # The unit suite with coverage, and nothing else: it is the longest single | |
| # command in the pipeline at close to seven minutes, so anything that shares | |
| # a job with it waits on it for no reason, and the only consumer of its | |
| # output is the SonarCloud scan below. | |
| coverage: | |
| name: "🧪 Coverage" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 | |
| with: | |
| go-version-file: go.mod | |
| - run: go mod download | |
| - name: Run tests with coverage | |
| # The suite also records every request it issues, which is what the | |
| # inventory gate below merges. Recording is a shard file per test | |
| # process and costs the run nothing measurable, so it rides along here | |
| # rather than paying for a second seven-minute suite of its own. | |
| env: | |
| GITLAB_MCP_TEST_INVENTORY_DIR: ${{ github.workspace }}/dist/request-inventory | |
| GITLAB_MCP_TEST_SNAPSHOT_PARITY: ${{ env.FRESHNESS }} | |
| run: | | |
| go test -count=1 -coverpkg=./cmd/...,./internal/... -coverprofile=coverage.out ./cmd/... ./internal/... | |
| go tool cover -func=coverage.out | |
| COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | tr -d '%') | |
| echo "Total coverage: ${COVERAGE}%" | |
| if awk "BEGIN {exit !(${COVERAGE} + 0 < ${COVERAGE_MIN} + 0)}"; then | |
| echo "FAIL: coverage ${COVERAGE}% is below minimum ${COVERAGE_MIN}%" | |
| exit 1 | |
| fi | |
| echo "PASS: coverage ${COVERAGE}% meets minimum ${COVERAGE_MIN}%" | |
| - name: Check the recorded request inventory | |
| if: env.FRESHNESS == 'checked' | |
| run: go run ./cmd/gen_request_inventory/ -check | |
| - name: Upload coverage report | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: coverage | |
| path: coverage.out | |
| retention-days: 1 | |
| # Every gate that compares a committed artifact with what the source tree | |
| # would generate now. They used to sit in front of the coverage run in one | |
| # job, which meant a stale README table delayed the coverage result by two | |
| # minutes and a failing one hid it entirely. None of them needs the suite. | |
| generated: | |
| name: "🔎 Generated artifacts" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 | |
| with: | |
| go-version-file: go.mod | |
| - run: go mod download | |
| - name: Check generated llms files | |
| if: env.FRESHNESS == 'checked' | |
| run: make check-llms | |
| - name: Check LobeHub manifest | |
| if: env.FRESHNESS == 'checked' | |
| run: make check-lhm-manifest | |
| - name: Check documented tool names exist | |
| run: make check-doc-tool-names | |
| - name: Check no read-only action reaches a GraphQL mutation | |
| run: make check-readonly-graphql | |
| - name: Check the pinned GraphQL schema parses | |
| run: make check-graphql-schema | |
| # The REST twin of the schema pin, and the oracle that is evaluated | |
| # rather than parsed: what a booted GitLab says its own API is. It | |
| # replaced two records that were readings of text, the OpenAPI document | |
| # GitLab generates and a scan of the Grape source beside it, and answers | |
| # both of their questions from one reading of one instance. The gate | |
| # reads the committed record and needs no Docker, which is the whole | |
| # reason the boot lives in a generator. | |
| - name: Check the pinned live GitLab record | |
| run: make check-api-live | |
| # A meta-tool's description is checked against the parameters its routes | |
| # take, which is the third party the golden snapshot never was (issue 574). | |
| - name: Check every meta-tool description against its routes | |
| run: make check-meta-descriptions | |
| # The test transport judges every document a test sends. This judges the | |
| # ones no test reaches, which is why it is a gate of its own. | |
| - name: Check every GraphQL document against the pinned schema | |
| run: make check-graphql-documents | |
| # The other half of the same exchange: the struct each document is | |
| # decoded into, judged against the selection set. No test can see a | |
| # decoder disagreeing with GitLab, because the fixture is written to | |
| # match the decoder. | |
| - name: Check every GraphQL decoder against its document | |
| run: make check-graphql-shapes | |
| # R-PATH, the dimension that reads the request rather than the surface. | |
| # It reads the committed inventory the coverage job regenerates, so it | |
| # needs no suite run and no network, and it fails on a package the suite | |
| # never drives as loudly as on a document GitLab would refuse. | |
| - name: Check the request every action issues | |
| run: make audit-1to1-paths | |
| - name: Check the one-click install buttons | |
| run: make check-install-buttons | |
| # The manifest validator is the registry's publisher, a module whose 77 | |
| # direct dependencies are not this project's, so it is not a go.mod tool. | |
| # It is installed once per pinned version into a directory named after | |
| # that version and cached by that version, so a push resolves nothing | |
| # over the network and a stale binary can never validate with another | |
| # release. | |
| - name: Read the pinned publisher version | |
| id: publisher | |
| run: echo "version=$(make -s mcp-publisher-version)" >> "$GITHUB_OUTPUT" | |
| - name: Cache the MCP Registry publisher | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.cache/gitlab-mcp-server/mcp-publisher/${{ steps.publisher.outputs.version }} | |
| key: mcp-publisher-${{ steps.publisher.outputs.version }}-${{ runner.os }} | |
| - name: Check MCP Registry manifest | |
| run: make check-server-json | |
| # Downloads every declared artifact, so it runs on main only. A pull | |
| # request does not move server.json — the release stamp does — and paying | |
| # a 40 MB download plus a registry round-trip on every push would trade a | |
| # real gate for a flaky one. | |
| - name: Check MCP Registry packages are published | |
| if: github.event_name == 'push' | |
| run: make check-server-json-packages | |
| - name: Check plugin manifests (Agent Plugins + legacy Open Plugins) | |
| run: make check-openplugin | |
| - name: Check README repository statistics | |
| if: env.FRESHNESS == 'checked' | |
| run: make check-stats | |
| - name: Check test-goroutine aborts | |
| run: make check-test-goroutines | |
| - name: Check case loops run as subtests | |
| run: make check-test-subtests | |
| - name: Check Markdown formatters escape what they interpolate | |
| run: make check-md-escaping | |
| - name: Check gateway-safe served characters | |
| run: make check-gateway-chars | |
| - name: Check test-file naming convention | |
| run: make check-test-file-names | |
| - name: Check token footprint | |
| if: env.FRESHNESS == 'checked' | |
| run: make check-footprint | |
| - name: Check site stats | |
| if: env.FRESHNESS == 'checked' | |
| run: make check-site-stats | |
| - name: Check brand assets | |
| run: make brand-check | |
| # check-icon-webp is deliberately NOT run here: it is a byte-level | |
| # comparison whose output depends on the librsvg/cwebp versions doing | |
| # the rendering — CI's Ubuntu toolchain produces different (equally | |
| # valid) bytes than the maintainer machine for a handful of icons, so | |
| # the gate only holds on the machine that committed the assets. It | |
| # stays maintainer tooling, run with gen-icon-webp after icon edits. | |
| # | |
| # The benchmark pages take their numbers from the committed measurement | |
| # record, so nothing is measured here and the step costs seconds. It is a | |
| # freshness gate all the same, because the drawing around those numbers | |
| # comes from the source tree: the palette from site/src/styles/theme.css | |
| # and the chart and table strings from the command itself, either of | |
| # which makes the committed SVGs and page blocks stale. update-all | |
| # redraws them, so a stack refreshes them once at its top like the rest. | |
| - name: Check the committed benchmark charts and tables | |
| if: env.FRESHNESS == 'checked' | |
| run: make check-bench-resources | |
| - name: Check action catalog manifest | |
| if: env.FRESHNESS == 'checked' | |
| run: make check-action-catalog-manifest | |
| # Everything in the testing reference that the source tree determines: | |
| # test counts, naming breakdown, per-layer tables, and which packages | |
| # appear in the coverage tables, which is what caught one missing from | |
| # them. Not the coverage percentages: those depend on the machine that | |
| # measured them (tests asserting permission refusals skip for uid 0, | |
| # cmd/gen_icon_webp needs rsvg-convert and cwebp, cmd/server has a | |
| # load-dependent branch), so a gate on them would go red over tenths of a | |
| # percent nobody can fix. Carrying them forward instead also means no | |
| # coverage pass here at all, so this costs seconds on a job SonarCloud | |
| # already waits on. `make gen-testing-docs` refreshes the numbers. | |
| - name: Check the generated testing reference | |
| if: env.FRESHNESS == 'checked' | |
| run: make check-testing-docs | |
| sonarcloud: | |
| name: "📊 SonarCloud" | |
| runs-on: ubuntu-latest | |
| # The only job here with a bound, because a hung scanner would otherwise | |
| # sit on a runner for the six-hour default. It has to leave room for the | |
| # scan to grow with the repository: at 5 minutes it started failing every | |
| # attempt once the analysis reached 250s on top of ~51s of setup — 302s | |
| # against a 300s ceiling. The scan itself had finished; what was cut off | |
| # was the upload of the 8 MB report, so the run was killed after doing | |
| # all the work and before recording any of it. 15 keeps the guard against | |
| # a genuine hang while giving the scan roughly three times its current | |
| # runtime, so ordinary growth does not re-break it. | |
| timeout-minutes: 15 | |
| needs: [coverage] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: coverage | |
| - name: SonarCloud Scan | |
| if: env.SONAR_TOKEN != '' | |
| uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8 | |
| continue-on-error: true | |
| env: | |
| SONAR_HOST_URL: https://sonarcloud.io | |
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Split out of the SonarCloud job, where it was the step before the scan and | |
| # the only one that could fail it. The scan itself is continue-on-error, so a | |
| # required check named after SonarCloud could not fail because of SonarCloud | |
| # and could fail because npmjs.org did not answer, which happened on three | |
| # pull requests in one afternoon: `pnpm audit` retried twice and timed out, | |
| # the scan was skipped entirely, and the merge was blocked by something that | |
| # is neither code quality nor under our control. | |
| # | |
| # Separated, the audit reports its own verdict under its own name and a | |
| # registry outage stops blocking merges, while the analysis still runs and is | |
| # still published. This job is deliberately not in the required-check list: | |
| # what it reports is the state of somebody else's advisory database, which | |
| # can turn red without a commit. | |
| docs-audit: | |
| name: "📦 Site dependency audit" | |
| runs-on: ubuntu-latest | |
| # The whole job is one call to somebody else's registry, and that call has | |
| # already stalled here: pnpm retried twice over two minutes and gave up. | |
| # Without a bound, a registry that accepts the connection and never answers | |
| # holds a runner for the six-hour default. | |
| timeout-minutes: 15 | |
| steps: | |
| # This job runs third-party code: `pnpm install` may execute a | |
| # dependency's lifecycle scripts. Without this the checkout token stays | |
| # in .git/config while they run, so a compromised dependency in the | |
| # documentation site's tree could read it. Nothing here talks to git | |
| # after the checkout. | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| # pnpm/action-setup installs pnpm itself from the npm registry on every | |
| # run, into ~/setup-pnpm, and that download took 54 seconds here and 164 | |
| # in the Pages build on a slow registry day, against 8 seconds for the | |
| # dependency install that follows it from cache. So the installed pnpm is | |
| # cached too, keyed on the exact packageManager string in package.json, | |
| # and on a hit the action is skipped and its bin directory put on PATH. | |
| - name: Read the pinned pnpm version | |
| id: pnpm | |
| run: echo "spec=$(jq -r .packageManager site/package.json)" >> "$GITHUB_OUTPUT" | |
| - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| id: pnpm-cache | |
| with: | |
| path: ~/setup-pnpm | |
| key: pnpm-${{ runner.os }}-${{ steps.pnpm.outputs.spec }} | |
| - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 | |
| if: steps.pnpm-cache.outputs.cache-hit != 'true' | |
| with: | |
| package_json_file: site/package.json | |
| - name: Put the cached pnpm on PATH | |
| if: steps.pnpm-cache.outputs.cache-hit == 'true' | |
| run: echo "$HOME/setup-pnpm/node_modules/.bin" >> "$GITHUB_PATH" | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 | |
| with: | |
| node-version-file: site/.node-version | |
| cache: pnpm | |
| cache-dependency-path: site/pnpm-lock.yaml | |
| - run: pnpm install --frozen-lockfile | |
| working-directory: site | |
| # Advisories already judged live in site/pnpm-workspace.yaml, under | |
| # auditConfig.ignoreGhsas, each with the reason beside it. That is the | |
| # only place pnpm reads them from: from v10 the settings moved out of | |
| # package.json, and a `pnpm` field there is now ignored with a warning | |
| # rather than an error, so an exception added to it looks applied and is | |
| # not. | |
| # | |
| # A registry that does not answer produces no verdict, and reporting one | |
| # anyway is the mistake this job was split out of the SonarCloud job to | |
| # stop making: a red check that means "npmjs.org timed out" is | |
| # indistinguishable from one that means "a dependency has a high | |
| # advisory", so the second stops being read. pnpm exits non-zero for | |
| # both, so the two are told apart by what it printed, and an unanswered | |
| # run is annotated rather than failed. | |
| - name: Audit docs dependencies | |
| shell: bash | |
| working-directory: site | |
| run: | | |
| set +e | |
| output=$(pnpm audit --audit-level=high 2>&1) | |
| status=$? | |
| printf '%s\n' "$output" | |
| if [ "$status" -ne 0 ] && printf '%s' "$output" | | |
| grep -qE 'TimeoutError|operation was aborted|ENOTFOUND|EAI_AGAIN|ECONNRESET|ECONNREFUSED|socket hang up'; then | |
| echo "::warning title=Dependency audit did not run::the npm advisories endpoint did not answer, so this run says nothing about the dependencies" | |
| exit 0 | |
| fi | |
| exit "$status" | |
| # The one thing every job below this one has in common: the server and the | |
| # e2e suite compile. It is a minute, and it is the parent of the type-check | |
| # matrix and the two transport modules so that the graph shows them as what | |
| # they are, three questions asked of one binary. The children do not receive | |
| # it as an artifact, since each transport harness builds ./cmd/server itself | |
| # in its TestMain; waiting on this job costs them a minute and saves nothing | |
| # when it passes, but it is a minute well below the pipeline's longest path, | |
| # so the picture is free. | |
| compile: | |
| name: "🔨 Compile" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 | |
| with: | |
| go-version-file: go.mod | |
| - run: go mod download | |
| - run: go build -o /dev/null ./cmd/server | |
| - name: Verify E2E tests compile | |
| run: go test -tags e2e -c -o /dev/null ./test/e2e/suite/ | |
| # A second compile rather than a second tag on the line above: the | |
| # suite's *_ce_test.go and *_ee_test.go files exclude each other, so one | |
| # compile sees one half and never the other. Between the day the first | |
| # _ee_test.go was written and issue 570, nothing compiled the 41 EE files | |
| # except a person running make test-e2e-docker-enterprise by hand. | |
| - name: Verify the Enterprise E2E tests compile | |
| run: go test -tags "e2e enterprise" -c -o /dev/null ./test/e2e/suite/ | |
| # Type-checks every build-constrained file including the tests, for each | |
| # platform this project ships, on a Linux runner. The cross-platform matrix | |
| # does this too, on real runners, but that matrix is expensive enough that | |
| # somebody may reasonably narrow it later; this job means narrowing it can | |
| # never take the compile of cmd/server/socket_mode_windows.go with it, which | |
| # before either existed was first compiled by GoReleaser during a release. | |
| # | |
| # One job per target rather than a loop in one job: the three vets took four | |
| # minutes in series and take under two in parallel, and a failure names the | |
| # platform in the job list instead of in a log. | |
| typecheck: | |
| name: "🧭 Type-check (${{ matrix.target }})" | |
| runs-on: ubuntu-latest | |
| needs: [compile] | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: [windows/amd64, darwin/arm64, linux/arm64] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 | |
| with: | |
| go-version-file: go.mod | |
| - run: go mod download | |
| - name: Vet for ${{ matrix.target }} | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| run: GOOS="${TARGET%%/*}" GOARCH="${TARGET##*/}" go vet ./... | |
| # The HTTP transport module needs no GitLab and no credentials, so it runs | |
| # here rather than only on demand. It is the gate for the handler chain in | |
| # package main — cross-origin, preflight, auth modes, rate limiting, and every | |
| # flag that restricts something — which unit tests cannot reach and which has | |
| # shipped broken more than once. The nginx cases skip when Docker is | |
| # unavailable. | |
| e2e-http: | |
| name: "🌐 HTTP transport e2e" | |
| runs-on: ubuntu-latest | |
| needs: [compile] | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 | |
| with: | |
| go-version-file: go.mod | |
| - run: go mod download | |
| - name: HTTP transport end-to-end | |
| run: go test -tags httpe2e -count=1 -timeout 900s ./test/e2e/http/ | |
| # stdio is the primary transport and nothing drove it until this module | |
| # existed: the e2e suite uses an in-memory transport in the same process, so | |
| # no pipes, no process, no separation of stdout from stderr, and none of the | |
| # environment-variable configuration stdio mode actually uses. Two defects | |
| # that shipped were reachable here and invisible everywhere else, one of them | |
| # held in place by a unit test asserting the opposite. | |
| e2e-stdio: | |
| name: "🔌 stdio transport e2e" | |
| runs-on: ubuntu-latest | |
| needs: [compile] | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 | |
| with: | |
| go-version-file: go.mod | |
| - run: go mod download | |
| - name: stdio transport end-to-end | |
| run: go test -tags stdioe2e -count=1 -timeout 900s ./test/e2e/stdio/ | |
| # The linter used to be `go install …@latest`: fifty seconds compiling it | |
| # from source on every run, and whichever version the proxy served that | |
| # morning, so two runs of the same commit could disagree. The official action | |
| # downloads a release binary for an exact version and caches it. It only | |
| # installs; the Makefile target is still what runs, so CI and a developer's | |
| # `make golangci-lint` are the same three commands. | |
| golangci-lint: | |
| name: "🧹 golangci-lint" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 | |
| with: | |
| go-version-file: go.mod | |
| - run: go mod download | |
| # The version lives in the Makefile beside the other tool pins, so it is | |
| # stated once; the reason it is not a go.mod tool directive is recorded | |
| # there. | |
| - name: Read the pinned linter version | |
| id: lint | |
| run: echo "version=$(sed -n 's/^GOLANGCI_LINT_VERSION := //p' Makefile)" >> "$GITHUB_OUTPUT" | |
| - uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 | |
| with: | |
| version: ${{ steps.lint.outputs.version }} | |
| install-only: true | |
| # golangci-lint keeps a per-package analysis cache and skips what has | |
| # not changed, which is most of the tree on most pull requests. The key | |
| # names the linter config and go.sum, so a change to either starts | |
| # fresh; restore-keys lets a run reuse the newest cache for the same | |
| # config even when go.sum moved, since a dependency bump does not | |
| # invalidate the analysis of packages that do not import it. | |
| - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.cache/golangci-lint | |
| key: golangci-lint-${{ hashFiles('.golangci.yml', 'go.sum') }}-${{ github.sha }} | |
| restore-keys: | | |
| golangci-lint-${{ hashFiles('.golangci.yml', 'go.sum') }}- | |
| golangci-lint- | |
| - run: make golangci-lint | |
| govulncheck: | |
| name: "🛡️ govulncheck" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 | |
| with: | |
| go-version-file: go.mod | |
| - run: go mod download | |
| # No version here on purpose: govulncheck is a `tool` directive in go.mod, | |
| # so `go install` without @version installs the version go.mod names and | |
| # Dependabot bumps it with the other modules. A scanner that changed under | |
| # us would change what "no known vulnerability" meant last week. | |
| - run: go install golang.org/x/vuln/cmd/govulncheck | |
| - run: make govulncheck | |
| # This job took six and a half minutes on a day the npm registry was slow, of | |
| # which the lint itself was eight seconds: `npx --yes` resolved and installed | |
| # markdownlint-cli2 from the registry on every run, and then did the same for | |
| # the mcpb CLI. The linter now comes bundled in its own action, which touches | |
| # no registry, and the CLI's npx cache is kept between runs keyed on the | |
| # version the Makefile pins, so the registry is asked once per version. | |
| analyze-md: | |
| name: "📝 Markdown" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24.2.0 | |
| with: | |
| globs: | | |
| **/*.{md,mdx} | |
| #plan | |
| #node_modules | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 | |
| with: | |
| node-version-file: site/.node-version | |
| - name: Read the pinned mcpb CLI version | |
| id: mcpb | |
| run: echo "version=$(sed -n 's/^MCPB_CLI_VERSION := //p' Makefile)" >> "$GITHUB_OUTPUT" | |
| - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.npm/_npx | |
| key: npx-mcpb-${{ steps.mcpb.outputs.version }} | |
| # Node-based freshness gates live here rather than beside the Go gates, | |
| # which have no Node toolchain. check-doc-links resolves every relative | |
| # link in tracked Markdown, so it needs the working tree, not just the Go | |
| # module. | |
| - name: Check documentation local links | |
| run: make check-doc-links | |
| - name: Check Claude Desktop extension manifest | |
| run: make check-mcpb | |
| site-lint: | |
| name: "🎨 Site lint" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| # Same reasoning as docs-audit: this is the other job that installs the | |
| # documentation site's dependencies, so it is the other one that runs | |
| # code this repository did not write. | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| # Same arrangement as docs-audit, for the same 54-second reason. This job | |
| # used to ask for "version: 11" here while package.json pins pnpm to an | |
| # exact version with a hash; reading that field is what makes the two | |
| # jobs, Pages and a developer's machine install the same pnpm. | |
| - name: Read the pinned pnpm version | |
| id: pnpm | |
| run: echo "spec=$(jq -r .packageManager site/package.json)" >> "$GITHUB_OUTPUT" | |
| - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| id: pnpm-cache | |
| with: | |
| path: ~/setup-pnpm | |
| key: pnpm-${{ runner.os }}-${{ steps.pnpm.outputs.spec }} | |
| - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 | |
| if: steps.pnpm-cache.outputs.cache-hit != 'true' | |
| with: | |
| package_json_file: site/package.json | |
| - name: Put the cached pnpm on PATH | |
| if: steps.pnpm-cache.outputs.cache-hit == 'true' | |
| run: echo "$HOME/setup-pnpm/node_modules/.bin" >> "$GITHUB_PATH" | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 | |
| with: | |
| node-version-file: site/.node-version | |
| cache: pnpm | |
| cache-dependency-path: site/pnpm-lock.yaml | |
| # The build-independent half of `pnpm run lint`: type check, palette | |
| # contrast, chip parity, i18n parity, llms index coverage, eslint, | |
| # prettier. The dist-dependent gates (html-validate, htmlhint, pa11y) | |
| # stay in the Pages workflow, which is the one that builds the site. | |
| - name: Install site dependencies | |
| run: pnpm install --frozen-lockfile | |
| working-directory: site | |
| - name: Static site gates | |
| run: | | |
| pnpm run check | |
| pnpm run contrast:check | |
| pnpm run chips:check | |
| pnpm run facts:check | |
| pnpm run i18n:check | |
| # The site's own /llms.txt is generated from the content collection | |
| # against a section table that has to be edited by hand. A page added | |
| # to the collection and not listed there would simply be missing from | |
| # the index, which nothing else would notice. | |
| pnpm run llms:check | |
| # The Person entity is fetched from its canonical source at build | |
| # time, with this snapshot as the offline fallback. A stale snapshot | |
| # is invisible while the fetch works and ships a Person missing | |
| # several properties the moment it does not, announced by nothing | |
| # louder than a console.warn. The guard existed and nothing ran it. | |
| pnpm run identity:check | |
| pnpm run eslint | |
| pnpm run format:check | |
| working-directory: site | |
| hadolint: | |
| name: "📐 hadolint" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387 # v3.5.0 | |
| with: | |
| dockerfile: Dockerfile | |
| failure-threshold: warning | |
| ignore: DL3008,DL3018 | |
| server-json-schema: | |
| name: "📄 server.json schema" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 | |
| with: | |
| python-version: "3.x" | |
| - name: Install jsonschema | |
| run: pip install --quiet jsonschema | |
| - name: Validate server.json against MCP Registry schema (2025-12-11) | |
| run: | | |
| set -euo pipefail | |
| SCHEMA_URL=$(jq -r '."$schema"' server.json) | |
| echo "Schema referenced by server.json: $SCHEMA_URL" | |
| # SSRF guard: only allow the official MCP Registry schema host. | |
| # Prevents an attacker from changing $schema in a PR to probe | |
| # arbitrary URLs from the runner. | |
| ALLOWED_PREFIX="https://static.modelcontextprotocol.io/" | |
| case "$SCHEMA_URL" in | |
| "$ALLOWED_PREFIX"*) ;; | |
| *) | |
| echo "::error::Schema URL must start with $ALLOWED_PREFIX (got: $SCHEMA_URL)" | |
| exit 1 | |
| ;; | |
| esac | |
| curl -fsSL --proto '=https' --tlsv1.2 "$SCHEMA_URL" -o /tmp/mcp-schema.json | |
| python3 -c "import json,jsonschema; \ | |
| schema=json.load(open('/tmp/mcp-schema.json')); \ | |
| inst=json.load(open('server.json')); \ | |
| jsonschema.validate(inst, schema); \ | |
| print('server.json: VALID against', schema.get('\$id'))" | |
| # Both published platforms, each one built AND started. This job used to | |
| # build only the runner's native architecture and never run it, so the | |
| # linux/arm64 image that could not exec at all — the cross-compiled PIE | |
| # binary asked for the glibc loader the Alpine runtime does not ship — | |
| # passed CI and every release gate on its way to the registry. | |
| docker: | |
| name: "🐳 Docker (${{ matrix.platform }})" | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| platform: [linux/amd64, linux/arm64] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for Docker changes | |
| id: filter | |
| run: | | |
| CHANGED=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.sha }} -- Dockerfile docker-compose*.yml .dockerignore) | |
| if [ -n "$CHANGED" ]; then | |
| echo "docker=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "docker=false" >> "$GITHUB_OUTPUT" | |
| echo "No Docker-related files changed — skipping build" | |
| fi | |
| - name: Compute build date | |
| if: steps.filter.outputs.docker == 'true' | |
| id: build-date | |
| run: echo "date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT" | |
| # QEMU so the non-native variant can be executed here, not only built. | |
| - name: Set up QEMU | |
| if: steps.filter.outputs.docker == 'true' | |
| uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4 | |
| - name: Set up Docker Buildx | |
| if: steps.filter.outputs.docker == 'true' | |
| uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 | |
| - name: Build Docker image | |
| if: steps.filter.outputs.docker == 'true' | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 | |
| with: | |
| context: . | |
| push: false | |
| load: true | |
| platforms: ${{ matrix.platform }} | |
| tags: gitlab-mcp-server:ci | |
| build-args: | | |
| VERSION=ci | |
| COMMIT=${{ github.sha }} | |
| BUILD_DATE=${{ steps.build-date.outputs.date }} | |
| - name: Smoke test the image | |
| if: steps.filter.outputs.docker == 'true' | |
| run: bash scripts/smoke-test-image.sh ci "gitlab-mcp-server:ci=${{ matrix.platform }}" | |
| # One node for the matrix above. It began life because branch protection | |
| # required a check named exactly "Docker Build" and a matrix job reports | |
| # "Docker Build (linux/amd64)" instead, which left the required check | |
| # permanently "expected" and every pull request unmergeable. The single | |
| # required check is "CI" now, so that reason is gone; this stays because the | |
| # final job wants one answer for "did the images build" rather than one per | |
| # platform, and because the graph reads better with the fan-in drawn. | |
| docker-build: | |
| name: "🐳 Docker images" | |
| needs: docker | |
| # always(), so a failing matrix is reported rather than skipped along with | |
| # it. The event guard matches the matrix's own, so pushes to main see this | |
| # check no more than they see the job it summarizes. | |
| if: always() && github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Report the platform builds | |
| env: | |
| RESULT: ${{ needs.docker.result }} | |
| run: | | |
| echo "Docker matrix result: $RESULT" | |
| # skipped is a pass: the matrix skips its own steps when no Docker | |
| # file changed, and a skipped job must not hold a pull request. | |
| case "$RESULT" in | |
| success|skipped) ;; | |
| *) echo "::error::the Docker platform builds did not succeed ($RESULT)"; exit 1 ;; | |
| esac | |
| # Configuration invariants nothing else in this pipeline can see: pinned | |
| # actions, release jobs that run no code resolved at run time, stated | |
| # Dependabot cooldowns, a security policy that names the shipping major, and | |
| # installers that verify the release signature. | |
| supply-chain: | |
| name: "🔗 Supply chain" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 | |
| with: | |
| go-version-file: go.mod | |
| # Still needed by the installer and packaging tests below, which are | |
| # Python because what they test is. The auditor no longer is, so nothing | |
| # here installs a package at run time any more. | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 | |
| with: | |
| python-version: "3.x" | |
| - run: go mod download | |
| - name: Audit the supply chain configuration | |
| run: make check-supply-chain | |
| - name: Test the auditor itself | |
| run: go test ./cmd/audit_supply_chain/ -count=1 | |
| - name: Test the installer and packaging scripts | |
| run: python3 -m unittest discover -s scripts -p "*_test.py" -v | |
| # One node for the jobs that depend on nothing. They are fanned in here | |
| # rather than straight into "ci" for the graph's sake, explained at the top | |
| # of the file: this node lands below the matrix summaries, because its | |
| # parents are drawn last, and every edge stays between neighbouring columns. | |
| # | |
| # The site dependency audit reaches here and is printed, but does not | |
| # decide: it reports the state of somebody else's advisory database. | |
| checks: | |
| name: "🧰 Checks" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| if: always() | |
| # Everything that can fail a change is listed, the test jobs included: the | |
| # verdict used to take only the analysis jobs, so a failing unit suite, a | |
| # compile error on another platform, or a red Windows leg could sit under | |
| # a green required check. The cross-platform summary already tells the | |
| # known Go runtime crash on AMX hosts apart from a failing test (issue | |
| # 467), so listing it fails the verdict on real failures only. | |
| needs: | |
| - generated | |
| - golangci-lint | |
| - govulncheck | |
| - analyze-md | |
| - site-lint | |
| - hadolint | |
| - server-json-schema | |
| - supply-chain | |
| - docs-audit | |
| - coverage | |
| - compile | |
| - typecheck | |
| - cross_platform_summary | |
| - docker-build | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| sparse-checkout: .github/scripts | |
| - name: Decide | |
| env: | |
| RESULTS: ${{ toJSON(needs) }} | |
| INFORMATIONAL: docs-audit | |
| # The image build only runs for a pull request; on a push it is | |
| # skipped by design rather than failed. | |
| MAY_SKIP: docker-build | |
| run: bash .github/scripts/needs-verdict.sh | |
| # Every job above runs on Linux, and until this one existed that was the only | |
| # platform any test in this repository had ever seen, while the project ships | |
| # binaries for three. cmd/server/socket_mode_windows.go was first compiled by | |
| # GoReleaser during a release, where a compile error breaks the release | |
| # instead of a pull request. | |
| # | |
| # This job asks only the platform question, and deliberately runs none of the | |
| # generated-artifact gates or the coverage floor: their answers cannot depend | |
| # on the operating system, so running them three times would triple the flake | |
| # surface and add no signal. What does depend on the platform is here, namely | |
| # the build constraints, every path the code touches, and the two transport | |
| # modules that spawn the real binary. | |
| # | |
| # Linux is in the matrix even though Test already runs the suite there, | |
| # because it is the control: without it, a macOS-only failure cannot be told | |
| # apart from a command that would have failed anywhere. | |
| cross_platform: | |
| name: "🖥️ Cross-platform (${{ matrix.os }})" | |
| runs-on: ${{ matrix.os }} | |
| # A hung test must not sit for the six-hour default on a runner billing ten | |
| # times the Linux rate (macOS) or twice it (Windows). The Linux unit suite | |
| # takes about twenty minutes, so this allows being substantially slower | |
| # without allowing a hang. | |
| timeout-minutes: 60 | |
| strategy: | |
| # Every platform reports. Stopping at the first failure would hide a | |
| # second, different one, and telling them apart is the entire point. | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 | |
| with: | |
| go-version-file: go.mod | |
| - run: go mod download | |
| # Build constraints resolve against the host, so these two steps are the | |
| # first thing in the pipeline that compiles the windows-only source at | |
| # all, and the first that type-checks the unix-only source anywhere other | |
| # than Linux. | |
| - name: Build every package | |
| run: go build ./... | |
| - name: Vet every package | |
| run: go vet ./... | |
| # The whole tree, tooling included. Everything under cmd/ other than | |
| # the server is maintainer tooling that runs on Linux from a Makefile | |
| # target, and its tests used to assume "/" and Linux error text, so the | |
| # leg ran the shipped code alone. The fifteen tests that failed on a | |
| # real Windows were made portable (issue 439): reported paths are | |
| # spelled with slashes on every platform, not-found is asserted with | |
| # errors.Is rather than by its text, and the one case a platform cannot | |
| # produce is skipped there and says why. Running them here is what keeps | |
| # that true. | |
| # | |
| # The bound is raised from the 10 minute default because cmd/server | |
| # exceeded it on Windows, where the suite runs roughly three times slower | |
| # than on Linux. | |
| # | |
| # Through the crash-aware runner rather than a bare go test: the Windows | |
| # leg dies inside the Go runtime now and then, for a reason recorded in | |
| # the script and in issue 467, and the runner tells that crash apart | |
| # from a failing test, reruns only the crashed package, and puts the | |
| # outcome in the job summary. A failing test fails the leg as before. | |
| # Flags carry their value in the same word, which is how the runner | |
| # tells them from packages. | |
| - name: Install gotestsum | |
| run: go install gotest.tools/gotestsum | |
| - name: Unit suite | |
| shell: bash | |
| # The same freshness answer the coverage job passes. This job asks the | |
| # platform question and none of the generated-artifact ones, but the | |
| # suite it runs carries a handful of tests that compare a committed | |
| # artifact with what the tree generates now, and with the variable | |
| # unset they compared on all three platforms even on a layer the | |
| # policy exempts, three times over (issue 644). | |
| env: | |
| GITLAB_MCP_TEST_SNAPSHOT_PARITY: ${{ env.FRESHNESS }} | |
| run: bash .github/scripts/go-test-crash-aware.sh test-results/unit-suite -count=1 -timeout=30m ./cmd/... ./internal/... | |
| # The two transport modules need neither GitLab nor a credential, and | |
| # they are why this is not a build-only check: they start the real binary | |
| # and drive it the way a client does, over pipes and sockets, which is | |
| # where a platform difference actually surfaces. | |
| # | |
| # macOS and Windows, not Linux. Linux has them as jobs of their own, | |
| # e2e-http and e2e-stdio, on the same runner image with the same | |
| # commands, so running them here as well was six minutes of runner time | |
| # answering a question already answered in the same run. The unit suite | |
| # above does stay on Linux, as the control that tells a failure of one | |
| # platform from one that would fail anywhere. | |
| # | |
| # The harnesses terminate the server the way each platform does: SIGTERM | |
| # where it exists, and on Windows a CTRL_BREAK event to the server's own | |
| # console process group, which the Go runtime delivers as the | |
| # os.Interrupt the server stops on (terminate_*_test.go in each module). | |
| # The unix-socket cases assert only what Windows' AF_UNIX support shares | |
| # with the others. | |
| - name: HTTP transport end-to-end | |
| if: matrix.os != 'ubuntu-latest' | |
| run: go test -tags httpe2e -count=1 -timeout 900s ./test/e2e/http/ | |
| - name: stdio transport end-to-end | |
| if: matrix.os != 'ubuntu-latest' | |
| run: go test -tags stdioe2e -count=1 -timeout 900s ./test/e2e/stdio/ | |
| # One node for the matrix above, for the same reason the Docker images job | |
| # exists: the final job wants one answer for the platform question, and the | |
| # graph draws the fan-in. | |
| cross_platform_summary: | |
| name: "🖥️ Cross-platform" | |
| needs: cross_platform | |
| # always(), so a failing platform is reported rather than skipped along | |
| # with the job it belongs to. | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Report the platform suites | |
| env: | |
| # Underscored job ids on purpose: a hyphen in a job id has to be | |
| # reached as needs['cross-platform'], because the expression parser | |
| # reads needs.cross-platform as a subtraction. | |
| RESULT: ${{ needs.cross_platform.result }} | |
| run: | | |
| echo "Cross-platform matrix result: $RESULT" | |
| case "$RESULT" in | |
| success) ;; | |
| *) echo "::error::the cross-platform suites did not succeed ($RESULT)"; exit 1 ;; | |
| esac | |
| # The one check branch protection requires. Everything converges here, so | |
| # the job list above can change shape without a repository setting having to | |
| # follow it, and the graph has an end. | |
| # | |
| # It needs only second-column nodes, for the graph's sake (see the top of | |
| # the file), and loses nothing by it: compile and coverage are decided | |
| # through their children, which are skipped when the parent fails, and the | |
| # standalone jobs through "checks". One result is read but does not decide, | |
| # for a reason recorded where the job is defined: the platform matrix has a | |
| # Windows leg that crashes inside the Go runtime now and then, tracked in | |
| # https://github.com/jmrplens/gitlab-mcp-server/issues/467. Its unit suite | |
| # runs through .github/scripts/go-test-crash-aware.sh, which reruns a | |
| # crashed package alone and records the crash in the job summary, so a red | |
| # leg now means a failing test or a package that crashed twice. It is | |
| # printed so a red one is seen; it becomes a deciding result once a run of | |
| # weeks shows it red for those reasons only. | |
| # | |
| # "skipped" passes only for the Docker summary, which skips itself on pushes | |
| # to main along with the matrix it summarizes. For every other job a skip | |
| # means an upstream failure took it down, which is a failure. | |
| ci: | |
| name: "✅ CI" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| if: always() | |
| needs: | |
| - typecheck | |
| - e2e-http | |
| - e2e-stdio | |
| - sonarcloud | |
| - docker-build | |
| - cross_platform_summary | |
| - checks | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| sparse-checkout: .github/scripts | |
| - name: Decide | |
| env: | |
| RESULTS: ${{ toJSON(needs) }} | |
| INFORMATIONAL: cross_platform_summary | |
| MAY_SKIP: docker-build | |
| run: bash .github/scripts/needs-verdict.sh |