Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
18 changes: 18 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ jobs:
- run:
name: Install Dependencies
command: pnpm install --frozen-lockfile
- run:
# Build/link the CS3D ref from .cs3d-ref so unit tests run against it
# (no-op when .cs3d-ref is absent). Runs after install.
name: 'CS3D integration: build & link ref from .cs3d-ref'
command: bash .scripts/ci/setup-cs3d.sh
# RUN TESTS
- run:
name: 'JavaScript Test Suite'
Expand Down Expand Up @@ -160,6 +165,11 @@ jobs:
- run:
name: Install Dependencies
command: pnpm install
- run:
# Build/link the CS3D ref from .cs3d-ref so the package build uses it
# (no-op when .cs3d-ref is absent). Runs after install.
name: 'CS3D integration: build & link ref from .cs3d-ref'
command: bash .scripts/ci/setup-cs3d.sh
- run:
name: Avoid hosts unknown for github
command: |
Expand Down Expand Up @@ -431,6 +441,14 @@ jobs:
command: export DISPLAY=:99
- cypress/install:
install-command: pnpm install
- run:
# Build OHIF against the CS3D ref in .cs3d-ref (a branch is cloned,
# built with build:esm, and symlinked into node_modules; a version is
# pinned + reinstalled). No-op when .cs3d-ref is absent, so master and
# non-integration PRs are unaffected. Runs after the install above so
# the link lands on the freshly-installed node_modules.
name: 'CS3D integration: build & link ref from .cs3d-ref'
command: bash .scripts/ci/setup-cs3d.sh
Comment thread
wayfarer3130 marked this conversation as resolved.
- cypress/run-tests:
# CI runs headless under Xvfb with no GPU, so Electron uses software
# WebGL. Newer Chromium deprecated that implicit fallback (canvas
Expand Down
40 changes: 40 additions & 0 deletions .cs3d-ref
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# .cs3d-ref — canonical Cornerstone3D (CS3D) ref to build/test OHIF against.
#
# This file is the SINGLE SOURCE OF TRUTH for CS3D integration across all CI:
# - CircleCI .scripts/ci/setup-cs3d.sh (UNIT_TESTS / CYPRESS / package build)
# - Netlify netlify.toml -> setup-cs3d.sh
# - GitHub CI .scripts/ci/cs3d-check-integration.sh (Playwright workflow)
#
# HOW IT WORKS
# The first uncommented, non-blank line below is the active ref. Two kinds:
# version e.g. 5.0.2, 4.19+, 4.x, 4.18.2-beta.1
# -> @cornerstonejs/* are pinned to that published version + reinstalled.
# branch e.g. main, feat/foo, or owner:branch for a fork
# -> that CS3D branch is cloned, built (build:esm), and symlinked into
# OHIF's node_modules. A branch ref is BLOCKED from merging to
# master/release/* (it points at mutable, unpublished code).
#
# ENABLING / DISABLING
# Enable : add a single uncommented line with the ref (see examples below).
# Disable: COMMENT THE LINE OUT (prefix it with "#"). Do NOT delete this file —
# keeping it preserves these instructions for the next integration.
# Steady state on master/release is "disabled" (no active line).
#
# BUILD / DEPLOY WITH A LINKED CS3D BUILD
# Local : set the active line below, then
# bash .scripts/ci/setup-cs3d.sh # clone+build+link a branch, or pin+reinstall a version
# pnpm run build # or: pnpm run dev / pnpm run build:ci
# Override ad hoc without editing this file:
# CS3D_REF=<ref> bash .scripts/ci/setup-cs3d.sh
# CI : push the branch with the active line set. CircleCI, Netlify, and the
# Playwright workflow all pick it up automatically; Playwright also
# deploys a Netlify preview.
# Merge : comment the line out (or point it at a published version) BEFORE
# merging so master/release CI builds against the lockfile.
#
# EXAMPLES (uncomment ONE, or add your own):
# 4.19+
# main
# cornerstonejs:feat/my-feature

fix/grow-cut-suv-pt
39 changes: 26 additions & 13 deletions .scripts/ci/cs3d-branch-merge-guard.sh
Original file line number Diff line number Diff line change
@@ -1,42 +1,55 @@
#!/usr/bin/env bash
# CS3D branch merge guard: blocks merge when tests ran against a CS3D branch (not a version).
# Exits 0 when merge is allowed or guard is skipped; exits 1 when merge must be blocked.
# CS3D branch merge guard: blocks merge when OHIF would be built against a CS3D
# *branch* (not a published version). Mirrors the ref resolution in
# cs3d-check-integration.sh so the guard and the tests agree on the ref.
#
# Canonical ref source: the committed .cs3d-ref file (first uncommented line).
# Falls back to the ohif-integration label + PR-body CS3D_REF: line.
#
# Exits 0 when merge is allowed or the guard is skipped; exits 1 when merge must
# be blocked.
#
# Required env: GH_TOKEN, EVENT_NAME, REPO, PR_NUMBER
# Optional env: CS3D_REF_INPUT (for workflow_dispatch, default 4.19+)

