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
177 changes: 177 additions & 0 deletions .claude/skills/monthly-newsletter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
---
name: monthly-newsletter
description: Generate a short, factual monthly activity report across all go-openapi and go-swagger repositories. Use when asked to produce the monthly newsletter/report for a given month, or when run unattended by the monthly-reports routine. The companion to quarterly-newsletter, but concise and autonomous.
---

# Monthly newsletter (go-openapi & go-swagger)

A **short, fact-led** report of the previous calendar month's activity across all
go-openapi and go-swagger repositories. It is the low-effort counterpart to
`quarterly-newsletter`: same scope, shorter window, **no strategy narrative**.

This skill is designed to run **unattended end-to-end** (e.g. from a scheduled cloud
routine). **Never ask the user a question** — if something is ambiguous, make the
conservative choice and report only what is verifiable. Omit, do not invent.

## Window

- The report covers the **previous calendar month**.
- Derive the bounds from today's date — do not hard-code:
- `--since` = first day of last month, `00:00:00Z`
- `--until` = first day of the current month, `00:00:00Z`
- Refer to the month in prose by name and year (e.g. "May 2026").

## Output file

- Path: `docs/doc-site/blog/monthly/<YYYY>-<MM>.md` (zero-padded month, e.g. `2026-05.md`).
- Hugo slug → `<YYYY>-<MM>`; published URL → `…/blog/monthly/<YYYY>-<MM>/index.html`.

## Length target

Roughly **a third of a quarterly** — skimmable in about a minute. If in doubt, cut.
Drop the quarterly's impact-assessment essay, risk rating, and long per-repo prose.

## Document structure

### 0. Front matter (Hugo) — two distinct summary fields

```yaml
---
title: May 2026
description: go-openapi & go-swagger activity in May 2026 # plain text — Hugo card text
weight: 797394 # 999999 - (YYYY*100 + MM); lower sorts first
discord_description: |-
<~8-12 line Discord-flavored summary — see below>
---
```

- **`title`** — `<Month> <YYYY>` (e.g. `May 2026`).
- **`description`** — short **plain-text** one/two-liner. Hugo renders it as the page
meta description and the blog-card text, so keep it free of markup (`•`, `**…**`
would show literally on the cards).
- **`weight`** — `999999 - (YYYY*100 + MM)`. This sorts newest-first automatically with
no shared state to maintain (e.g. `2026-05` → `999999 - 202605 = 797394`).
- **`discord_description`** — the rich **~8-12 line** summary, posted verbatim by the
`announce-monthly.yml` workflow as the Discord embed body. Hugo ignores this custom
field. Discord embeds render **markdown, not HTML** — use `• ` bullets, `**bold**`
for repo/product names, `` `code` ``. Lead with one framing sentence, then the
headline items. **Do not** include the title or the report URL — the workflow adds
those. Keep it shorter and flatter than the quarterly's.

### 1. Intro (one paragraph)

A single short paragraph framing the month: the overall shape of activity and the one
or two things worth noticing. No grand strategy, no marketing language. State the
**overall effort** inline (e.g. "N commits across M repositories").

### 2. Themes (compact list)

A short bulleted list of the month's cross-cutting themes — group similar changes
across repos rather than listing per-repo. Typical buckets: features, bug fixes,
dependency/CI maintenance, docs, releases. **Keep it to the few themes that actually
mattered this month**; do not pad with boilerplate categories that saw no real change.

### 3. Repository highlights (short table)

A compact table, one row per repo that saw **notable** change (skip repos with only
routine dependabot/CI noise unless that was the whole month):

```markdown
| Repository | Latest release | Highlights |
|---|---|---|
| runtime | v0.30.1 | connection diagnostic; security pass |
```

Do **not** include per-repo commit counts.

### 4. Quarter-overlap note (only in quarter-end months)

If the reported month is the last month of a calendar quarter (Mar, Jun, Sep, Dec),
add a single line pointing readers to the quarterly for the strategic picture, e.g.:
"> A quarterly report covering this period in more depth will follow." Otherwise omit
this section entirely.

