Skip to content

Drift Auto-Detection #5

Drift Auto-Detection

Drift Auto-Detection #5

name: Drift Auto-Detection
on:
schedule:
- cron: '0 9 * * 1' # Monday 9:00 UTC
workflow_dispatch:
inputs:
coherence_root:
description: 'Coherence root directory (default: coherence)'
required: false
default: 'coherence'
type: string
permissions:
contents: read
issues: write
jobs:
detect-drift:
name: Detect Assumption Expiry Drift
runs-on: ubuntu-latest
env:
COHERENCE_ROOT: ${{ inputs.coherence_root || 'coherence' }}
steps:
- uses: actions/checkout@v4
- name: Check for expired assumptions
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const { owner, repo } = context.repo;
const root = process.env.COHERENCE_ROOT;
const path = `${root}/intel/assumptions.yaml`;
if (!fs.existsSync(path)) {
core.info(`No assumptions.yaml found at ${path}. Nothing to check.`);
return;
}
const content = fs.readFileSync(path, 'utf8');
const today = new Date();
today.setHours(0, 0, 0, 0);
// Parse assumptions from YAML.
// Handles: empty files, assumptions: [], indented lists, multi-line statements.
if (!content.includes('- id:')) {
core.info('No assumptions defined. Nothing to check.');
return;
}
const blocks = content.split(/^\s*- id:/m).slice(1);
const assumptions = blocks.map(block => {
const id = (block.match(/^\s*(.+)/m) || ['', ''])[1].trim();
const status = (block.match(/^\s*status:\s*(.+)/m) || ['', ''])[1].trim();
const expires = (block.match(/^\s*expires:\s*(.+)/m) || ['', ''])[1].trim();
const owner = (block.match(/^\s*owner:\s*"?(.+?)"?\s*$/m) || ['', ''])[1].trim();
// Handle multi-line statements (> or | YAML folding)
let statement = '';
const stmtMatch = block.match(/^\s*statement:\s*(.+)/m);
if (stmtMatch) {
const val = stmtMatch[1].trim().replace(/^["']|["']$/g, '');
if (/^[>|][-+]?$/.test(val)) {
const lines = block.split('\n');
const idx = lines.findIndex(l => /^\s*statement:/.test(l));
const keyIndent = (lines[idx].match(/^(\s*)/)||['',''])[1].length;
const parts = [];
for (let i = idx + 1; i < lines.length; i++) {
const indent = (lines[i].match(/^(\s*)/)||['',''])[1].length;
if (lines[i].trim() && indent <= keyIndent) break;
if (lines[i].trim()) parts.push(lines[i].trim());
}
statement = parts.join(' ');
} else {
statement = val;
}
}
return {
id: id.startsWith('ASM-') ? id : `ASM-${id}`,
status, expires, statement, owner,
};
});
const expired = assumptions.filter(a => {
if (a.status !== 'active' || !a.expires) return false;
return new Date(a.expires + 'T00:00:00') < today;
});
if (expired.length === 0) {
core.info('No expired assumptions. No drift issues needed.');
return;
}
// Check existing open issues to avoid duplicates
const { data: existingIssues } = await github.rest.issues.listForRepo({
owner, repo,
state: 'open',
labels: 'drift,auto-detected',
per_page: 100,
});
const existingTitles = existingIssues.map(i => i.title);
for (const asm of expired) {
const title = `[DRIFT] Assumption expired: ${asm.id}`;
if (existingTitles.includes(title)) {
core.info(`Issue already exists for ${asm.id}, skipping.`);
continue;
}
const todayStr = today.toISOString().split('T')[0];
const body = [
'## Severity',
'medium',
'',
'## What Drifted',
`Assumption **${asm.id}** has expired.`,
'',
`> ${asm.statement}`,
'',
`Expiry date: \`${asm.expires}\``,
'',
'## Evidence',
`Auto-detected by [drift-auto-detect](.github/workflows/drift-auto-detect.yml) workflow on ${todayStr}.`,
"The assumption's `expires` field is past today's date while `status` is still `active`.",
'',
'## Affected',
`- Assumption: ${asm.id}`,
`- Owner: ${asm.owner}`,
'',
'## Action Required',
'1. Re-validate the assumption — is it still true?',
'2. If still true: update `expires` date in `assumptions.yaml`',
'3. If no longer true: set `status: expired`, create a DRIFT file in `coherence/drift/`, and open a patch PR',
'',
'## Status',
'open',
'',
'---',
'_Auto-detected by [CoherenceOps](https://github.com/8ryanWh1t3/CoherenceOps) drift auto-detection._',
].join('\n');
const { data: issue } = await github.rest.issues.create({
owner, repo,
title,
body,
labels: ['drift', 'auto-detected'],
});
core.info(`Created drift issue #${issue.number} for ${asm.id}`);
}