Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 1 addition & 29 deletions .github/workflows/pr-docker-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,32 +61,4 @@ jobs:
--tag ivi-cli-mock:pr-smoke .

- name: Smoke test (HEALTHCHECK + SCPI roundtrip)
run: |
set -euo pipefail
docker run -d --name mock-smoke ivi-cli-mock:pr-smoke
for i in $(seq 1 30); do
status="$(docker inspect --format '{{.State.Health.Status}}' mock-smoke)"
if [ "$status" = "healthy" ]; then
echo "container healthy after ${i}s"
break
fi
sleep 1
done
if [ "$(docker inspect --format '{{.State.Health.Status}}' mock-smoke)" != "healthy" ]; then
echo "container failed to become healthy"
docker logs mock-smoke
exit 1
fi
response="$(docker exec mock-smoke bash -c 'echo "*IDN?" | nc -w 2 127.0.0.1 5025')"
echo "got: $response"
case "$response" in
*IVICLI-MOCK*)
echo "smoke passed"
;;
*)
echo "smoke FAILED — unexpected response"
docker logs mock-smoke
exit 1
;;
esac
docker rm -f mock-smoke
run: sh docker/smoke-test.sh ivi-cli-mock:pr-smoke
94 changes: 58 additions & 36 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,20 @@ jobs:
timeout-minutes: 30
strategy:
fail-fast: false
# Each RID builds, tests, and smoke-runs on a runner of the SAME
# OS/architecture, so every shipped binary has been executed at
# least once before release (ADR 0016).
matrix:
include:
- os: ubuntu-latest
rid: linux-x64
- os: ubuntu-latest
- os: ubuntu-24.04-arm
rid: linux-arm64
- os: windows-latest
rid: win-x64
- os: windows-latest
- os: windows-11-arm
rid: win-arm64
- os: macos-latest
- os: macos-15-intel
rid: osx-x64
- os: macos-latest
rid: osx-arm64
Expand Down Expand Up @@ -110,6 +113,28 @@ jobs:
-p:Version=${{ steps.version.outputs.value }} \
-o ./artifacts/${{ matrix.rid }}-fxdep

# Execute the exact artifact that ships: the runner arch matches
# the RID, so a publish that produces a non-runnable binary
# (bad RID mapping, trimming fault, missing native lib) fails
# here instead of on a user's machine.
- name: Smoke-run published binary
shell: bash
run: |
set -euo pipefail
bin="./artifacts/${{ matrix.rid }}-selfcontained/ivicli"
[ -f "$bin" ] || bin="${bin}.exe"
out="$("$bin" --version)"
echo "ivicli --version -> $out"
case "$out" in
"${{ steps.version.outputs.value }}"*)
echo "smoke passed"
;;
*)
echo "smoke FAILED — expected version ${{ steps.version.outputs.value }}"
exit 1
;;
esac

- name: Upload binaries
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand Down Expand Up @@ -175,11 +200,39 @@ jobs:
name: ivicli-nupkg
path: ./artifacts/nupkg

# Native-arm64 counterpart of the amd64 smoke gate inside the
# `docker` job below: builds the linux/arm64 single-arch image on an
# arm64 runner and runs the same HEALTHCHECK + SCPI round-trip, so
# the arm64 half of the multi-arch manifest is executed before the
# push (ADR 0016). No QEMU — emulated .NET is slow and flaky.
docker-smoke-arm64:
name: docker / mock-VISA smoke (arm64)
runs-on: ubuntu-24.04-arm
timeout-minutes: 20
needs: publish
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Download linux-arm64 self-contained artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ivicli-linux-arm64
path: ./artifacts

- name: Build single-arch (arm64) for smoke
run: |
docker build -f docker/Dockerfile \
--build-arg RID=linux-arm64 \
--tag ivi-cli-mock:smoke-arm64 .

- name: Run smoke test
run: sh docker/smoke-test.sh ivi-cli-mock:smoke-arm64

docker:
name: docker / mock-VISA container
runs-on: ubuntu-latest
timeout-minutes: 20
needs: publish
needs: [publish, docker-smoke-arm64]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

