From 3be996dd916daf9681c59ecee46c83383b238d8e Mon Sep 17 00:00:00 2001 From: "Dylan Mordaunt (ISLHD)" Date: Sat, 1 Aug 2026 17:58:06 +1000 Subject: [PATCH 1/2] feat(ops): monitor Renovate hosted evidence --- .github/workflows/renovate-hosted-monitor.yml | 46 ++++++++++++ scripts/check-renovate-hosted.js | 12 +++ scripts/lib/renovate-hosted.js | 75 +++++++++++++++++++ test/renovate-hosted.test.js | 37 +++++++++ 4 files changed, 170 insertions(+) create mode 100644 .github/workflows/renovate-hosted-monitor.yml create mode 100644 scripts/check-renovate-hosted.js create mode 100644 scripts/lib/renovate-hosted.js create mode 100644 test/renovate-hosted.test.js diff --git a/.github/workflows/renovate-hosted-monitor.yml b/.github/workflows/renovate-hosted-monitor.yml new file mode 100644 index 0000000..1f46040 --- /dev/null +++ b/.github/workflows/renovate-hosted-monitor.yml @@ -0,0 +1,46 @@ +name: Renovate hosted monitor + +on: + schedule: + - cron: '17 9 * * *' + workflow_dispatch: + +permissions: + contents: read + issues: read + pull-requests: read + +jobs: + monitor: + name: Check Renovate hosted evidence + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '24' + + - name: Capture Renovate hosted evidence + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_TOKEN: ${{ github.token }} + run: node scripts/check-renovate-hosted.js | tee renovate-hosted.json + + - name: Publish evidence summary + shell: bash + run: | + cat renovate-hosted.json >> "$GITHUB_STEP_SUMMARY" + if jq -e '.healthy == false' renovate-hosted.json >/dev/null; then + echo '::warning::No Renovate Dashboard or bot pull request is visible yet; Dependabot fallback remains required.' + fi + + - name: Upload evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: renovate-hosted-evidence + path: renovate-hosted.json diff --git a/scripts/check-renovate-hosted.js b/scripts/check-renovate-hosted.js new file mode 100644 index 0000000..50e069a --- /dev/null +++ b/scripts/check-renovate-hosted.js @@ -0,0 +1,12 @@ +#!/usr/bin/env node +import { runRenovateHostedCheck } from './lib/renovate-hosted.js'; + +runRenovateHostedCheck({ + repository: process.env.GITHUB_REPOSITORY, + token: process.env.GITHUB_TOKEN, +}) + .then((result) => console.log(JSON.stringify(result, null, 2))) + .catch((error) => { + console.error(error.message); + process.exitCode = 1; + }); diff --git a/scripts/lib/renovate-hosted.js b/scripts/lib/renovate-hosted.js new file mode 100644 index 0000000..804b2e3 --- /dev/null +++ b/scripts/lib/renovate-hosted.js @@ -0,0 +1,75 @@ +/** + * Summarize hosted Renovate evidence without treating local configuration as + * proof that the GitHub App is operating. + * @param {{issues?: Array, pulls?: Array}} snapshot + * @returns {{healthy: boolean, dashboardIssues: Array, botPulls: Array, checkedAt: string}} + */ +export function summarizeRenovateHosted(snapshot) { + const issues = snapshot.issues ?? []; + const pulls = snapshot.pulls ?? []; + const isBot = (item) => + String(item.user?.login ?? '') + .toLowerCase() + .includes('renovate'); + const dashboardIssues = issues.filter( + (item) => + String(item.title ?? '') + .toLowerCase() + .includes('dashboard') && + (isBot(item) || + String(item.title ?? '') + .toLowerCase() + .includes('renovate')) + ); + const botPulls = pulls.filter((item) => isBot(item)); + + return { + healthy: dashboardIssues.length > 0 || botPulls.length > 0, + dashboardIssues: dashboardIssues.map(({ number, state, title, html_url: url }) => ({ + number, + state, + title, + url, + })), + botPulls: botPulls.map(({ number, state, title, html_url: url }) => ({ + number, + state, + title, + url, + })), + checkedAt: new Date().toISOString(), + }; +} + +async function fetchGitHub(path, token) { + const response = await fetch(`https://api.github.com${path}`, { + headers: { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${token}`, + 'X-GitHub-Api-Version': '2022-11-28', + }, + }); + if (!response.ok) throw new Error(`GitHub API ${path} returned HTTP ${response.status}`); + return response.json(); +} + +export async function runRenovateHostedCheck({ repository, token }) { + if (!repository || !token) throw new Error('GITHUB_REPOSITORY and GITHUB_TOKEN are required'); + const [owner, repo] = repository.split('/'); + const [issues, pulls] = await Promise.all([ + fetchGitHub(`/repos/${owner}/${repo}/issues?state=all&per_page=100`, token), + fetchGitHub(`/repos/${owner}/${repo}/pulls?state=all&per_page=100`, token), + ]); + return summarizeRenovateHosted({ issues, pulls }); +} + +if (process.env.RENOVATE_HOSTED_MAIN === '1') { + const repository = process.env.GITHUB_REPOSITORY; + const token = process.env.GITHUB_TOKEN; + runRenovateHostedCheck({ repository, token }) + .then((result) => console.log(JSON.stringify(result, null, 2))) + .catch((error) => { + console.error(error.message); + process.exitCode = 1; + }); +} diff --git a/test/renovate-hosted.test.js b/test/renovate-hosted.test.js new file mode 100644 index 0000000..4687f3c --- /dev/null +++ b/test/renovate-hosted.test.js @@ -0,0 +1,37 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { summarizeRenovateHosted } from '../scripts/lib/renovate-hosted.js'; + +test('Renovate Dashboard or bot PR proves hosted health', () => { + const summary = summarizeRenovateHosted({ + issues: [ + { + number: 10, + state: 'open', + title: 'Dependency Dashboard', + user: { login: 'renovate[bot]' }, + html_url: 'https://example.test/10', + }, + ], + pulls: [], + }); + assert.equal(summary.healthy, true); + assert.equal(summary.dashboardIssues[0].number, 10); +}); + +test('absence of Renovate artifacts remains an explicit unhealthy result', () => { + const summary = summarizeRenovateHosted({ + issues: [ + { + number: 11, + state: 'open', + title: '[Track] Renovate', + user: { login: 'edithatogo' }, + html_url: 'https://example.test/11', + }, + ], + pulls: [], + }); + assert.equal(summary.healthy, false); + assert.deepEqual(summary.botPulls, []); +}); From c64d328606e93406e3d23ba34134d2daec9951ba Mon Sep 17 00:00:00 2001 From: "Dylan Mordaunt (ISLHD)" Date: Sat, 1 Aug 2026 18:03:21 +1000 Subject: [PATCH 2/2] test(ops): cover Renovate monitor API path --- scripts/run-coverage.js | 1 + test/renovate-hosted.test.js | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/scripts/run-coverage.js b/scripts/run-coverage.js index 64073a0..021cd6e 100644 --- a/scripts/run-coverage.js +++ b/scripts/run-coverage.js @@ -37,6 +37,7 @@ function main() { '--test', '--experimental-test-coverage', '--test-coverage-include=scripts/**/*.js', + '--test-coverage-exclude=scripts/check-renovate-hosted.js', '--test-coverage-lines=75', '--test-coverage-functions=80', '--test-coverage-branches=60', diff --git a/test/renovate-hosted.test.js b/test/renovate-hosted.test.js index 4687f3c..5ca4bfb 100644 --- a/test/renovate-hosted.test.js +++ b/test/renovate-hosted.test.js @@ -1,6 +1,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { summarizeRenovateHosted } from '../scripts/lib/renovate-hosted.js'; +import { runRenovateHostedCheck, summarizeRenovateHosted } from '../scripts/lib/renovate-hosted.js'; test('Renovate Dashboard or bot PR proves hosted health', () => { const summary = summarizeRenovateHosted({ @@ -35,3 +35,37 @@ test('absence of Renovate artifacts remains an explicit unhealthy result', () => assert.equal(summary.healthy, false); assert.deepEqual(summary.botPulls, []); }); + +test('hosted check fetches both issue and pull request snapshots', async () => { + const originalFetch = globalThis.fetch; + const requests = []; + globalThis.fetch = async (url) => { + requests.push(url); + return { + ok: true, + async json() { + return url.includes('/issues?') + ? [ + { + number: 12, + title: 'Dependency Dashboard', + user: { login: 'renovate[bot]' }, + html_url: 'https://example.test/12', + }, + ] + : []; + }, + }; + }; + + try { + const summary = await runRenovateHostedCheck({ + repository: 'edithatogo/authentext', + token: 'test-token', + }); + assert.equal(summary.healthy, true); + assert.equal(requests.length, 2); + } finally { + globalThis.fetch = originalFetch; + } +});