diff --git a/.github/workflows/claude-remediation.yml b/.github/workflows/claude-remediation.yml index 440393d8f..815a2560b 100644 --- a/.github/workflows/claude-remediation.yml +++ b/.github/workflows/claude-remediation.yml @@ -18,8 +18,10 @@ jobs: # Only PR comments (issue_comment fires for both issues and PRs) that # contain the trigger phrase. This workflow never runs on a bare scan # failure - a maintainer has to ask for it explicitly. + # Only the owner, members and collaborators can trigger it, so a stranger's comment cannot spend the API key. if: > github.event.issue.pull_request != null && + contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) && (contains(github.event.comment.body, '/remediate') || contains(github.event.comment.body, '/claude fix')) runs-on: ubuntu-latest diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index 0022cd050..a37fab21a 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -11,7 +11,15 @@ jobs: runs-on: ubuntu-latest if: github.actor == 'dependabot[bot]' steps: + - name: Read the update type + id: meta + uses: dependabot/fetch-metadata@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + # Major version bumps wait for a person. - name: Enable auto-merge + if: steps.meta.outputs.update-type == 'version-update:semver-patch' || steps.meta.outputs.update-type == 'version-update:semver-minor' run: gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 61112c5cf..401c7c5b8 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -10,6 +10,10 @@ on: - 'demo-btree/**' - 'demo-intent/**' - 'intent/**' + - 'verify/**' + - 'inmemory/**' + - 'go.mod' + - 'go.sum' - 'docs/assets/**' - 'docs/index.html' - 'scripts/assemble-site.sh' @@ -153,6 +157,7 @@ jobs: - uses: actions/setup-node@v7 with: node-version: 20 + cache: 'npm' - name: Deploy id: deploy @@ -162,12 +167,12 @@ jobs: MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'production' }} run: | if [ "$MODE" = production ]; then - npx --yes wrangler@4 deploy + npx --yes wrangler@4.151.3 deploy echo "url=https://joltrinhq.com" >> "$GITHUB_OUTPUT" else # Wrangler writes a JSON line per event to this file, so the URL is read as a field. export WRANGLER_OUTPUT_FILE_PATH="$RUNNER_TEMP/wrangler-output.json" - npx --yes wrangler@4 versions upload + npx --yes wrangler@4.151.3 versions upload url=$(jq -r 'select(.type == "version-upload") | .preview_url // empty' "$WRANGLER_OUTPUT_FILE_PATH" | head -1) [ -n "$url" ] || { echo "Error: no preview_url in wrangler output" >&2; exit 1; } echo "url=$url" >> "$GITHUB_OUTPUT" @@ -238,3 +243,31 @@ jobs: curl -fsS "${SITE_URL}/" -o /tmp/live-index.html mkdir -p /tmp/live && cp /tmp/live-index.html /tmp/live/index.html scripts/check-youtube.sh /tmp/live + + # Rollback. If the live site fails its smoke run right after a production deploy, put the + # previous Cloudflare version back so visitors are not left on a broken page. Preview + # uploads never reach production, so they are not rolled back. Run history shows which + # version came back; to do the same by hand: npx --yes wrangler@4.151.3 rollback --message "reason" + # (needs CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID). After a rollback, fix forward + # in a new commit. This job leaves the workflow red so the failure is not missed. + rollback: + name: Roll back a failed production deploy + needs: [deploy, verify-live] + if: ${{ always() && needs.verify-live.result == 'failure' && (github.event_name != 'workflow_dispatch' || inputs.mode == 'production') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-node@v7 + with: + node-version: 20 + cache: 'npm' + + - name: Roll back to the previous version + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: | + npx --yes wrangler@4.151.3 rollback --message "verify-live failed for ${GITHUB_SHA::7}" --yes + echo "Rolled back. Production serves the previous version; fix forward in a new commit." >> "$GITHUB_STEP_SUMMARY" + exit 1 diff --git a/.github/workflows/uptime.yml b/.github/workflows/uptime.yml index 3dce652aa..ff90d8a6f 100644 --- a/.github/workflows/uptime.yml +++ b/.github/workflows/uptime.yml @@ -2,8 +2,10 @@ # /api/health (liveness) and /api/ready (readiness) and fails the run if either # does not return 200 with the expected body after three tries. Run history is the availability record: # gh run list --workflow uptime.yml --limit 500 --json conclusion -# GitHub's scheduler can delay or skip runs, so treat the history as a -# sample, not a complete measurement. It uses no secrets and changes nothing. +# It also opens https://joltrinhq.com/ (the marketing site on Cloudflare), which the Azure +# health endpoints do not cover. GitHub's scheduler can delay or skip runs, so this is not +# an SLO and the history is a sample, not a complete measurement. For a real availability +# target use an external monitor. It uses no secrets and changes nothing. name: Live app check on: @@ -47,4 +49,14 @@ jobs: echo "| $path | $code | $secs |" >> "$GITHUB_STEP_SUMMARY" [ "$ok" = yes ] || { echo "$path failed: status $code, expected body with $want" >&2; fail=1; } done + # The marketing site is served by Cloudflare, so it is probed separately from the Azure app. + code=000; secs=0; ok=no + for try in 1 2 3; do + out=$(curl -s -o /tmp/site --max-time 20 -w '%{http_code} %{time_total}' https://joltrinhq.com/ || true) + code=${out%% *}; secs=${out##* } + if [ "$code" = 200 ] && grep -q '