### 5. Thanks to our contributors

Close with a short, warm thank-you to the **external human contributors** for the month.

- **Include** anyone who authored a commit in the window across all go-openapi *and*
go-swagger repos.
- **Exclude**: the maintainer(s) (`fredbi` / `Frédéric BIDON`), AI agents (Claude,
`Copilot`), and bots (`dependabot[bot]`, `bot-go-openapi[bot]`, `go-openapi-bot`).
- List each by **GitHub handle** and the repo(s) they touched. Never list email
addresses. Do **not** count individual contributions.
- If there were no external contributors this month, omit the section rather than
writing an empty thank-you.

## Data collection (cloud environment)

The routine starts with **only `doc-site` checked out**, so prefer the **GitHub API**
over cloning ~18 repos. **All reads must be authenticated** — unauthenticated GitHub
API is 60 req/hour (far too low); authenticated is 5,000 req/hour. The `gh` CLI uses
the `GH_TOKEN` env var automatically.

1. **Enumerate repositories** in both orgs (skip archived repos and forks):
```bash
gh repo list go-openapi --no-archived --source --limit 100 --json name,isArchived,isFork \
--jq '.[] | select(.isArchived|not) | select(.isFork|not) | .name'
gh repo list go-swagger --no-archived --source --limit 100 --json name,isArchived,isFork \
--jq '.[] | select(.isArchived|not) | select(.isFork|not) | .name'
```

2. **Per repo, over the window**, pull what you need via the API (replace `<owner>/<repo>`,
`<since>`, `<until>`):
- Commits on the default branch:
```bash
gh api -X GET "repos/<owner>/<repo>/commits" \
-f since=<since> -f until=<until> --paginate \
--jq '.[] | {sha:.sha, msg:(.commit.message|split("\n")[0]), login:.author.login, name:.commit.author.name}'
```
- Releases published in the window:
```bash
gh api "repos/<owner>/<repo>/releases" --paginate \
--jq '.[] | {tag:.tag_name, published:.published_at}'
```
- Latest release tag (for the highlights table). **Do not** use `releases/latest` —
it returns HTTP 404 for repos with no published release and `gh` leaks the error
body to stdout. Ask for the newest release (or newest tag) instead, which yields an
empty array, not a 404:
```bash
tag=$(gh api "repos/<owner>/<repo>/releases?per_page=1" --jq '.[0].tag_name // empty' 2>/dev/null)
[ -z "$tag" ] && tag=$(gh api "repos/<owner>/<repo>/tags?per_page=1" --jq '.[0].name // empty' 2>/dev/null)
# repos with neither (e.g. codegen, doc-site) → leave the cell as "—"
```
- Merged PRs in the window (optional, for richer highlights):
```bash
gh api -X GET "search/issues" \
-f q='repo:<owner>/<repo> is:pr is:merged merged:<since>..<until>' \
--jq '.items[] | {n:.number, title:.title, user:.user.login}'
```

3. **Shallow-clone only if needed** for file-level inspection of one repo:
`git clone --depth=50 --shallow-since=<since> https://github.com/<owner>/<repo>`.

4. **Contributor handles** come straight from the commit API (`.author.login` above);
no SHA-to-login mapping needed. Skip null logins (web-flow / unmatched) rather than
guessing.

5. **Cap and log truncation.** If any repo's history is too large to page fully within
reason, `log` what was capped rather than silently truncating — a report that hides
gaps reads as complete when it is not.

## Style

**DO:** factual, developer-focused language; factorize similar changes across repos;
extract latest release tags; keep it short.

**DON'T:** per-repo commit counts; marketing language ("demonstrates maturity"); names
or emails in the themes/highlights (the thanks section is the one place to credit
external contributors, by handle); commit-by-commit logs; subjective praise; questions
to the user.

## After writing

