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
32 changes: 32 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!--
Thanks for opening a PR! This template auto-fills the body of new PRs.
Replace the placeholder text below; remove sections that don't apply.
Dependabot bypasses this template (it supplies its own body); see
`.github/workflows/dependabot-changelog.yml` for how Dependabot PRs
get a CHANGELOG entry automatically.
-->

## Summary

<!-- Two or three sentences on what changed and why. -->

## Test plan

<!-- Checklist the maintainer can walk to verify the change. -->

- [ ] `.venv/bin/python -m pytest tests/` — passes
- [ ] `.venv/bin/ruff check src/ tests/` — clean
- [ ] `bash scripts/smoke-test.sh` — clean
- [ ] Confirm no regression in the affected module

## CHANGELOG

<!--
Confirm the matching CHANGELOG.md entry under `## [Unreleased]`.
See CONTRIBUTING.md § "PR requirements" for category guidance.
Categories: Added / Changed / Fixed / Security.
-->

- [ ] Added a `## [Unreleased]` entry to `CHANGELOG.md` under the appropriate Keep-a-Changelog category (Added / Changed / Fixed / Security)

Closes #
78 changes: 78 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Dependabot version-update configuration for yt-dont-recommend.
#
# Two ecosystems are tracked:
# - pip: runtime + dev dependencies declared in
# pyproject.toml (also picks up the [clickbait]
# extras: ollama, pyyaml, youtube-transcript-api)
# - github-actions: workflow files under .github/workflows/
# (CI, publish, label automation, qa-gate,
# sync-labels, dependabot-changelog)
#
# No docker ecosystem because this repo has no Dockerfile.
#
# Schedule is weekly (Monday 06:00 America/Chicago) so PRs don't pile
# up faster than they can be reviewed solo. Each ecosystem groups its
# updates into a single PR per week to reduce notification noise.
#
# Labels named here are applied only if they already exist on the
# repo; Dependabot does not auto-create labels. The QA-workflow
# labels (Awaiting CI / Ready for QA / etc.) are applied separately
# by .github/workflows/pr-labels*.yml.
#
# commit-message uses `prefix: chore` plus `include: scope` (NOT
# `prefix: chore(deps)`) — Dependabot auto-appends the (deps) scope
# when include: scope is set, so the canonical title shape is
# `chore(deps): bump foo from X to Y`. Doubling the prefix produces
# `chore(deps)(deps): bump foo`.
#
# Dependabot PRs are augmented by .github/workflows/dependabot-changelog.yml
# which prepends a CHANGELOG entry under [Unreleased] / Changed using
# a GitHub App installation token so the bot's commit re-fires the
# QA-Gate-required CI checks.
#
# Docs:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
# Python deps: pyproject.toml at the repo root. Covers Playwright
# (runtime), the [dev] extras (pytest, pytest-cov, ruff), and the
# optional [clickbait] extras (ollama, pyyaml, youtube-transcript-api).
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
timezone: "America/Chicago"
open-pull-requests-limit: 5
groups:
python:
patterns:
- "*"
labels:
- "dependencies"
- "python"
commit-message:
prefix: "chore"
include: "scope"

# GitHub Actions used by .github/workflows/*.yml.
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
timezone: "America/Chicago"
open-pull-requests-limit: 5
groups:
github-actions:
patterns:
- "*"
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "chore"
include: "scope"
6 changes: 6 additions & 0 deletions .github/labels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@
- name: dependencies
color: "0075ca"
description: Dependency updates
- name: python
color: "3572A5"
description: Python ecosystem (pyproject.toml / runtime / dev deps)
- name: github-actions
color: "2088FF"
description: GitHub Actions workflows
- name: testing
color: "0E8A16"
description: Platform and integration testing
Expand Down
214 changes: 214 additions & 0 deletions .github/workflows/dependabot-changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
name: Dependabot CHANGELOG

