175 lines
7.6 KiB
YAML
175 lines
7.6 KiB
YAML
# 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}`);
|
|
}
|