fix(TST-25,TST-26): restore Windows/x86 CI tier via SSH-driven windev job
The `windows`/`live-mxaccess` CI jobs were removed in abb0930 because Gitea
act_runner host-mode on Windows is broken and a runs-on gate with no runner
wedges the queue. This left the entire x86/net48 Worker + Worker.Tests tier
(and the live-MXAccess smoke) unguarded by CI.
Restore it without a native Windows runner: Linux jobs on ubuntu-latest (which
always schedule) SSH to windev (10.100.0.48), fetch+checkout the SHA under test
in an isolated clone C:\build\mxaccessgw-ci, run the x86 build/tests there, and
propagate the remote exit code back — so a Worker regression turns the job red
and an unreachable host fails loud (never stuck).
- scripts/ci/run-windev-ci.sh: Linux driver (key/known-hosts, UTF-16LE base64
EncodedCommand bootstrap, ssh, exit-code passthrough).
- scripts/ci/windev-worker-ci.ps1: windev stage — worktree lock, re-fetch/
checkout under lock, build|test|live modes; PS 5.1-safe, checks $LASTEXITCODE.
- scripts/ci/windev.known_hosts: pinned host keys for StrictHostKeyChecking.
- scripts/ci/README.md: operator bring-up + acceptance checklist.
- ci.yml: windows-x86 (per-push `test`) + nightly-windev (scheduled `live` +
on-failure Gitea issue); drop the removal note; fix header/cron comments.
TST-26 (same commit, by rule): correct docs/scripts that still describe the
removed jobs — GatewayTesting.md, Contracts.md, check-codegen.ps1 — and
reattribute the Generated/-commit guard (primary = check-codegen diff in the
portable job; secondary = windows-x86 net48 compile).
Mechanism hand-verified on windev: build->0, bogus-SHA->nonzero (lock released),
test->356 passed/0 failed in ~50s, ssh exit-code propagation confirmed. Merge
requires operator bring-up first (dedicated ci@ key + Gitea secrets) or the
per-push job is red every push — see scripts/ci/README.md.
This commit is contained in:
@@ -14,7 +14,8 @@
|
||||
# protos. A drift means a .proto was edited without refreshing the vendored copies, which would
|
||||
# publish a stale wire contract to crate consumers while the in-repo build stays correct.
|
||||
#
|
||||
# The x86 Worker + Worker.Tests are Windows-only and are guarded by the Windows CI job, not here.
|
||||
# The x86 Worker + Worker.Tests are Windows-only and are guarded by the SSH-driven `windows-x86`
|
||||
# CI job (see docs/GatewayTesting.md, Continuous Integration), not here.
|
||||
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Windows/x86 CI tier (TST-25)
|
||||
|
||||
The x86 / net48 Worker and its tests cannot build on a Linux runner, and a native Windows
|
||||
`act_runner` is broken (host-mode writes each step's script to a path it can't resolve; a
|
||||
`runs-on` gate with no runner wedges the queue forever). Instead, a Linux CI job SSHes to
|
||||
windev (`10.100.0.48`) and runs the Worker build/tests there, propagating the remote exit
|
||||
code back so a regression turns the job red.
|
||||
|
||||
## Pieces
|
||||
|
||||
| File | Runs on | Role |
|
||||
|---|---|---|
|
||||
| `run-windev-ci.sh <build\|test\|live>` | Linux CI job | Writes the SSH key/known-hosts, base64-encodes a bootstrap, SSHes to windev, returns the remote exit code. |
|
||||
| `windev-worker-ci.ps1 -Sha -Mode` | windev | Locks the CI clone, checks out the SHA, runs the x86 build / `Worker.Tests` / live smoke. |
|
||||
| `windev.known_hosts` | committed | Pinned windev host keys so `StrictHostKeyChecking=yes` has no TOFU prompt. Host keys are public; committing is intentional. |
|
||||
|
||||
Modes are cumulative: `build` = x86 Worker build; `test` = build + `Worker.Tests`;
|
||||
`live` = test + full-`slnx` build + live-MXAccess smoke (`MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1`).
|
||||
|
||||
CI jobs (`.gitea/workflows/ci.yml`): `windows-x86` runs `test` per push/PR; `nightly-windev`
|
||||
runs `live` on the `0 6 * * *` schedule and opens a Gitea issue on failure.
|
||||
|
||||
## One-time bring-up (operator — required before this lands on `main`)
|
||||
|
||||
Until these are done, `windows-x86` is **red on every push** (no key → SSH fails). Do them
|
||||
first, then merge.
|
||||
|
||||
1. **CI clone** — `C:\build\mxaccessgw-ci`, a fresh clone of the Gitea origin (done during
|
||||
TST-25 verification; recreate with `git clone https://gitea.dohertylan.com/dohertj2/mxaccessgw C:\build\mxaccessgw-ci` if missing). Never the dirty Desktop checkout.
|
||||
2. **Dedicated CI key** — generate a NEW ed25519 keypair for CI (do not reuse the operator's
|
||||
personal windev key): `ssh-keygen -t ed25519 -f ci_windev -N ''`. Append `ci_windev.pub`
|
||||
to the windev CI account's `authorized_keys` (optionally with a forced `command=` limiting
|
||||
it to this bootstrap).
|
||||
3. **Gitea repo secrets** — store the private key as `WINDEV_SSH_KEY` and, optionally, the
|
||||
pinned host keys as `WINDEV_SSH_KNOWN_HOSTS` (the committed `windev.known_hosts` is the
|
||||
fallback). `run-windev-ci.sh` connects as user `ci` by default (`WINDEV_SSH_USER` to
|
||||
override); ensure that account exists on windev, or set `WINDEV_SSH_USER` to the intended one.
|
||||
4. **Network precheck** — confirm the Gitea runner container (on the `traefik` docker network,
|
||||
host `10.100.0.35`) can reach `10.100.0.48:22`: a one-off `nc -z -w5 10.100.0.48 22` step.
|
||||
The network was created for gitea name resolution, not LAN egress, so verify.
|
||||
5. **Actions token** — the `nightly-windev` on-failure step creates a Gitea issue with the
|
||||
built-in token; confirm Actions tokens have issue-write on this repo.
|
||||
|
||||
## Verifying it end-to-end (design's acceptance checks)
|
||||
|
||||
- Push a branch → `portable`, `java`, `windows-x86` all green; windev's clone sits at the pushed SHA.
|
||||
- Deliberate red: add a failing `[Fact]` to `Worker.Tests`, push, confirm `windows-x86` goes
|
||||
red; revert. (Proves exit-code propagation — the whole point.)
|
||||
- Unreachable-host red: point at a bogus port / stop sshd, confirm the job fails fast, not hangs.
|
||||
- Concurrency: push two branches back-to-back, confirm the second remote run waits on the lock.
|
||||
- Nightly: trigger the schedule path, confirm `live` runs and a forced failure opens an issue.
|
||||
- Confirm no key material appears in job logs.
|
||||
|
||||
## Degraded mode
|
||||
|
||||
If the tier is down for infra reasons, fall back to the manual windev worktree procedure
|
||||
(isolated `C:\build` worktree, `dotnet build`/`test` `-p:Platform=x86` by hand) per merge for
|
||||
worker-touching changes until `windows-x86` is green again. See `docs/GatewayTesting.md`.
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# TST-25: Linux side of the Windows/x86 Worker CI tier.
|
||||
#
|
||||
# Runs in the `windows-x86` / nightly Gitea jobs on a Linux runner (which always schedules,
|
||||
# unlike a broken Windows act_runner). SSHes to windev (10.100.0.48), where it fetches +
|
||||
# checks out the SHA under test in the isolated CI clone C:\build\mxaccessgw-ci and runs
|
||||
# scripts/ci/windev-worker-ci.ps1 there. The remote exit code propagates through
|
||||
# ssh -> this script -> the CI step, so a Worker regression turns the job red.
|
||||
#
|
||||
# Usage: run-windev-ci.sh <build|test|live>
|
||||
#
|
||||
# Environment:
|
||||
# CI_SHA commit to test (default: `git rev-parse HEAD` in this checkout)
|
||||
# WINDEV_SSH_KEY private key PEM (Gitea secret). If empty, falls back to the
|
||||
# runner's default ssh identity/agent (used for local hand-testing).
|
||||
# WINDEV_SSH_KNOWN_HOSTS pinned host keys (Gitea secret). If empty, uses the committed
|
||||
# scripts/ci/windev.known_hosts.
|
||||
# WINDEV_SSH_USER ssh user on windev (default: ci)
|
||||
# WINDEV_SSH_HOST windev host/IP (default: 10.100.0.48)
|
||||
#
|
||||
# The private key is never echoed (written 0600 to a temp file, removed on exit). git on
|
||||
# windev writes progress to stderr, so the remote bootstrap does NOT use -ErrorActionPreference
|
||||
# Stop around git — it checks $LASTEXITCODE instead (known windev gotcha).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MODE="${1:-}"
|
||||
case "$MODE" in
|
||||
build|test|live) ;;
|
||||
*) echo "usage: $0 <build|test|live>" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CI_SHA="${CI_SHA:-$(git -C "$SCRIPT_DIR" rev-parse HEAD)}"
|
||||
WINDEV_SSH_USER="${WINDEV_SSH_USER:-ci}"
|
||||
WINDEV_SSH_HOST="${WINDEV_SSH_HOST:-10.100.0.48}"
|
||||
REMOTE_CLONE='C:\build\mxaccessgw-ci'
|
||||
REMOTE_SCRIPT='scripts\ci\windev-worker-ci.ps1'
|
||||
|
||||
# Validate the SHA shape before it reaches a remote shell command.
|
||||
if ! printf '%s' "$CI_SHA" | grep -Eq '^[0-9a-fA-F]{7,40}$'; then
|
||||
echo "run-windev-ci: refusing to run — CI_SHA '$CI_SHA' is not a hex commit id" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
cleanup() { rm -rf "$WORK"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=yes -o ConnectTimeout=15)
|
||||
|
||||
# known_hosts: secret if provided, else the committed pins.
|
||||
KNOWN_HOSTS="$WORK/known_hosts"
|
||||
if [ -n "${WINDEV_SSH_KNOWN_HOSTS:-}" ]; then
|
||||
printf '%s\n' "$WINDEV_SSH_KNOWN_HOSTS" > "$KNOWN_HOSTS"
|
||||
else
|
||||
cp "$SCRIPT_DIR/windev.known_hosts" "$KNOWN_HOSTS"
|
||||
fi
|
||||
SSH_OPTS+=(-o "UserKnownHostsFile=$KNOWN_HOSTS")
|
||||
|
||||
# Private key: secret if provided, else fall back to the default identity/agent.
|
||||
if [ -n "${WINDEV_SSH_KEY:-}" ]; then
|
||||
KEY="$WORK/id_ci"
|
||||
( umask 077; printf '%s\n' "$WINDEV_SSH_KEY" > "$KEY" )
|
||||
SSH_OPTS+=(-o IdentitiesOnly=yes -i "$KEY")
|
||||
fi
|
||||
|
||||
echo "run-windev-ci: mode=$MODE sha=$CI_SHA target=${WINDEV_SSH_USER}@${WINDEV_SSH_HOST}"
|
||||
|
||||
# Remote PowerShell bootstrap: fetch + checkout enough to load the versioned CI script, then
|
||||
# hand off to it. Not Stop around git (stderr progress). windev-worker-ci.ps1 re-fetches and
|
||||
# re-checks-out under a lock, so this checkout only needs to load the right script version.
|
||||
read -r -d '' BOOTSTRAP <<PS || true
|
||||
\$ErrorActionPreference = 'Continue'
|
||||
Set-Location '$REMOTE_CLONE'
|
||||
git fetch --prune origin
|
||||
git checkout --force --detach $CI_SHA
|
||||
if (\$LASTEXITCODE -ne 0) { Write-Error "bootstrap checkout failed"; exit \$LASTEXITCODE }
|
||||
powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File '$REMOTE_SCRIPT' -Sha '$CI_SHA' -Mode '$MODE'
|
||||
exit \$LASTEXITCODE
|
||||
PS
|
||||
|
||||
# PowerShell -EncodedCommand wants UTF-16LE base64 on one line (avoids all ssh/quoting hazards).
|
||||
ENCODED="$(printf '%s' "$BOOTSTRAP" | iconv -t UTF-16LE | base64 | tr -d '\n')"
|
||||
|
||||
set +e
|
||||
ssh "${SSH_OPTS[@]}" "${WINDEV_SSH_USER}@${WINDEV_SSH_HOST}" \
|
||||
"powershell -NoProfile -NonInteractive -EncodedCommand $ENCODED"
|
||||
RC=$?
|
||||
set -e
|
||||
|
||||
echo "run-windev-ci: remote exit code $RC"
|
||||
exit $RC
|
||||
@@ -0,0 +1,147 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Windows/x86 Worker CI stage — runs ON windev (10.100.0.48), invoked over SSH by
|
||||
scripts/ci/run-windev-ci.sh at the exact SHA under test. Restores the x86/net48
|
||||
Worker test tier that the removed `windows` CI job used to cover (TST-25).
|
||||
|
||||
.DESCRIPTION
|
||||
The Linux `windows-x86` / nightly CI jobs cannot build the x86/net48 Worker, so they
|
||||
ssh here and run this script inside the isolated CI clone C:\build\mxaccessgw-ci.
|
||||
|
||||
The script is deliberately self-contained and versioned with the code under test: the
|
||||
SSH bootstrap only fetches + checks out the SHA (enough to load *this* file), then this
|
||||
script re-fetches and re-checks-out $Sha INSIDE the worktree lock so the checkout+build
|
||||
are fully serialized against a concurrent run and can never stomp each other's tree.
|
||||
|
||||
Modes (cumulative):
|
||||
build x86 Worker build only (net48/CS0246/x86 compile guard — always fast).
|
||||
test build + Worker.Tests (x86). The per-push default.
|
||||
live test + the live-MXAccess smoke (MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1) and a full
|
||||
slnx build (surfaces Windows-only analyzer diagnostics, e.g. xUnit1030). Nightly.
|
||||
|
||||
Runs under Windows PowerShell 5.1 (`powershell.exe`), so it avoids PS7-only syntax and
|
||||
checks $LASTEXITCODE after every native command rather than relying on stderr-as-error
|
||||
(git writes progress to stderr — the known windev gotcha).
|
||||
|
||||
Exits nonzero on the first failure so the SSH exit code propagates to the CI job.
|
||||
|
||||
.PARAMETER Sha
|
||||
The commit SHA under test. HEAD is force-checked-out to it and verified.
|
||||
|
||||
.PARAMETER Mode
|
||||
build | test | live.
|
||||
|
||||
.PARAMETER RepoRoot
|
||||
The CI clone root. Defaults to C:\build\mxaccessgw-ci.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Sha,
|
||||
[Parameter(Mandatory = $true)][ValidateSet('build', 'test', 'live')][string]$Mode,
|
||||
[string]$RepoRoot = 'C:\build\mxaccessgw-ci'
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$lockDir = "$RepoRoot.lock"
|
||||
$logFile = Join-Path $RepoRoot 'ci.log'
|
||||
$lockTimeout = [TimeSpan]::FromMinutes(20)
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message)
|
||||
$line = ('[{0}] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message)
|
||||
Write-Host $line
|
||||
# Best-effort file log for the "is the tier down?" diagnosis in docs/GatewayTesting.md.
|
||||
try { Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue } catch { }
|
||||
}
|
||||
|
||||
# Run a native command and throw on nonzero exit. Native stderr (git progress) does NOT
|
||||
# set $LASTEXITCODE, so it is not treated as failure — only a real nonzero exit is.
|
||||
function Invoke-Native {
|
||||
param([Parameter(Mandatory = $true)][string]$What, [Parameter(Mandatory = $true)][scriptblock]$Command)
|
||||
Write-Log "RUN: $What"
|
||||
& $Command
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$What failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
# --- acquire the worktree lock (mkdir is atomic; retry until timeout) ---
|
||||
$deadline = (Get-Date).Add($lockTimeout)
|
||||
$haveLock = $false
|
||||
Write-Log "Acquiring worktree lock $lockDir (mode=$Mode sha=$Sha)"
|
||||
while (-not $haveLock) {
|
||||
try {
|
||||
New-Item -Path $lockDir -ItemType Directory -ErrorAction Stop | Out-Null
|
||||
$haveLock = $true
|
||||
}
|
||||
catch {
|
||||
if ((Get-Date) -ge $deadline) {
|
||||
throw "Timed out after $($lockTimeout.TotalMinutes) min waiting for worktree lock $lockDir. A stuck run may have left it; if no CI job is active, remove it by hand."
|
||||
}
|
||||
Start-Sleep -Seconds 10
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Push-Location $RepoRoot
|
||||
try {
|
||||
# Re-fetch + re-checkout under the lock so a concurrent run's bootstrap checkout
|
||||
# (done before we held the lock) cannot leave us on the wrong tree.
|
||||
Invoke-Native 'git fetch origin' { git fetch --prune origin }
|
||||
Invoke-Native "git checkout --force $Sha" { git checkout --force --detach $Sha }
|
||||
# Drop any stray tracked/untracked residue from a prior run so the build is clean.
|
||||
Invoke-Native 'git reset --hard' { git reset --hard $Sha }
|
||||
Invoke-Native 'git clean -fdx (build outputs)' { git clean -fdx -e ci.log }
|
||||
|
||||
$head = (& git rev-parse HEAD).Trim()
|
||||
if ($head -ne $Sha) {
|
||||
throw "Worktree HEAD $head does not match requested SHA $Sha after checkout."
|
||||
}
|
||||
Write-Log "Worktree at $head"
|
||||
|
||||
$workerProj = 'src/ZB.MOM.WW.MxGateway.Worker/ZB.MOM.WW.MxGateway.Worker.csproj'
|
||||
$workerTests = 'src/ZB.MOM.WW.MxGateway.Worker.Tests/ZB.MOM.WW.MxGateway.Worker.Tests.csproj'
|
||||
$integrationTests = 'src/ZB.MOM.WW.MxGateway.IntegrationTests/ZB.MOM.WW.MxGateway.IntegrationTests.csproj'
|
||||
|
||||
# build: always. The x86/net48 compile is the floor guard (catches CS0246 on
|
||||
# unregenerated Generated/, x86-only breaks) even when the test step is demoted.
|
||||
Invoke-Native 'x86 Worker build' { dotnet build $workerProj -c Release -p:Platform=x86 }
|
||||
|
||||
if ($Mode -eq 'test' -or $Mode -eq 'live') {
|
||||
Invoke-Native 'x86 Worker.Tests' { dotnet test $workerTests -c Release -p:Platform=x86 }
|
||||
}
|
||||
|
||||
if ($Mode -eq 'live') {
|
||||
# Full-solution build surfaces Windows-only analyzer diagnostics (xUnit1030 etc.)
|
||||
# that never fire on the macOS/Linux NonWindows build.
|
||||
Invoke-Native 'full slnx build' { dotnet build src/ZB.MOM.WW.MxGateway.slnx -c Release }
|
||||
|
||||
$env:MXGATEWAY_RUN_LIVE_MXACCESS_TESTS = '1'
|
||||
try {
|
||||
Invoke-Native 'live-MXAccess smoke' {
|
||||
dotnet test $integrationTests -c Release --filter 'FullyQualifiedName~WorkerLiveMxAccessSmokeTests'
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item Env:\MXGATEWAY_RUN_LIVE_MXACCESS_TESTS -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
Write-Log "SUCCESS mode=$Mode sha=$Sha"
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Log "FAILURE mode=$Mode sha=$Sha : $($_.Exception.Message)"
|
||||
throw
|
||||
}
|
||||
finally {
|
||||
if ($haveLock) {
|
||||
Remove-Item -Path $lockDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Write-Log "Released worktree lock $lockDir"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# Pinned SSH host keys for the windev CI target (10.100.0.48), used by the
|
||||
# `windows-x86` / nightly jobs so `StrictHostKeyChecking=yes` can verify the host
|
||||
# without an interactive TOFU prompt. Host keys are public data — committing them is
|
||||
# intentional (greppable, versioned) per TST-25's design.
|
||||
#
|
||||
# Regenerate if windev's host keys rotate:
|
||||
# ssh-keyscan -t ed25519,rsa,ecdsa 10.100.0.48 > scripts/ci/windev.known_hosts
|
||||
# (then re-add this header). run-windev-ci.sh prefers the WINDEV_SSH_KNOWN_HOSTS
|
||||
# secret when set and falls back to this file.
|
||||
10.100.0.48 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHRqxpRq/G0BR5vAomHMNc/H7eyTkdqliHNrFlMQ9HCb
|
||||
10.100.0.48 ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBIvTHAgf7Q+rK2dCyjSUgkYgos4j2eqyJN/AAfs2TXGJgrpp9AfOMnkbPtmMX3CvsrPoZxzBttIYcBYbf9KXb98JAu6fWqeglI/CeXH4oHw6oqMRtM0K2hl2gm3DicKKCQ==
|
||||
10.100.0.48 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCxKOhjdVawf3ziO9MB4VfLA1zDwnNK028siIzjTHZec4uJumInvSFD5S3tkJvSKGfCzXHtLu9m0zHmvJ4gIvLx4gt60TPABPC/QrdRG8mP4uOlPCciukWNIBhMWFU6oxHpZlQgrjQ3a+6WZxIjZBtaZQt3S5f9zEvOu6NUrLmP9cmrrXtAByMYyjISQf2BswxZ5ugeDHn0rVo3p3qYZ3M8lbXV+CRTwKj0I+VVceffJrYwLT0LyWxi0tDxmHxK1PgkhcIjLDWKhk4UxOA6gdO6MjuHvXZv5+UXJzjgGxom+xqxGhAge0F66p5IEJ/xrJMWoPBtFQ1by7tDZAsRhcbQJVeCbBjEDv7ShCbyh2Gq+PfJhWVVaYnMKcqBWkhPrZH4BE9yhAtSlV8QZsZ1iYUYKNAt9Kcg3ugRYx414KUf2fvUWSuoaRkJnBqQq1Mi0yK1JtTZOoYvN4XEYIr56hXQG1RfA8xWqYzjmbHYTgO8zv6tJJTeXLMqubmDdLP9H+U=
|
||||
Reference in New Issue
Block a user