Compare commits
6 Commits
claude/ama
...
cursor/ci-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3bf9ef4ee | ||
|
|
80656d522c | ||
|
|
f1bd94dda2 | ||
|
|
d2df25fd9d | ||
|
|
0b5c0ceb61 | ||
|
|
467d94b5b7 |
66
.github/MERGE_GATES.md
vendored
Normal file
66
.github/MERGE_GATES.md
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
# CI merge gates
|
||||
|
||||
This repository splits continuous integration into **product** checks that should block merge and **infra** checks that provide signal without blocking Autopilot merges.
|
||||
|
||||
## Required for merge (product)
|
||||
|
||||
Configure branch protection on `main` to require exactly one status check from the `ci` workflow:
|
||||
|
||||
| Check name | Workflow | What it covers |
|
||||
| --- | --- | --- |
|
||||
| **Product CI** | `ci.yml` | Lint, Typecheck, Production builds, Unit tests, Postgres journeys, Web E2E |
|
||||
|
||||
The aggregate `Product CI` job fails when any of those product jobs fail. Individual job names remain visible on the pull request for debugging.
|
||||
|
||||
Optional but recommended repository settings:
|
||||
|
||||
- Enable **Allow auto-merge**
|
||||
- Enable **Automatically delete head branches** after merge
|
||||
|
||||
## Infra-only (never required)
|
||||
|
||||
These workflows may fail without blocking product delivery. Do **not** add them to required status checks.
|
||||
|
||||
| Check / workflow | Why infra |
|
||||
| --- | --- |
|
||||
| `macOS setup screenshot (infra)` (`desktop-macos-screenshot.yml`) | Native screenshot capture; skips when the runner has no display |
|
||||
| Playwright / desktop artifact uploads | Soft-fail (`continue-on-error`) when GitHub artifact quota is exhausted |
|
||||
| `publish-server-image.yml` (PR paths) | Docker image validation for infra changes |
|
||||
| `mobile Android screenshots` | Manual `workflow_dispatch` only |
|
||||
| `nightly verification` | Scheduled visual regression |
|
||||
| `manual topology verification` | Manual smoke |
|
||||
| `release-desktop.yml` | Tag-driven release pipeline |
|
||||
| `publish-playwright-report.yml` | Report publishing helper |
|
||||
|
||||
## Autopilot auto-merge
|
||||
|
||||
Workflow: `.github/workflows/autopilot-auto-merge.yml` (informational — **not** a required check)
|
||||
|
||||
1. Open a pull request from an Autopilot/cloud agent.
|
||||
2. Add label **`autopilot`** or **`automerge`**.
|
||||
3. Wait until **Product CI** is green on the head commit.
|
||||
4. The workflow runs `gh pr merge --auto --squash`. GitHub merges when branch protection is satisfied (required checks green, reviews if configured).
|
||||
|
||||
### Dry-run mode
|
||||
|
||||
Set repository variable `AUTOPILOT_MERGE_DRY_RUN` to `true` to log the merge command without enabling auto-merge.
|
||||
|
||||
### Optional secret: `AUTOPILOT_MERGE_TOKEN`
|
||||
|
||||
Most repositories can rely on the built-in `GITHUB_TOKEN` (`pull-requests: write`).
|
||||
|
||||
Add a fine-grained PAT or GitHub App installation token as **`AUTOPILOT_MERGE_TOKEN`** when auto-merge is rejected because branch protection requires a human actor or bypass permission the default token lacks.
|
||||
|
||||
Minimum scopes:
|
||||
|
||||
- Pull requests: read and write
|
||||
- Contents: read (if merge commits need it under your rules)
|
||||
|
||||
After adding the secret, remove `AUTOPILOT_MERGE_DRY_RUN` or set it to `false`.
|
||||
|
||||
## One-time branch protection checklist (maintainer)
|
||||
|
||||
1. Settings → Branches → `main` → Require status checks before merging.
|
||||
2. Search for **Product CI** and select only that check.
|
||||
3. Remove **macOS setup screenshot**, artifact-only jobs, and other infra workflows from required checks if they were added earlier.
|
||||
4. Enable **Allow auto-merge** if Autopilot should merge without manual clicking.
|
||||
119
.github/workflows/autopilot-auto-merge.yml
vendored
Normal file
119
.github/workflows/autopilot-auto-merge.yml
vendored
Normal file
@ -0,0 +1,119 @@
|
||||
# Enables GitHub auto-merge for Autopilot PRs once the Product CI gate is green.
|
||||
# Label a pull request with `autopilot` or `automerge` to opt in.
|
||||
# This workflow is informational only — never add it to required status checks.
|
||||
name: autopilot auto-merge
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, unlabeled, opened, synchronize, reopened, ready_for_review]
|
||||
check_suite:
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
checks: read
|
||||
|
||||
concurrency:
|
||||
group: autopilot-auto-merge-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
auto-merge:
|
||||
name: Autopilot auto-merge
|
||||
runs-on: [self-hosted, evoo-ci, Linux]
|
||||
steps:
|
||||
- name: Resolve pull request
|
||||
id: pr
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
event_name="${{ github.event_name }}"
|
||||
number=""
|
||||
if [[ "$event_name" == "pull_request" ]]; then
|
||||
number="${{ github.event.pull_request.number }}"
|
||||
elif [[ "$event_name" == "check_suite" ]]; then
|
||||
number="$(jq -r '.check_suite.pull_requests[0].number // empty' "$GITHUB_EVENT_PATH")"
|
||||
fi
|
||||
if [[ -z "$number" ]]; then
|
||||
echo "No pull request in event; skipping."
|
||||
echo "enabled=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
pr_json="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${number}")"
|
||||
labels="$(jq -r '[.labels[].name] | join(",")' <<<"$pr_json")"
|
||||
enabled=false
|
||||
if [[ ",${labels}," == *",autopilot,"* || ",${labels}," == *",automerge,"* ]]; then
|
||||
enabled=true
|
||||
fi
|
||||
{
|
||||
echo "number=$number"
|
||||
echo "head_sha=$(jq -r .head.sha <<<"$pr_json")"
|
||||
echo "draft=$(jq -r .draft <<<"$pr_json")"
|
||||
echo "labels=$labels"
|
||||
echo "enabled=$enabled"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Skip unless Autopilot label is present
|
||||
if: steps.pr.outputs.enabled != 'true'
|
||||
run: echo "No autopilot/automerge label; skipping."
|
||||
|
||||
- name: Skip draft pull requests
|
||||
if: steps.pr.outputs.enabled == 'true' && steps.pr.outputs.draft == 'true'
|
||||
run: echo "Draft pull request; auto-merge waits for ready_for_review."
|
||||
|
||||
- name: Verify Product CI on head commit
|
||||
id: gate
|
||||
if: steps.pr.outputs.enabled == 'true' && steps.pr.outputs.draft != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mapfile -t conclusions < <(
|
||||
gh api "repos/${GITHUB_REPOSITORY}/commits/${HEAD_SHA}/check-runs" --paginate \
|
||||
--jq '.check_runs[] | select(.name == "Product CI") | .conclusion'
|
||||
)
|
||||
if [[ "${#conclusions[@]}" -eq 0 ]]; then
|
||||
echo "Product CI has not finished yet."
|
||||
echo "ready=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
for conclusion in "${conclusions[@]}"; do
|
||||
case "$conclusion" in
|
||||
success)
|
||||
echo "Product CI succeeded."
|
||||
echo "ready=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
;;
|
||||
failure|cancelled|timed_out|action_required)
|
||||
echo "Product CI failed ($conclusion); not enabling auto-merge."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
echo "Product CI still pending."
|
||||
echo "ready=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Enable auto-merge
|
||||
if: steps.pr.outputs.enabled == 'true' && steps.pr.outputs.draft != 'true' && steps.gate.outputs.ready == 'true'
|
||||
env:
|
||||
AUTOPILOT_MERGE_TOKEN: ${{ secrets.AUTOPILOT_MERGE_TOKEN }}
|
||||
DRY_RUN: ${{ vars.AUTOPILOT_MERGE_DRY_RUN }}
|
||||
PR_NUMBER: ${{ steps.pr.outputs.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$DRY_RUN" == 'true' ]]; then
|
||||
echo "AUTOPILOT_MERGE_DRY_RUN=true — would run: gh pr merge $PR_NUMBER --auto --squash"
|
||||
exit 0
|
||||
fi
|
||||
if [[ -n "$AUTOPILOT_MERGE_TOKEN" ]]; then
|
||||
export GH_TOKEN="$AUTOPILOT_MERGE_TOKEN"
|
||||
echo "Using AUTOPILOT_MERGE_TOKEN."
|
||||
else
|
||||
export GH_TOKEN="${{ github.token }}"
|
||||
echo "Using GITHUB_TOKEN. Add AUTOPILOT_MERGE_TOKEN if branch protection requires a PAT."
|
||||
fi
|
||||
gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --auto --squash
|
||||
echo "Auto-merge enabled (squash). GitHub merges when branch protection allows."
|
||||
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@ -1,3 +1,6 @@
|
||||
# Product merge gate: the `Product CI` job aggregates lint, typecheck, builds, unit,
|
||||
# Postgres journeys, and Web E2E. Branch protection should require only that check.
|
||||
# Deploy, OTA, and artifact uploads here are post-merge or soft-fail infra helpers.
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
@ -210,6 +213,36 @@ jobs:
|
||||
with:
|
||||
upload_artifacts: true
|
||||
|
||||
product-gate:
|
||||
name: Product CI
|
||||
if: always()
|
||||
needs: [lint, check, build, test, test-integration, test-e2e]
|
||||
runs-on: [self-hosted, evoo-ci, Linux]
|
||||
steps:
|
||||
- name: Verify product checks
|
||||
env:
|
||||
LINT: ${{ needs.lint.result }}
|
||||
CHECK: ${{ needs.check.result }}
|
||||
BUILD: ${{ needs.build.result }}
|
||||
TEST: ${{ needs.test.result }}
|
||||
TEST_INTEGRATION: ${{ needs.test-integration.result }}
|
||||
TEST_E2E: ${{ needs.test-e2e.result }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
failed=0
|
||||
for name in LINT CHECK BUILD TEST TEST_INTEGRATION TEST_E2E; do
|
||||
result="${!name}"
|
||||
printf '%s=%s\n' "$name" "$result"
|
||||
if [[ "$result" != "success" ]]; then
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
if [[ "$failed" -ne 0 ]]; then
|
||||
echo "One or more product checks failed." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "All product checks passed."
|
||||
|
||||
publish-mobile-update:
|
||||
name: Publish compatible mobile update
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
|
||||
27
.github/workflows/desktop-macos-screenshot.yml
vendored
27
.github/workflows/desktop-macos-screenshot.yml
vendored
@ -1,3 +1,5 @@
|
||||
# Infra-only: native macOS screenshots for PR review. Never add to branch protection.
|
||||
# Skips cleanly on headless runners (no display). Artifact quota failures are soft.
|
||||
name: desktop-macos-screenshot
|
||||
on:
|
||||
push:
|
||||
@ -24,21 +26,37 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
screenshot:
|
||||
name: macOS setup screenshot
|
||||
name: macOS setup screenshot (infra)
|
||||
runs-on: [self-hosted, evoo-ci, macOS]
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Detect interactive display
|
||||
id: display
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if system_profiler SPDisplaysDataType 2>/dev/null | grep -q "Resolution:"; then
|
||||
echo "available=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "available=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::No display attached on this macOS runner. Skipping infra screenshot; merge is not blocked."
|
||||
fi
|
||||
- uses: actions/checkout@v5
|
||||
if: steps.display.outputs.available == 'true'
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@v4
|
||||
if: steps.display.outputs.available == 'true'
|
||||
- uses: actions/setup-node@v4
|
||||
if: steps.display.outputs.available == 'true'
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
if: steps.display.outputs.available == 'true'
|
||||
- run: pnpm --filter @rakazo/desktop build
|
||||
if: steps.display.outputs.available == 'true'
|
||||
- name: Verify packaged Tahoe icon and legacy fallback
|
||||
if: steps.display.outputs.available == 'true'
|
||||
run: |
|
||||
pnpm --filter @rakazo/web build
|
||||
CSC_IDENTITY_AUTO_DISCOVERY=false pnpm --filter @rakazo/desktop exec electron-builder --mac --dir --publish never -c.mac.identity=null -c.mac.notarize=false
|
||||
@ -51,6 +69,8 @@ jobs:
|
||||
env:
|
||||
RAKAZO_ALLOW_DEV_SECRETS: "1"
|
||||
- name: Capture packaged macOS setup window and Dock
|
||||
if: steps.display.outputs.available == 'true'
|
||||
continue-on-error: true
|
||||
env:
|
||||
RAKAZO_E2E_EXECUTABLE: out/mac-arm64/Rakazo.app/Contents/MacOS/Rakazo
|
||||
run: >-
|
||||
@ -58,12 +78,13 @@ jobs:
|
||||
--config e2e/playwright.config.ts setup.spec.ts
|
||||
--grep "first run asks whether to use a local or existing instance"
|
||||
- name: Upload native macOS setup screenshot
|
||||
if: always()
|
||||
if: always() && steps.display.outputs.available == 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: desktop-e2e-macos-screenshot
|
||||
path: |
|
||||
apps/desktop/e2e/screenshots/08-setup-macos-native-controls.png
|
||||
apps/desktop/e2e/screenshots/09-macos-dock.png
|
||||
if-no-files-found: error
|
||||
if-no-files-found: warn
|
||||
retention-days: 1
|
||||
|
||||
5
.github/workflows/playwright.yml
vendored
5
.github/workflows/playwright.yml
vendored
@ -111,6 +111,8 @@ jobs:
|
||||
env:
|
||||
BOX_API_KEY: ${{ secrets.BOX_API_KEY }}
|
||||
run: test -n "$BOX_API_KEY"
|
||||
- name: Free stale E2E ports
|
||||
run: bash scripts/free-e2e-ports.sh
|
||||
- name: Run Playwright tests
|
||||
id: playwright_tests
|
||||
env:
|
||||
@ -121,6 +123,9 @@ jobs:
|
||||
run: pnpm test:e2e -- --sandbox="$SANDBOX_TEST_PROVIDER"
|
||||
# Marketing homepage only: separate config so product chromium does not start Astro.
|
||||
# Runs after product e2e so shared apps/web/test-results is not wiped by the product run.
|
||||
- name: Free stale marketing E2E ports
|
||||
if: always() && (steps.playwright_tests.outcome == 'success' || steps.playwright_tests.outcome == 'failure')
|
||||
run: bash scripts/free-e2e-ports.sh
|
||||
- name: Run marketing homepage E2E
|
||||
id: www_playwright_tests
|
||||
if: always() && (steps.playwright_tests.outcome == 'success' || steps.playwright_tests.outcome == 'failure')
|
||||
|
||||
@ -87,8 +87,12 @@ export async function signup(
|
||||
name: string,
|
||||
testInfo?: TestInfo,
|
||||
) {
|
||||
await page.goto("/sign-up");
|
||||
await expect(page.getByRole("heading", { name: "Create your EvOo" })).toBeVisible();
|
||||
await expect(async () => {
|
||||
await page.goto("/sign-up", { waitUntil: "domcontentloaded" });
|
||||
await expect(page.getByRole("heading", { name: "Create your EvOo" })).toBeVisible({
|
||||
timeout: 5_000,
|
||||
});
|
||||
}).toPass({ timeout: 45_000 });
|
||||
if (testInfo) await captureScreenshot(page, testInfo, "01-sign-up");
|
||||
await page.getByPlaceholder("Your name").fill(name);
|
||||
await page.getByPlaceholder("Your email address").fill(email);
|
||||
|
||||
@ -10,6 +10,25 @@ async function sendComposerMessage(page: Page, composer: Locator, text: string)
|
||||
await expect(composer).toHaveValue("");
|
||||
}
|
||||
|
||||
/** Copy is unique to the message More menu while it is open. */
|
||||
function messageMoreCopyItem(page: Page) {
|
||||
return page.getByRole("menuitem", { name: "Copy" });
|
||||
}
|
||||
|
||||
async function expectMessageMoreMenu(page: Page, open: boolean) {
|
||||
const copyItem = messageMoreCopyItem(page);
|
||||
if (open) {
|
||||
await expect(copyItem).toBeVisible();
|
||||
return;
|
||||
}
|
||||
await expect(async () => {
|
||||
if (await copyItem.isVisible()) {
|
||||
await page.keyboard.press("Escape");
|
||||
}
|
||||
await expect(copyItem).toBeHidden({ timeout: 500 });
|
||||
}).toPass({ timeout: 10_000 });
|
||||
}
|
||||
|
||||
async function revealHoverRail(row: Locator): Promise<Locator> {
|
||||
const rail = row.getByTestId("message-hover-rail");
|
||||
await expect
|
||||
@ -180,12 +199,13 @@ test("message hover shows beside-bubble actions; reply links to parent", async (
|
||||
expect(timeBox).not.toBeNull();
|
||||
expect(rowBox).not.toBeNull();
|
||||
expect(Math.abs(timeBox!.x - rowBox!.x)).toBeLessThan(2);
|
||||
await toolbar.getByRole("button", { name: "More" }).click();
|
||||
const moreButton = toolbar.getByRole("button", { name: "More" });
|
||||
await moreButton.click();
|
||||
await expectMessageMoreMenu(page, true);
|
||||
await expect(page.getByRole("menu").locator("time")).toHaveCount(0);
|
||||
// Escape closes More; the hover rail stays visible for the pinned row.
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page.getByRole("menu")).toHaveCount(0);
|
||||
await expect(toolbar.getByRole("button", { name: "More" })).toBeVisible();
|
||||
await expectMessageMoreMenu(page, false);
|
||||
await expect(moreButton).toBeVisible();
|
||||
await captureScreenshot(page, testInfo, "message-user-actions-hover-desktop");
|
||||
await page.getByRole("combobox", { name: "Message" }).focus();
|
||||
// Default transcript shot: rail at rest (no hover pin, mouse clear).
|
||||
|
||||
@ -85,6 +85,10 @@ async function main() {
|
||||
cwd: path.resolve("packages/db"),
|
||||
});
|
||||
|
||||
if (e2e && process.platform === "linux") {
|
||||
execSync("bash scripts/free-e2e-ports.sh", { stdio: "inherit", env: process.env });
|
||||
}
|
||||
|
||||
if (integration) {
|
||||
const suites = [
|
||||
"packages/testkit/src/pi-offline.postgres.test.ts",
|
||||
|
||||
47
packages/testkit/src/e2e-ports.test.ts
Normal file
47
packages/testkit/src/e2e-ports.test.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, "../../..");
|
||||
const scriptPath = path.join(repoRoot, "scripts/free-e2e-ports.sh");
|
||||
const playwrightWorkflow = readFileSync(
|
||||
path.join(repoRoot, ".github/workflows/playwright.yml"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
function portsFromShellArray(source: string, name: string): number[] {
|
||||
const match = source.match(new RegExp(`readonly ${name}=\\(([^)]*)\\)`));
|
||||
expect(match, `${name} array`).toBeTruthy();
|
||||
return [...match![1].matchAll(/\b(\d+)\b/g)].map(([, port]) => Number(port));
|
||||
}
|
||||
|
||||
describe("free-e2e-ports.sh", () => {
|
||||
const script = readFileSync(scriptPath, "utf8");
|
||||
|
||||
it("declares dedicated E2E ports separate from live dev ports", () => {
|
||||
const livePorts = portsFromShellArray(script, "LIVE_PORTS");
|
||||
const e2ePorts = portsFromShellArray(script, "E2E_PORTS");
|
||||
|
||||
expect(livePorts).toEqual([3100, 5173]);
|
||||
expect(e2ePorts).toEqual([3110, 3111, 5180, 4321]);
|
||||
for (const livePort of livePorts) {
|
||||
expect(e2ePorts).not.toContain(livePort);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses fuser and skips non-Linux hosts", () => {
|
||||
expect(script).toContain('fuser -k "$spec"');
|
||||
expect(script).toContain('uname -s)" != "Linux"');
|
||||
});
|
||||
|
||||
it("is wired into the Playwright workflow before product and marketing E2E", () => {
|
||||
const occurrences = playwrightWorkflow.match(/free-e2e-ports\.sh/g) ?? [];
|
||||
expect(occurrences.length).toBeGreaterThanOrEqual(2);
|
||||
expect(playwrightWorkflow.indexOf("free-e2e-ports.sh")).toBeLessThan(
|
||||
playwrightWorkflow.indexOf("Run Playwright tests"),
|
||||
);
|
||||
expect(playwrightWorkflow.lastIndexOf("free-e2e-ports.sh")).toBeLessThan(
|
||||
playwrightWorkflow.indexOf("Run marketing homepage E2E"),
|
||||
);
|
||||
});
|
||||
});
|
||||
47
scripts/free-e2e-ports.sh
Executable file
47
scripts/free-e2e-ports.sh
Executable file
@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# Free stale listeners on dedicated Playwright/E2E ports before harness bind.
|
||||
# Never touches live dev stack ports 3100 (API) or 5173 (web).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
readonly LIVE_PORTS=(3100 5173)
|
||||
readonly E2E_PORTS=(3110 3111 5180 4321)
|
||||
|
||||
if [[ "$(uname -s)" != "Linux" ]]; then
|
||||
echo "free-e2e-ports: skip (not Linux)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v fuser >/dev/null 2>&1; then
|
||||
echo "free-e2e-ports: warning: fuser not found; cannot pre-free E2E ports" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for port in "${LIVE_PORTS[@]}"; do
|
||||
for e2e_port in "${E2E_PORTS[@]}"; do
|
||||
if [[ "$port" -eq "$e2e_port" ]]; then
|
||||
echo "free-e2e-ports: internal error: live port $port must not be in E2E_PORTS" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "free-e2e-ports: clearing stale listeners on E2E ports (${E2E_PORTS[*]}); live ports ${LIVE_PORTS[*]} untouched"
|
||||
|
||||
for port in "${E2E_PORTS[@]}"; do
|
||||
spec="${port}/tcp"
|
||||
if fuser "$spec" >/dev/null 2>&1; then
|
||||
echo "free-e2e-ports: port $port in use:"
|
||||
fuser -v "$spec" 2>&1 || true
|
||||
echo "free-e2e-ports: killing processes on port $port (fuser -k)"
|
||||
fuser -k "$spec" 2>&1 || true
|
||||
sleep 0.25
|
||||
if fuser "$spec" >/dev/null 2>&1; then
|
||||
echo "free-e2e-ports: warning: port $port still in use after fuser -k" >&2
|
||||
else
|
||||
echo "free-e2e-ports: port $port is now free"
|
||||
fi
|
||||
else
|
||||
echo "free-e2e-ports: port $port already free"
|
||||
fi
|
||||
done
|
||||
Loading…
x
Reference in New Issue
Block a user