# Auto-appends a CHANGELOG entry to Dependabot-authored PRs so they
# satisfy the project's CHANGELOG-per-PR rule (see CONTRIBUTING.md
# § "PR requirements") without manual intervention.
#
# Triggered on pull_request_target so the workflow runs in the base
# branch's context with write permissions (Dependabot's pull_request
# event runs with read-only GITHUB_TOKEN).
#
# IMPORTANT: pushes back via a GitHub App installation token rather
# than secrets.GITHUB_TOKEN. Pushes authenticated with GITHUB_TOKEN
# do NOT trigger downstream `pull_request` workflows (GitHub's
# anti-loop policy), which would leave the QA-Gate-required CI
# checks (ci.yml's test ubuntu / test macos / smoke-macos jobs)
# unsatisfied on the bot's follow-up commit and block merge under
# the repo's main-branch ruleset. App-token-authored pushes do
# trigger those workflows normally.
#
# Required repo secrets (operator must configure once):
# BOT_APP_ID — GitHub App ID (numeric)
# BOT_APP_PRIVATE_KEY — PEM contents of the App's private key
#
# Loop guard: skips if the latest commit on the head branch was
# authored by the bot itself — i.e. our own previous run.
#
# Idempotency: skips if a CHANGELOG entry referencing the PR number
# already exists.

on:
pull_request_target:
types: [opened, synchronize, reopened]

permissions:
contents: read
pull-requests: read

jobs:
changelog:
if: github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Mint GitHub App installation token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.BOT_APP_ID }}
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}

- name: Checkout PR branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 2

- name: Loop guard — skip if last commit was ours
id: guard
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
LAST_AUTHOR=$(git log -1 --pretty=%an)
echo "last_author=$LAST_AUTHOR" >> "$GITHUB_OUTPUT"
if [ "$LAST_AUTHOR" = "cmeans-claude-dev[bot]" ]; then
echo "Last commit authored by the bot; skipping to avoid loop."
echo "skip=true" >> "$GITHUB_OUTPUT"
elif grep -qE "\(#${PR_NUMBER}\)" CHANGELOG.md; then
echo "CHANGELOG.md already references (#${PR_NUMBER}); skipping."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: Fetch Dependabot metadata
if: steps.guard.outputs.skip != 'true'
id: meta
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
with:
github-token: ${{ steps.app-token.outputs.token }}

- name: Compose and prepend CHANGELOG entry
if: steps.guard.outputs.skip != 'true'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
DEPS_JSON: ${{ steps.meta.outputs.updated-dependencies-json }}
DEPENDENCY_GROUP: ${{ steps.meta.outputs.dependency-group }}
ECOSYSTEM: ${{ steps.meta.outputs.package-ecosystem }}
UPDATE_TYPE: ${{ steps.meta.outputs.update-type }}
run: |
python3 - <<'PY'
import json
import os
import pathlib

pr_number = os.environ["PR_NUMBER"]
deps_json = os.environ.get("DEPS_JSON") or "[]"
# Prefer the named Dependabot group (matches PR title:
# "Bump the <group> group across N directories with M updates").
# Fall back to the ecosystem identifier (pip / github-actions / ...).
group = (os.environ.get("DEPENDENCY_GROUP") or "").strip()
ecosystem = (os.environ.get("ECOSYSTEM") or "deps").strip()
label = group if group else ecosystem
update_type = os.environ.get("UPDATE_TYPE") or "version-update"

deps = json.loads(deps_json)
if not deps:
# Nothing to record — exit cleanly.
print("No dependencies in metadata; skipping.")
raise SystemExit(0)

parts = [
f"{d['dependencyName']} {d.get('prevVersion', '?')}→{d.get('newVersion', '?')}"
for d in deps
]
summary = ", ".join(parts)

# Severity hint from update-type — security updates are the
# interesting category at release time.
tag = ""
if "security" in update_type:
tag = " — picks up upstream security advisory; see PR body for CVE links."

entry = (
f"- **Bump {label} group: {summary}** (#{pr_number}){tag}\n"
)

path = pathlib.Path("CHANGELOG.md")
text = path.read_text()
lines = text.splitlines(keepends=True)

