feat: send daily repository status to Discord
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
const {
|
||||
buildDiscordPayload,
|
||||
normalizeGitHubUrl,
|
||||
parsePorcelain,
|
||||
postDiscordStatus,
|
||||
summarizeChecks
|
||||
} = require('../src/services/repositoryStatusDiscordService');
|
||||
|
||||
describe('Repository Discord status', () => {
|
||||
test('normalizes GitHub remotes without leaking transport details', () => {
|
||||
expect(normalizeGitHubUrl('[email protected]:alexpolo1/tilbudgivern.git'))
|
||||
.toBe('https://github.com/alexpolo1/tilbudgivern');
|
||||
expect(normalizeGitHubUrl('https://github.com/alexpolo1/tilbudgivern.git'))
|
||||
.toBe('https://github.com/alexpolo1/tilbudgivern');
|
||||
});
|
||||
|
||||
test('counts staged, modified and untracked files', () => {
|
||||
expect(parsePorcelain('M staged.js\n M modified.js\n?? new.js\nMM both.js\n')).toEqual({
|
||||
changed: 4,
|
||||
staged: 2,
|
||||
modified: 2,
|
||||
untracked: 1
|
||||
});
|
||||
});
|
||||
|
||||
test('summarizes pull request checks', () => {
|
||||
expect(summarizeChecks([{ statusCheckRollup: [
|
||||
{ status: 'COMPLETED', conclusion: 'SUCCESS' },
|
||||
{ status: 'IN_PROGRESS', conclusion: '' },
|
||||
{ status: 'COMPLETED', conclusion: 'FAILURE' }
|
||||
] }])).toEqual({ passed: 1, pending: 1, failed: 1 });
|
||||
});
|
||||
|
||||
test('builds a red, mention-safe Discord card when drift needs action', () => {
|
||||
const payload = buildDiscordPayload({
|
||||
repositoryUrl: 'https://github.com/alexpolo1/tilbudgivern',
|
||||
branch: 'main',
|
||||
upstream: 'origin/main',
|
||||
ahead: 0,
|
||||
behind: 1,
|
||||
worktree: { changed: 0, staged: 0, modified: 0, untracked: 0 },
|
||||
latestCommit: { hash: 'abc1234', subject: 'fix: test' },
|
||||
pullRequests: [],
|
||||
issues: [],
|
||||
checks: { passed: 1, pending: 0, failed: 1 },
|
||||
health: { ok: false, label: 'Utilgængelig' }
|
||||
}, new Date('2026-08-18T06:30:00.000Z'));
|
||||
|
||||
expect(payload.allowed_mentions).toEqual({ parse: [] });
|
||||
expect(payload.embeds[0]).toMatchObject({ color: 15548997 });
|
||||
expect(payload.embeds[0].title).toContain('Kræver handling');
|
||||
expect(JSON.stringify(payload)).not.toContain('webhook');
|
||||
});
|
||||
|
||||
test('posts JSON with wait enabled without embedding the webhook in payload', async () => {
|
||||
const fetchImpl = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ id: 'message-1' })
|
||||
});
|
||||
const payload = { allowed_mentions: { parse: [] }, embeds: [] };
|
||||
|
||||
await expect(postDiscordStatus({
|
||||
webhookUrl: 'https://discord.example/webhook-id/token',
|
||||
payload,
|
||||
fetchImpl
|
||||
})).resolves.toEqual({ id: 'message-1' });
|
||||
|
||||
expect(fetchImpl.mock.calls[0][0].searchParams.get('wait')).toBe('true');
|
||||
expect(JSON.parse(fetchImpl.mock.calls[0][1].body)).toEqual(payload);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const DISCORD_LIMITS = {
|
||||
fieldValue: 1024,
|
||||
description: 4096
|
||||
};
|
||||
|
||||
const truncate = (value, maxLength) => {
|
||||
const text = String(value || '');
|
||||
if (text.length <= maxLength) return text;
|
||||
return `${text.slice(0, Math.max(0, maxLength - 1))}…`;
|
||||
};
|
||||
|
||||
const normalizeGitHubUrl = (remoteUrl = '') => {
|
||||
const value = String(remoteUrl).trim();
|
||||
const sshMatch = value.match(/^git@github\.com:(.+?)(?:\.git)?$/);
|
||||
if (sshMatch) return `https://github.com/${sshMatch[1]}`;
|
||||
|
||||
const httpsMatch = value.match(/^https:\/\/github\.com\/(.+?)(?:\.git)?$/);
|
||||
if (httpsMatch) return `https://github.com/${httpsMatch[1]}`;
|
||||
return '';
|
||||
};
|
||||
|
||||
const parsePorcelain = (output = '') => {
|
||||
const lines = String(output).split('\n').filter(Boolean);
|
||||
return lines.reduce((summary, line) => {
|
||||
summary.changed += 1;
|
||||
if (line.startsWith('??')) {
|
||||
summary.untracked += 1;
|
||||
return summary;
|
||||
}
|
||||
if (line[0] && line[0] !== ' ') summary.staged += 1;
|
||||
if (line[1] && line[1] !== ' ') summary.modified += 1;
|
||||
return summary;
|
||||
}, { changed: 0, staged: 0, modified: 0, untracked: 0 });
|
||||
};
|
||||
|
||||
const summarizeChecks = (pullRequests = []) => {
|
||||
const summary = { passed: 0, pending: 0, failed: 0 };
|
||||
pullRequests.forEach((pullRequest) => {
|
||||
(pullRequest.statusCheckRollup || []).forEach((check) => {
|
||||
const conclusion = String(check.conclusion || '').toUpperCase();
|
||||
const status = String(check.status || '').toUpperCase();
|
||||
if (['FAILURE', 'TIMED_OUT', 'CANCELLED', 'ACTION_REQUIRED'].includes(conclusion)) {
|
||||
summary.failed += 1;
|
||||
} else if (status !== 'COMPLETED' || !conclusion) {
|
||||
summary.pending += 1;
|
||||
} else if (['SUCCESS', 'NEUTRAL', 'SKIPPED'].includes(conclusion)) {
|
||||
summary.passed += 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
return summary;
|
||||
};
|
||||
|
||||
const safeJson = (value, fallback) => {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (_error) {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
const createCommandRunner = (repoDir) => (command, args, options = {}) => {
|
||||
try {
|
||||
return execFileSync(command, args, {
|
||||
cwd: repoDir,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
timeout: options.timeout || 15000
|
||||
}).trim();
|
||||
} catch (_error) {
|
||||
return options.fallback ?? '';
|
||||
}
|
||||
};
|
||||
|
||||
const collectRepositoryStatus = async ({
|
||||
repoDir,
|
||||
runCommand = createCommandRunner(repoDir),
|
||||
fetchImpl = global.fetch,
|
||||
healthUrl = 'http://127.0.0.1:4032/api/health'
|
||||
}) => {
|
||||
// Refresh remote refs so ahead/behind is based on today's GitHub state.
|
||||
runCommand('git', ['fetch', '--quiet', '--prune', 'origin'], { timeout: 30000 });
|
||||
const branch = runCommand('git', ['branch', '--show-current'], { fallback: 'detached HEAD' }) || 'detached HEAD';
|
||||
const remoteUrl = runCommand('git', ['remote', 'get-url', 'origin']);
|
||||
const repositoryUrl = normalizeGitHubUrl(remoteUrl);
|
||||
const worktree = parsePorcelain(runCommand('git', ['status', '--porcelain=v1']));
|
||||
const latestCommitRaw = runCommand('git', ['log', '-1', '--pretty=format:%h%x09%s%x09%cI']);
|
||||
const [commitHash = 'ukendt', commitSubject = 'Ingen commits', commitDate = ''] = latestCommitRaw.split('\t');
|
||||
const upstream = runCommand('git', ['rev-parse', '--abbrev-ref', '@{upstream}']);
|
||||
let ahead = 0;
|
||||
let behind = 0;
|
||||
if (upstream) {
|
||||
const counts = runCommand('git', ['rev-list', '--left-right', '--count', `HEAD...${upstream}`]);
|
||||
[ahead, behind] = counts.split(/\s+/).map((value) => Number(value) || 0);
|
||||
}
|
||||
|
||||
const pullRequests = safeJson(runCommand('gh', [
|
||||
'pr', 'list', '--state', 'open', '--limit', '20',
|
||||
'--json', 'number,title,url,isDraft,reviewDecision,statusCheckRollup'
|
||||
], { fallback: '[]', timeout: 20000 }), []);
|
||||
const issues = safeJson(runCommand('gh', [
|
||||
'issue', 'list', '--state', 'open', '--limit', '20',
|
||||
'--json', 'number,title,url,labels'
|
||||
], { fallback: '[]', timeout: 20000 }), []);
|
||||
const checks = summarizeChecks(pullRequests);
|
||||
|
||||
let health = { ok: false, label: 'Utilgængelig' };
|
||||
if (typeof fetchImpl === 'function') {
|
||||
try {
|
||||
const response = await fetchImpl(healthUrl, { signal: AbortSignal.timeout(5000) });
|
||||
const payload = response.ok ? await response.json() : null;
|
||||
health = {
|
||||
ok: response.ok && payload?.status === 'ok',
|
||||
label: response.ok && payload?.status === 'ok' ? 'Online' : `HTTP ${response.status}`
|
||||
};
|
||||
} catch (_error) {
|
||||
health = { ok: false, label: 'Utilgængelig' };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
repositoryUrl,
|
||||
branch,
|
||||
upstream,
|
||||
ahead,
|
||||
behind,
|
||||
worktree,
|
||||
latestCommit: { hash: commitHash, subject: commitSubject, date: commitDate },
|
||||
pullRequests,
|
||||
issues,
|
||||
checks,
|
||||
health
|
||||
};
|
||||
};
|
||||
|
||||
const formatPullRequests = (pullRequests = []) => {
|
||||
if (!pullRequests.length) return 'Ingen åbne pull requests';
|
||||
return pullRequests.slice(0, 5).map((pr) => (
|
||||
`[#${pr.number}](${pr.url}) ${truncate(pr.title, 90)}${pr.isDraft ? ' · kladde' : ''}`
|
||||
)).join('\n');
|
||||
};
|
||||
|
||||
const formatIssues = (issues = []) => {
|
||||
if (!issues.length) return 'Ingen åbne issues';
|
||||
return issues.slice(0, 5).map((issue) => (
|
||||
`[#${issue.number}](${issue.url}) ${truncate(issue.title, 90)}`
|
||||
)).join('\n');
|
||||
};
|
||||
|
||||
const buildDiscordPayload = (status, now = new Date()) => {
|
||||
const hasFailures = !status.health.ok || status.checks.failed > 0;
|
||||
const needsAttention = status.worktree.changed > 0 || status.behind > 0 || status.checks.pending > 0;
|
||||
const color = hasFailures ? 15548997 : needsAttention ? 16753920 : 5763719;
|
||||
const state = hasFailures ? 'Kræver handling' : needsAttention ? 'Kræver opmærksomhed' : 'Alt ser grønt ud';
|
||||
const encodedBranch = String(status.branch).split('/').map(encodeURIComponent).join('/');
|
||||
const branchValue = status.repositoryUrl
|
||||
? `[${status.branch}](${status.repositoryUrl}/tree/${encodedBranch})`
|
||||
: status.branch;
|
||||
|
||||
const payload = {
|
||||
username: 'Tilbudsgivern Repo Status',
|
||||
allowed_mentions: { parse: [] },
|
||||
embeds: [{
|
||||
title: `🔨 Daglig repo-status · ${state}`,
|
||||
description: truncate('Automatisk status for Tilbudsgivern. Ingen kundeoplysninger eller secrets indgår.', DISCORD_LIMITS.description),
|
||||
color,
|
||||
fields: [
|
||||
{ name: 'Branch', value: branchValue, inline: true },
|
||||
{ name: 'Drift', value: status.health.label, inline: true },
|
||||
{
|
||||
name: 'Arbejdsmappe',
|
||||
value: `${status.worktree.changed} ændret · ${status.worktree.staged} staged · ${status.worktree.modified} modified · ${status.worktree.untracked} untracked`,
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: 'Upstream',
|
||||
value: status.upstream ? `${status.ahead} ahead · ${status.behind} behind ${status.upstream}` : 'Ingen upstream',
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: 'Seneste commit',
|
||||
value: `\`${status.latestCommit.hash}\` ${truncate(status.latestCommit.subject, 180)}`,
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: `Pull requests (${status.pullRequests.length})`,
|
||||
value: truncate(formatPullRequests(status.pullRequests), DISCORD_LIMITS.fieldValue),
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: `Checks · ${status.checks.passed} grønne / ${status.checks.pending} afventer / ${status.checks.failed} fejler`,
|
||||
value: status.checks.failed > 0
|
||||
? 'Mindst ét PR-check fejler og bør undersøges.'
|
||||
: status.checks.pending > 0 ? 'Der er checks i gang eller uden resultat.' : 'Ingen kendte fejlende checks.',
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: `Issues (${status.issues.length})`,
|
||||
value: truncate(formatIssues(status.issues), DISCORD_LIMITS.fieldValue),
|
||||
inline: false
|
||||
}
|
||||
],
|
||||
footer: { text: 'Tilbudsgivern · daglig repository-kontrol' },
|
||||
timestamp: now.toISOString()
|
||||
}]
|
||||
};
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
const postDiscordStatus = async ({ webhookUrl, payload, fetchImpl = global.fetch }) => {
|
||||
if (!webhookUrl) throw new Error('DISCORD_REPO_STATUS_WEBHOOK_URL mangler');
|
||||
const url = new URL(webhookUrl);
|
||||
url.searchParams.set('wait', 'true');
|
||||
const response = await fetchImpl(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
signal: AbortSignal.timeout(20000)
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Discord svarede HTTP ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
buildDiscordPayload,
|
||||
collectRepositoryStatus,
|
||||
normalizeGitHubUrl,
|
||||
parsePorcelain,
|
||||
postDiscordStatus,
|
||||
summarizeChecks,
|
||||
truncate
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
# Daglig repository-status på Discord
|
||||
|
||||
## Formål
|
||||
|
||||
Tilbudsgivern sender hver morgen en kompakt drifts- og Git-status til Discord. Beskeden er beregnet som dagens tekniske startpunkt og indeholder ikke kundeoplysninger, fritekst fra tilbud eller secrets.
|
||||
|
||||
## Tidspunkt og drift
|
||||
|
||||
- Tidspunkt: dagligt kl. 08:30.
|
||||
- Tidszone: `Europe/Copenhagen` via brugerens eksisterende crontab.
|
||||
- Kommando: `node scripts/send-daily-repo-status.js --send`.
|
||||
- Log: `logs/daily-repo-status.log` (ignoreret af Git).
|
||||
- Webhook-konfiguration: `/mnt/HC_Volume_103713257/tilbudsgivern-data/secrets/discord.env`.
|
||||
- Kompatibilitet: `/home/alex/.config/codex-weekly-discord.env` er et symlink til samme eksterne secret-fil, så den eksisterende Codex-status fortsætter.
|
||||
|
||||
Cronlinjen er:
|
||||
|
||||
```cron
|
||||
30 8 * * * /home/alex/.nvm/versions/node/v22.22.0/bin/node /mnt/HC_Volume_103713257/tilbudgivern/scripts/send-daily-repo-status.js --send >> /mnt/HC_Volume_103713257/tilbudgivern/logs/daily-repo-status.log 2>&1
|
||||
```
|
||||
|
||||
## Indhold i beskeden
|
||||
|
||||
- Aktiv branch med GitHub-link.
|
||||
- Seneste commit.
|
||||
- Antal staged, modificerede og untracked filer.
|
||||
- Ahead/behind i forhold til upstream efter `git fetch --prune origin`.
|
||||
- Op til fem åbne pull requests og issues med links.
|
||||
- Samlet antal grønne, afventende og fejlende PR-checks.
|
||||
- Lokal sundhedsstatus fra `http://127.0.0.1:4032/api/health`.
|
||||
|
||||
Kortet er grønt, når alt er rent og online, orange ved fx lokale ændringer, manglende upstream-synkronisering eller afventende checks og rødt ved offline drift eller fejlende checks. Discord-mentions er deaktiveret i payloaden.
|
||||
|
||||
## Manuel kontrol
|
||||
|
||||
Vis payload uden at sende:
|
||||
|
||||
```bash
|
||||
npm run repo-status:dry
|
||||
```
|
||||
|
||||
Send en rigtig status:
|
||||
|
||||
```bash
|
||||
npm run repo-status:send
|
||||
```
|
||||
|
||||
Kontrollér cronjobbet:
|
||||
|
||||
```bash
|
||||
crontab -l | grep -A1 "Tilbudgivern daily repo status"
|
||||
```
|
||||
|
||||
## Fejlfinding
|
||||
|
||||
1. Kontrollér `logs/daily-repo-status.log`.
|
||||
2. Kør dry-run og kontrollér, at GitHub CLI kan læse PR’er og issues.
|
||||
3. Kontrollér kun variabelnavnet i secret-filen; webhookværdien må aldrig kopieres til kode, dokumentation eller logs.
|
||||
4. Kør en manuel sendetest efter webhookrotation.
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
> Dette indeks skjuler arkiveret historik. Brug `Dokumentindeks.md` når ældre beslutninger eller fejlforløb skal findes.
|
||||
|
||||
Sidst synkroniseret: 2026-08-18 00:48:12 CEST
|
||||
Sidst synkroniseret: 2026-08-18 10:07:51 CEST
|
||||
|
||||
|
||||
## AI og Codex
|
||||
@@ -86,6 +86,7 @@ Sidst synkroniseret: 2026-08-18 00:48:12 CEST
|
||||
- [[10 Dokumenter/Vejledninger/AI_TILBUDS_PROMPT_GUIDE|AI_TILBUDS_PROMPT_GUIDE]] — `docs/guides/AI_TILBUDS_PROMPT_GUIDE.md`
|
||||
- [[10 Dokumenter/Vejledninger/CARPENTER_5_HOUR_EXECUTION_LOOP|CARPENTER_5_HOUR_EXECUTION_LOOP]] — `docs/guides/CARPENTER_5_HOUR_EXECUTION_LOOP.md`
|
||||
- [[10 Dokumenter/Vejledninger/CODEX_SUPPORT_WORKLIST|CODEX_SUPPORT_WORKLIST]] — `docs/guides/CODEX_SUPPORT_WORKLIST.md`
|
||||
- [[10 Dokumenter/Vejledninger/DAILY_DISCORD_REPO_STATUS|DAILY_DISCORD_REPO_STATUS]] — `docs/guides/DAILY_DISCORD_REPO_STATUS.md`
|
||||
- [[10 Dokumenter/Vejledninger/MAANEDLIG_TOEMRER_KVALITETSGENNEMGANG|MAANEDLIG_TOEMRER_KVALITETSGENNEMGANG]] — `docs/guides/MAANEDLIG_TOEMRER_KVALITETSGENNEMGANG.md`
|
||||
- [[10 Dokumenter/Vejledninger/MANUAL_SCRAPER_README|MANUAL_SCRAPER_README]] — `docs/guides/MANUAL_SCRAPER_README.md`
|
||||
- [[10 Dokumenter/Vejledninger/ORDRESTYRING_REFERENCE_CASE_2591_INSTRUKS|ORDRESTYRING_REFERENCE_CASE_2591_INSTRUKS]] — `docs/guides/ORDRESTYRING_REFERENCE_CASE_2591_INSTRUKS.md`
|
||||
@@ -162,4 +163,4 @@ Sidst synkroniseret: 2026-08-18 00:48:12 CEST
|
||||
|
||||
---
|
||||
|
||||
**Aktive dokumenter: 120. Arkiverede dokumenter: 200.**
|
||||
**Aktive dokumenter: 121. Arkiverede dokumenter: 200.**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
> Kør `./scripts/sync-obsidian-vault.sh` fra projektroden for at opdatere kopier og indeks.
|
||||
|
||||
Sidst synkroniseret: 2026-08-18 00:48:12 CEST
|
||||
Sidst synkroniseret: 2026-08-18 10:07:51 CEST
|
||||
|
||||
|
||||
## AI og Codex
|
||||
@@ -86,6 +86,7 @@ Sidst synkroniseret: 2026-08-18 00:48:12 CEST
|
||||
- [[10 Dokumenter/Vejledninger/AI_TILBUDS_PROMPT_GUIDE|AI_TILBUDS_PROMPT_GUIDE]] — `docs/guides/AI_TILBUDS_PROMPT_GUIDE.md`
|
||||
- [[10 Dokumenter/Vejledninger/CARPENTER_5_HOUR_EXECUTION_LOOP|CARPENTER_5_HOUR_EXECUTION_LOOP]] — `docs/guides/CARPENTER_5_HOUR_EXECUTION_LOOP.md`
|
||||
- [[10 Dokumenter/Vejledninger/CODEX_SUPPORT_WORKLIST|CODEX_SUPPORT_WORKLIST]] — `docs/guides/CODEX_SUPPORT_WORKLIST.md`
|
||||
- [[10 Dokumenter/Vejledninger/DAILY_DISCORD_REPO_STATUS|DAILY_DISCORD_REPO_STATUS]] — `docs/guides/DAILY_DISCORD_REPO_STATUS.md`
|
||||
- [[10 Dokumenter/Vejledninger/MAANEDLIG_TOEMRER_KVALITETSGENNEMGANG|MAANEDLIG_TOEMRER_KVALITETSGENNEMGANG]] — `docs/guides/MAANEDLIG_TOEMRER_KVALITETSGENNEMGANG.md`
|
||||
- [[10 Dokumenter/Vejledninger/MANUAL_SCRAPER_README|MANUAL_SCRAPER_README]] — `docs/guides/MANUAL_SCRAPER_README.md`
|
||||
- [[10 Dokumenter/Vejledninger/ORDRESTYRING_REFERENCE_CASE_2591_INSTRUKS|ORDRESTYRING_REFERENCE_CASE_2591_INSTRUKS]] — `docs/guides/ORDRESTYRING_REFERENCE_CASE_2591_INSTRUKS.md`
|
||||
@@ -365,4 +366,4 @@ Sidst synkroniseret: 2026-08-18 00:48:12 CEST
|
||||
|
||||
---
|
||||
|
||||
**I alt: 320 dokumenter.**
|
||||
**I alt: 321 dokumenter.**
|
||||
|
||||
@@ -291,6 +291,7 @@ docs/features/TOMRER_SMART_PACKAGES_GUIDE.md 10 Dokumenter/Funktioner/TOMRER_SMA
|
||||
docs/guides/AI_TILBUDS_PROMPT_GUIDE.md 10 Dokumenter/Vejledninger/AI_TILBUDS_PROMPT_GUIDE.md
|
||||
docs/guides/CARPENTER_5_HOUR_EXECUTION_LOOP.md 10 Dokumenter/Vejledninger/CARPENTER_5_HOUR_EXECUTION_LOOP.md
|
||||
docs/guides/CODEX_SUPPORT_WORKLIST.md 10 Dokumenter/Vejledninger/CODEX_SUPPORT_WORKLIST.md
|
||||
docs/guides/DAILY_DISCORD_REPO_STATUS.md 10 Dokumenter/Vejledninger/DAILY_DISCORD_REPO_STATUS.md
|
||||
docs/guides/MAANEDLIG_TOEMRER_KVALITETSGENNEMGANG.md 10 Dokumenter/Vejledninger/MAANEDLIG_TOEMRER_KVALITETSGENNEMGANG.md
|
||||
docs/guides/MANUAL_SCRAPER_README.md 10 Dokumenter/Vejledninger/MANUAL_SCRAPER_README.md
|
||||
docs/guides/ORDRESTYRING_REFERENCE_CASE_2591_INSTRUKS.md 10 Dokumenter/Vejledninger/ORDRESTYRING_REFERENCE_CASE_2591_INSTRUKS.md
|
||||
|
||||
|
@@ -29,6 +29,10 @@ En ny tre-profils tilbudsgennemgang er gennemført på mobil: komplet B7-tagskif
|
||||
|
||||
Den praktiske browserkontrol viste efter rettelserne 1/4 trin på et nyt projekt, gemt fladtagsgeometri på 0,26 m/48,18 m², projektsøgning fra 20 til 1 resultat og en tydelig blokering øverst på det historiske B7-tilbud. Historiske projekter med materialelinjer uden materiale-id er fortsat med vilje blokeret; de skal relinkes til rigtige materialer eller have en verificeret Smart Pakke anvendt igen og må ikke auto-matches ved gæt. Se [Tre tømrerprofiler: tilbudsgennemgang 18. august 2026](docs/status-reports/THREE_CARPENTER_OFFER_REVIEW_2026-08-18.md).
|
||||
|
||||
### Daglig Discord repo-status — 18. august 2026
|
||||
|
||||
Repository, GitHub og lokal drift rapporteres automatisk til Discord hver morgen kl. 08:30 dansk tid. Statuskortet viser branch, seneste commit, lokale ændringer, upstream-afvigelse, åbne PR’er/issues, PR-checks og `/api/health`. En rigtig webhooktest blev leveret til Discord, og secret-konfigurationen ligger i den eksterne data-mappe med rettighed `600`. Se [Daglig repository-status på Discord](docs/guides/DAILY_DISCORD_REPO_STATUS.md).
|
||||
|
||||
Ved hver fremtidig kontrol opdateres tiderne, enhed/skærmstørrelse, antal dobbeltindtastninger, største flaskehals og resultatet `BESTÅET` eller `IKKE BESTÅET`. Hvis et P2/P3-punkt viser sig at blokere feltflowet, flyttes det straks til P0 eller P1.
|
||||
|
||||
### Fast regel for smartpakker
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Daglig repository-status på Discord
|
||||
|
||||
## Formål
|
||||
|
||||
Tilbudsgivern sender hver morgen en kompakt drifts- og Git-status til Discord. Beskeden er beregnet som dagens tekniske startpunkt og indeholder ikke kundeoplysninger, fritekst fra tilbud eller secrets.
|
||||
|
||||
## Tidspunkt og drift
|
||||
|
||||
- Tidspunkt: dagligt kl. 08:30.
|
||||
- Tidszone: `Europe/Copenhagen` via brugerens eksisterende crontab.
|
||||
- Kommando: `node scripts/send-daily-repo-status.js --send`.
|
||||
- Log: `logs/daily-repo-status.log` (ignoreret af Git).
|
||||
- Webhook-konfiguration: `/mnt/HC_Volume_103713257/tilbudsgivern-data/secrets/discord.env`.
|
||||
- Kompatibilitet: `/home/alex/.config/codex-weekly-discord.env` er et symlink til samme eksterne secret-fil, så den eksisterende Codex-status fortsætter.
|
||||
|
||||
Cronlinjen er:
|
||||
|
||||
```cron
|
||||
30 8 * * * /home/alex/.nvm/versions/node/v22.22.0/bin/node /mnt/HC_Volume_103713257/tilbudgivern/scripts/send-daily-repo-status.js --send >> /mnt/HC_Volume_103713257/tilbudgivern/logs/daily-repo-status.log 2>&1
|
||||
```
|
||||
|
||||
## Indhold i beskeden
|
||||
|
||||
- Aktiv branch med GitHub-link.
|
||||
- Seneste commit.
|
||||
- Antal staged, modificerede og untracked filer.
|
||||
- Ahead/behind i forhold til upstream efter `git fetch --prune origin`.
|
||||
- Op til fem åbne pull requests og issues med links.
|
||||
- Samlet antal grønne, afventende og fejlende PR-checks.
|
||||
- Lokal sundhedsstatus fra `http://127.0.0.1:4032/api/health`.
|
||||
|
||||
Kortet er grønt, når alt er rent og online, orange ved fx lokale ændringer, manglende upstream-synkronisering eller afventende checks og rødt ved offline drift eller fejlende checks. Discord-mentions er deaktiveret i payloaden.
|
||||
|
||||
## Manuel kontrol
|
||||
|
||||
Vis payload uden at sende:
|
||||
|
||||
```bash
|
||||
npm run repo-status:dry
|
||||
```
|
||||
|
||||
Send en rigtig status:
|
||||
|
||||
```bash
|
||||
npm run repo-status:send
|
||||
```
|
||||
|
||||
Kontrollér cronjobbet:
|
||||
|
||||
```bash
|
||||
crontab -l | grep -A1 "Tilbudgivern daily repo status"
|
||||
```
|
||||
|
||||
## Fejlfinding
|
||||
|
||||
1. Kontrollér `logs/daily-repo-status.log`.
|
||||
2. Kør dry-run og kontrollér, at GitHub CLI kan læse PR’er og issues.
|
||||
3. Kontrollér kun variabelnavnet i secret-filen; webhookværdien må aldrig kopieres til kode, dokumentation eller logs.
|
||||
4. Kør en manuel sendetest efter webhookrotation.
|
||||
+3
-1
@@ -37,7 +37,9 @@
|
||||
"test:ui:all": "npm run test:pw && npm run test:selenium",
|
||||
"carpenter:openai:quotes": "node carpenter-agent-openai.js --generate-quotes=5 --save",
|
||||
"carpenter:openai:ui": "cd tests && PLAYWRIGHT_BASE_URL=http://localhost:4032 ./node_modules/.bin/playwright test carpenter-agent-openai.spec.js",
|
||||
"carpenter:openai:ui:report": "cd tests && ./node_modules/.bin/playwright show-report"
|
||||
"carpenter:openai:ui:report": "cd tests && ./node_modules/.bin/playwright show-report",
|
||||
"repo-status:dry": "node scripts/send-daily-repo-status.js --dry-run",
|
||||
"repo-status:send": "node scripts/send-daily-repo-status.js --send"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {
|
||||
buildDiscordPayload,
|
||||
collectRepositoryStatus,
|
||||
postDiscordStatus
|
||||
} = require('../backend/src/services/repositoryStatusDiscordService');
|
||||
|
||||
const repoDir = path.resolve(__dirname, '..');
|
||||
const defaultEnvFile = '/mnt/HC_Volume_103713257/tilbudsgivern-data/secrets/discord.env';
|
||||
|
||||
const loadEnvFile = (filePath) => {
|
||||
if (!filePath || !fs.existsSync(filePath)) return;
|
||||
fs.readFileSync(filePath, 'utf8').split(/\r?\n/).forEach((line) => {
|
||||
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=(.*)\s*$/);
|
||||
if (!match || process.env[match[1]]) return;
|
||||
let value = match[2].trim();
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
process.env[match[1]] = value;
|
||||
});
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const mode = process.argv.includes('--send') ? 'send' : 'dry-run';
|
||||
loadEnvFile(process.env.REPO_STATUS_ENV_FILE || defaultEnvFile);
|
||||
|
||||
const status = await collectRepositoryStatus({ repoDir });
|
||||
const payload = buildDiscordPayload(status);
|
||||
|
||||
if (mode === 'dry-run') {
|
||||
process.stdout.write(`${JSON.stringify({ status, payload }, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
const webhookUrl = process.env.DISCORD_REPO_STATUS_WEBHOOK_URL || process.env.DISCORD_WEBHOOK_URL;
|
||||
const result = await postDiscordStatus({ webhookUrl, payload });
|
||||
process.stdout.write(`Discord repo-status sendt${result?.id ? ` som besked ${result.id}` : ''}.\n`);
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`Repo-status fejlede: ${error.message}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -29,6 +29,10 @@ En ny tre-profils tilbudsgennemgang er gennemført på mobil: komplet B7-tagskif
|
||||
|
||||
Den praktiske browserkontrol viste efter rettelserne 1/4 trin på et nyt projekt, gemt fladtagsgeometri på 0,26 m/48,18 m², projektsøgning fra 20 til 1 resultat og en tydelig blokering øverst på det historiske B7-tilbud. Historiske projekter med materialelinjer uden materiale-id er fortsat med vilje blokeret; de skal relinkes til rigtige materialer eller have en verificeret Smart Pakke anvendt igen og må ikke auto-matches ved gæt. Se [Tre tømrerprofiler: tilbudsgennemgang 18. august 2026](docs/status-reports/THREE_CARPENTER_OFFER_REVIEW_2026-08-18.md).
|
||||
|
||||
### Daglig Discord repo-status — 18. august 2026
|
||||
|
||||
Repository, GitHub og lokal drift rapporteres automatisk til Discord hver morgen kl. 08:30 dansk tid. Statuskortet viser branch, seneste commit, lokale ændringer, upstream-afvigelse, åbne PR’er/issues, PR-checks og `/api/health`. En rigtig webhooktest blev leveret til Discord, og secret-konfigurationen ligger i den eksterne data-mappe med rettighed `600`. Se [Daglig repository-status på Discord](docs/guides/DAILY_DISCORD_REPO_STATUS.md).
|
||||
|
||||
Ved hver fremtidig kontrol opdateres tiderne, enhed/skærmstørrelse, antal dobbeltindtastninger, største flaskehals og resultatet `BESTÅET` eller `IKKE BESTÅET`. Hvis et P2/P3-punkt viser sig at blokere feltflowet, flyttes det straks til P0 eller P1.
|
||||
|
||||
### Fast regel for smartpakker
|
||||
|
||||
Reference in New Issue
Block a user