set -e

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"

if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
echo "::notice::workflow_dispatch — no merge to block, skipping guard."
exit 0
elif [[ "$EVENT_NAME" == "pull_request" ]]; then
fi

# 1. canonical .cs3d-ref file (first active, non-comment, non-blank line)
CS3D_REF=""
if [[ -f "$ROOT/.cs3d-ref" ]]; then
CS3D_REF="$(grep -vE '^[[:space:]]*(#|$)' "$ROOT/.cs3d-ref" | head -1 | tr -d '[:space:]' || true)"
fi

# 2. legacy fallback: ohif-integration label + PR-body CS3D_REF: line
if [[ -z "$CS3D_REF" && "$EVENT_NAME" == "pull_request" ]]; then
LABELS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/labels" --jq '.[].name')
if echo "$LABELS" | grep -q "ohif-integration"; then
ENABLED=true
CS3D_REF=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.body' \
| sed -n 's/^[[:space:]]*CS3D_REF:[[:space:]]*\([^[:space:]]*\).*/\1/p' | head -1)
if [[ -z "$CS3D_REF" ]]; then
CS3D_REF="4.19+"
fi
else
ENABLED=false
fi
else
ENABLED=false
fi

if [[ "$ENABLED" != "true" ]]; then
echo "::notice::No ohif-integration label — skipping merge guard."
if [[ -z "$CS3D_REF" ]]; then
echo "::notice::No active .cs3d-ref and no ohif-integration label — skipping merge guard."
exit 0
fi

# Check if the ref is a branch (not a version)
# Versions are published + reproducible -> allowed. Branches -> blocked.
if [[ "$CS3D_REF" =~ ^[0-9]+\.[0-9x]+\+?(\.[0-9x]+)?(-[a-zA-Z0-9._]+)?$ ]]; then
echo "::notice::CS3D ref '$CS3D_REF' is a version — merge allowed."
exit 0
fi

echo "::error::Tests ran against CS3D branch '${CS3D_REF}' — this build cannot be merged."
echo "::error::Re-run with a published CS3D version (e.g. 4.19+) before merging."
echo "::error::OHIF is set to build against CS3D branch '${CS3D_REF}' — this build cannot be merged."
echo "::error::Comment out the line in .cs3d-ref (or set a published version, e.g. 4.19+) before merging."
exit 1
55 changes: 44 additions & 11 deletions .scripts/ci/cs3d-check-integration.sh
Original file line number Diff line number Diff line change
@@ -1,29 +1,62 @@
#!/usr/bin/env bash
# CS3D integration check: detects ohif-integration label and parses CS3D_REF.
# CS3D integration check: resolves whether to run CS3D integration and which ref.
# Writes to GITHUB_OUTPUT: enabled (true|false), cs3d_ref (when enabled).
#
# Canonical ref source: the committed .cs3d-ref file at the repo root (the first
# uncommented, non-blank line). This is the same file CircleCI and Netlify read via
# setup-cs3d.sh, so one file drives every CI system. Comment the line out to disable.
#
# Resolution order (highest priority first):
# 1. workflow_dispatch `cs3d_ref` input (manual run)
# 2. .cs3d-ref file (canonical committed source)
# 3. ohif-integration label + PR-body `CS3D_REF:` line (legacy fallback)
#
# Required env: GH_TOKEN, EVENT_NAME, REPO, PR_NUMBER, GITHUB_OUTPUT
# Optional env: CS3D_REF_INPUT (for workflow_dispatch, default 4.19+)

set -e

if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"

# First active (non-comment, non-blank) line of .cs3d-ref, or empty.
read_cs3d_ref_file() {
[[ -f "$ROOT/.cs3d-ref" ]] || return 0
grep -vE '^[[:space:]]*(#|$)' "$ROOT/.cs3d-ref" | head -1 | tr -d '[:space:]' || true
}

emit_enabled() {
echo "enabled=true" >> "$GITHUB_OUTPUT"
echo "cs3d_ref=${CS3D_REF_INPUT:-4.19+}" >> "$GITHUB_OUTPUT"
elif [[ "$EVENT_NAME" == "pull_request" ]]; then
echo "cs3d_ref=$1" >> "$GITHUB_OUTPUT"
}

# 1. workflow_dispatch input override
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
echo "::notice::CS3D ref from workflow_dispatch input: ${CS3D_REF_INPUT:-4.19+}"
emit_enabled "${CS3D_REF_INPUT:-4.19+}"
exit 0
fi

# 2. canonical .cs3d-ref file
FILE_REF="$(read_cs3d_ref_file)"
if [[ -n "$FILE_REF" ]]; then
echo "::notice::CS3D ref from .cs3d-ref: ${FILE_REF}"
emit_enabled "$FILE_REF"
exit 0
fi