Expand Down Expand Up @@ -239,38 +292,7 @@ jobs:
--tag ivi-cli-mock:smoke .

- name: Run smoke test
run: |
set -euo pipefail
docker run -d --name mock-smoke -p 4880:4880 -p 5025:5025 ivi-cli-mock:smoke
# Wait until the HEALTHCHECK reports healthy (max 30 s).
for i in $(seq 1 30); do
status="$(docker inspect --format '{{.State.Health.Status}}' mock-smoke)"
if [ "$status" = "healthy" ]; then
echo "container healthy after ${i}s"
break
fi
sleep 1
done
if [ "$(docker inspect --format '{{.State.Health.Status}}' mock-smoke)" != "healthy" ]; then
echo "container failed to become healthy"
docker logs mock-smoke
exit 1
fi
# SCPI round-trip over the SOCKET gateway (no client framing
# required). Expect the baked default IDN response.
response="$(docker exec mock-smoke bash -c 'echo "*IDN?" | nc -w 2 127.0.0.1 5025')"
echo "got: $response"
case "$response" in
*IVICLI-MOCK*)
echo "smoke passed"
;;
*)
echo "smoke FAILED — unexpected response"
docker logs mock-smoke
exit 1
;;
esac
docker rm -f mock-smoke
run: sh docker/smoke-test.sh ivi-cli-mock:smoke

- name: Build + push multi-arch manifest
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
Expand Down
44 changes: 44 additions & 0 deletions docker/smoke-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/sh
# HEALTHCHECK + SCPI round-trip smoke against a locally built
# ivi-cli-mock image. Shared by pr-docker-smoke.yml and both
# release.yml container smoke gates (amd64 + arm64).
#
# Usage: smoke-test.sh <image-tag>
set -eu

image="${1:?usage: smoke-test.sh <image-tag>}"
name="mock-smoke"

docker rm -f "$name" >/dev/null 2>&1 || true
docker run -d --name "$name" "$image"

for i in $(seq 1 30); do
status="$(docker inspect --format '{{.State.Health.Status}}' "$name")"
if [ "$status" = "healthy" ]; then
echo "container healthy after ${i}s"
break
fi
sleep 1
done
if [ "$(docker inspect --format '{{.State.Health.Status}}' "$name")" != "healthy" ]; then
echo "container failed to become healthy"
docker logs "$name"
exit 1
fi

# SCPI round-trip over the SOCKET gateway (no client framing
# required). Expect the baked default IDN response.
response="$(docker exec "$name" bash -c 'echo "*IDN?" | nc -w 2 127.0.0.1 5025')"
echo "got: $response"
case "$response" in
*IVICLI-MOCK*)
echo "smoke passed"
;;
*)
echo "smoke FAILED — unexpected response"
docker logs "$name"
exit 1
;;
esac

docker rm -f "$name"
90 changes: 85 additions & 5 deletions docs/adr/0016-cross-platform-policy.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,96 @@
# 0016. Cross-Platform Policy

- Status: Draft
- Date: 2026-05-19
- Status: Accepted
- Date: 2026-07-24

## Context

TBD
ivi-cli ships self-contained and framework-dependent binaries for six
RIDs (`linux-x64`, `linux-arm64`, `win-x64`, `win-arm64`, `osx-x64`,
`osx-arm64`) plus a multi-arch (`linux/amd64` + `linux/arm64`) mock
container ([ADR 0018](0018-deployment-strategy.md)). Until this ADR,
only three of those architectures were ever *executed* before a
release: the publish matrix ran on x64 (and Apple-silicon macOS)
runners, cross-publishing `linux-arm64`, `win-arm64`, and `osx-x64`
without running the result. A publish-side fault visible only at
runtime — a bad RID mapping, a missing native library in the
single-file bundle — would have surfaced on a user's machine, not in
CI. GitHub-hosted native arm64 runners (`ubuntu-24.04-arm`,
`windows-11-arm`) and the Intel macOS runner (`macos-15-intel`) are
now available to public repositories at no cost, which removes the
original reason for cross-publishing.

## Decision

