596 lines
24 KiB
YAML
596 lines
24 KiB
YAML
name: Backend Tests
|
|
|
|
# No trigger-level `paths:` filter, deliberately. A workflow skipped by one
|
|
# reports no conclusion at all, and a required check that never reports leaves
|
|
# a pull request unable to enter a merge queue and a merge group unable to
|
|
# leave it. `merge_group` cannot carry a `paths:` filter in the first place.
|
|
# The filter lives in the `changes` job below instead, and the `test` job
|
|
# reports the single conclusion the queue waits on. See docs/ci-merge-queue.md.
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
merge_group:
|
|
types: [checks_requested]
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
# Never cancel a merge-queue run. Its ref is unique per queue entry, so a
|
|
# cancellation here could only come from a mis-keyed group, and the queue
|
|
# would read the cancelled check as a failure and eject the pull request.
|
|
cancel-in-progress: ${{ github.event_name != 'merge_group' }}
|
|
|
|
# Least-privilege default. The Linux static/test jobs and the Windows `test-windows`
|
|
# job only read source and run tests — no PR/issue writes, no release ops.
|
|
# `packages: read` is required for the Linux job to pull the kandev-ci
|
|
# container image from GHCR (currently public; explicit grant keeps the
|
|
# workflow working if the package ever goes private). Declaring any scope
|
|
# explicitly zeroes out every unlisted one.
|
|
permissions:
|
|
contents: read
|
|
packages: read
|
|
|
|
jobs:
|
|
# The former trigger-level `paths:` list, applied one level down so a skip
|
|
# still produces a conclusion. `.github/scripts/changed-paths.py` implements
|
|
# the same last-match-wins semantics GitHub applies to `paths:`.
|
|
changes:
|
|
name: Detect relevant changes
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
outputs:
|
|
run: ${{ steps.detect.outputs.run }}
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
# The base commit has to be present locally for the diff below. Every
|
|
# other job in this workflow already pays for a full clone.
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- name: Detect relevant changes
|
|
id: detect
|
|
env:
|
|
PATTERNS: |
|
|
apps/backend/**
|
|
# check-make-shells also lints the root Makefile's $(shell ...)
|
|
# probes, so a change there has to be able to trip it.
|
|
Makefile
|
|
scripts/check-make-shells
|
|
.github/workflows/backend-tests.yml
|
|
.github/scripts/changed-paths.py
|
|
!**/*.md
|
|
!**/*.mdx
|
|
!**/*.markdown
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
BASE_REF: ${{ github.base_ref }}
|
|
MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
|
|
PUSH_BEFORE: ${{ github.event.before }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
case "${EVENT_NAME}" in
|
|
# The checkout is the merge ref, whose first parent is the base
|
|
# branch tip. Diffing against that tip directly collapses to a
|
|
# two-dot diff over everything main landed since the PR opened, so
|
|
# take the fork point instead.
|
|
pull_request) BASE="$(git merge-base HEAD "origin/${BASE_REF}")" ;;
|
|
# The queue branch is cut from this commit, so it is a true ancestor.
|
|
merge_group) BASE="${MERGE_GROUP_BASE_SHA}" ;;
|
|
push) BASE="${PUSH_BEFORE}" ;;
|
|
*) BASE="" ;;
|
|
esac
|
|
|
|
# Fail open. A first push to a branch, a force push, or an unknown
|
|
# event leaves no usable base, and running the suite needlessly is
|
|
# cheaper than merging something no job ever looked at.
|
|
if [[ -z "${BASE}" || "${BASE}" == "0000000000000000000000000000000000000000" ]] \
|
|
|| ! git cat-file -e "${BASE}^{commit}" 2>/dev/null; then
|
|
echo "No usable base commit for '${EVENT_NAME}'; running everything."
|
|
echo "run=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
# `-z`, not plain --name-only: git quotes non-ASCII paths under its
|
|
# default core.quotePath, and a quoted path matches no pattern, so a
|
|
# change to one would skip every job and still report success.
|
|
git diff --name-only -z "${BASE}" HEAD \
|
|
| python3 .github/scripts/changed-paths.py
|
|
|
|
static_checks:
|
|
name: Backend Static Checks
|
|
needs: changes
|
|
if: needs.changes.outputs.run == 'true'
|
|
runs-on: ubuntu-latest
|
|
# Pre-baked image bundles Go, golangci-lint, go-licenses, Docker CLI, Node,
|
|
# pnpm, and the Playwright base — see .github/docker/ci-base/Dockerfile.
|
|
# Skips the per-run setup-go / golangci-lint download steps below.
|
|
container: ghcr.io/kdlbs/kandev-ci:build-latest
|
|
defaults:
|
|
run:
|
|
# Container jobs default to `sh` (dash); pin to bash so `set -o pipefail`
|
|
# and other bash-isms in the steps below work without per-step overrides.
|
|
shell: bash
|
|
working-directory: apps/backend
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
# actions/checkout under a container job marks the workspace as a safe
|
|
# directory automatically, but subsequent `git` invocations from steps
|
|
# that run outside the action (e.g. the gofmt diff below) can still hit
|
|
# "dubious ownership" when the runner UID differs from the repo owner.
|
|
- name: Mark workspace safe for git
|
|
shell: bash
|
|
working-directory: ${{ github.workspace }}
|
|
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
|
|
|
- name: Cache Go modules
|
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: |
|
|
~/go/pkg/mod
|
|
~/.cache/go-build
|
|
~/.cache/golangci-lint
|
|
key: go-static-${{ runner.os }}-${{ hashFiles('apps/backend/go.mod', 'apps/backend/go.sum') }}
|
|
restore-keys: |
|
|
go-static-${{ runner.os }}-
|
|
go-${{ runner.os }}-
|
|
|
|
- name: Build
|
|
run: go build -v ./cmd/kandev
|
|
|
|
# Assert the run/dev/start-debug targets expand to the right per-shell
|
|
# command line (native Windows / Git Bash / Unix). The windows-latest job
|
|
# below compiles Go but never exercises this Make-target dispatch, so a
|
|
# regression in the OS/MSYSTEM guards would otherwise ship unnoticed.
|
|
- name: Check Makefile shell dispatch
|
|
run: make check-make-shells
|
|
|
|
- name: Resolve base SHA
|
|
shell: bash
|
|
working-directory: ${{ github.workspace }}
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
BASE_REF: ${{ github.base_ref }}
|
|
MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
|
|
PUSH_BEFORE: ${{ github.event.before }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# On `pull_request` the checkout is the merge ref, whose first parent
|
|
# is the current base branch tip. `pull_request.base.sha` is the base
|
|
# tip from when the PR event fired, so it can be stale after main
|
|
# advances. Use the merge ref's actual base tip to keep the diff and
|
|
# incremental lint scoped to this PR.
|
|
#
|
|
# A merge_group run has no pull request behind it, so the queue branch
|
|
# is cut from `merge_group.base_sha`, which is the equivalent base.
|
|
if [[ "${EVENT_NAME}" == "pull_request" ]]; then
|
|
BASE_SHA="$(git merge-base HEAD "origin/${BASE_REF}")"
|
|
elif [[ "${EVENT_NAME}" == "merge_group" ]]; then
|
|
BASE_SHA="${MERGE_GROUP_BASE_SHA}"
|
|
else
|
|
BASE_SHA="${PUSH_BEFORE}"
|
|
fi
|
|
|
|
if [[ -z "${BASE_SHA}" || "${BASE_SHA}" == "0000000000000000000000000000000000000000" ]]; then
|
|
BASE_SHA="$(git rev-list --max-parents=0 HEAD)"
|
|
fi
|
|
|
|
echo "BASE_SHA=${BASE_SHA}" >> "$GITHUB_ENV"
|
|
echo "Base SHA: ${BASE_SHA}"
|
|
|
|
- name: Check gofmt
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
CHANGED_GO_FILES="$(
|
|
git diff --name-only --diff-filter=ACMRT "${BASE_SHA}"..."${{ github.sha }}" -- apps/backend \
|
|
| grep -E '^apps/backend/.*\.go$' \
|
|
|| true
|
|
)"
|
|
|
|
if [[ -z "${CHANGED_GO_FILES}" ]]; then
|
|
echo "No backend Go files changed."
|
|
exit 0
|
|
fi
|
|
|
|
UNFORMATTED="$(echo "${CHANGED_GO_FILES}" | sed 's#^apps/backend/##' | xargs gofmt -l)"
|
|
if [[ -n "${UNFORMATTED}" ]]; then
|
|
echo "The following changed Go files are not formatted with gofmt:"
|
|
echo "${UNFORMATTED}"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Run golangci-lint
|
|
# golangci-lint is preinstalled in the kandev-ci image; invoke it
|
|
# directly instead of going through `golangci/golangci-lint-action`,
|
|
# which would re-download its own binary on every run. `--timeout=5m`
|
|
# matches the action's previous implicit default (the binary's own
|
|
# default is 1m, which can trip on cold caches).
|
|
shell: bash
|
|
run: golangci-lint run ./... --new-from-rev="${BASE_SHA}" --timeout=5m
|
|
|
|
test_shards:
|
|
name: Backend Tests (${{ matrix.shard }}/2)
|
|
needs: changes
|
|
if: needs.changes.outputs.run == 'true'
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- shard: 1
|
|
index: 0
|
|
- shard: 2
|
|
index: 1
|
|
# Pre-baked image bundles Go and jq, so test shards can start immediately
|
|
# without repeating tool installation.
|
|
container: ghcr.io/kdlbs/kandev-ci:build-latest
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
working-directory: apps/backend
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Mark workspace safe for git
|
|
shell: bash
|
|
working-directory: ${{ github.workspace }}
|
|
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
|
|
|
- name: Cache Go modules
|
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: |
|
|
~/go/pkg/mod
|
|
~/.cache/go-build
|
|
key: go-test-${{ matrix.shard }}-${{ runner.os }}-${{ hashFiles('apps/backend/go.mod', 'apps/backend/go.sum') }}
|
|
restore-keys: |
|
|
go-test-${{ matrix.shard }}-${{ runner.os }}-
|
|
go-${{ runner.os }}-
|
|
|
|
- name: Run tests
|
|
env:
|
|
SHARD_INDEX: ${{ matrix.index }}
|
|
SHARD_NUMBER: ${{ matrix.shard }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
package_list="$(go list ./...)"
|
|
if [[ -z "${package_list}" ]]; then
|
|
echo "go list returned no backend packages"
|
|
exit 1
|
|
fi
|
|
|
|
mapfile -t all_packages <<<"${package_list}"
|
|
shard_packages=()
|
|
for package in "${all_packages[@]}"; do
|
|
# Stable hashing keeps package assignments unchanged when another
|
|
# package is added or removed.
|
|
checksum="$(cksum <<<"${package}")"
|
|
checksum="${checksum%% *}"
|
|
if (( checksum % 2 == SHARD_INDEX )); then
|
|
shard_packages+=("${package}")
|
|
fi
|
|
done
|
|
|
|
if (( ${#shard_packages[@]} == 0 )); then
|
|
echo "Shard ${SHARD_NUMBER}/2 selected no packages"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Running ${#shard_packages[@]} packages in shard ${SHARD_NUMBER}/2"
|
|
# Keep complete JSON for reports/artifacts while limiting live output
|
|
# to compiler/process lines and package-level pass/fail summaries.
|
|
go test \
|
|
-race \
|
|
-coverprofile="coverage-${SHARD_NUMBER}.out" \
|
|
-covermode=atomic \
|
|
-json \
|
|
"${shard_packages[@]}" \
|
|
2>&1 \
|
|
| tee "test-results-${SHARD_NUMBER}.json" \
|
|
| jq --unbuffered -Rr '
|
|
. as $line
|
|
| (try fromjson catch null) as $event
|
|
| if $event == null then
|
|
$line
|
|
elif ($event.Test == null and ($event.Action == "pass" or $event.Action == "fail")) then
|
|
"\($event.Action): \($event.Package) (\($event.Elapsed // 0)s)"
|
|
else
|
|
empty
|
|
end
|
|
'
|
|
|
|
- name: Generate test report
|
|
uses: robherley/go-test-action@2f859e0c8769d755d3174eecb9af8f64660827f3 # v1.1.0
|
|
if: always()
|
|
with:
|
|
fromJSONFile: apps/backend/test-results-${{ matrix.shard }}.json
|
|
omit: successful
|
|
|
|
- name: Upload test artifacts
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
if: always()
|
|
with:
|
|
name: backend-test-results-${{ matrix.shard }}
|
|
path: |
|
|
apps/backend/coverage-${{ matrix.shard }}.out
|
|
apps/backend/test-results-${{ matrix.shard }}.json
|
|
retention-days: 30
|
|
|
|
test_ambient_env:
|
|
name: Backend (ambient integration env)
|
|
needs: changes
|
|
if: needs.changes.outputs.run == 'true'
|
|
runs-on: ubuntu-latest
|
|
# test_shards runs in the same clean container and never sets these vars,
|
|
# so it cannot catch a package that starts reading them again — the class
|
|
# of bug that shipped alongside internal/gitlab's host-trust tests (they
|
|
# only passed because CI never set KANDEV_GITLAB_HOST/GITLAB_HOST/
|
|
# GITLAB_TOKEN), and that internal/github hit for real with an ambient
|
|
# GH_TOKEN/GITHUB_TOKEN picking a PAT client over the "no auth configured"
|
|
# path. This job poisons them with obviously-fake values so the GitLab-
|
|
# and GitHub-facing packages are forced to prove they stay hermetic
|
|
# instead of accidentally depending on an unset ambient env.
|
|
container: ghcr.io/kdlbs/kandev-ci:build-latest
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
working-directory: apps/backend
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Mark workspace safe for git
|
|
shell: bash
|
|
working-directory: ${{ github.workspace }}
|
|
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
|
|
|
- name: Cache Go modules
|
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: |
|
|
~/go/pkg/mod
|
|
~/.cache/go-build
|
|
key: go-ambient-env-${{ runner.os }}-${{ hashFiles('apps/backend/go.mod', 'apps/backend/go.sum') }}
|
|
restore-keys: |
|
|
go-ambient-env-${{ runner.os }}-
|
|
go-${{ runner.os }}-
|
|
|
|
- name: Run GitLab-facing packages under a poisoned host environment
|
|
env:
|
|
KANDEV_GITLAB_HOST: https://gitlab.ambient-ci.invalid
|
|
GITLAB_HOST: https://gitlab.ambient-ci.invalid
|
|
GITLAB_TOKEN: glpat-ambient-ci-not-a-real-token
|
|
run: go test -race ./internal/gitlab/... ./internal/agentctl/server/process/...
|
|
|
|
- name: Run GitHub-facing package under a poisoned ambient environment
|
|
env:
|
|
GH_TOKEN: ghp-ambient-ci-not-a-real-token
|
|
KANDEV_MOCK_GITHUB: "true"
|
|
run: GITHUB_TOKEN=ghp-ambient-ci-not-a-real-token-legacy go test -race ./internal/github/...
|
|
|
|
# The required check. Runs on every event, including the ones where every job
|
|
# above is correctly skipped, so the merge queue always has a conclusion to
|
|
# read instead of a check that stays pending forever.
|
|
#
|
|
# `needs` covers every other job in this workflow on purpose. A merge queue
|
|
# gates on check names an admin selects by hand, and a job missing from this
|
|
# list is a job whose failure would never block a merge — silently, and
|
|
# without appearing anywhere in the ruleset. `postgres-boot` and
|
|
# `test-windows` were outside it before this workflow became a gate.
|
|
test:
|
|
name: Run Backend Tests
|
|
runs-on: ubuntu-latest
|
|
needs:
|
|
- changes
|
|
- static_checks
|
|
- test_shards
|
|
- test_ambient_env
|
|
- postgres-boot
|
|
- test-windows
|
|
if: ${{ always() }}
|
|
steps:
|
|
- name: Require backend checks to pass
|
|
env:
|
|
CHANGES_RESULT: ${{ needs.changes.result }}
|
|
STATIC_CHECKS_RESULT: ${{ needs.static_checks.result }}
|
|
TEST_SHARDS_RESULT: ${{ needs.test_shards.result }}
|
|
TEST_AMBIENT_ENV_RESULT: ${{ needs.test_ambient_env.result }}
|
|
POSTGRES_BOOT_RESULT: ${{ needs['postgres-boot'].result }}
|
|
TEST_WINDOWS_RESULT: ${{ needs['test-windows'].result }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
echo "Change detection: ${CHANGES_RESULT}"
|
|
echo "Static checks: ${STATIC_CHECKS_RESULT}"
|
|
echo "Test shards: ${TEST_SHARDS_RESULT}"
|
|
echo "Ambient env tests: ${TEST_AMBIENT_ENV_RESULT}"
|
|
echo "Postgres boot: ${POSTGRES_BOOT_RESULT}"
|
|
echo "Windows tests: ${TEST_WINDOWS_RESULT}"
|
|
|
|
# Change detection itself must succeed: if it errored we do not know
|
|
# whether the skips below were deliberate.
|
|
if [[ "${CHANGES_RESULT}" != "success" ]]; then
|
|
echo "Change detection did not succeed; refusing to report a pass." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# `skipped` is a pass — the change-detection job decided nothing this
|
|
# workflow covers was touched. `cancelled` is not: a cancelled job
|
|
# never reached a verdict, and treating that as a pass would let an
|
|
# untested merge group through.
|
|
#
|
|
# This gate is the one check a human opens when a merge is blocked, so
|
|
# name the job that failed here rather than making them correlate the
|
|
# list above by eye.
|
|
for entry in \
|
|
"static_checks:${STATIC_CHECKS_RESULT}" \
|
|
"test_shards:${TEST_SHARDS_RESULT}" \
|
|
"test_ambient_env:${TEST_AMBIENT_ENV_RESULT}" \
|
|
"postgres-boot:${POSTGRES_BOOT_RESULT}" \
|
|
"test-windows:${TEST_WINDOWS_RESULT}"; do
|
|
name="${entry%%:*}"
|
|
result="${entry#*:}"
|
|
if [[ "${result}" != "success" && "${result}" != "skipped" ]]; then
|
|
echo "${name} finished with result: ${result}" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
postgres-boot:
|
|
name: Backend Postgres
|
|
needs: changes
|
|
if: needs.changes.outputs.run == 'true'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
packages: read
|
|
# The env-gated package suite runs with the race detector and can exceed
|
|
# ten minutes on a hosted runner as the repository test set grows.
|
|
timeout-minutes: 20
|
|
container: ghcr.io/kdlbs/kandev-ci:build-latest
|
|
services:
|
|
postgres:
|
|
# Mirrored from the digest-pinned Docker Hub image by
|
|
# ci-base-image.yml. Service images are pulled before steps begin, so
|
|
# moving this pull to GHCR removes an otherwise unwrappable Docker Hub
|
|
# timeout from job startup.
|
|
image: ghcr.io/kdlbs/kandev-ci:postgres-16@sha256:fe03a7605299a34ddf5e4f285dff78c3d7190a576b3c6b46f2fcff69f4bffd54
|
|
credentials:
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
env:
|
|
POSTGRES_USER: kandev
|
|
POSTGRES_PASSWORD: kandev
|
|
POSTGRES_DB: kandev_admin
|
|
options: >-
|
|
--health-cmd "pg_isready -U kandev -d kandev_admin"
|
|
--health-interval 5s
|
|
--health-timeout 5s
|
|
--health-retries 12
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
working-directory: apps/backend
|
|
env:
|
|
KANDEV_TEST_POSTGRES_DSN: host=postgres port=5432 user=kandev password=kandev dbname=kandev_admin sslmode=disable
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Run env-gated Postgres tests
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
mapfile -t postgres_packages < <(
|
|
grep -RIl 'PostgresDSNFromEnv' . --include='*_test.go' \
|
|
| xargs -r dirname \
|
|
| sort -u
|
|
)
|
|
|
|
if [[ "${#postgres_packages[@]}" -eq 0 ]]; then
|
|
echo "::error::No Postgres test packages found"
|
|
exit 1
|
|
fi
|
|
|
|
printf 'Running Postgres tests in:\n'
|
|
printf ' %s\n' "${postgres_packages[@]}"
|
|
|
|
output_file="$(mktemp)"
|
|
go test -v -race "${postgres_packages[@]}" 2>&1 | tee "${output_file}"
|
|
|
|
if ! grep -q '^=== RUN TestPostgresBootInitializesRepositories$' "${output_file}"; then
|
|
echo "::error::Postgres boot smoke test matched no tests"
|
|
exit 1
|
|
fi
|
|
|
|
test-windows:
|
|
name: Backend (windows)
|
|
needs: changes
|
|
if: needs.changes.outputs.run == 'true'
|
|
runs-on: windows-latest
|
|
# The race-sensitive Windows package suite can approach 20 minutes on a
|
|
# busy hosted runner; keep the cap finite while allowing runner variance.
|
|
timeout-minutes: 30
|
|
# Least-privilege: this job only reads source and runs tests, no PR/issue
|
|
# writes or release operations. Flagged by CodeQL workflow audit + matching
|
|
# CodeRabbit nitpick.
|
|
permissions:
|
|
contents: read
|
|
defaults:
|
|
run:
|
|
working-directory: apps/backend
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Set up Go
|
|
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
|
with:
|
|
go-version-file: apps/backend/go.mod
|
|
cache-dependency-path: apps/backend/go.sum
|
|
|
|
# Compile-checks every `*_windows.go` file (pty/conpty wrapper, job-object
|
|
# launcher, shell, signals, sysprocattr, vscode cleanup, …). The rest of
|
|
# the suite is build-tag-gated out automatically.
|
|
- name: Build
|
|
run: go build ./...
|
|
|
|
- name: Vet
|
|
run: go vet ./...
|
|
|
|
# Scope to packages with non-trivial Windows-specific code paths.
|
|
# Tests that genuinely cannot run on Windows (Unix sockets, POSIX
|
|
# symlinks under default Windows ACLs, delete-while-open) call t.Skip
|
|
# in source with a documented reason. The shell-fixture tests now use
|
|
# a self-hosting Go helper (see testmain_test.go) so they run on
|
|
# every platform.
|
|
- name: Test windows-sensitive packages
|
|
run: go test -race -v ./internal/agentctl/server/process/... ./internal/agent/runtime/agentctl/launcher/... ./internal/common/ptyexec/... ./internal/backendapp/ownershiplock/... ./internal/system/storage/workspaces/...
|
|
|
|
- name: Test Windows managed runtime cache repair
|
|
run: go test -race -v ./internal/agent/managedruntime -run TestRemoveNpxExecutionTreeTreatsMissingWindowsChildAsAbsent
|
|
|
|
# Keep port-collision coverage targeted: the instance package contains
|
|
# broader stdio MCP fixture tests that depend on Unix command behavior.
|
|
- name: Test Windows port collision handling
|
|
shell: bash
|
|
run: |
|
|
go test -race -v ./internal/common/netutil -run '^TestIsAddrInUse'
|
|
go test -race -v ./internal/agentctl/server/instance -run '^TestAllocatePortAndListenerRetriesAddressInUse$'
|
|
go test -race -v ./internal/gateway/websocket -run '^TestResolveAndBindReportsOccupiedPort$'
|
|
|
|
# Keep this targeted: the service package has broad subprocess coverage,
|
|
# while this native test is the Windows path-semantics regression.
|
|
- name: Test Windows path semantics
|
|
run: >-
|
|
go test -race -v
|
|
-run '^(TestExplicitLocalRepositoryWindowsPathSemantics|TestListDirectory.*|TestDriveRootsFromMask|TestSplitAbsForRoot)$'
|
|
./internal/task/service
|