This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# ci.yml
|
||||
#
|
||||
# Fast quality gate on every push to main and every pull request.
|
||||
# Two checks:
|
||||
# 1. check-raw-siteconversion — no direct SiteConversion queries
|
||||
# that bypass countedConversionWhere (guards against count drift)
|
||||
# 2. tsc --noEmit — no TypeScript type errors
|
||||
#
|
||||
# No deploy steps. Median run time: ~60 seconds.
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
name: Quality gate
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Conversion query guard
|
||||
run: node scripts/check-raw-siteconversion.mjs
|
||||
|
||||
- name: TypeScript typecheck
|
||||
run: npx tsc --noEmit
|
||||
@@ -0,0 +1,174 @@
|
||||
# deploy-lag-monitor.yml
|
||||
#
|
||||
# Runs every hour. Compares the HEAD SHA of origin/main against the SHA
|
||||
# of the current Vercel production deployment. If they differ AND the
|
||||
# main HEAD commit is more than 1 hour old, opens a GitHub issue (or
|
||||
# comments on an existing open one) to alert about a stalled deploy.
|
||||
#
|
||||
# Required repository secrets (Settings -> Secrets and variables -> Actions):
|
||||
# VERCEL_TOKEN - Vercel personal access token (vercel.com -> Account Settings -> Tokens)
|
||||
# VERCEL_PROJECT_ID - Vercel project ID (vercel.com -> Project Settings -> General, "Project ID" field)
|
||||
#
|
||||
# To test manually: Actions tab -> "Deploy Lag Monitor" -> "Run workflow"
|
||||
# To silence a false alert: close the open "deploy-lag-alert" issue and
|
||||
# the next run will not re-open until a new lag condition is detected.
|
||||
|
||||
name: Deploy Lag Monitor
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *" # every hour at :00
|
||||
workflow_dispatch: # manual trigger for testing
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-deploy-lag:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Check deploy lag
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
with:
|
||||
script: |
|
||||
const ALERT_LABEL = "deploy-lag-alert";
|
||||
const LAG_THRESHOLD_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
// ── 1. Get HEAD SHA and commit timestamp from the checkout ──────
|
||||
const { execSync } = require("child_process");
|
||||
const mainSha = execSync("git rev-parse HEAD").toString().trim();
|
||||
const mainTimestampStr = execSync("git log -1 --format=%cI HEAD").toString().trim();
|
||||
const mainCommitMsg = execSync("git log -1 --format=%s HEAD").toString().trim();
|
||||
const mainTimestamp = new Date(mainTimestampStr);
|
||||
const commitAgeMs = Date.now() - mainTimestamp.getTime();
|
||||
|
||||
core.info(`main HEAD: ${mainSha} (${mainTimestampStr})`);
|
||||
core.info(`Commit age: ${Math.round(commitAgeMs / 60000)} minutes`);
|
||||
|
||||
// ── 2. Get current Vercel production deployment SHA ──────────────
|
||||
let vercelSha = null;
|
||||
let vercelDeployedAt = null;
|
||||
let vercelDeployUrl = null;
|
||||
|
||||
if (!process.env.VERCEL_TOKEN || !process.env.VERCEL_PROJECT_ID) {
|
||||
core.warning("VERCEL_TOKEN or VERCEL_PROJECT_ID secret not set. Skipping check.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://api.vercel.com/v6/deployments?projectId=${process.env.VERCEL_PROJECT_ID}&target=production&limit=1&state=READY`,
|
||||
{ headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` } }
|
||||
);
|
||||
if (!res.ok) {
|
||||
core.warning(`Vercel API returned ${res.status}. Skipping check.`);
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
const latest = data.deployments?.[0];
|
||||
if (latest) {
|
||||
vercelSha = latest.meta?.githubCommitSha ?? null;
|
||||
vercelDeployedAt = latest.createdAt ? new Date(latest.createdAt).toISOString() : "unknown";
|
||||
vercelDeployUrl = latest.url ? `https://${latest.url}` : null;
|
||||
}
|
||||
} catch (err) {
|
||||
core.warning(`Failed to fetch Vercel deployment: ${err.message}. Skipping check.`);
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Vercel production SHA: ${vercelSha ?? "unknown"}`);
|
||||
|
||||
// ── 3. Decide if there is a lag condition ───────────────────────
|
||||
// Only alert when:
|
||||
// a) SHAs differ (or Vercel SHA unknown), AND
|
||||
// b) The main commit is older than LAG_THRESHOLD_MS
|
||||
// (prevents noise on commits that just landed)
|
||||
const shasMismatch = vercelSha !== mainSha;
|
||||
const isOldEnough = commitAgeMs > LAG_THRESHOLD_MS;
|
||||
|
||||
if (!shasMismatch) {
|
||||
core.info("Production is up to date. No action needed.");
|
||||
return;
|
||||
}
|
||||
if (!isOldEnough) {
|
||||
const minutesOld = Math.round(commitAgeMs / 60000);
|
||||
core.info(`SHA mismatch but commit is only ${minutesOld}m old. Within grace period.`);
|
||||
return;
|
||||
}
|
||||
|
||||
core.warning(`Deploy lag detected! main=${mainSha} vercel=${vercelSha ?? "unknown"}`);
|
||||
|
||||
// ── 4. Check for an existing open alert issue ───────────────────
|
||||
const { data: existingIssues } = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ALERT_LABEL,
|
||||
state: "open",
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
const hoursOld = Math.round(commitAgeMs / 3_600_000);
|
||||
const body = [
|
||||
`**Production deploy lag detected.**`,
|
||||
``,
|
||||
`| Field | Value |`,
|
||||
`|---|---|`,
|
||||
`| \`main\` HEAD SHA | \`${mainSha}\` |`,
|
||||
`| Vercel production SHA | \`${vercelSha ?? "unknown"}\` |`,
|
||||
`| Last commit | ${mainCommitMsg} |`,
|
||||
`| Commit pushed | ${mainTimestampStr} (~${hoursOld}h ago) |`,
|
||||
`| Vercel last deploy | ${vercelDeployedAt} |`,
|
||||
`${vercelDeployUrl ? `| Deploy URL | ${vercelDeployUrl} |` : ""}`,
|
||||
``,
|
||||
`**Next steps:**`,
|
||||
`1. Open the Vercel dashboard and check for a failed or queued build.`,
|
||||
`2. Check GitHub Settings -> Webhooks -> Vercel webhook -> Recent Deliveries for failures.`,
|
||||
`3. If the webhook is healthy, trigger a manual redeploy from the Vercel dashboard.`,
|
||||
``,
|
||||
`_Close this issue once the deploy is confirmed live._`,
|
||||
].join("\n");
|
||||
|
||||
if (existingIssues.length > 0) {
|
||||
// Comment on the existing issue instead of opening a duplicate.
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: existingIssues[0].number,
|
||||
body: `**Still lagging** (hourly check at ${new Date().toISOString()})\n\n${body}`,
|
||||
});
|
||||
core.info(`Commented on existing alert issue #${existingIssues[0].number}`);
|
||||
} else {
|
||||
// Ensure the label exists before creating the issue.
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: ALERT_LABEL,
|
||||
});
|
||||
} catch {
|
||||
await github.rest.issues.createLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: ALERT_LABEL,
|
||||
color: "e11d48",
|
||||
description: "Auto-opened by deploy-lag-monitor workflow",
|
||||
});
|
||||
}
|
||||
const { data: newIssue } = await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: "[ALERT] Production deploy lag detected",
|
||||
body,
|
||||
labels: [ALERT_LABEL],
|
||||
});
|
||||
core.warning(`Opened alert issue #${newIssue.number}: ${newIssue.html_url}`);
|
||||
}
|
||||
Reference in New Issue
Block a user