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: 1 addition & 24 deletions .github/workflows/canary.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
name: Canary — plugin vs Claude Code latest

on:
schedule:
# 05:13 & 17:13 America/Los_Angeles (PDT/UTC-7 basis; cron is UTC) —
# leads the fleet's hour-staggered canary waves so upstream breakage
# surfaces here first.
- cron: "13 12 * * *"
- cron: "13 0 * * *"
workflow_call:
workflow_dispatch:

jobs:
Expand Down Expand Up @@ -40,21 +35,3 @@ jobs:
# this canary passes.
- name: Full offline suite vs latest host
run: pytest -q

# A separate always-evaluated job catches failures, cancellations, and other
# non-success results from the unattended twice-daily canary.
notify:
needs: contract
if: always() && github.event_name == 'schedule' && needs.contract.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\": \"🚨 claude-code-plugin canary FAILED against the latest Claude Code host: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
"$WEBHOOK_URL"
67 changes: 20 additions & 47 deletions .github/workflows/live-stack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,37 @@ name: Full stack e2e
# missing, skipped, cancelled, or failed.

on:
schedule:
- cron: "13 12 * * *"
- cron: "13 0 * * *"
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
workflow_run:
workflows: ["Canary — plugin vs Claude Code latest"]
types: [completed]

concurrency:
group: inkbox-live-aut-tunnel
cancel-in-progress: false
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 @@ -42,9 +51,7 @@ jobs:
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 @@ -58,9 +65,7 @@ jobs:
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 @@ -74,9 +79,7 @@ jobs:
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 @@ -91,9 +94,7 @@ jobs:
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 @@ -113,31 +114,3 @@ jobs:
fi
done
exit "$failed"

# One unattended page covers failures from any component or from the gate
# itself. PR authors and manual dispatchers can see their run directly.
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\": \"🚨 claude-code-plugin full-stack e2e FAILED (chained off the canary): ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
"$WEBHOOK_URL"
95 changes: 95 additions & 0 deletions .github/workflows/scheduled-failure-report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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 pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "claude-code-plugin"
version = "0.2.10"
version = "0.2.11"
description = "Inkbox bridge for Claude Code — talk to your coding agent over email, SMS, iMessage, and voice"
requires-python = ">=3.11"
dependencies = [
Expand Down
25 changes: 25 additions & 0 deletions tests/test_scheduled_failure_reporting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]


def test_scheduled_failures_use_one_verified_reporting_path():
canary = (ROOT / ".github/workflows/canary.yml").read_text()
stack = (ROOT / ".github/workflows/live-stack.yml").read_text()
report = (ROOT / ".github/workflows/scheduled-failure-report.yml").read_text()

assert "workflow_call:" in canary
assert "schedule:" not in canary
assert "notify:" not in canary
assert "schedule:" in stack
assert "uses: ./.github/workflows/canary.yml" in stack
assert "workflow_run:" not in stack
assert "notify:" not in stack
assert 'workflows: ["Full stack e2e"]' in report
assert "github.event.workflow_run.event == 'schedule'" in report
assert "failure" in report and "timed_out" in report and "startup_failure" in report
assert "if: always()" in report
assert "actions: read" in report
assert "X-Hub-Signature-256" in report
assert "chat_thread_key" in report
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.