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
25 changes: 2 additions & 23 deletions .github/workflows/canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@ name: Canary — plugin vs OpenClaw latest

# OpenClaw ships new releases constantly, so the host can break us even when we
# don't push. Run the host-interface contract tests (plus a full typecheck)
# against the latest published OpenClaw twice a day and post one Google Chat
# message on failure. The live channel suite chains off this run, so the canary
# leads and live follows on the same cadence.
# against the latest published host before full-stack suites.
on:
schedule:
# 2x/day at 7 AM and 7 PM America/Los_Angeles (PDT/UTC-7 basis; cron is UTC).
- cron: "13 14 * * *" # 07:00 PT
- cron: "13 2 * * *" # 19:00 PT
workflow_call:
workflow_dispatch: {}

permissions:
Expand Down Expand Up @@ -37,19 +32,3 @@ jobs:

- name: Complete offline suite vs latest OpenClaw
run: npm test

notify:
needs: canary
if: always() && github.event_name == 'schedule' && needs.canary.result != 'success'
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Notify Google Chat on scheduled failure
env:
WEBHOOK_URL: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
run: |
[ -n "$WEBHOOK_URL" ] || { echo "GOOGLE_CHAT_WEBHOOK_URL is missing"; exit 1; }
curl --fail-with-body --retry 3 --retry-all-errors --max-time 20 \
-sS -X POST -H 'Content-Type: application/json' \
-d "{\"text\": \"🚨 openclaw-plugin canary FAILED: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
"$WEBHOOK_URL"
56 changes: 19 additions & 37 deletions .github/workflows/live-stack.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
name: Full stack e2e

on:
schedule:
- cron: "13 14 * * *"
- cron: "13 2 * * *"
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
workflow_run:
workflows: ["Canary — plugin vs OpenClaw latest"]
types: [completed]

permissions:
contents: read
Expand All @@ -17,13 +17,22 @@ concurrency:
queue: max

jobs:
channels:
canary:
if: >
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository)
uses: ./.github/workflows/canary.yml

channels:
needs: canary
if: >
!cancelled() && needs.canary.result == 'success' &&
((github.event_name == 'pull_request' && github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository) ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main')
github.event_name == 'schedule')
uses: ./.github/workflows/live-channels.yml
with:
orchestrated: true
Expand All @@ -36,8 +45,7 @@ jobs:
((github.event_name == 'pull_request' && github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository) ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main'))
github.event_name == 'schedule')
uses: ./.github/workflows/live-a2a.yml
with:
orchestrated: true
Expand All @@ -50,8 +58,7 @@ jobs:
((github.event_name == 'pull_request' && github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository) ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main'))
github.event_name == 'schedule')
uses: ./.github/workflows/live-voice.yml
with:
orchestrated: true
Expand All @@ -67,8 +74,7 @@ jobs:
((github.event_name == 'pull_request' && github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository) ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main'))
github.event_name == 'schedule')
uses: ./.github/workflows/live-external-events.yml
with:
orchestrated: true
Expand All @@ -82,8 +88,7 @@ jobs:
((github.event_name == 'pull_request' && github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository) ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main'))
github.event_name == 'schedule')
runs-on: ubuntu-latest
steps:
- name: Require every live suite to pass
Expand All @@ -101,26 +106,3 @@ jobs:
if [ "$result" != "success" ]; then failed=1; fi
done
exit "$failed"

notify:
needs: [channels, a2a, voice, external-events, full-stack]
if: >
always() && github.event_name == 'workflow_run' &&
github.event.workflow_run.event == 'schedule' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main' &&
(needs.channels.result != 'success' || needs.a2a.result != 'success' ||
needs.voice.result != 'success' ||
needs.external-events.result != 'success' || needs.full-stack.result != 'success')
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Notify Google Chat
env:
WEBHOOK_URL: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
run: |
[ -n "$WEBHOOK_URL" ] || { echo "GOOGLE_CHAT_WEBHOOK_URL is missing"; exit 1; }
curl --fail-with-body --retry 3 --retry-all-errors --max-time 20 \
-sS -X POST -H 'Content-Type: application/json' \
-d "{\"text\": \"🚨 openclaw-plugin full-stack e2e FAILED (chained off the canary): ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
"$WEBHOOK_URL"
64 changes: 64 additions & 0 deletions .github/workflows/scheduled-failure-report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Scheduled failure reporting

on:
workflow_run:
workflows: ["Full stack e2e"]
types: [completed]

permissions:
actions: read

