426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
429 lines
19 KiB
YAML
429 lines
19 KiB
YAML
name: Hetzner Agent E2E
|
|
|
|
# Nightly end-to-end smoke that provisions a fresh Hetzner cpx11 server,
|
|
# waits for cloud-init, deploys a trivial agent via the Eliza Cloud
|
|
# staging API, runs a bridge-ping healthcheck plus one REAL chat turn
|
|
# (message.send through the production bridge path — #15603 A3), then
|
|
# tears everything down. Gracefully skips when the required secrets are
|
|
# unset (so this workflow can land on develop and be activated later), but a
|
|
# configured secret that is rejected by Hetzner or the Cloud API is a red
|
|
# workflow: green-by-dead-credential is indistinguishable from a real pass.
|
|
#
|
|
# Required secrets / config (GitHub environment: ci-hetzner-e2e):
|
|
# HCLOUD_TOKEN_CI - Hetzner Cloud API token (CI-scoped project)
|
|
# CLOUD_E2E_API_KEY - Eliza Cloud staging bearer token (long-lived)
|
|
# CI_SSH_PRIVATE_KEY - OpenSSH private key (matches the public key
|
|
# uploaded to Hetzner)
|
|
# CI_SSH_PUBLIC_KEY_ID - Numeric Hetzner SSH key id for the above
|
|
#
|
|
# See packages/scripts/cloud/admin/hetzner-e2e/README.md for setup.
|
|
|
|
on:
|
|
schedule:
|
|
- cron: '0 7 * * *' # 07:00 UTC nightly
|
|
workflow_dispatch:
|
|
push:
|
|
branches: [develop]
|
|
paths:
|
|
- '.github/workflows/hetzner-e2e.yml'
|
|
- '.github/workflows/hetzner-e2e-reaper.yml'
|
|
- 'packages/scripts/cloud/admin/hetzner-e2e/**'
|
|
- 'packages/cloud/shared/src/lib/services/containers/**'
|
|
- 'packages/scripts/cloud/admin/daemons/**'
|
|
- 'packages/scripts/cloud/admin/bootstrap-provisioning-worker-host.mjs'
|
|
|
|
concurrency:
|
|
group: hetzner-e2e
|
|
cancel-in-progress: false
|
|
|
|
# Default to least privilege. Override per-job where needed.
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
deploy:
|
|
name: Provision + deploy + healthcheck
|
|
runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }}
|
|
environment: ci-hetzner-e2e
|
|
timeout-minutes: 25
|
|
outputs:
|
|
provisioned: ${{ steps.provision.outputs.provisioned }}
|
|
env:
|
|
HCLOUD_TOKEN_CI: ${{ secrets.HCLOUD_TOKEN_CI }}
|
|
CLOUD_E2E_API_KEY: ${{ secrets.CLOUD_E2E_API_KEY || secrets.ELIZACLOUD_API_KEY }}
|
|
CI_SSH_PRIVATE_KEY: ${{ secrets.CI_SSH_PRIVATE_KEY }}
|
|
CI_SSH_PUBLIC_KEY_ID: ${{ secrets.CI_SSH_PUBLIC_KEY_ID }}
|
|
HETZNER_E2E_STATE_FILE: /tmp/hetzner-e2e-state.json
|
|
steps:
|
|
- name: Check secret configuration
|
|
id: secret_config
|
|
run: |
|
|
missing=()
|
|
[ -z "$HCLOUD_TOKEN_CI" ] && missing+=("HCLOUD_TOKEN_CI")
|
|
[ -z "$CLOUD_E2E_API_KEY" ] && missing+=("CLOUD_E2E_API_KEY")
|
|
[ -z "$CI_SSH_PRIVATE_KEY" ] && missing+=("CI_SSH_PRIVATE_KEY")
|
|
[ -z "$CI_SSH_PUBLIC_KEY_ID" ] && missing+=("CI_SSH_PUBLIC_KEY_ID")
|
|
if [ "${#missing[@]}" -ne 0 ]; then
|
|
echo "configured=false" >> "$GITHUB_OUTPUT"
|
|
echo "::warning::Missing secrets: ${missing[*]}; skipping Hetzner E2E."
|
|
{
|
|
echo "### Hetzner E2E skipped"
|
|
echo ""
|
|
echo "Missing secrets in GitHub environment \`ci-hetzner-e2e\`:"
|
|
for s in "${missing[@]}"; do echo "- \`$s\`"; done
|
|
echo ""
|
|
echo "See \`packages/scripts/cloud/admin/hetzner-e2e/README.md\` for setup."
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
echo "::error::Manual dispatch requires all hetzner-e2e secrets."
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
fi
|
|
echo "configured=true" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Checkout
|
|
if: steps.secret_config.outputs.configured == 'true'
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
|
|
- name: Setup Bun
|
|
if: steps.secret_config.outputs.configured == 'true'
|
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
|
|
with:
|
|
bun-version: canary
|
|
|
|
- name: Debug workspace
|
|
if: steps.secret_config.outputs.configured == 'true'
|
|
run: |
|
|
echo "cwd=$(pwd)"
|
|
echo "workspace=$GITHUB_WORKSPACE"
|
|
ls -la "$GITHUB_WORKSPACE" | head -30
|
|
test -f "$GITHUB_WORKSPACE/package.json" && echo HAS_PKG || echo NO_PKG
|
|
|
|
- name: Install dependencies
|
|
if: steps.secret_config.outputs.configured == 'true'
|
|
run: bun install --no-save --ignore-scripts
|
|
|
|
- name: Ensure generated shared i18n data
|
|
if: steps.secret_config.outputs.configured == 'true'
|
|
run: bun packages/app-core/scripts/ensure-shared-i18n-data.mjs
|
|
|
|
- name: Check cloud API auth
|
|
id: cloud_auth
|
|
if: steps.secret_config.outputs.configured == 'true'
|
|
run: |
|
|
set +e
|
|
response="$(
|
|
bun -e '
|
|
const base = (process.env.CLOUD_SMOKE_BASE_URL ?? "https://api-staging.elizacloud.ai").replace(/\/+$/, "");
|
|
const response = await fetch(`${base}/api/v1/eliza/agents`, {
|
|
headers: {
|
|
authorization: `Bearer ${process.env.CLOUD_E2E_API_KEY ?? ""}`,
|
|
accept: "application/json",
|
|
"user-agent": "hetzner-e2e-preflight/1.0",
|
|
},
|
|
signal: AbortSignal.timeout(30_000),
|
|
});
|
|
const text = await response.text();
|
|
console.log(JSON.stringify({ status: response.status, body: text.slice(0, 500) }));
|
|
'
|
|
)"
|
|
status=$?
|
|
set -e
|
|
if [ "$status" -ne 0 ]; then
|
|
echo "ready=false" >> "$GITHUB_OUTPUT"
|
|
echo "::warning::Cloud API auth preflight could not reach staging; skipping Hetzner E2E."
|
|
{
|
|
echo "### Hetzner E2E skipped"
|
|
echo ""
|
|
echo "Cloud API auth preflight could not reach staging before provisioning Hetzner capacity."
|
|
echo ""
|
|
echo "\`\`\`json"
|
|
echo "$response"
|
|
echo "\`\`\`"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
status_code="$(printf '%s' "$response" | bun -e 'const chunks=[]; for await (const chunk of Bun.stdin.stream()) chunks.push(Buffer.from(chunk)); const data = JSON.parse(Buffer.concat(chunks).toString() || "{}"); console.log(data.status ?? "");')"
|
|
if [ "$status_code" = "401" ] || [ "$status_code" = "403" ]; then
|
|
echo "ready=false" >> "$GITHUB_OUTPUT"
|
|
echo "::error::Cloud API rejected CLOUD_E2E_API_KEY with HTTP $status_code; refresh the ci-hetzner-e2e environment secret."
|
|
{
|
|
echo "### Hetzner E2E blocked by rejected Cloud API credential"
|
|
echo ""
|
|
echo "Cloud API rejected \`CLOUD_E2E_API_KEY\` with HTTP \`$status_code\` before provisioning Hetzner capacity."
|
|
echo ""
|
|
echo "Refresh the \`CLOUD_E2E_API_KEY\` / \`ELIZACLOUD_API_KEY\` secret in the \`ci-hetzner-e2e\` environment, or check the staging API-key lookup path."
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
exit 1
|
|
fi
|
|
|
|
if [ "${status_code#2}" != "$status_code" ]; then
|
|
echo "ready=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "${status_code#5}" != "$status_code" ]; then
|
|
echo "ready=false" >> "$GITHUB_OUTPUT"
|
|
echo "::warning::Cloud API auth preflight returned HTTP $status_code; skipping Hetzner E2E before provisioning."
|
|
{
|
|
echo "### Hetzner E2E skipped"
|
|
echo ""
|
|
echo "Cloud API auth preflight returned HTTP \`$status_code\` before provisioning Hetzner capacity."
|
|
echo ""
|
|
echo "\`\`\`json"
|
|
echo "$response"
|
|
echo "\`\`\`"
|
|
echo ""
|
|
echo "Check staging Cloud API logs for \`user-agent: hetzner-e2e-preflight/1.0\` on \`/api/v1/eliza/agents\`."
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
echo "::error::Unexpected cloud API auth preflight response: $response"
|
|
exit 1
|
|
|
|
- name: Check Hetzner API token
|
|
id: hcloud_auth
|
|
if: steps.secret_config.outputs.configured == 'true' && steps.cloud_auth.outputs.ready == 'true'
|
|
run: |
|
|
set +e
|
|
response="$(
|
|
bun -e '
|
|
const response = await fetch("https://api.hetzner.cloud/v1/ssh_keys?per_page=1", {
|
|
headers: {
|
|
authorization: `Bearer ${process.env.HCLOUD_TOKEN_CI ?? ""}`,
|
|
accept: "application/json",
|
|
"user-agent": "hetzner-e2e-preflight/1.0",
|
|
},
|
|
signal: AbortSignal.timeout(30_000),
|
|
});
|
|
const text = await response.text();
|
|
console.log(JSON.stringify({ status: response.status, body: text.slice(0, 500) }));
|
|
'
|
|
)"
|
|
status=$?
|
|
set -e
|
|
if [ "$status" -ne 0 ]; then
|
|
echo "ready=false" >> "$GITHUB_OUTPUT"
|
|
echo "::warning::Hetzner API token preflight could not reach api.hetzner.cloud; skipping Hetzner E2E."
|
|
{
|
|
echo "### Hetzner E2E skipped"
|
|
echo ""
|
|
echo "Hetzner API token preflight could not reach \`api.hetzner.cloud\` before provisioning capacity."
|
|
echo ""
|
|
echo "\`\`\`json"
|
|
echo "$response"
|
|
echo "\`\`\`"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
status_code="$(printf '%s' "$response" | bun -e 'const chunks=[]; for await (const chunk of Bun.stdin.stream()) chunks.push(Buffer.from(chunk)); const data = JSON.parse(Buffer.concat(chunks).toString() || "{}"); console.log(data.status ?? "");')"
|
|
if [ "$status_code" = "401" ] || [ "$status_code" = "403" ]; then
|
|
echo "ready=false" >> "$GITHUB_OUTPUT"
|
|
echo "::error::Hetzner rejected HCLOUD_TOKEN_CI with HTTP $status_code; refresh the ci-hetzner-e2e environment secret."
|
|
{
|
|
echo "### Hetzner E2E blocked by rejected Hetzner credential"
|
|
echo ""
|
|
echo "Hetzner rejected \`HCLOUD_TOKEN_CI\` with HTTP \`$status_code\` before provisioning capacity."
|
|
echo ""
|
|
echo "Refresh \`HCLOUD_TOKEN_CI\` in the \`ci-hetzner-e2e\` environment, or confirm the Hetzner project is still active."
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
exit 1
|
|
fi
|
|
|
|
if [ "${status_code#2}" != "$status_code" ]; then
|
|
echo "ready=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "${status_code#5}" != "$status_code" ]; then
|
|
echo "ready=false" >> "$GITHUB_OUTPUT"
|
|
echo "::warning::Hetzner API token preflight returned HTTP $status_code; skipping Hetzner E2E before provisioning."
|
|
{
|
|
echo "### Hetzner E2E skipped"
|
|
echo ""
|
|
echo "Hetzner API token preflight returned HTTP \`$status_code\` before provisioning capacity."
|
|
echo ""
|
|
echo "\`\`\`json"
|
|
echo "$response"
|
|
echo "\`\`\`"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
echo "::error::Unexpected Hetzner API token preflight response: $response"
|
|
exit 1
|
|
|
|
- name: Provision Hetzner server
|
|
id: provision
|
|
if: steps.secret_config.outputs.configured == 'true' && steps.cloud_auth.outputs.ready == 'true' && steps.hcloud_auth.outputs.ready == 'true'
|
|
run: |
|
|
log=/tmp/hetzner-e2e-provision.log
|
|
set +e
|
|
bun run packages/scripts/cloud/admin/hetzner-e2e/hetzner-e2e-provision.ts 2>&1 | tee "$log"
|
|
rc=${PIPESTATUS[0]}
|
|
set -e
|
|
if [ "$rc" -eq 0 ]; then
|
|
echo "provisioned=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
# A rejected / expired / read-only HCLOUD_TOKEN_CI (or a deactivated project)
|
|
# surfaces as HTTP 401/403 missing_token / "permission denied" when CREATING the
|
|
# server — the read-only token preflight above can pass while this write fails.
|
|
# That is an operator secret problem, not a code regression, but the
|
|
# token is configured and rejected. Fail red for every event so the
|
|
# nightly cannot look healthy while real-infra coverage is disabled.
|
|
if grep -qE "Hetzner rejected the token|missing_token|permission denied|HTTP 401|HTTP 403|status: 401|status: 403" "$log"; then
|
|
echo "provisioned=false" >> "$GITHUB_OUTPUT"
|
|
echo "::error::Hetzner rejected HCLOUD_TOKEN_CI (HTTP 401/403); refresh the token in the ci-hetzner-e2e environment."
|
|
{
|
|
echo "### Hetzner E2E blocked by rejected Hetzner credential"
|
|
echo ""
|
|
echo "Hetzner rejected \`HCLOUD_TOKEN_CI\` (HTTP 401/403 \`missing_token\` / permission denied) when creating the server."
|
|
echo ""
|
|
echo "The token may be read-only or expired for writes. Refresh \`HCLOUD_TOKEN_CI\` in the \`ci-hetzner-e2e\` environment with a read-write CI token, or confirm the project is active."
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
exit 1
|
|
fi
|
|
# Any other provision failure is real — surface it (red).
|
|
echo "provisioned=false" >> "$GITHUB_OUTPUT"
|
|
exit "$rc"
|
|
|
|
- name: Surface provision failure diagnostic
|
|
if: always() && steps.secret_config.outputs.configured == 'true' && steps.cloud_auth.outputs.ready == 'true' && steps.hcloud_auth.outputs.ready == 'true' && steps.provision.outcome == 'failure'
|
|
run: |
|
|
log=/tmp/hetzner-e2e-provision.log
|
|
{
|
|
echo "### Hetzner E2E provisioning failed"
|
|
echo ""
|
|
if [ -f "$log" ] && grep -qE "quota exhausted|server limit reached|limit_reached|resource_limit_exceeded|quota_exceeded" "$log"; then
|
|
echo "**Cause:** Hetzner project quota exhausted (server cap reached)."
|
|
echo ""
|
|
echo "Operator action:"
|
|
echo "1. Open https://console.hetzner.cloud/ and check the CI project for leaked servers (filter labels \`ci=true\`, \`workflow=hetzner-e2e\`)."
|
|
echo "2. Delete any leaked servers, or wait for the half-hourly reaper workflow to sweep them."
|
|
echo "3. Re-run this workflow."
|
|
echo ""
|
|
echo "If the project itself has a tighter cap than the fallback ladder can survive, request a quota increase from Hetzner support or rotate \`HCLOUD_TOKEN_CI\` to a project with capacity."
|
|
elif [ -f "$log" ] && grep -qE "Hetzner rejected the token|missing_token|HTTP 401|HTTP 403" "$log"; then
|
|
echo "**Cause:** Hetzner rejected the API token (HTTP 401/403)."
|
|
echo ""
|
|
echo "Operator action: refresh \`HCLOUD_TOKEN_CI\` in the \`ci-hetzner-e2e\` GitHub environment, or confirm the project is still active."
|
|
else
|
|
echo "**Cause:** see the \"Provision Hetzner server\" step log for the raw error."
|
|
echo ""
|
|
echo "If the error message is unfamiliar, check \`packages/scripts/cloud/admin/hetzner-e2e/README.md\` and the fallback ladder in \`hetzner-e2e-provision.ts\`."
|
|
fi
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
- name: Wait for host ready
|
|
if: steps.provision.outputs.provisioned == 'true'
|
|
run: bun run packages/scripts/cloud/admin/hetzner-e2e/hetzner-e2e-wait-ready.ts
|
|
|
|
- name: Deploy trivial agent
|
|
if: steps.provision.outputs.provisioned == 'true'
|
|
run: bun run packages/scripts/cloud/admin/hetzner-e2e/hetzner-e2e-deploy-agent.ts
|
|
|
|
- name: Healthcheck
|
|
if: steps.provision.outputs.provisioned == 'true'
|
|
run: bun run packages/scripts/cloud/admin/hetzner-e2e/hetzner-e2e-healthcheck.ts
|
|
|
|
# A real user-message round-trip through the same Worker bridge path a
|
|
# production chat takes. status.get alone lets "provisioned but chat
|
|
# dead-ends" regressions (#15347) pass the nightly. Failure here fails
|
|
# the deploy job; the teardown job runs regardless (needs+always()).
|
|
- name: Chat turn
|
|
if: steps.provision.outputs.provisioned == 'true'
|
|
run: bun run packages/scripts/cloud/admin/hetzner-e2e/hetzner-e2e-chat.ts
|
|
|
|
- name: Upload state artifact
|
|
if: always() && steps.provision.outputs.provisioned == 'true'
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
|
|
with:
|
|
name: hetzner-e2e-state
|
|
path: /tmp/hetzner-e2e-state.json
|
|
if-no-files-found: ignore
|
|
retention-days: 7
|
|
|
|
teardown:
|
|
name: Teardown
|
|
needs: deploy
|
|
if: always()
|
|
runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }}
|
|
environment: ci-hetzner-e2e
|
|
timeout-minutes: 10
|
|
env:
|
|
HCLOUD_TOKEN_CI: ${{ secrets.HCLOUD_TOKEN_CI }}
|
|
HETZNER_E2E_STATE_FILE: /tmp/hetzner-e2e-state.json
|
|
steps:
|
|
- name: Check secret configuration
|
|
id: secret_config
|
|
run: |
|
|
if [ -z "$HCLOUD_TOKEN_CI" ]; then
|
|
echo "configured=false" >> "$GITHUB_OUTPUT"
|
|
echo "::warning::HCLOUD_TOKEN_CI missing; nothing to tear down."
|
|
exit 0
|
|
fi
|
|
echo "configured=true" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Checkout
|
|
if: steps.secret_config.outputs.configured == 'true'
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
|
|
- name: Setup Bun
|
|
if: steps.secret_config.outputs.configured == 'true'
|
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
|
|
with:
|
|
bun-version: canary
|
|
|
|
- name: Install dependencies
|
|
if: steps.secret_config.outputs.configured == 'true'
|
|
run: bun install --no-save --ignore-scripts
|
|
|
|
- name: Ensure generated shared i18n data
|
|
if: steps.secret_config.outputs.configured == 'true'
|
|
run: bun packages/app-core/scripts/ensure-shared-i18n-data.mjs
|
|
|
|
- name: Download state artifact
|
|
if: steps.secret_config.outputs.configured == 'true'
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
|
|
with:
|
|
name: hetzner-e2e-state
|
|
path: /tmp/hetzner-e2e-state
|
|
continue-on-error: true
|
|
|
|
- name: Restore state file
|
|
if: steps.secret_config.outputs.configured == 'true'
|
|
run: |
|
|
if [ -f /tmp/hetzner-e2e-state/hetzner-e2e-state.json ]; then
|
|
cp /tmp/hetzner-e2e-state/hetzner-e2e-state.json /tmp/hetzner-e2e-state.json
|
|
echo "State file restored:"
|
|
cat /tmp/hetzner-e2e-state.json
|
|
else
|
|
echo "No state artifact; teardown will fall back to label sweep."
|
|
fi
|
|
|
|
- name: Teardown
|
|
if: steps.secret_config.outputs.configured == 'true'
|
|
env:
|
|
# Lets the teardown script distinguish "dead token, nothing was
|
|
# provisioned, nothing can leak" (soft-skip) from "dead token but a
|
|
# server exists" (hard fail so the leak is never silent).
|
|
DEPLOY_PROVISIONED: ${{ needs.deploy.outputs.provisioned }}
|
|
run: bun run packages/scripts/cloud/admin/hetzner-e2e/hetzner-e2e-teardown.ts
|