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
27 changes: 3 additions & 24 deletions .github/workflows/canary.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
name: Canary — plugin vs Codex main

# Codex main moves fast and ships a prerelease cut (@alpha) near-daily, so the
# host can break us even when we don't push. Run the host-interface contract
# tests against the freshest main prerelease twice a day and alert on failure.
# The live channel suite chains off this run, so the canary leads and live
# follows on the same cadence.
# host can break us even when we don't push. This reusable check exercises the
# host interface against the freshest main prerelease before full-stack suites.
on:
schedule:
# 2x/day at 6 AM and 6 PM America/Los_Angeles (PDT/UTC-7 basis; cron is UTC).
- cron: "13 13 * * *" # 06:13 PT
- cron: "13 1 * * *" # 18:13 PT
workflow_call:
workflow_dispatch: {}

permissions:
Expand Down Expand Up @@ -46,19 +41,3 @@ jobs:
# only after this latest-host canary succeeds.
- name: Full offline suite vs real Codex
run: pytest -q

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
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\": \"🚨 codex-plugin canary FAILED against Codex main: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
"$WEBHOOK_URL"
65 changes: 20 additions & 45 deletions .github/workflows/live-stack.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
name: Full stack e2e

on:
schedule:
- cron: "13 13 * * *"
- cron: "13 1 * * *"
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
workflow_run:
workflows: ["Canary — plugin vs Codex main"]
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 @@ -36,9 +45,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 @@ -52,9 +59,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 @@ -71,9 +76,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 @@ -88,9 +91,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 @@ -108,29 +109,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\": \"🚨 codex-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 pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "codex-plugin"
version = "0.2.10"
version = "0.2.11"
description = "Inkbox bridge for Codex — 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