Write the file to `docs/doc-site/blog/monthly/<YYYY>-<MM>.md`. Do **not** post to
Discord and do **not** commit from the skill — the routine handles the commit (via the
GitHub Contents API) and the PR; the Discord announcement happens on merge via
`announce-monthly.yml`.
110 changes: 110 additions & 0 deletions .github/workflows/announce-monthly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: "Announce monthly report on Discord"

# Posts an announcement to the #monthly-news Discord channel when a new
# monthly report is published. It chains off the "Update documentation"
# workflow so we only announce once the doc site has been built and deployed
# to GitHub Pages (i.e. the report URL is actually live).
#
# Monthly only — other report kinds (quarterly, ...) are announced to their own
# channels by their own workflows. The webhook is the DISCORD_MONTHLY_WEBHOOK_URL
# repo secret (the #monthly-news channel webhook).

permissions:
contents: read

on:
workflow_run:
workflows:
- "Update documentation"
types:
- completed
branches:
- master

concurrency:
group: discord-monthly-announce
cancel-in-progress: false

defaults:
run:
shell: bash

jobs:
announce:
name: Announce new monthly report
runs-on: ubuntu-latest
# Only when the doc build/deploy succeeded, and only for pushes to master
# (not PR builds of the upstream workflow). Note: workflow_run does not
# support a `paths` filter, so the monthly-file check is done in-job below.
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' }}
steps:
-
name: Checkout the deployed commit
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 2
-
name: Detect a newly added monthly report
id: detect
run: |
# Announce only when a NEW monthly report file is added in this
# commit (--diff-filter=A); edits/typo-fixes to existing reports do
# not re-trigger an announcement.
file=$(git diff --name-only --diff-filter=A HEAD^ HEAD \
| grep -E '^docs/doc-site/blog/monthly/[^/]+\.md$' \
| head -1 || true)
if [ -n "${file}" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "file=${file}" >> "$GITHUB_OUTPUT"
echo "New monthly report added: ${file}"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "No new monthly report in this commit; skipping announcement."
fi
-
name: Post announcement to Discord
if: ${{ steps.detect.outputs.changed == 'true' }}
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_MONTHLY_WEBHOOK_URL }}
REPORT_FILE: ${{ steps.detect.outputs.file }}
run: |
if [ -z "${DISCORD_WEBHOOK_URL}" ]; then
echo "::error title=discord::DISCORD_MONTHLY_WEBHOOK_URL secret is not set"; exit 1
fi

# The embed body IS the report's front-matter `discord_description` —
# a short, Discord-flavored (markdown) summary authored with the
# report (see the monthly-newsletter skill). It is a dedicated field
# (not Hugo's `description`, which is plain-text card text). Title and
# URL are derived from the file, so they aren't repeated in the body.
title=$(yq --front-matter=extract '.title' "${REPORT_FILE}")
desc=$(yq --front-matter=extract '.discord_description' "${REPORT_FILE}")
if [ -z "${desc}" ] || [ "${desc}" = "null" ]; then
echo "::error title=discord::${REPORT_FILE} has no front-matter discord_description to announce"; exit 1
fi

slug=$(basename "${REPORT_FILE}" .md | tr '[:upper:]' '[:lower:]')
url="https://go-openapi.github.io/doc-site/blog/monthly/${slug}/index.html"

jq -n \
--arg title "go-openapi & go-swagger — ${title} Monthly Report" \
--arg url "${url}" \
--arg desc "${desc}" \
'{
username: "go-openapi news",
content: "🗓️ **New monthly report is out!**",
allowed_mentions: { parse: [] },
embeds: [ { title: $title, url: $url, color: 3447003, description: $desc } ]
}' > payload.json

