From fc2ecc08cb9dd034f5ff3f34204b5cc3369731de Mon Sep 17 00:00:00 2001 From: SMTP Relay & Email - Free Send <60409540+msgwing@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:13:46 +0200 Subject: [PATCH] feat(ci): a progress report every three days, in Polish The owner delegated approval of changes to the team. A delegation without a report is not delegation, it is disappearance - so every three days this opens one issue with the numbers, assigned to the owner so it arrives. It reports what did not move as well as what did. Three days with nothing merged is not a quiet period, it is stopped work, and the report says so in those words. GitHub keeps traffic for 14 days and nobody else is writing it down, so this is also the only trend the project has. It never asks for a decision. Decisions about this repository belong to the change board now; anything genuinely needing the owner - a submission into somebody else's repository, a credential - carries the `do-akceptacji` label and arrives separately. --- .github/workflows/progress-report.yml | 168 ++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 .github/workflows/progress-report.yml diff --git a/.github/workflows/progress-report.yml b/.github/workflows/progress-report.yml new file mode 100644 index 0000000..b6f4d78 --- /dev/null +++ b/.github/workflows/progress-report.yml @@ -0,0 +1,168 @@ +name: Raport postępów + +# Every three days, opens one issue in Polish with the numbers the owner asked +# for: what moved, what shipped, what is stuck and what still needs them. +# +# It exists because the owner delegated approval of changes to the team and +# asked, reasonably, to see whether any of it is producing anything. A +# delegation without a report is not delegation, it is disappearance. +# +# Two rules it is built around: +# +# 1. It reports the numbers, including the ones that did not move. A report +# that only lists activity looks like progress even when nothing changed; +# GitHub keeps traffic for 14 days and nobody else is writing it down, so +# this is also the only trend the project has. +# 2. It never asks for a decision. Decisions about this repository belong to +# the change board now. Anything genuinely needing the owner - a +# third-party submission, a credential - is labelled `do-akceptacji` and +# arrives separately. + +on: + schedule: + - cron: '0 6 */3 * *' # every three days, 06:00 UTC + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + report: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v8 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + script: | + const owner = context.repo.owner, repo = context.repo.repo; + const since = new Date(Date.now() - 3 * 86400_000).toISOString(); + const day = new Date().toISOString().slice(0, 10); + + const num = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ' '); + + // --- traffic --------------------------------------------------- + let views = null, referrers = [], paths = []; + try { + views = (await github.rest.repos.getViews({ owner, repo })).data; + referrers = (await github.rest.repos.getTopReferrers({ owner, repo })).data; + paths = (await github.rest.repos.getTopPaths({ owner, repo })).data; + } catch (e) { + core.warning(`Ruch niedostępny: ${e.message}`); + } + + const meta = (await github.rest.repos.get({ owner, repo })).data; + + // --- what shipped ---------------------------------------------- + const prs = (await github.rest.pulls.list({ + owner, repo, state: 'closed', sort: 'updated', + direction: 'desc', per_page: 50, + })).data.filter(p => p.merged_at && p.merged_at >= since); + + // --- queues ---------------------------------------------------- + const count = async (labels, state = 'open') => + (await github.rest.issues.listForRepo({ + owner, repo, state, labels, per_page: 100, + })).data.filter(i => !i.pull_request).length; + + const outreach = await count('outreach'); + const listing = await count('listing'); + const waiting = await count('do-akceptacji'); + const alerts = await count('service-alert'); + + // --- automation health ----------------------------------------- + const runs = (await github.rest.actions.listWorkflowRunsForRepo({ + owner, repo, per_page: 100, + })).data.workflow_runs.filter(r => r.created_at >= since); + const failed = runs.filter(r => r.conclusion === 'failure'); + const byWorkflow = {}; + for (const r of failed) byWorkflow[r.name] = (byWorkflow[r.name] || 0) + 1; + + // --- the relay -------------------------------------------------- + let relay = 'nieznany'; + try { + const res = await fetch( + `https://raw.githubusercontent.com/${owner}/${repo}/status/status.json`, + { headers: { 'User-Agent': 'zerosmtp-progress-report' } }); + if (res.ok) relay = (await res.json()).message; + } catch (e) { core.warning(e.message); } + + // --- write it ---------------------------------------------------- + const L = []; + L.push(`Raport za ostatnie trzy dni, do **${day}**.`, ''); + + L.push('## Liczby', ''); + L.push('| Miara | Wartość |', '|---|---|'); + if (views) { + L.push(`| Unikalni odwiedzający (14 dni) | **${num(views.uniques)}** |`); + L.push(`| Odsłony (14 dni) | ${num(views.count)} |`); + } else { + L.push('| Ruch | **niedostępny — API nie odpowiedziało** |'); + } + L.push(`| Gwiazdki | **${meta.stargazers_count}** |`); + L.push(`| Forki | ${meta.forks_count} |`); + L.push(`| Obserwujący | ${meta.subscribers_count} |`); + L.push(`| Przekaźnik | ${relay === 'operational' ? '✅ działa' : `⚠️ ${relay}`} |`); + L.push(''); + + if (referrers.length) { + L.push('### Skąd przyszli', '', '| Źródło | Unikalni |', '|---|---|'); + for (const r of referrers.slice(0, 6)) { + L.push(`| \`${r.referrer}\` | ${num(r.uniques)} |`); + } + L.push(''); + } + + if (paths.length) { + L.push('### Co czytali', ''); + for (const p of paths.slice(0, 5)) { + L.push(`- ${num(p.uniques)} — \`${p.path}\``); + } + L.push(''); + } + + L.push('## Co weszło', ''); + if (prs.length) { + for (const p of prs) L.push(`- #${p.number} ${p.title}`); + } else { + L.push('**Nic.** Trzy dni bez zmergowanej zmiany to nie jest cisza — to jest zatrzymana praca.'); + } + L.push(''); + + L.push('## Kolejki', ''); + L.push('| Kolejka | Otwarte |', '|---|---|'); + L.push(`| Kandydaci outreach | ${outreach} |`); + L.push(`| Kandydaci na listy | ${listing} |`); + L.push(`| **Czeka na Ciebie** (\`do-akceptacji\`) | **${waiting}** |`); + if (alerts) L.push(`| ⚠️ Awarie usługi | **${alerts}** |`); + L.push(''); + + L.push('## Automatyka', ''); + if (failed.length === 0) { + L.push(`Wszystkie ${runs.length} biegów zakończonych bez porażki.`); + } else { + L.push(`**${failed.length} porażek** na ${runs.length} biegów:`, ''); + for (const [name, n] of Object.entries(byWorkflow)) { + L.push(`- \`${name}\` — ${n}×`); + } + } + L.push(''); + + L.push('---', ''); + if (waiting > 0) { + L.push(`**${waiting} spraw czeka na Twoje jedno kliknięcie** — etykieta \`do-akceptacji\`.`); + } else { + L.push('**Nic nie czeka na Twoją decyzję.** Zmiany w tym repozytorium zatwierdza rada;'); + L.push('do Ciebie trafia wyłącznie publikacja u obcych i tworzenie poświadczeń.'); + } + L.push(''); + L.push('_Raport generowany automatycznie co trzy dni. Nie wymaga odpowiedzi — zamknij go._'); + + const issue = await github.rest.issues.create({ + owner, repo, + title: `Raport postępów — ${day}`, + body: L.join('\n'), + assignees: [owner], + }); + core.notice(`Raport: ${issue.data.html_url}`);