# 3. legacy fallback: ohif-integration label + PR-body CS3D_REF: line
if [[ "$EVENT_NAME" == "pull_request" ]]; then
LABELS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/labels" --jq '.[].name')
if echo "$LABELS" | grep -q "ohif-integration"; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
REF=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.body' \
| sed -n 's/^[[:space:]]*CS3D_REF:[[:space:]]*\([^[:space:]]*\).*/\1/p' | head -1)
if [[ -z "$REF" ]]; then
REF="4.19+"
fi
echo "cs3d_ref=${REF}" >> "$GITHUB_OUTPUT"
echo "::notice::CS3D ref from PR body: ${REF}"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::notice::CS3D ref from PR body (ohif-integration label): ${REF}"
emit_enabled "$REF"
exit 0
fi
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
fi

echo "enabled=false" >> "$GITHUB_OUTPUT"
160 changes: 160 additions & 0 deletions .scripts/ci/free-ohif-e2e-port.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* Frees the OHIF Playwright e2e dev-server port before CI runs.
* Self-hosted runners (macOS and Linux) often leave `yarn start` / nyc processes
* bound to 3335 after cancelled or failed jobs, which makes Playwright fail with
* "http://localhost:3335 is already used".
*/
import { execSync } from 'node:child_process';

const DEFAULT_E2E_PORT = 3335;

export function getOhifE2ePort() {
const port = Number(process.env.OHIF_PORT || DEFAULT_E2E_PORT);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error(`Invalid OHIF_PORT: ${process.env.OHIF_PORT}`);
}
return port;
}

function runQuiet(command) {
try {
return execSync(command, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
} catch {
return '';
}
}

function parsePidList(output) {
return [...new Set(output.split(/\s+/).filter(Boolean))];
}

function parseSsListeningPids(output) {
const pids = [];
for (const match of output.matchAll(/pid=(\d+)/g)) {
pids.push(match[1]);
}
return [...new Set(pids)];
}

function killPids(pids, port, method) {
const killed = [];

for (const pid of pids) {
try {
process.kill(Number(pid), 'SIGKILL');
killed.push(pid);
} catch {
// Process may have already exited.
}
}

if (killed.length > 0) {
console.log(
`[free-ohif-e2e-port] Freed port ${port} via ${method}: killed PID(s) ${killed.join(', ')}`
);
}
}

function getListeningPidsDarwin(port) {
// macOS: -sTCP:LISTEN is supported and avoids matching outbound connections.
const output = runQuiet(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`);
if (output) {
return { pids: parsePidList(output), method: 'lsof (darwin)' };
}

const fallback = runQuiet(`lsof -nP -i :${port} -t`);
return { pids: parsePidList(fallback), method: 'lsof (darwin, fallback)' };
}

function getListeningPidsLinux(port) {
// Prefer LISTEN filter when supported (util-linux / recent lsof).
let output = runQuiet(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`);
if (output) {
return { pids: parsePidList(output), method: 'lsof (linux)' };
}

// Broader match — some Linux images lack -sTCP:LISTEN.
output = runQuiet(`lsof -nP -i :${port} -t`);
if (output) {
return { pids: parsePidList(output), method: 'lsof (linux, fallback)' };
}

// iproute2 ss — common on minimal Linux runners without lsof.
output = runQuiet(`ss -H -lptn 'sport = :${port}'`);
const ssPids = parseSsListeningPids(output);
if (ssPids.length > 0) {
return { pids: ssPids, method: 'ss' };
}

return { pids: [], method: null };
}

function freePortLinuxWithFuser(port) {
try {
execSync(`fuser -k ${port}/tcp`, { stdio: 'ignore' });
console.log(`[free-ohif-e2e-port] Freed port ${port} via fuser`);
return true;
} catch {
return false;
}
}

function freeOhifE2ePortUnix(port) {
const { pids, method } =
process.platform === 'darwin'
? getListeningPidsDarwin(port)
: getListeningPidsLinux(port);

if (pids.length > 0) {
killPids(pids, port, method);
return;
}

if (process.platform === 'linux') {
freePortLinuxWithFuser(port);
}
}

function freeOhifE2ePortWindows(port) {
const output = runQuiet(
`netstat -ano | findstr ":${port} " | findstr LISTENING`
);
Comment thread
wayfarer3130 marked this conversation as resolved.

if (!output) {
return;
}

const pids = [
...new Set(
output
.split(/\r?\n/)
.map(line => line.trim().split(/\s+/).pop())
.filter(Boolean)
),
];

killPids(pids, port, 'netstat');
}

export function freeOhifE2ePort(port = getOhifE2ePort()) {
const { platform } = process;

if (platform === 'darwin' || platform === 'linux') {
freeOhifE2ePortUnix(port);
return;
}

if (platform === 'win32') {
freeOhifE2ePortWindows(port);
}
}

const isDirectRun =
process.argv[1]?.replace(/\\/g, '/').endsWith('free-ohif-e2e-port.mjs') ?? false;

if (isDirectRun) {
freeOhifE2ePort();
}
Loading
Loading