jobs:
report:
if: >-
${{
github.event.workflow_run.event == 'schedule' &&
(
github.event.workflow_run.conclusion == 'failure' ||
github.event.workflow_run.conclusion == 'timed_out' ||
github.event.workflow_run.conclusion == 'startup_failure'
)
}}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Post failure notification
env:
NOTIFICATION_URL: ${{ secrets.SCHEDULED_FAILURE_NOTIFICATION_URL }}
REPOSITORY: ${{ github.repository }}
WORKFLOW_NAME: ${{ github.event.workflow_run.name }}
RUN_ID: ${{ github.event.workflow_run.id }}
RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
RUN_URL: ${{ github.event.workflow_run.html_url }}
run: |
set -euo pipefail
[ -n "${NOTIFICATION_URL}" ] || { echo "::error::Missing failure notification URL"; exit 1; }
repo_slug="${REPOSITORY//\//-}"
thread_key="ci-${repo_slug}-${RUN_ID}-${RUN_ATTEMPT}"
payload="$(jq -n --arg repository "${REPOSITORY}" --arg workflow "${WORKFLOW_NAME}" --arg run_url "${RUN_URL}" '{text: ("Scheduled integration checks failed\n\nRepository: " + $repository + "\nWorkflow: " + $workflow + "\nRun: " + $run_url)}')"
curl --fail-with-body --silent --show-error --retry 3 --retry-connrefused --retry-delay 2 --retry-max-time 90 --connect-timeout 10 --max-time 30 -X POST "${NOTIFICATION_URL}&threadKey=${thread_key}&messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD" -H 'Content-Type: application/json' --data "${payload}"

- name: Send signed failure event
if: always()
env:
RECEIVER_URL: ${{ secrets.SCHEDULED_FAILURE_RECEIVER_URL }}
SIGNING_SECRET: ${{ secrets.SCHEDULED_FAILURE_SIGNING_SECRET }}
FAILURE_ENVIRONMENT: ${{ secrets.SCHEDULED_FAILURE_ENVIRONMENT }}
REPOSITORY: ${{ github.repository }}
WORKFLOW_NAME: ${{ github.event.workflow_run.name }}
RUN_ID: ${{ github.event.workflow_run.id }}
RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
RUN_URL: ${{ github.event.workflow_run.html_url }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
[ -n "${RECEIVER_URL}" ] || { echo "::error::Missing failure receiver URL"; exit 1; }
[ -n "${SIGNING_SECRET}" ] || { echo "::error::Missing failure signing secret"; exit 1; }
[ -n "${FAILURE_ENVIRONMENT}" ] || { echo "::error::Missing failure environment"; exit 1; }
echo "::add-mask::${SIGNING_SECRET}"
repo_slug="${REPOSITORY//\//-}"
thread_key="ci-${repo_slug}-${RUN_ID}-${RUN_ATTEMPT}"
payload="$(jq -c -n --arg event_type "scheduled_ci_failure" --arg source "${REPOSITORY}" --arg repository "${REPOSITORY}" --arg workflow "${WORKFLOW_NAME}" --arg source_job "${WORKFLOW_NAME}" --arg environment "${FAILURE_ENVIRONMENT}" --argjson run_id "${RUN_ID}" --argjson run_attempt "${RUN_ATTEMPT}" --arg run_url "${RUN_URL}" --arg head_sha "${HEAD_SHA}" --arg chat_thread_key "${thread_key}" '{$event_type, $source, $repository, $workflow, $source_job, $environment, $run_id, $run_attempt, $run_url, $head_sha, $chat_thread_key}')"
signature="$(printf '%s' "${payload}" | openssl dgst -sha256 -hmac "${SIGNING_SECRET}" -binary | xxd -p -c 256)"
request_id="ci:${REPOSITORY}:${RUN_ID}:${RUN_ATTEMPT}:${FAILURE_ENVIRONMENT}"
curl --fail-with-body --silent --show-error --retry 3 --retry-connrefused --retry-delay 2 --retry-max-time 90 --connect-timeout 10 --max-time 30 -X POST "${RECEIVER_URL}" -H 'Content-Type: application/json' -H 'X-GitHub-Event: workflow_run' -H "X-Hub-Signature-256: sha256=${signature}" -H "X-Inkbox-Request-Id: ${request_id}" --data-binary "${payload}"
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@inkbox/inkbox",
"version": "0.2.10",
"version": "0.2.11",
"description": "Inkbox plugin for OpenClaw \u2014 adds email, SMS, iMessage, voice, and A2A tools with inbound webhook handling",
"license": "MIT",
"type": "module",
Expand Down
32 changes: 32 additions & 0 deletions tests/scheduled-failure-reporting.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
import { describe, expect, it } from "vitest";

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const readWorkflow = (name: string) =>
readFileSync(path.join(root, ".github", "workflows", name), "utf8");

describe("scheduled failure reporting", () => {
it("uses one scheduled and signed reporting path", () => {
const canary = readWorkflow("canary.yml");
const stack = readWorkflow("live-stack.yml");
const report = readWorkflow("scheduled-failure-report.yml");

expect(canary).toContain("workflow_call:");
expect(canary).not.toContain("schedule:");
expect(canary).not.toContain("notify:");
expect(stack).toContain("schedule:");
expect(stack).toContain("uses: ./.github/workflows/canary.yml");
expect(stack).not.toContain("workflow_run:");
expect(stack).not.toContain("notify:");
expect(report).toContain('workflows: ["Full stack e2e"]');
expect(report).toContain("github.event.workflow_run.event == 'schedule'");
expect(report).toContain("timed_out");
expect(report).toContain("startup_failure");
expect(report).toContain("if: always()");
expect(report).toContain("actions: read");
expect(report).toContain("X-Hub-Signature-256");
expect(report).toContain("chat_thread_key");
});
});