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
134 changes: 134 additions & 0 deletions .github/workflows/monthly-gap-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
name: Monthly Gap Analysis
# Runs the Pathfinder gap-analysis pipeline on a 30-day lookback and publishes a
# ranked gap report to Notion, alerting Slack only when NEW high-severity gaps
# appear vs the prior run.
#
# IMPORTANT: this job works only from the analytics JSON API. It does NOT read
# the indexed repos, and it does NOT reproduce queries against the live MCP —
# doing so self-inflates the analytics it is trying to measure (that bug
# poisoned the first manual run).
#
# Required repository secrets (the job runs but no-ops until these are set):
# PATHFINDER_ANALYTICS_TOKEN Bearer token for GET /api/analytics/* on the
# production MCP (https://mcp.copilotkit.ai). When
# unset, the script logs "skipping live fetch" and
# exits 0, so lint/dry runs stay green.
# ANTHROPIC_API_KEY Anthropic key for the single LLM classification
# pass. When unset, a deterministic fallback report
# is produced from the clusters (no LLM call).
# NOTION_TOKEN Notion integration token used to publish the
# report page. When unset, the Notion step is
# skipped.
# SLACK_WEBHOOK_OSS_ALERTS Incoming-webhook URL (org-level secret shared by
# every workflow). Posted to ONLY when new
# high-severity gaps are detected. When unset, no
# alert is sent.
#
# Prior-run state (for new-gap diffing) is carried across runs as the
# `gap-analysis-state` artifact rather than the Actions cache: caches are
# evicted after 7 days of no access, but this job runs only every ~30 days, so
# the cache was always gone and EVERY high-severity gap re-alerted as "new".
# Artifacts persist 90 days regardless of access, comfortably past the cadence.
on:
schedule:
# Monthly, 1st of the month at 04:00 UTC — after the nightly reindex so the
# 30-day window reflects a freshly indexed corpus.
- cron: "0 4 1 * *"
workflow_dispatch:

permissions: {}

jobs:
gap-analysis:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
# Needed to list and download the prior run's state artifact via `gh`.
actions: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"

- run: npm ci

# Download the prior run's state so the script can diff new high-severity
# gaps. Pull the `gap-analysis-state` artifact from the most recent
# SUCCESSFUL prior run of this workflow ON THE DEFAULT BRANCH FROM THE
# SCHEDULE EVENT — so a manual workflow_dispatch run from a feature branch
# can never seed the scheduled run's new-gap baseline (which would make a
# branch experiment suppress or skew real production alerts). Tolerate the
# first run / a run whose artifact has aged out: continue with no prior
# state (the script then treats every high-severity gap as new, which is
# correct for a cold start).
- name: Download prior gap-analysis state
env:
GH_TOKEN: ${{ github.token }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
mkdir -p /tmp/gap-state
ID=$(gh run list \
--workflow=monthly-gap-analysis.yml \
--branch "$DEFAULT_BRANCH" \
--event schedule \
--status=success \
--limit=1 \
--json databaseId -q '.[0].databaseId' || true)
if [ -n "$ID" ]; then
if ! gh run download "$ID" -n gap-analysis-state --dir /tmp/gap-state; then
# The selected run was "success" but uploaded no state artifact
# (e.g. it exited early before provisioning). Emit a distinct,
# greppable warning so a BROKEN STATE CHAIN is visible in the logs
# rather than silently cold-starting and re-alerting every gap.
echo "::warning::GAP_STATE_CHAIN_BROKEN — run $ID had no gap-analysis-state artifact; cold-starting (every high-severity gap will re-alert)."
fi
else
echo "No prior successful scheduled run on $DEFAULT_BRANCH — cold start, no prior state."
fi

- name: Run gap analysis
env:
# Read-only analytics access. Unset → script exits 0 (dry/no-secrets).
PATHFINDER_ANALYTICS_TOKEN: ${{ secrets.PATHFINDER_ANALYTICS_TOKEN }}
# Single LLM classification pass. Unset → deterministic fallback.
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# Notion publish target. Unset → publish skipped.
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
# Gap-Reports parent page.
NOTION_PARENT_PAGE_ID: "3793aa38-1852-80a5-89d3-c3d37147aa22"
# Slack alert (new high-severity gaps only). Unset → no alert.
# Org-level secret shared by every workflow (see header).
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
# Stable path the download step writes to and the upload step reads
# from, so prior-run state survives across runs via the artifact.
GAP_STATE_PATH: /tmp/gap-state/pathfinder-gap-analysis-state.json
GAP_ANALYSIS_DAYS: "30"
run: npx tsx scripts/gap-analysis/monthly-gap-analysis.ts --report /tmp/gap-report.md

# Keep the rendered report as a build artifact for inspection even when
# Notion publishing is not yet configured.
- name: Upload gap report artifact
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: gap-report
path: /tmp/gap-report.md
if-no-files-found: ignore

# Persist updated state as a durable artifact so the next run (≈30 days
# later) can diff new high-severity gaps. Artifacts live 90 days
# regardless of access — unlike the Actions cache, which evicts after 7
# days idle and so was always gone by the next monthly run. Upload only
# when the script actually wrote the state file.
- name: Upload gap-analysis state
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: gap-analysis-state
path: /tmp/gap-state/pathfinder-gap-analysis-state.json
if-no-files-found: ignore
20 changes: 19 additions & 1 deletion .github/workflows/static-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,25 @@ jobs:
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: { node-version: 22 }
- run: npm ci
- run: npx prettier --check "src/**/*.ts"
# Cover the gap-analysis script too: it ships from scripts/ and was
# previously neither format-checked nor type-checked in CI (the other
# scripts/ files predate this gate and are out of scope here).
- run: npx prettier --check "src/**/*.ts" "scripts/gap-analysis/**/*.ts"

typecheck-scripts:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: { node-version: 22 }
- run: npm ci
# The root tsconfig excludes scripts/ (rootDir: src), so `npm run build`
# never type-checks the shipped scheduled scripts. tsconfig.scripts.json
# type-checks them (and their tests) without emitting.
- run: npx tsc --noEmit -p tsconfig.scripts.json

build:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .npmignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Source (dist/ is the published artifact)
src/
tsconfig.json
tsconfig*.json

# Tests and fixtures
**/__tests__/
Expand Down
79 changes: 79 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
}
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.101.0",
"@electric-sql/pglite": "^0.4.2",
"@types/compression": "^1.8.1",
"@types/cors": "^2.8.19",
Expand Down
Loading