code=$(curl -sS -o resp.txt -w '%{http_code}' \
-H "Content-Type: application/json" \
-X POST --data @payload.json \
"${DISCORD_WEBHOOK_URL}?wait=true")
echo "Discord HTTP ${code}"
cat resp.txt || true; echo
case "${code}" in
2*) echo "::notice title=discord::Posted monthly announcement to Discord (HTTP ${code})" ;;
*) echo "::error title=discord::Discord webhook returned HTTP ${code}"; exit 1 ;;
esac
58 changes: 58 additions & 0 deletions docs/doc-site/blog/monthly/2026-05.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: May 2026
description: go-openapi & go-swagger activity in May 2026 — runtime client diagnostics, go-swagger v0.34.0, and the swag API migration.
weight: 797394
discord_description: |-
A busy month centred on **runtime** and a wave of releases:

• **runtime** — new `Runtime.Trace` for connection-level diagnostics (incl. a TLS diagnostic mode), a build-tagged `SkipAuth` feature, a JSON-dialects fix, and context cancellation in multipart upload. Six releases cut (**v0.29.5 → v0.32.2**), plus a large documentation pass (media-types guide, keep-alive primer, per-module READMEs).
• **go-swagger v0.34.0** — generator migrated to the new **go-openapi/swag** API, more idiomatic context-aware clients, configurable custom producers/consumers, and an enum-constant fix (#1047).
• **codegen** also moved onto the new swag API.
• Org-wide: a shared **bot-PR monitoring** CI workflow rolled out across most repos.

258 commits across 18 repositories. Thanks to **@Abzaek**, **@KuaaMU** and **@dashitongzhi** for their contributions.
---

go-openapi and go-swagger had a busy May, with **258 commits across 18 repositories**. The
month's centre of gravity was **runtime**, which gained connection-level client diagnostics
and a documentation overhaul over the course of six releases, while **go-swagger** shipped
**v0.34.0** on the back of a migration to the new `go-openapi/swag` API.

## Themes

- **Client diagnostics in runtime.** A new `Runtime.Trace` exposes connection-level
diagnostics, including a TLS diagnostic mode, alongside a build-tagged `SkipAuth` feature
and a fix for honoring context cancellation in the multipart upload goroutine.
- **The swag API migration.** Both **go-swagger** and **codegen** migrated their generators
onto the new `go-openapi/swag` API; go-swagger's generated clients became more idiomatic
(context-aware `SubmitContext`, internal `Params` fields moved to their own struct).
- **A wave of releases.** runtime cut six releases (**v0.29.5 → v0.32.2**), go-swagger
reached **v0.34.0**, and analysis, jsonreference, strfmt and validate all tagged patch
releases; ci-workflows iterated through **v0.3.x**.
- **Documentation.** runtime received a large docs pass — a media-types selection guide, a
keep-alive primer, a constant-time-comparison contract for auth callbacks, and per-module
READMEs — and analysis clarified its mixin precedence rules.
- **CI maintenance, org-wide.** A shared **bot-PR monitoring** workflow was rolled out across
most repositories (runtime, analysis, validate, spec, swag, strfmt, …), with follow-up
fixes to bot identity, filters and permissions.

## Repository highlights

| Repository | Latest release | Highlights |
|---|---|---|
| runtime | v0.32.4 | `Runtime.Trace` connection diagnostics + TLS mode; build-tagged `SkipAuth`; JSON-dialects fix; context-cancelling multipart upload; large docs pass |
| go-swagger | v0.34.1 | v0.34.0 cut; generator on the new swag API; idiomatic context-aware clients; configurable custom producers/consumers; enum-constant fix (#1047) |
| codegen | — | Generator migrated to the new go-openapi/swag API |
| examples | v0.33.2 | Regenerated for the new swag methods; new middleware-composition example |
| analysis | v0.25.2 | Mixin precedence rules clarified and limitations documented |
| codescan | v0.34.1 | v0.34.0 released; consumed by go-swagger's `swagger generate spec` |

## Thanks to our contributors

A warm thank-you to the external contributors who landed changes this month:

- **[@Abzaek](https://github.com/Abzaek)** — codegen, go-swagger
- **[@KuaaMU](https://github.com/KuaaMU)** — runtime
- **[@dashitongzhi](https://github.com/dashitongzhi)** — go-swagger

Your contributions are genuinely appreciated.
Loading