TBD
### 1. Supported platform set

The supported OS/architecture set **is** the six shipped RIDs. No RID
is shipped without the verification in §2; conversely, adding a RID to
the release matrix implies adding its native verification.

### 2. Release gate: native execution per RID

`release.yml`'s publish matrix pairs every RID with a runner of the
**same OS and architecture**:

| RID | Runner |
| --- | --- |
| `linux-x64` | `ubuntu-latest` |
| `linux-arm64` | `ubuntu-24.04-arm` |
| `win-x64` | `windows-latest` |
| `win-arm64` | `windows-11-arm` |
| `osx-x64` | `macos-15-intel` |
| `osx-arm64` | `macos-latest` |

Each matrix job builds, runs the unit + architecture test suite, and
**smoke-runs the published self-contained binary** (`ivicli --version`
must succeed and report the tag-derived version). A binary that cannot
start on its target architecture fails the release before anything is
published.

The framework-dependent binary is published per-RID but not separately
smoke-run: it carries the same IL as the self-contained bundle and its
runtime is supplied by the user.

### 3. Container: both manifest halves executed

The mock container's smoke gate runs **natively on both
architectures** before the multi-arch push: amd64 in the `docker` job,
arm64 in `docker-smoke-arm64` on an arm64 runner (shared script
`docker/smoke-test.sh` — HEALTHCHECK + SCPI round-trip). QEMU
emulation is deliberately not used for the smoke: emulated .NET is
slow and flaky, and a native runner exists.

### 4. PR and nightly scope

Cross-platform spread is a **release gate, not a PR gate**. PR CI
(`pr.yml`) stays on `ubuntu-latest` only, keeping the feedback loop
fast; `nightly.yml` runs the Integration suite on `ubuntu-latest`,
`windows-latest`, and `macos-latest`. Rationale: the unit suite is
platform-neutral .NET, and the architecture-specific failure modes
observed in practice live in publish/packaging — exactly what §2
covers on every release.

### 5. Platform-specific functionality

The Local backend's `Ivi.Visa` reflection path is **Windows-only**
(the IVI Shared Components have no Linux/macOS distribution). On other
platforms it degrades to a clean `BackendError`, and its
positive-side tests gate on the `ni-visa` prerequisite probe
([ADR 0037](0037-real-hardware-integration-tests.md)). All other
features — gateways, mock/replay, discovery, Management API — are
platform-neutral by construction.

## Consequences

TBD
- Every shipped binary and both container manifest halves have been
executed at least once before users can download them; the
"cross-published but never run" class of release defect is closed.
- The release pipeline uses three additional runner images
(`ubuntu-24.04-arm`, `windows-11-arm`, `macos-15-intel`). All are
free for public repositories, but runner-label retirement (GitHub
rotates macOS/Windows images) will occasionally force a label bump
here and in `release.yml`.
- Release wall-clock changes little: the matrix was already six jobs;
they now differ in host arch rather than duplicating hosts.
- PR feedback speed is unchanged — no PR job was added.
13 changes: 8 additions & 5 deletions docs/adr/0018-deployment-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,14 @@ Three smoke gates protect the image quality:
`src/IviCli.Cli/**`. Publishes, builds single-arch (amd64),
runs HEALTHCHECK + SCPI roundtrip. No push. Status check
appears only on relevant PRs.
3. **Release-time** (`.github/workflows/release.yml` `docker`
job): single-arch smoke gate **before** the multi-arch push.
Failure aborts the push so `latest` is never updated to a
broken image. After smoke passes, buildx pushes the multi-arch
manifest with all four tags.
3. **Release-time** (`.github/workflows/release.yml`): native
smoke gates on **both architectures** — amd64 inside the
`docker` job, arm64 in `docker-smoke-arm64` on an arm64 runner
— **before** the multi-arch push. Failure of either aborts the
push so `latest` is never updated to a broken image. After both
smokes pass, buildx pushes the multi-arch manifest with all
four tags. All three gates share `docker/smoke-test.sh`
(HEALTHCHECK + SCPI round-trip).

### 10. Required runtime composition env

Expand Down