# Locate the `## [Unreleased]` block (with or without
# Keep-a-Changelog brackets) and the `### Changed` subsection
# within it. If `### Changed` is absent, create it at the
# right Keep-a-Changelog v1.1.0 position.
unreleased_idx = None
unreleased_heading = "## [Unreleased]"
for i, line in enumerate(lines):
stripped = line.strip()
if stripped == "## Unreleased" or stripped == "## [Unreleased]":
unreleased_idx = i
unreleased_heading = stripped
break
if unreleased_idx is None:
# Insert a fresh `## [Unreleased]` section after the title.
insert_at = 0
for i, line in enumerate(lines):
if line.startswith("# "):
insert_at = i + 1
break
new_block = ["\n", f"{unreleased_heading}\n", "\n", "### Changed\n", "\n", entry]
lines = lines[:insert_at] + new_block + lines[insert_at:]
else:
# Search for `### Changed` between Unreleased and next `## ` heading.
changed_idx = None
end_idx = len(lines)
for j in range(unreleased_idx + 1, len(lines)):
if lines[j].startswith("## "):
end_idx = j
break
if lines[j].strip() == "### Changed":
changed_idx = j
break
if changed_idx is not None:
# Insert entry directly after the `### Changed` header
# (after the blank line that follows it, if present).
insert_at = changed_idx + 1
if insert_at < end_idx and lines[insert_at].strip() == "":
insert_at += 1
lines.insert(insert_at, entry)
else:
# `### Changed` doesn't exist within Unreleased — create it
# at the right position per Keep-a-Changelog v1.1.0 ordering:
# Added → Changed → Deprecated → Removed → Fixed → Security.
# Walk forward from `## [Unreleased]` to find either the first
# subsection that should sort AFTER `### Changed`, or the next
# `## ` release heading; insert immediately before whichever
# comes first. Default insertion point is the end of the
# Unreleased section (just before the next `## ` heading).
after_changed = {"### Deprecated", "### Removed", "### Fixed", "### Security"}
insert_at = end_idx
for j in range(unreleased_idx + 1, end_idx):
if lines[j].strip() in after_changed:
insert_at = j
break
block = ["### Changed\n", "\n", entry, "\n"]
for k, ln in enumerate(block):
lines.insert(insert_at + k, ln)

path.write_text("".join(lines))
print(f"Inserted CHANGELOG entry: {entry.strip()}")
PY

- name: Commit and push (via App token so CI re-fires)
if: steps.guard.outputs.skip != 'true'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
GH_BOT_USER_ID: '272174644'
run: |
if git diff --quiet CHANGELOG.md; then
echo "No changes to commit."
exit 0
fi
# Commit author/committer aligns with the App's bot account so
# commits are attributed to cmeans-claude-dev[bot]. The numeric
# user id is required for GitHub's noreply-email format to
# resolve back to the bot account; otherwise commits show up
# under the App ID rather than the bot user.
git config user.name "cmeans-claude-dev[bot]"
git config user.email "${GH_BOT_USER_ID}+cmeans-claude-dev[bot]@users.noreply.github.com"
git add CHANGELOG.md
git commit -m "chore(changelog): record dep bumps from #${PR_NUMBER}"
git push
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). Versions follow
### Added

- **Community-health files**: `CONTRIBUTING.md` (inbound = outbound Apache-2.0, no-bounty policy, dev setup, PR requirements, review process), `CODE_OF_CONDUCT.md` (Contributor Covenant 2.1, private reporting via GitHub Security Advisory), `SECURITY.md` (private vulnerability reporting, in-scope / out-of-scope boundaries covering page-derived-string injection, blocklist source trust, state file integrity, auto-upgrade safety, data-dir permissions, subscription protection, and selector self-healing), and three GitHub issue templates under `.github/ISSUE_TEMPLATE/` (`bug_report.yml`, `feature_request.yml`, `config.yml`). Mirrors the community-health pass on `cmeans/pypi-winnow-downloads` and brings the repo in line with the rest of the `cmeans/*` projects.
- **Dependabot weekly version updates + auto-CHANGELOG workflow**: `.github/dependabot.yml` (pip + github-actions, weekly Monday 06:00 America/Chicago, grouped per ecosystem, `chore` prefix with `include: scope`) plus `.github/workflows/dependabot-changelog.yml` (`pull_request_target` workflow filtered to `dependabot[bot]` that prepends a Keep-a-Changelog-ordered entry under `[Unreleased] / Changed`, authenticated via a GitHub App installation token so the bot's commit re-fires the QA-Gate-required CI checks). `.github/PULL_REQUEST_TEMPLATE.md` auto-fills human-authored PR bodies with the project's Summary / Test plan / CHANGELOG checklist (Dependabot bypasses templates and supplies its own body, which is why the workflow exists). Labels `python` and `github-actions` added to `.github/labels.yml` so the Dependabot grouped PRs get their domain labels at open time. Cascades the post-`mcp-synology#63` Keep-a-Changelog ordering fix from the validated `cmeans/mcp-clipboard#96` rollout. Operator prereq: configure `BOT_APP_ID` + `BOT_APP_PRIVATE_KEY` repo secrets before merge.

### Security

Expand Down
Loading