
Newproject
- 5 installs
- 2 repo stars
- Updated August 1, 2026
- byheaven/byheaven-skills
Scaffold a new project or upgrade an existing repo with foundation files, code-quality tooling, release automation, CI, and security scanning from bundled templates.
About
Sets up a project end to end with README, LICENSE, AGENTS.md, release workflows, CI, dependency management, and security scanning using bundled templates and scripts. A developer uses it to bootstrap a new codebase or bring an existing repo up to a baseline.
- Tiered setup: foundation files, code quality, release automation, CI, security
- Ships its own assets/ templates, workflows, and scripts
Newproject by the numbers
- 5 all-time installs (skills.sh)
- Ranked #1,085 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/byheaven/byheaven-skills --skill newprojectAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| repo stars | ★ 2 |
| Last updated | August 1, 2026 |
| Repository | byheaven/byheaven-skills ↗ |
What it does
Scaffold a new project or upgrade an existing repo with foundation files, code-quality tooling, release automation, CI, and security scanning from bundled templates.
Files
newproject
Sets up a new or existing project end-to-end. It includes the templates, workflows, and scripts it needs under its own assets/ directory.
When this setup needs structured user input, first detect which question tool is available in the current host environment:
- if
AskUserQuestionis available, useAskUserQuestion - otherwise, if
request_user_inputis available, userequest_user_input - if neither structured question tool is available, ask the user directly in plain text
Do this detection before the first question and keep using the same question tool for the rest of the run.
What This Skill Sets Up
Tier 1 — Foundation
project scaffold README, LICENSE, .gitignore, .editorconfig, CONTRIBUTING.md
AGENTS.md baseline shared AI instructions + CLAUDE.md symlink
release workflow conventional commits, changelog-driven releases, release.yml
ci pipeline GitHub Actions CI for the detected project type
Tier 2 — Quality and Governance
code quality ESLint/Prettier or Ruff/golangci-lint/rustfmt + markdownlint
GitHub repo setup PR template, issue forms, labels, optional CODEOWNERS, branch protection
dependencies Dependabot + auto-merge workflow
Tier 3 — Security
security scanning CodeQL when supported, dependency review, secret scanning guidanceAsset Layout
All required files live inside this package:
assets/foundation/— README, LICENSE, CONTRIBUTING, .gitignore, .editorconfigassets/quality/— ESLint, Prettier, Ruff, markdownlint, pre-commit hookassets/ci/— GitHub Actions CI templatesassets/release/— commitlint config, release workflow, extract script, referencesassets/github/— PR template, issue forms, labels, CODEOWNERS template, branch protection scriptsassets/dependencies/— Dependabot templates and auto-merge workflowassets/security/— CodeQL and dependency review workflows
---
Step 1: Detect the Project and Current State
Inspect the repo before asking anything:
# Project type indicators
ls package.json pyproject.toml setup.py requirements.txt go.mod Cargo.toml 2>/dev/null
# Web framework indicators
ls next.config.* nuxt.config.* vite.config.* angular.json svelte.config.* astro.config.* remix.config.* 2>/dev/null
# Existing foundation files
ls README.md LICENSE .gitignore .editorconfig CONTRIBUTING.md CHANGELOG.md AGENTS.md CLAUDE.md 2>/dev/null
file AGENTS.md CLAUDE.md 2>/dev/null
# Existing GitHub configuration
ls .github/ 2>/dev/null
ls .github/workflows/ 2>/dev/null
ls .github/pull_request_template.md .github/ISSUE_TEMPLATE/ .github/CODEOWNERS .github/dependabot.yml 2>/dev/null
# Existing tooling
ls eslint.config.* .eslintrc* prettier.config.* .prettierrc* ruff.toml .pre-commit-config.yaml .golangci.yml rustfmt.toml .markdownlint.json 2>/dev/null
ls .husky/ 2>/dev/null
# Existing release / CI / security workflows
ls .github/workflows/ci* .github/workflows/release* .github/workflows/commitlint* .github/workflows/codeql* .github/workflows/dependency-review* 2>/dev/null
# Package manager and scripts for Node projects
ls package-lock.json yarn.lock pnpm-lock.yaml bun.lockb 2>/dev/null
node -e "const p=require('./package.json'); console.log(JSON.stringify(p.scripts||{}, null, 2))" 2>/dev/null || true
# Git state
git status --short 2>/dev/null || echo "git not initialized"
git remote -v 2>/dev/null || echo "no remote"
git branch --show-current 2>/dev/null || trueDetermine:
- Project type:
webifpackage.jsonexists and a web framework config existsnodeifpackage.jsonexists without a web framework configpythonifpyproject.toml,setup.py, orrequirements.txtexistsgoifgo.modexistsrustifCargo.tomlexistsotherotherwise- Project state:
- brand new if there is no package manifest, no README, no
.github/, and no git remote - existing otherwise
- Package manager for Node projects:
npmifpackage-lock.jsonexistsyarnifyarn.lockexistspnpmifpnpm-lock.yamlexistsbunifbun.lockbexists- if none exist, default to
npm - Available scripts for Node projects:
- note whether
lint,test, andbuildexist - Version source of truth:
package.jsonfor Node and webpyproject.tomlorsetup.pyfor PythonCargo.tomlfor Rust- tag-only unless another version file already exists for Go or other projects
---
Step 2: Choose the Run Scope
Path A — Brand New Project
For a brand-new directory, collect the basic project context before recommending a stack.
Use the AskUserQuestion/request_user_input tool explicitly:
- if the project name is not obvious from the directory name:
- "What is the project name?"
- always:
- "Please provide a short description (1-2 sentences)."
Then use the project name and description to suggest the best default stack, for example:
"Based on your description, I suggest a Node.js + TypeScript setup with Vitest,
ESLint, and Prettier. That gives you a fast default for libraries and apps.
Confirm this stack or tell me what you prefer."
Use the AskUserQuestion/request_user_input tool explicitly to confirm or override the stack.
Then show the checklist:
New [type] project: [name]
Tier 1 — Foundation
[x] scaffold and repo baseline
[x] release workflow
[x] CI pipeline
Tier 2 — Quality and Governance
[x] code quality
[x] GitHub repository setup
[x] dependency management
Tier 3 — Security
[x] security scanningUse the AskUserQuestion/request_user_input tool explicitly:
- "Press Enter to run everything, or tell me what to skip: for example
skip security,tier1 only, orcode quality only."
Path B — Existing Project
For an existing repository, do not ask for the project name or description. Read the repository and infer the context first.
Before showing the checklist, read the most relevant files that describe the project:
README.mdAGENTS.mdCLAUDE.mdif it exists and is not just a symlink toAGENTS.md- package metadata such as
package.json,pyproject.toml,go.mod, orCargo.toml CHANGELOG.md- existing workflow files under
.github/workflows/
Then present a short understanding summary to the user before asking for scope. That summary should include:
- the inferred project name
- what the project appears to do
- the detected tech stack
- what setup already appears to exist
- the highest-value gaps that
newprojectcould fill
After that summary, mark what already appears configured and default to the unchecked Tier 1 items first:
Project: [detected type] — [project name]
Tier 1 — Foundation
[done or empty] scaffold and repo baseline
[done or empty] release workflow
[done or empty] CI pipeline
Tier 2 — Quality and Governance
[done or empty] code quality
[done or empty] GitHub repository setup
[done or empty] dependency management
Tier 3 — Security
[done or empty] security scanningUse the AskUserQuestion/request_user_input tool explicitly:
- "Which parts should I run? Press Enter to run unchecked Tier 1 items, or say
all,tier2, or specific parts."
Treat the selected scope as the source of truth for the rest of the run.
---
Step 3: Foundation and Repository Baseline
Run this section when the user selected scaffold and repo baseline.
3.1 Initialize Git if Needed
If the directory is not a git repo:
git init
git checkout -b mainIf git already exists, skip this step.
3.2 Create or Update the Foundation Files
Use only the vendored assets in this package:
assets/foundation/templates/README.md.templateassets/foundation/templates/LICENSE-MIT.templateassets/foundation/templates/CONTRIBUTING.md.templateassets/foundation/gitignore/assets/foundation/editorconfig/.editorconfig
Apply these rules:
README.md- if missing, create it from the template
- replace
{{PROJECT_NAME}},{{DESCRIPTION}}, and{{PROJECT_TYPE}} - if it already exists, add only missing standard sections such as installation, usage, and contributing
LICENSE- default to MIT when missing
- replace
{{YEAR}}with the current year - replace
{{AUTHOR}}with the user's name - if the name is unknown, use the
AskUserQuestion/request_user_inputtool explicitly .gitignore- if missing, copy the language-appropriate template
- if present, append only clearly missing language-specific sections
.editorconfig- if missing, copy the vendored template
CONTRIBUTING.md- if missing, create it from the template
- if present, preserve existing content and add any missing sections later in the release workflow step
3.3 Create Standard Directories
Create only the directories that are missing:
- Node or web:
src/,tests/,docs/,scripts/ - Python:
src/<package_name>/,tests/,docs/,scripts/ - Go:
cmd/<project_name>/,internal/,pkg/,docs/,scripts/ - Rust:
docs/,scripts/ - Other:
src/,tests/,docs/,scripts/
For Python, also create src/<package_name>/__init__.py when the package directory is new.
3.4 Make AGENTS.md the Source of Truth
Unify AGENTS.md and CLAUDE.md so all AI tools read the same guidance.
Detect the current state:
[ -L CLAUDE.md ] && echo "CLAUDE.md is a symlink" || echo "CLAUDE.md is not a symlink"
[ -f AGENTS.md ] && echo "AGENTS.md exists" || echo "AGENTS.md missing"
[ -f CLAUDE.md ] && echo "CLAUDE.md exists" || echo "CLAUDE.md missing"Handle each state:
- Already unified:
CLAUDE.mdis a symlink toAGENTS.md - keep it as-is
- Neither file exists
- create a minimal
AGENTS.md:
# AGENTS.md
This file provides guidance to AI coding assistants (Claude Code, OpenAI Codex,
and others) when working with code in this repository.
## Contributor Conventions
Follow [CONTRIBUTING.md](CONTRIBUTING.md) for all contribution conventions.- then run
ln -s AGENTS.md CLAUDE.md - Only `CLAUDE.md` exists as a regular file
- move it to
AGENTS.md - if its title is
# CLAUDE.md, rename it to# AGENTS.md - then create the symlink with
ln -s AGENTS.md CLAUDE.md - Only `AGENTS.md` exists
- create the symlink with
ln -s AGENTS.md CLAUDE.md - Both exist as regular files
- use the
AskUserQuestion/request_user_inputtool explicitly before merging - keep
AGENTS.mdas the base - append only the sections from
CLAUDE.mdthat are not already present - normalize the title to
# AGENTS.md - replace
CLAUDE.mdwith the symlink
After the file state is correct, ensure AGENTS.md includes a ## Contributor Conventions section that points to CONTRIBUTING.md.
---
Step 4: Code Quality and Release Workflow
Run this section when the user selected code quality, release workflow, or both. If both are selected for a Node project, set up code quality before release workflow so husky can be shared cleanly.
4.1 Code Quality
Use only the vendored assets in this package:
assets/quality/config/eslint.config.jsassets/quality/config/prettier.config.jsassets/quality/config/ruff.tomlassets/quality/config/.markdownlint.jsonassets/quality/hooks/pre-commit
Node or Web
Install the baseline tooling:
npm install --save-dev \
eslint \
@eslint/js \
prettier \
eslint-config-prettier \
lint-staged \
huskyFor TypeScript projects, also install:
npm install --save-dev \
typescript-eslint \
@typescript-eslint/eslint-plugin \
@typescript-eslint/parserThen:
- copy
assets/quality/config/eslint.config.jstoeslint.config.jsif no flat config exists - copy
assets/quality/config/prettier.config.jstoprettier.config.jsif missing - add a
lint-stagedblock topackage.json:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,css,md,yml,yaml}": ["prettier --write"],
"*.md": ["markdownlint-cli2 --fix"]
}
}- initialize husky only if
.husky/does not already exist:
ls .husky/ 2>/dev/null || npx husky init- ensure
.husky/pre-commitrunsnpx lint-staged
Python
Install and configure Ruff:
pip install ruff pre-commitThen:
- copy
assets/quality/config/ruff.tomltoruff.tomlif missing - copy
assets/quality/hooks/pre-committo.pre-commit-config.yamlif missing - run
pre-commit install
Go
Install golangci-lint and create .golangci.yml if missing:
brew install golangci-lintUse this baseline config:
run:
timeout: 5m
linters:
enable:
- gofmt
- goimports
- govet
- errcheck
- staticcheck
- unused
- gosimple
issues:
exclude-rules:
- path: _test\.go
linters: [errcheck]Rust
Ensure the standard tooling is installed:
rustup component add rustfmt clippyCreate rustfmt.toml if missing:
edition = "2021"
max_width = 100
tab_spaces = 4All Project Types
Configure markdown linting:
npm install --save-dev markdownlint-cli2Copy assets/quality/config/.markdownlint.json to .markdownlint.json if missing.
Add this line to AGENTS.md if it is not already present:
Code quality: run the configured formatter and linter before committing.4.2 Release Workflow
Use only the vendored assets in this package:
assets/release/config/commitlint.config.jsassets/release/workflows/commitlint-check.ymlassets/release/workflows/release.ymlassets/release/scripts/extract-release-notes.shassets/release/references/changelog-style-guide.md
Conventional Commits
For Node or web projects, install commitlint locally:
npm install --save-dev \
@commitlint/cli \
@commitlint/config-conventional \
huskyCopy assets/release/config/commitlint.config.js to commitlint.config.js.
Ensure .husky/commit-msg exists and runs:
npx --no -- commitlint --edit $1For non-Node projects, copy assets/release/workflows/commitlint-check.yml to .github/workflows/commitlint-check.yml.
Release Workflow Files
Copy these files:
assets/release/workflows/release.yml→.github/workflows/release.ymlassets/release/scripts/extract-release-notes.sh→scripts/extract-release-notes.shassets/release/references/changelog-style-guide.md→docs/changelog-style-guide.md
Then:
- make
scripts/extract-release-notes.shexecutable - configure the tag glob in
release.yml - use
'*.*.*'for no prefix - use
'myapp-*.*.*'if the repo already uses a tag prefix - configure version validation in
release.yml package.jsonfor Node or webpyproject.tomlor existing version file for PythonCargo.tomlfor Rust- skip version-file validation for tag-only Go or generic repos
CHANGELOG.md
If CHANGELOG.md is missing, create it with linked headers:
# Changelog
All notable changes to this project will be documented in this file.
Versions follow [Semantic Versioning](https://semver.org).
## [Unreleased](https://github.com/OWNER/REPO/compare/0.0.0...HEAD)For real releases, use this format:
## [1.2.0](https://github.com/OWNER/REPO/compare/1.1.0...1.2.0) (2026-03-18)Rules:
Unreleasedmust always be a linked header- every commit or merge to
mainmust manually add a short user-facing changelog entry underUnreleased - each release header must compare the previous tag to the new tag
- default release tags should omit a leading
vunless the repository already has an establishedv-prefixed tag history - the first bold line in a release section becomes the GitHub Release title
CONTRIBUTING.md and AGENTS.md
Ensure CONTRIBUTING.md documents this release flow:
1. every change merged to main must update CHANGELOG.md 2. add the new entry under the Unreleased section before or as part of the merge commit 3. keep Unreleased current throughout normal development 4. on release day, convert the accumulated Unreleased notes into the new version section 5. bump the version file if the project uses one 6. commit the release changes 7. tag that exact commit with an explicit message so release tagging does not block on an editor in non-interactive environments
- if tag signing is enabled, prefer
GIT_EDITOR=true git tag -s -m "release 1.2.0" 1.2.0 - if tag signing is disabled, prefer
git tag -a -m "release 1.2.0" 1.2.0
1. push the commit and tag
Add this line to AGENTS.md if missing:
Release: every change merged to main must update CHANGELOG.md under the Unreleased section. When the user says "release" or "ship", follow the Release Workflow section in CONTRIBUTING.md and use docs/changelog-style-guide.md for changelog editing.Verification
If the repo already has a version section in CHANGELOG.md, dry-run the extract script:
CHANGELOG_FILE=CHANGELOG.md ./scripts/extract-release-notes.sh 1.2.0If the script fails, fix the changelog format before calling the release setup done.
Also verify that Unreleased already contains ongoing development notes and is not left empty after normal feature or fix work lands on main.
---
Step 5: CI Pipeline and GitHub Repository Setup
Run this section when the user selected CI pipeline, GitHub repository setup, or both.
5.1 CI Pipeline
Use only the vendored assets in this package:
assets/ci/workflows/ci-node.ymlassets/ci/workflows/ci-python.ymlassets/ci/workflows/ci-go.ymlassets/ci/workflows/ci-rust.ymlassets/ci/workflows/ci-generic.yml
Create .github/workflows/ if needed.
Node or Web
If package.json exists but no lockfile exists, generate one before writing CI:
npm installCopy assets/ci/workflows/ci-node.yml to .github/workflows/ci.yml.
Then customize the workflow:
- set the Node matrix
- libraries and CLIs:
[18, 20, 22] - web apps: a single current LTS version is fine
- set the package manager install command:
npm ciyarn install --frozen-lockfilepnpm install --frozen-lockfilebun install --frozen-lockfile- remove or comment out the lint step if there is no
lintscript - remove or comment out the test step if there is no
testscript - remove or comment out the build step if there is no
buildscript - if the test command cannot be inferred from
package.json, use theAskUserQuestion/request_user_inputtool explicitly
Python
Copy assets/ci/workflows/ci-python.yml to .github/workflows/ci.yml.
Then customize:
- Python matrix: default to
["3.11", "3.12", "3.13"] - install command: usually
pip install -e ".[dev]"orpip install -r requirements-dev.txt - test command: usually
pytest - lint command:
ruff check .when Ruff is configured
Go
Copy assets/ci/workflows/ci-go.yml to .github/workflows/ci.yml.
Then:
- set the Go version based on
go.mod - keep
go test ./... - keep
go build ./... - if
.golangci.ymlexists, add the official golangci-lint action
Rust
Copy assets/ci/workflows/ci-rust.yml to .github/workflows/ci.yml.
Then:
- keep the stable toolchain unless the project clearly requires nightly
- keep
cargo fmt --check,cargo clippy,cargo test, andcargo build - if security scanning is selected too, add
cargo auditonly when the user wants Rust dependency auditing in CI
Other
Copy assets/ci/workflows/ci-generic.yml to .github/workflows/ci.yml.
Then replace the placeholder install, test, lint, and build commands with the real commands. If the repo does not reveal them, use the AskUserQuestion/request_user_input tool explicitly.
Add this line to AGENTS.md if missing:
CI: keep .github/workflows/ci.yml aligned with the repository's real install, lint, test, and build commands.5.2 GitHub Repository Setup
Use only the vendored assets in this package:
assets/github/templates/pull-request-template.mdassets/github/templates/bug-report.ymlassets/github/templates/feature-request.ymlassets/github/templates/CODEOWNERS.templateassets/github/scripts/bootstrap-labels.shassets/github/scripts/configure-branch-protection.sh
Before making GitHub API changes, confirm:
gh auth status
git remote -v
gh repo view --json nameWithOwner --jq .nameWithOwnerIf gh is not installed or authenticated, guide the user to install and log in.
Then:
- create or update the standard label catalog before any templates or Dependabot rules rely on it
- use
assets/github/scripts/bootstrap-labels.sh - ensure these labels exist with the vendored colors and descriptions:
bug,enhancement,needs-triage,dependencies,ci,major-update,documentation,security,release - treat label bootstrapping as idempotent: update existing labels in place instead of failing
- copy the PR template to
.github/pull_request_template.mdif missing - copy the issue templates to
.github/ISSUE_TEMPLATE/ - keep the issue-template labels aligned to the standard catalog unless the user explicitly asks for a different taxonomy
- create
.github/CODEOWNERS.examplefrom the vendored template by default - keep it as a documented opt-in scaffold and do not auto-request reviewers by default
- if the user explicitly wants automatic review requests, copy the customized template to
.github/CODEOWNERSinstead - use
* @usernamefor solo repos - use directory or file-type ownership only when the repo layout clearly supports it
- if ownership is ambiguous, use the
AskUserQuestion/request_user_inputtool explicitly - apply branch protection to the primary branch
- require 0 approvals by default
- disable force pushes and deletions
- leave required status check contexts empty until CI has run once
- if the user explicitly wants mandatory human approval, enable required reviews separately and only then turn on stale review dismissal
You can either run the vendored script after customizing it, or call gh api directly:
REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
gh api \
--method PUT \
"repos/${REPO}/branches/main/protection" \
--field required_status_checks='{"strict":true,"contexts":[]}' \
--field enforce_admins=false \
--field required_pull_request_reviews=null \
--field restrictions=null \
--field allow_force_pushes=false \
--field allow_deletions=falseAdd this line to AGENTS.md if missing:
PRs: all pull requests must use the PR template (.github/pull_request_template.md). Branch protection keeps force pushes and deletions disabled by default; add required reviews only when the team wants mandatory human approval.---
Step 6: Dependency Management and Security Scanning
Run this section when the user selected dependency management, security scanning, or both.
6.1 Dependency Management
Use only the vendored assets in this package:
assets/dependencies/config/dependabot-node.ymlassets/dependencies/config/dependabot-python.ymlassets/dependencies/config/dependabot-go.ymlassets/dependencies/config/dependabot-rust.ymlassets/dependencies/config/dependabot-generic.ymlassets/dependencies/workflows/dependabot-auto-merge.yml
Determine every ecosystem present:
npmfrompackage.jsonpipfrompyproject.toml,setup.py, orrequirements.txtgomodfromgo.modcargofromCargo.tomlgithub-actionsalways
Before writing .github/dependabot.yml, ensure the standard GitHub label catalog exists by running assets/github/scripts/bootstrap-labels.sh. This keeps the Dependabot labels: entries valid even when dependency management is configured without the broader GitHub repository setup section.
Create .github/dependabot.yml from the matching base template, then ensure the github-actions ecosystem is also included. For polyglot repositories, merge the relevant sections into a single file.
Use the default schedule:
- weekly grouped updates
- auto-merge only for patch and minor updates after required status checks pass
- do not require human review by default; if the repo later opts into review requirements, bot approval is best-effort only and a human review may still be needed depending on repository policy
Copy assets/dependencies/workflows/dependabot-auto-merge.yml to .github/workflows/dependabot-auto-merge.yml.
After the workflows are pushed, enable auto-merge if the repo supports it:
REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
gh api --method PATCH "repos/${REPO}" --field allow_auto_merge=trueAdd this line to AGENTS.md if missing:
Dependencies: Dependabot opens PRs for updates automatically. Patch and minor updates are auto-merged after required status checks pass; major updates require manual review.6.2 Security Scanning
Use only the vendored assets in this package:
assets/security/workflows/codeql.ymlassets/security/workflows/dependency-review.yml
Determine the CodeQL language:
- Node or web →
javascript-typescript - Python →
python - Go →
go - Rust or other unsupported languages → skip CodeQL
If CodeQL is supported:
- copy
assets/security/workflows/codeql.ymlto.github/workflows/codeql.yml - update the matrix language to the detected language
Always copy assets/security/workflows/dependency-review.yml to .github/workflows/dependency-review.yml.
For Rust projects, explain that CodeQL is not supported and recommend:
cargo install cargo-audit
cargo auditSecret scanning is a repo setting, not a workflow file. Guide the user:
1. GitHub repo → Settings → Security & analysis 2. Enable Secret scanning 3. Enable Push protection when available
If the repository is public, you can attempt to enable secret scanning through gh api:
REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
gh api --method PATCH "repos/${REPO}" \
--field "security_and_analysis[secret_scanning][status]=enabled" \
--field "security_and_analysis[secret_scanning_push_protection][status]=enabled"Add this line to AGENTS.md if missing:
Security: CodeQL runs on supported languages, dependency review blocks high and critical CVEs in PRs, and the Security tab must stay clean.---
Step 7: GitHub Permissions, Commit Strategy, and Verification
7.1 Automate GitHub Repository Settings
After the selected files are committed and pushed, automate the matching GitHub settings.
If release workflow was configured, ensure Actions can write releases:
REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
gh api --method PUT "repos/${REPO}/actions/permissions/workflow" \
--field default_workflow_permissions=write \
--field can_approve_pull_request_reviews=trueIf dependency management was configured, ensure auto-merge is enabled:
gh api --method PATCH "repos/${REPO}" --field allow_auto_merge=trueIf security scanning was configured, attempt secret scanning as described above, but report clearly when GitHub plan limits block it.
7.2 Commit the Work
Default to focused conventional commits after each selected section unless the user explicitly asks for one squashed setup commit.
Suggested commit messages:
- scaffold and repo baseline →
chore: initialize project scaffold - code quality →
chore: add code quality tooling - release workflow →
ci: add release workflow automation - CI pipeline →
ci: add project CI pipeline - GitHub repository setup →
chore: add GitHub repository configuration - dependency management →
chore: add Dependabot dependency management - security scanning →
ci: add security scanning workflows
7.3 Verify Before Declaring Success
Check only the sections that were selected:
- Foundation
README.md,LICENSE,.gitignore,.editorconfig, andCONTRIBUTING.mdexistAGENTS.mdexists andCLAUDE.mdis a symlink- Code quality
- the config files exist
- husky or pre-commit is installed when expected
- Release workflow
release.ymlexistsscripts/extract-release-notes.shis executableCHANGELOG.mduses linked headers- CI
.github/workflows/ci.ymlmatches the real install, lint, test, and build commands- GitHub repo setup
- the standard label catalog exists and matches every shipped label reference
- PR template and issue forms exist
.github/CODEOWNERS.exampleexists by default, or.github/CODEOWNERSexists only when the user explicitly requested active automatic review assignment- branch protection was applied or the failure reason is clearly reported
- Dependency management
.github/dependabot.ymlexistsdependabot-auto-merge.ymlexists- Security
codeql.ymlexists when the language is supporteddependency-review.ymlexists- secret scanning follow-up steps are explicit if automation could not enable it
7.4 Final Summary
End with a concrete summary that lists only the sections that actually ran, for example:
Setup complete for [project name] ([type] project)
Configured
[x] scaffold and repo baseline
[x] release workflow
[x] CI pipeline
[x] code quality
[x] GitHub repository setup
[x] dependency management
[x] security scanning
Manual follow-up
[ ] add required status checks to branch protection after the first CI run
[ ] enable secret scanning manually if GitHub plan limits blocked automationOnly list the relevant sections, failures, and follow-up items.
interface:
display_name: "New Project"
short_description: "Bootstrap or upgrade a repository with foundation files, quality tooling, release automation, CI, GitHub setup, dependency management, and security scanning"
icon_small: "./assets/favicon.png"
icon_large: "./assets/favicon.png"
brand_color: "#3B82F6"
policy:
allow_implicit_invocation: true
Design Decisions & Alternatives
---
Why concurrency groups to cancel stale runs?
Every push to a branch triggers a new CI run. Without concurrency groups, pushing twice in quick succession runs both. The second run is always the relevant one; the first wastes CI minutes.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: trueThis cancels the older run for the same workflow + branch combination. On PRs this means only the latest push is tested. On main it still allows concurrent runs across different branches.
---
Why one CI file per language instead of composable fragments?
Composable reusable workflows (.github/workflows/reusable-setup.yml) are powerful but add indirection. For small-to-medium projects, one self-contained ci.yml is easier to read, debug, and modify.
The templates are designed to be modified in place. Copy, customize, and own it.
---
Why fail-fast: false in the matrix strategy?
With fail-fast: true (the default), if Node 18 fails, GitHub cancels the Node 20 and 22 jobs. This can hide failures that are version-specific.
With fail-fast: false, all matrix jobs run to completion, giving a full picture of which versions are affected.
---
Why cache 'pip' using setup-python's built-in cache?
actions/setup-python@v5 has built-in pip caching (cache: 'pip') that automatically caches based on requirements files. It avoids needing a separate actions/cache step for most Python projects.
---
Why go-version-file: go.mod for Go projects?
Specifying go-version-file: go.mod pins CI to the exact same Go version declared in the module file — no drift between local and CI environments.
---
Why -race flag in Go tests?
The -race flag enables Go's built-in race condition detector. It adds ~20% overhead but catches concurrency bugs that are otherwise non-deterministic and difficult to reproduce. In CI where correctness matters over speed, it's always worth enabling.
---
Why cargo clippy -- -D warnings?
-D warnings treats all clippy warnings as errors, ensuring the CI fails on any lint issue. Without this, clippy warnings appear in logs but the job passes — they accumulate and never get fixed.
---
Why check format before running tests?
Format checks (eslint, ruff format --check, cargo fmt --check) are fast and fail loudly. Placing them before tests means formatting issues surface immediately without waiting for the full test suite.
---
Why include a build step?
A build step catches compilation errors, broken imports, and bundling failures that tests alone won't detect (especially in TypeScript/JSX projects where test files might use loose types). For Go and Rust, go build ./... and cargo build confirm the entire workspace compiles cleanly.
# .github/workflows/ci.yml
# Generic CI pipeline — customize the steps for your project
# Replace the placeholder steps with your actual commands
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# Cancel in-progress runs on the same branch when a new push arrives
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
name: CI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Add your language setup step here, for example:
# - uses: actions/setup-java@v4
# with:
# java-version: '21'
# distribution: 'temurin'
# Add your dependency install step here, for example:
# - name: Install dependencies
# run: make install
# Add your lint step here, for example:
# - name: Lint
# run: make lint
# Add your test step here, for example:
# - name: Test
# run: make test
# Add your build step here, for example:
# - name: Build
# run: make build
# .github/workflows/ci.yml
# Go CI pipeline
# Features: Go module caching, vet, test with race detection, build
# Customize: go-version, test flags, build target
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# Cancel in-progress runs on the same branch when a new push arrives
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
name: CI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod # Uses the version declared in go.mod
cache: true # Caches Go module downloads
- name: Download modules
run: go mod download
- name: Verify dependencies
run: go mod verify
- name: Vet
run: go vet ./...
- name: Test
run: go test -race -coverprofile=coverage.out ./...
- name: Build
run: go build ./...
# For a specific binary: go build -o bin/myapp ./cmd/myapp
- name: Upload coverage
uses: actions/upload-artifact@v4
if: matrix.go-version == 'stable' # Only upload once
with:
name: coverage
path: coverage.out
# .github/workflows/ci.yml
# Node.js / Web CI pipeline
# Features: matrix testing, npm/pnpm/yarn caching, lint, test, build
# Customize: node-version matrix, package manager, test/lint/build commands
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# Cancel in-progress runs on the same branch when a new push arrives
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
name: CI (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Adjust versions as needed. For web apps, a single LTS version is usually fine.
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm' # Change to 'yarn' or 'pnpm' if needed
- name: Install dependencies
run: npm ci # Change to: yarn install --frozen-lockfile | pnpm install --frozen-lockfile
- name: Lint
run: npm run lint # Change or remove if no lint script
- name: Test
run: npm test # Change to: npm run test | vitest run | etc.
- name: Build
run: npm run build # Remove if no build step (e.g., pure Node.js libraries)
# .github/workflows/ci.yml
# Python CI pipeline
# Features: matrix testing, pip caching, ruff lint, pytest
# Customize: python-version matrix, install command, test command, lint tool
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# Cancel in-progress runs on the same branch when a new push arrives
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
name: CI (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
# Alternative: pip install -r requirements-dev.txt
- name: Lint (ruff)
run: ruff check .
# Alternative: flake8 . | pylint src/
- name: Format check (ruff)
run: ruff format --check .
- name: Type check (mypy)
run: mypy src/
# Remove if not using mypy
- name: Test
run: pytest --tb=short
# Alternative: python -m pytest -v
# .github/workflows/ci.yml
# Rust CI pipeline
# Features: Cargo caching, clippy lint, fmt check, test
# Customize: toolchain, features, test flags
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# Cancel in-progress runs on the same branch when a new push arrives
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
name: CI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable # Change to 'nightly' if needed
components: rustfmt, clippy
- name: Cache Cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Check format
run: cargo fmt --all -- --check
- name: Clippy (lint)
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Test
run: cargo test --all-features
- name: Build (release)
run: cargo build --release
# Remove if just checking compilation: cargo check --all-features
# .github/dependabot.yml — Generic project (GitHub Actions only)
# Add your package ecosystem below.
# Docs: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates
version: 2
updates:
# GitHub Actions — always include, regardless of project type
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "UTC"
groups:
github-actions:
patterns:
- "*"
labels:
- "dependencies"
- "ci"
commit-message:
prefix: "ci(deps)"
# Add your package ecosystem below:
# - package-ecosystem: "docker"
# directory: "/"
# schedule:
# interval: "weekly"
# .github/dependabot.yml — Go project
# Docs: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates
version: 2
updates:
# Go modules
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "UTC"
groups:
dependencies:
update-types:
- "minor"
- "patch"
open-pull-requests-limit: 10
labels:
- "dependencies"
commit-message:
prefix: "chore(deps)"
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "UTC"
groups:
github-actions:
patterns:
- "*"
labels:
- "dependencies"
- "ci"
commit-message:
prefix: "ci(deps)"
# .github/dependabot.yml — Node.js / Web project
# Docs: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates
version: 2
updates:
# npm dependencies
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "UTC"
# Group all non-major updates into a single PR to reduce noise
groups:
dependencies:
update-types:
- "minor"
- "patch"
# Open separate PRs for major updates (breaking changes need manual review)
open-pull-requests-limit: 10
labels:
- "dependencies"
commit-message:
prefix: "chore(deps)"
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "UTC"
groups:
github-actions:
patterns:
- "*"
labels:
- "dependencies"
- "ci"
commit-message:
prefix: "ci(deps)"
# .github/dependabot.yml — Python project
# Docs: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates
version: 2
updates:
# pip dependencies
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "UTC"
groups:
dependencies:
update-types:
- "minor"
- "patch"
open-pull-requests-limit: 10
labels:
- "dependencies"
commit-message:
prefix: "chore(deps)"
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "UTC"
groups:
github-actions:
patterns:
- "*"
labels:
- "dependencies"
- "ci"
commit-message:
prefix: "ci(deps)"
# .github/dependabot.yml — Rust project
# Docs: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates
version: 2
updates:
# Cargo dependencies
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "UTC"
groups:
dependencies:
update-types:
- "minor"
- "patch"
open-pull-requests-limit: 10
labels:
- "dependencies"
commit-message:
prefix: "chore(deps)"
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "UTC"
groups:
github-actions:
patterns:
- "*"
labels:
- "dependencies"
- "ci"
commit-message:
prefix: "ci(deps)"
Design Decisions & Alternatives
---
Why Dependabot over Renovate?
Dependabot is built into GitHub — no app installation, no third-party service, no additional secrets or access grants required. It works on day one with zero configuration for public repos, and the configuration format is stable and well-documented.
Renovate is more powerful: it supports more ecosystems, has richer grouping options, and can self-host. But it requires either a GitHub App installation or running a self-hosted bot, which adds setup friction and external dependencies.
Choose Renovate if: you have many ecosystems to manage, need monorepo support, want to automerge based on test results (Renovate has better merge confidence integration), or need ecosystems Dependabot doesn't support.
---
Why group minor and patch updates together?
Without grouping, Dependabot opens one PR per package per update. A project with 50 npm dependencies might get 30 PRs in a single week. Grouped updates consolidate all minor/patch bumps into one or two PRs, which is manageable.
Major updates are left ungrouped because each one needs individual attention — they may have breaking changes that affect different parts of the codebase.
---
Why auto-merge minor updates, not just patch?
Semantic versioning guarantees that minor updates are backwards-compatible. A library bumping from 2.3.0 to 2.4.0 added features but did not break existing interfaces. If your CI is passing, auto-merging minor updates is safe.
The risk is that libraries don't always follow semver perfectly. Mitigating this:
- Required status checks must pass before auto-merge
- Human review remains opt-in for teams that want stricter merge policy
- Auto-merge only runs
--merge(not--squashor--rebase), preserving history
---
Why always include the github-actions ecosystem?
Outdated GitHub Actions (e.g., actions/checkout@v2) introduce security risks: known vulnerabilities in old versions, deprecated features, and action runner compatibility issues. Keeping Actions updated is low-risk (they typically only change behavior between major versions) and high-value for security.
---
Why commit-message.prefix: "chore(deps)"?
Conventional commit prefixes on Dependabot commits ensure that:
1. Release-please correctly classifies them as non-release-triggering (chore) 2. The commits appear in the changelog if desired (under a ### Refactoring section) 3. Commit history is consistent with the rest of the project
# .github/workflows/dependabot-auto-merge.yml
# Enables auto-merge for Dependabot patch and minor PRs.
# The default newproject branch protection does not require human review.
# If a repository later opts into review requirements, bot approval is best-effort
# and may not satisfy repository policy on its own.
# Major updates are left for manual review.
#
# Requires: Settings → General → Pull Requests → Allow auto-merge (enabled)
name: Dependabot Auto-merge
on:
pull_request:
permissions:
contents: write
pull-requests: write
jobs:
auto-merge:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- name: Get Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# Best-effort approval for repositories that later enable review requirements.
- name: Best-effort approve patch and minor updates
if: |
steps.metadata.outputs.update-type == 'version-update:semver-patch' ||
steps.metadata.outputs.update-type == 'version-update:semver-minor'
continue-on-error: true
run: gh pr review --approve "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Auto-merge patch updates after required status checks pass.
- name: Auto-merge patch updates
if: steps.metadata.outputs.update-type == 'version-update:semver-patch'
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Auto-merge minor updates after required status checks pass.
- name: Auto-merge minor updates
if: steps.metadata.outputs.update-type == 'version-update:semver-minor'
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Major updates: add label for visibility but do not auto-merge
- name: Label major updates for manual review
if: steps.metadata.outputs.update-type == 'version-update:semver-major'
run: gh pr edit "$PR_URL" --add-label "major-update"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# EditorConfig — https://editorconfig.org
# Ensures consistent coding style across editors and IDEs.
# Works with VS Code, JetBrains IDEs, Vim, Emacs, and most modern editors.
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
# Markdown: trailing spaces are significant (line breaks)
trim_trailing_whitespace = false
max_line_length = off
[*.py]
indent_size = 4
[*.go]
# Go uses tabs — gofmt enforces this
indent_style = tab
indent_size = 4
[*.rs]
indent_size = 4
[Makefile]
# Make requires tabs
indent_style = tab
[*.{yml,yaml}]
indent_size = 2
[*.{json,jsonc}]
indent_size = 2
[*.toml]
indent_size = 4
[*.sh]
indent_size = 2
[*.{sql}]
indent_size = 2
# Generic project
# Build output
build/
dist/
out/
bin/
# Environment / secrets
.env
.env.local
.env.*
*.env
secrets/
credentials/
*.pem
*.key
*.cert
*.local.*
# Dependency directories
vendor/
packages/
# Logs
logs/
*.log
npm-debug.log*
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Testing
coverage/
.coverage
*.lcov
# IDE / Editor
.idea/
.vscode/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*~
# OS generated
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary files
tmp/
temp/
*.tmp
*.bak
*.swp
# Go
# Binaries
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with go test -c
*.test
# Output of the go coverage tool
*.out
coverage.txt
coverage.html
# Build output
bin/
dist/
# Go workspace file (only if not using workspaces intentionally)
# go.work
# Module download cache (managed by Go toolchain)
# vendor/ is intentionally NOT ignored — some teams commit it
# Environment
.env
.env.local
# IDE / OS
.DS_Store
Thumbs.db
.idea/
.vscode/
# Node.js / Web
# Note: package-lock.json / yarn.lock / pnpm-lock.yaml / bun.lockb are intentionally
# NOT listed here. Always commit your lockfile. npm ci requires package-lock.json.
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.pnpm-store/
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
# Build output
dist/
build/
out/
.next/
.nuxt/
.svelte-kit/
.astro/
# Environment
.env
.env.local
.env.*.local
.env.development.local
.env.test.local
.env.production.local
# Cache
.cache/
.parcel-cache/
.eslintcache
.stylelintcache
.turbo/
# Testing
coverage/
.nyc_output/
# Runtime
*.pid
*.seed
*.pid.lock
pids/
logs/
*.log
# IDE / OS
.DS_Store
Thumbs.db
.idea/
.vscode/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
*.egg
*.egg-info/
dist/
build/
eggs/
parts/
var/
sdist/
develop-eggs/
.installed.cfg
lib/
lib64/
MANIFEST
# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
.conda/
# Distribution / packaging
.Python
pip-log.txt
pip-delete-this-directory.txt
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
pytestdebug.log
# Type checking
.mypy_cache/
.dmypy.json
dmypy.json
.pytype/
# Jupyter
.ipynb_checkpoints/
profile_default/
ipython_config.py
# pyenv
.python-version
# Environment
.env
.env.local
*.env
# IDE / OS
.DS_Store
Thumbs.db
.idea/
.vscode/
# Rust
# Generated by Cargo
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Cargo lock — commit for binaries, do NOT commit for libraries
# Cargo.lock
# Environment
.env
.env.local
# IDE / OS
.DS_Store
Thumbs.db
.idea/
.vscode/
Design Decisions & Alternatives
---
Why .editorconfig instead of per-tool formatting config?
.editorconfig is the only formatting config that works universally across editors without plugins (VS Code, JetBrains, Vim, Emacs all support it natively). It handles the baseline: indentation, line endings, and trailing whitespace.
Per-tool configs (Prettier, gofmt, rustfmt) layer on top of .editorconfig for language-specific concerns. The foundation step adds .editorconfig; the quality step adds the language-specific tools.
---
Why MIT as the default license?
MIT is the most permissive common open-source license: short, readable, and widely understood. It imposes minimal obligations on users.
For projects that need copyleft protections, GPL-3.0 is the alternative. For corporate contributors who want patent protection, Apache-2.0 is preferred.
The skill defaults to MIT and asks — rather than assuming — for other choices.
---
Why create these specific directories?
src/, tests/, docs/, scripts/ are the conventional minimum that signals a well-organized project to contributors. They're not enforced by any tool — they're a social convention that makes navigation predictable.
The skill only creates empty directories for languages where the convention is strong. Go, for example, gets cmd/, internal/, and pkg/ because those are near-universal in the Go ecosystem.
---
Why check for existing files before creating?
Overwriting an existing README or .gitignore would destroy user work. The skill always inventories first and only creates what's missing. For .gitignore specifically, it may append missing sections rather than replacing the file.
---
Why a minimal CONTRIBUTING.md template?
A minimal template sets the right expectations without being prescriptive. Teams with complex contribution workflows will customize it anyway — a 200-line CONTRIBUTING.md template would just create noise to delete.
The template intentionally points to conventional commits and a simple PR flow. It assumes the project will use the release setup in this package for versioning.
---
Why initialize with main as the default branch?
main has been the GitHub default since 2020. Starting with main avoids a rename operation later and aligns with current GitHub conventions.
If the project already exists and uses master, skip git init entirely — renaming an active branch is out of scope for this skill.
# Contributing to {{PROJECT_NAME}}
Thank you for your interest in contributing!
## Getting Started
1. Fork the repository
2. Create a feature branch: `git checkout -b feat/your-feature`
3. Make your changes
4. Run the tests
5. Commit using [conventional commits](https://www.conventionalcommits.org/): `git commit -m "feat: add your feature"`
6. Push and open a pull request
## Commit Convention
This project uses [Conventional Commits](https://www.conventionalcommits.org/):
| Type | When to use |
|------|-------------|
| `feat` | New feature |
| `fix` | Bug fix |
| `docs` | Documentation only |
| `refactor` | Code change, no feature/fix |
| `test` | Adding or updating tests |
| `chore` | Maintenance, dependencies |
## Pull Request Guidelines
- Keep PRs focused on a single concern
- Include tests for new behavior
- Update documentation if needed
- Ensure CI passes before requesting review
## Reporting Issues
Use the GitHub issue tracker. For bugs, include:
- Steps to reproduce
- Expected vs actual behavior
- Environment details (OS, version)
MIT License
Copyright (c) {{YEAR}} {{AUTHOR}}
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# {{PROJECT_NAME}}
{{DESCRIPTION}}
## Prerequisites
<!-- List what the user needs installed before they can use this project -->
## Installation
<!-- {{PROJECT_TYPE}} installation instructions -->
## Usage
<!-- How to run, basic examples -->
## Development
```bash
# Clone the repo
git clone <repo-url>
cd {{PROJECT_NAME}}
# Install dependencies and run
```
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
## License
[MIT](LICENSE)
Design Decisions & Alternatives
---
Why YAML issue forms over markdown templates?
GitHub issue forms (YAML) create structured forms with dropdowns, text areas, and validation. Markdown templates are plain text that users can ignore entirely.
YAML forms:
- Enforce required fields
- Provide dropdowns for structured data (priority, category)
- Automatically apply labels
- Auto-populate the PR title prefix (
title: "bug: ")
The main drawback is that YAML forms are slightly more complex to author. The templates in this skill make that a one-time cost.
---
Why bootstrap labels up front?
The vendored issue templates and Dependabot config already reference labels. Creating the label catalog first avoids a half-configured repository where forms and bot workflows refer to labels that do not exist yet.
The bootstrap step is idempotent:
- missing labels are created
- existing labels are updated to the standard colors and descriptions
- repeated runs converge on the same baseline instead of failing
---
Why gh api for branch protection instead of documenting the UI?
The GitHub UI for branch protection changes frequently and varies by plan tier. gh api is:
- Reproducible and scriptable
- Works the same across all plan tiers
- Can be committed as a script and re-run after repo recreation
- Faster than clicking through nested settings pages
---
Why leave required_status_checks.contexts empty initially?
Status check names are only known after a workflow has run at least once. GitHub requires exact check names (e.g., CI (Node 20)) that come from the name field of the GitHub Actions job. Running branch protection setup before the first CI run would require hardcoding names that may not match.
The recommended workflow: apply protection without status checks → push to main → check Actions tab for job names → update protection with those exact names.
---
Why leave human review opt-in by default?
The default target for newproject is a solo-maintained or lightly maintained repository. Requiring a human review by default blocks common automation flows, especially Dependabot patch and minor PR auto-merge.
The safer default is:
- require status checks
- block force pushes and deletions
- keep mandatory human review optional
Teams that want review gates can opt in later without changing the rest of the setup.
---
Why only dismiss stale reviews when review requirements are enabled?
dismiss_stale_reviews matters only when the repository requires approvals. When no approval is required, enabling review-specific settings adds noise without changing merge policy.
If a team later enables required reviews, stale review dismissal is still the right companion setting because it prevents the "approve now, change later" pattern after new commits are pushed.
---
Why one PR template at .github/pull_request_template.md?
GitHub supports multiple PR templates but requires users to select them manually. A single template that covers all PR types is simpler and has higher adoption. Teams that genuinely need different templates for different PR types (e.g., release vs. feature) can add multiple templates after the default is established.
---
Why CODEOWNERS in .github/ instead of the root?
GitHub checks three locations for CODEOWNERS in this priority order: .github/, root, and docs/. .github/ is the most explicit and conventional location for all GitHub-specific configuration files.
---
Why ship CODEOWNERS.example instead of active CODEOWNERS by default?
An active CODEOWNERS file immediately changes repository behavior by requesting reviewers on every matching pull request. That is useful for teams with clear ownership boundaries, but it is too opinionated as a default for solo repos.
Shipping CODEOWNERS.example preserves the template and the documentation while keeping automatic reviewer assignment opt-in.
#!/bin/bash
# bootstrap-labels.sh
#
# Creates or updates the standard GitHub label catalog used by the vendored
# issue templates and Dependabot workflow.
#
# Requires: gh CLI installed and authenticated.
set -euo pipefail
declare -a LABEL_SPECS=(
"bug|d73a4a|Something is not working"
"enhancement|a2eeef|New feature or improvement request"
"needs-triage|fef2c0|Needs initial triage and routing"
"dependencies|0366d6|Dependency updates and maintenance"
"ci|1d76db|Continuous integration and automation"
"major-update|b60205|Major dependency update requiring manual review"
"documentation|0075ca|Documentation improvements or fixes"
"security|b60205|Security fixes, reviews, or follow-up work"
"release|5319e7|Release planning, packaging, or publication work"
)
for spec in "${LABEL_SPECS[@]}"; do
IFS="|" read -r name color description <<< "${spec}"
if gh label edit "${name}" \
--color "${color}" \
--description "${description}" >/dev/null 2>&1; then
echo "Updated label: ${name}"
continue
fi
gh label create "${name}" \
--color "${color}" \
--description "${description}" >/dev/null
echo "Created label: ${name}"
done
echo "Standard label catalog is configured."
#!/bin/bash
# configure-branch-protection.sh
#
# Applies default branch protection rules to the main branch via GitHub CLI.
# Human review is opt-in; the default policy relies on required status checks.
# Requires: gh CLI installed and authenticated.
#
# Usage: ./configure-branch-protection.sh [branch-name]
# Default branch: main
set -euo pipefail
BRANCH="${1:-main}"
REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
echo "Configuring branch protection for '${BRANCH}' on ${REPO}..."
gh api \
--method PUT \
"repos/${REPO}/branches/${BRANCH}/protection" \
--field 'required_status_checks={"strict":true,"contexts":[]}' \
--field 'enforce_admins=false' \
--field 'required_pull_request_reviews=null' \
--field 'restrictions=null' \
--field 'allow_force_pushes=false' \
--field 'allow_deletions=false' \
--field 'block_creations=false'
echo "✅ Branch protection applied to '${BRANCH}'"
echo ""
echo "Next steps:"
echo " 1. After your CI workflow runs once, add status check names:"
echo " gh api repos/${REPO}/branches/${BRANCH}/protection --jq .required_status_checks"
echo " 2. If your team wants mandatory human review, add required reviews separately"
echo " 3. Re-run this script or update via GitHub UI: Settings → Branches"
name: Bug Report
description: Report a bug or unexpected behavior
title: "bug: "
labels: ["bug", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report a bug. Please fill in as much detail as possible.
- type: textarea
id: description
attributes:
label: What happened?
description: A clear description of the bug.
placeholder: Describe the unexpected behavior...
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: How can we reproduce this issue?
placeholder: |
1. Go to...
2. Click on...
3. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What did you expect to happen?
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment
description: OS, version, browser (if applicable)
placeholder: |
- OS: macOS 15.0
- Version: 1.2.3
- Browser: Chrome 132 (if applicable)
validations:
required: false
- type: textarea
id: logs
attributes:
label: Relevant logs or screenshots
description: Paste error messages, stack traces, or attach screenshots.
render: shell
validations:
required: false
# CODEOWNERS
# Opt-in template for repositories that explicitly want automatic review requests.
# The default newproject setup writes this as `.github/CODEOWNERS.example`.
# Rename it to `.github/CODEOWNERS` only when you want GitHub to request reviewers.
# Format: pattern @owner-or-team
# Docs: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Default owner for everything
* @OWNER
# CI and infrastructure
.github/ @OWNER
# Add more specific rules below:
# src/frontend/ @org/frontend-team
# src/backend/ @org/backend-team
# *.tf @org/infra-team
name: Feature Request
description: Suggest a new feature or improvement
title: "feat: "
labels: ["enhancement", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Thanks for suggesting a feature. Please describe the problem you're trying to solve.
- type: textarea
id: problem
attributes:
label: What problem does this solve?
description: Describe the pain point or use case.
placeholder: I'm frustrated when... / I need to be able to...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed solution
description: How would you like this to work?
placeholder: Describe the solution you'd like...
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Any workarounds or alternative approaches you've tried?
validations:
required: false
- type: dropdown
id: priority
attributes:
label: Priority
description: How important is this to you?
options:
- Nice to have
- Important
- Critical / blocking
validations:
required: true
Summary
<!-- What does this PR do? 1–3 bullet points. -->
- -
Type of change
- [ ] Bug fix
- [ ] New feature
- [ ] Refactor / cleanup
- [ ] Documentation
- [ ] CI / configuration
Testing
<!-- How did you test this? What should reviewers try? -->
- [ ] Existing tests pass
- [ ] New tests added (if behavior changed)
- [ ] Tested manually — describe how:
Checklist
- [ ] Code follows the project's style (linter passes)
- [ ] Self-reviewed the diff
- [ ] No console.log / debug statements left in
- [ ] Documentation updated if needed
- [ ] Breaking changes documented (if any)
{
"$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint/main/schema/markdownlint-config-schema.json",
"default": true,
"MD013": false,
"MD033": false,
"MD041": false,
"MD007": { "indent": 2 },
"MD024": { "siblings_only": true },
"MD029": { "style": "ordered" }
}
// eslint.config.js — ESLint flat config (ESLint v9+)
// Supports: JavaScript, TypeScript, JSX/TSX
// Remove the TypeScript section if your project is JavaScript-only.
import js from '@eslint/js'
import prettierConfig from 'eslint-config-prettier'
// Uncomment for TypeScript projects:
// import tseslint from 'typescript-eslint'
export default [
// Base JS rules
js.configs.recommended,
// TypeScript (remove if not using TS):
// ...tseslint.configs.recommended,
// Disable rules that conflict with Prettier (must be last)
prettierConfig,
{
// Apply to all JS/TS files
files: ['**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts}'],
rules: {
// Errors
'no-console': 'warn',
'no-debugger': 'error',
// Best practices
'eqeqeq': ['error', 'always'],
'no-var': 'error',
'prefer-const': 'error',
'prefer-template': 'warn',
},
},
{
// Relax rules for config files and scripts
files: ['*.config.{js,mjs,cjs,ts}', 'scripts/**/*.{js,ts}'],
rules: {
'no-console': 'off',
},
},
{
// Global ignores
ignores: [
'dist/',
'build/',
'out/',
'.next/',
'.nuxt/',
'node_modules/',
'coverage/',
],
},
]
// prettier.config.js
// Prettier formatting defaults — opinionated but widely accepted.
// All options here are deliberate; see inline comments.
/** @type {import("prettier").Config} */
const config = {
// Indentation
tabWidth: 2,
useTabs: false,
// Line length — 100 is a reasonable balance between readability and wrapping
printWidth: 100,
// Strings — single quotes are common in JS/TS ecosystems
singleQuote: true,
jsxSingleQuote: false,
// Semicolons — explicit semicolons prevent ASI edge cases
semi: false,
// Trailing commas in multi-line expressions (ES5-compatible)
trailingComma: 'es5',
// Brackets
bracketSpacing: true,
bracketSameLine: false,
// Arrow functions — always include parens for consistency
arrowParens: 'always',
// End of line — LF for cross-platform consistency (matches .editorconfig)
endOfLine: 'lf',
}
export default config
# ruff.toml — Ruff linter + formatter configuration
# Ruff replaces flake8, isort, black, and many plugins in one fast tool.
# Target Python version — adjust to match your pyproject.toml
target-version = "py311"
# Line length — matches black's default
line-length = 88
# Files to exclude
exclude = [
".git",
".venv",
"venv",
"__pycache__",
"*.egg-info",
"dist",
"build",
"migrations", # Django/Alembic migrations are auto-generated
]
[lint]
# Enable rules — E/W: pycodestyle, F: pyflakes, I: isort, UP: pyupgrade
# B: flake8-bugbear, S: flake8-bandit (security), ANN: type annotations
select = ["E", "W", "F", "I", "UP", "B", "S"]
# Ignore specific rules
ignore = [
"S101", # Use of assert — fine in tests
"S603", # subprocess without shell=True check — too noisy
"ANN101", # Missing self type annotation — unnecessary
"ANN102", # Missing cls type annotation — unnecessary
]
# Allow auto-fix for safe rules
fixable = ["I", "UP", "F401"] # isort, pyupgrade, unused imports
# Per-file rule overrides
[lint.per-file-ignores]
"tests/**/*.py" = ["S", "ANN"] # Security and annotation rules too strict in tests
"scripts/**/*.py" = ["S603", "S605"]
[format]
# Use the same style as black
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "lf"
# .pre-commit-config.yaml
# Python pre-commit hook configuration
# Install: pre-commit install
# Run all: pre-commit run --all-files
# Update hooks: pre-commit autoupdate
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.0 # Update with: pre-commit autoupdate
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 # Update with: pre-commit autoupdate
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-toml
- id: check-merge-conflict
- id: check-added-large-files
args: [--maxkb=500]
- id: debug-statements
- id: mixed-line-ending
args: [--fix=lf]
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.43.0 # Update with: pre-commit autoupdate
hooks:
- id: markdownlint-fix
args: [--config, .markdownlint.json]
Design Decisions & Alternatives
---
Why Ruff over flake8 + black + isort?
Ruff is a single tool that replaces flake8 (linting), black (formatting), isort (import sorting), and 50+ flake8 plugins. It runs 10–100× faster than the individual tools, has a single config file (ruff.toml), and is actively maintained by Astral.
Choose flake8 + black if: you have existing configuration that migration would disrupt, or you need plugins that Ruff doesn't support yet.
---
Why ESLint flat config (v9 format)?
The legacy .eslintrc.* format is deprecated in ESLint v9+. The flat config (eslint.config.js) is the current format with better composition, clearer inheritance, and native ES module support.
All new projects should use the flat config. Only use .eslintrc.* for projects locked to ESLint v8.
---
Why Prettier with semi: false?
This is a stylistic choice. semi: false (no semicolons) is common in modern JS/TS projects and relies on automatic semicolon insertion (ASI). It's the prettier standard for many popular frameworks (Nuxt, Astro).
To use semicolons, change semi: true in prettier.config.js. Both work fine — the important thing is consistency, not which style.
---
Why lint-staged instead of running ESLint on the whole project?
Pre-commit hooks that run ESLint on the entire project slow down commits as the codebase grows. lint-staged only runs linters on git-staged files — the files being committed — which keeps the hook fast regardless of project size.
---
Why check if husky is already installed before running husky init?
The release setup in this package may have already installed husky for the commit-msg hook. Running husky init again would overwrite the existing .husky/pre-commit file. This package checks first and appends to the existing hook if present.
---
Why golangci-lint over go vet alone?
go vet catches only a subset of issues. golangci-lint runs multiple linters in parallel (gofmt, goimports, errcheck, staticcheck, etc.) and is the standard tool for Go code quality in production teams.
---
Why markdownlint in every project?
Markdown is ubiquitous — README, CHANGELOG, docs, PR templates. Inconsistent heading levels, broken link references, and trailing spaces accumulate over time. markdownlint catches these automatically with zero cognitive load on developers.
The config disables MD013 (line length) because long lines in markdown tables and code blocks are unavoidable and flagging them creates more noise than value.
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
// Enforce these types only
'type-enum': [
2,
'always',
[
'feat', // New feature (triggers minor bump)
'fix', // Bug fix (triggers patch bump)
'perf', // Performance improvement (triggers patch bump)
'refactor', // Code change, no feature/fix (no bump)
'docs', // Documentation only (no bump)
'test', // Adding/updating tests (no bump)
'chore', // Maintenance (no bump)
'ci', // CI/CD changes (no bump)
'build', // Build system changes (no bump)
'revert', // Revert a previous commit
],
],
// Subject line max length
'subject-max-length': [1, 'always', 100],
// Scope is optional but must be lowercase if provided
'scope-case': [2, 'always', 'lower-case'],
},
}
Changelog Editing Workflow
How to prepare a curated release commit and tag it, using AI to produce polished release notes from rough engineering inputs.
---
When to Do This
Do this when the team has decided a release is ready.
Normal merges to main do not publish releases and do not rewrite CHANGELOG.md. The release happens only after a human prepares the release commit.
One source of truth: edit CHANGELOG.md and let release.yml publish from it.
---
Step-by-Step
1. Gather the release inputs
Collect the commits, PRs, or notes that should be covered in this release.
You are preparing one new version section, for example:
## [1.2.0](https://github.com/OWNER/REPO/compare/1.1.0...1.2.0) (2026-03-18)2. Draft the section with AI
Use the changelog style guide as the instruction set and provide the rough release inputs.
The AI should produce only the section body, not the header.
3. Edit CHANGELOG.md
Add or update:
## [Unreleased](https://github.com/OWNER/REPO/compare/1.2.0...HEAD)
## [1.2.0](https://github.com/OWNER/REPO/compare/1.1.0...1.2.0) (2026-03-18)Then paste the rewritten release notes below the 1.2.0 header.
Rules:
- keep
Unreleasedempty - keep the version header linked
- default release tags should omit a leading
vunless the repository already has av-prefixed history - make sure the first bold line can serve as the GitHub Release title
4. Bump the project version
Update the project's real version source if it has one:
package.jsonpyproject.tomlCargo.toml- another established version file
If the project is tag-only, there may be no version file to edit.
5. Commit the release changes
Example:
git commit -m "chore(release): prepare 1.2.0"6. Tag that exact commit
Example:
GIT_EDITOR=true git tag -s -m "release 1.2.0" 1.2.0
git push origin main --follow-tagsWhy this form:
-screates a signed tag when the repository or user defaults require signing-msupplies the tag message explicitly so Git does not open an editorGIT_EDITOR=truekeeps the command non-blocking in CI, agents, and other non-interactive shells
If the repository does not sign tags, use:
git tag -a -m "release 1.2.0" 1.2.0
git push origin main --follow-tags7. What happens next
After the tag is pushed:
release.ymlruns- it validates the configured version metadata if applicable
- it extracts the
1.2.0section fromCHANGELOG.md - it creates or updates the GitHub Release
- it runs any optional publish step
---
Tips
Rotate changelog ownership — the person closest to the work usually writes the best user-facing summary.
Skip headlines when the release is small — if a release is mostly bug fixes, go straight to the list sections.
Do not invent details — if a metric or precise behavior is unknown, write conservatively.
The header matters — the extract script depends on the version header matching the tag version exactly.
Changelog Style Guide
Use this guide when writing curated plugin release notes in Linear-style prose.
Works as both a human reference and an AI system prompt.
---
Role
You are a technical writer helping a software team publish release notes. Your job is to turn a set of merged changes into a human-readable changelog entry that follows the style of Linear's public changelog (linear.app/changelog).
You will receive:
1. Release inputs — merged PRs, commits, diffs, or rough notes for one plugin release 2. The version number and date — already present in the release header
You will output:
1. A polished changelog entry — ready to add to CHANGELOG.md
---
The Core Principle: User Value, Not Developer Activity
Raw engineering notes describe what developers did. Your job is to describe what users get.
❌ Raw (developer-centric)
feat(auth): implement OAuth2 PKCE flow with state parameter validation (#228)
✅ Rewritten (user-centric)
**Sign in with Google and GitHub** — You can now log in without a password.
Existing accounts are automatically linked.Every sentence should answer: "Why does this matter to the person using the product?"
---
Output Structure
Part 1 — Headline Features (1–3 items)
Pick the 1 to 3 most significant changes. For each:
- Write a bold feature name as a short, punchy title (3–6 words)
- Follow with 1–3 sentences of prose explaining what changed and why it matters
- Use plain, direct language — no jargon, no passive voice
- Write from the user's perspective
What qualifies as a headline:
- New user-facing capabilities that change how someone works
- Significant performance or UX improvements a user would notice
- Major integrations or platform additions
What does NOT qualify:
- Internal refactors with no user-visible effect
- Normal bug fixes (unless critical/widespread)
- API-only changes with no UI change
- Dependency upgrades, CI/CD changes
Part 2 — Complete Changes List
After headlines, include ALL remaining changes in these sections (omit a section if empty):
### Improvements
### Bug Fixes
### API
### Breaking ChangesEach item:
- Starts with scope in bold if applicable:
**Auth** Fixed token refresh... - Is a single concise sentence
- Starts with capital letter, no trailing period
- Does NOT use commit syntax (
feat:,fix:)
---
Voice and Tone
| Do | Don't |
|---|---|
| Second person: "You can now..." | Third person: "Users can now..." |
| Present tense: "Filters now support..." | Past tense: "We added support for..." |
| Specific benefits | Vague: "Improved performance" |
| Concrete numbers: "2× faster" | Filler: "significantly faster" |
| Short, direct sentences | Multi-clause run-ons |
Tone: Confident, precise, human. Not marketing-speak, not dry technical docs.
---
GitHub Release Title Convention
The first bold line (**...**) immediately after the version header becomes the GitHub Release title. The release workflow automatically extracts it and formats the title as newproject-0.2.1 - Headline.
Rules
- Keep it 3–6 words, user-centric — like a product feature announcement
- It doubles as the opening of your changelog section, so write it as a bold feature name
- If no bold line is present, the release title falls back to the bare tag name
Examples
✅ Good titles (short, user-centric, punchy)
**Multi-Language CI Templates** → newproject-0.2.0 - Multi-Language CI Templates
**Smarter Project Initialization** → newproject-0.1.0 - Smarter Project Initialization
**Sign in with Google and GitHub** → xhs-publisher-1.0.0 - Sign in with Google and GitHub
❌ Bad titles
**This release adds support for multiple CI template languages across the board**
→ Too long; exceeds 6 words; restates the version
**refactor: migrate logger to structured logging**
→ Commit-style syntax; not user-centric
**Improvements and Bug Fixes**
→ Generic; gives no information about what changed---
Format Rules
Version Number Default
Unless the release owner explicitly chooses a different version, use SemVer to select the next release number.
Version Header — Always Use Linked Headers
## [1.2.0](https://github.com/OWNER/REPO/compare/PLUGIN_NAME-1.1.0...PLUGIN_NAME-1.2.0) (2026-03-15)When editing a plugin changelog:
- Always link the version header to the compare view from the previous tag to the new tag
- Never change
[1.2.0]tov1.2.0 - Always keep the release date on the header line
Unreleased Header — Maintain It During Development
The ## [Unreleased] section sits above all versioned entries and should collect ongoing user-facing notes during normal development. It should also be a linked header:
## [Unreleased](https://github.com/OWNER/REPO/compare/PLUGIN_NAME-1.2.0...HEAD)
## [1.2.0](https://github.com/OWNER/REPO/compare/PLUGIN_NAME-1.1.0...PLUGIN_NAME-1.2.0) (2026-03-15)Normal development rule:
- Every merge to
mainshould add a short user-facing note underUnreleased - Keep these notes concise and ready to be promoted into the next release section
- Skip purely internal changes unless they matter to plugin users
When preparing a release commit:
- Convert the accumulated
Unreleasednotes into the new version section - Update
Unreleasedto compare from the new tag toHEAD - Add the new linked version header comparing the previous tag to the new tag
- Reset the
Unreleasedsection body after promoting those notes into the new version section
The tag does not exist yet while you're editing the release commit. This is expected and normal.
Headline Format
## [1.2.0](https://github.com/OWNER/REPO/compare/PLUGIN_NAME-1.1.0...PLUGIN_NAME-1.2.0) (2026-03-15)
**Feature Name** — One or two sentences describing the user benefit.
Additional context or usage instruction if needed.
**Second Feature** — Description. Available on Enterprise plans.- Blank line between each headline feature
- Em dash (—) connects title to description, same line
- No
###heading for individual headline features
Changes List Format
### Improvements
- **Scope** Description of the improvement
- Description without a scope
### Bug Fixes
- **Scope** Fixed description of what was broken---
Decision Guide
Is it user-visible?
├── No → Omit (refactor, CI, chore, test, docs)
└── Yes → Does it change how users accomplish a task?
├── Yes, meaningfully → Headline candidate
└── Yes, incrementally → Improvements list
Is it a bug fix?
├── Critical (security, data loss, widespread) → Headline candidate
└── Normal → Bug Fixes list
Is it API-only?
└── API section
Is it a breaking change?
└── Breaking Changes section — always explicit, never buriedCommit Type Handling
| Type | Placement | Notes |
|---|---|---|
feat | Headline or Improvements | Evaluate impact |
fix | Bug Fixes | Headline only if critical |
perf | Improvements or Headline | Include metric if known |
refactor | Omit | Unless user-visible |
docs/chore/test/ci/build | Omit | Internal |
BREAKING CHANGE | Breaking Changes | Always include |
---
Rewriting Examples
feat → Headline
Input:
feat(filters): add advanced filter groups with AND/OR conditions (#301)
feat(filters): support AI natural language filter input (#302)Output:
**Advanced filters** — Refine any view with complex AND/OR conditions.
Combine filters like Priority, Label, and Customer status to define exactly
what you want to see. Or describe what you're looking for in plain language
using the new AI filter option.Bug fixes → List
Input:
fix(board): horizontal scroll position not restored on board view (#310)
fix(editor): slash command sub-menus not selectable with mouse (#312)
fix(search): search input cleared when switching result tabs (#313)Output:
### Bug Fixes
- **Board** Fixed horizontal scroll position not being restored when using row grouping
- **Editor** Fixed slash command sub-menus to be selectable with the mouse
- **Search** Fixed search input being cleared when switching between result type tabsFull example
Input (release inputs):
## [Unreleased](https://github.com/OWNER/REPO/compare/example-2.4.0...HEAD)
## [2.4.0](https://github.com/OWNER/REPO/compare/example-2.3.0...example-2.4.0) (2026-03-15)
### Features
* feat(auth): add OAuth2 PKCE flow (#228)
* feat(api): support batch processing with configurable chunk size (#234)
* feat(mobile): add customizable bottom navigation bar (#241)
### Bug Fixes
* fix(pool): memory leak after 8+ hours continuous operation (#250)
* fix(mobile): button overflow on screens under 375px (#247)
### Performance
* perf(dashboard): reduce initial load by ~40% through lazy panel loading (#245)
### Refactoring
* refactor(logger): migrate to structured logging library (#238)Output (rewritten):
## [Unreleased](https://github.com/OWNER/REPO/compare/example-2.4.0...HEAD)
## [2.4.0](https://github.com/OWNER/REPO/compare/example-2.3.0...example-2.4.0) (2026-03-15)
**Customizable mobile navigation** — Personalize the bottom toolbar to prioritize
the features you use most. Rearrange navigation items or pin specific projects
and documents for quick access.
**Batch API** — Process multiple items in a single request with configurable
chunk sizes. Useful for bulk operations that previously required multiple round-trips.
**Dashboard loads ~40% faster** — Panels now load progressively, so the most
relevant content appears immediately without waiting for the full page.
### Bug Fixes
- **Connection pool** Fixed a memory leak that caused slowdowns after 8+ hours
of continuous operation
- **Mobile** Fixed button overflow on small screens (under 375px wide)
### Improvements
- **Auth** Login now uses the OAuth2 PKCE flow, improving security for public clients(The logger refactor is omitted — no user-visible change.)
---
When Context Is Missing
1. Infer the user benefit from domain knowledge — feat(auth): add PKCE flow → benefit is "more secure login." Use it. 2. Never invent specifics — if you don't know a metric or exact behavior, describe what you know conservatively. 3. Mark uncertainty — add <!-- TODO: verify with PR author --> if needed.
---
Final Checklist
- [ ] Version number follows SemVer by default unless the release owner explicitly chose otherwise
- [ ]
Unreleasedis a linked header tocompare/<new-tag>...HEAD - [ ] During normal development, new user-facing changes are recorded under
Unreleased - [ ] Version header is a linked header to
compare/<previous-tag>...<new-tag> - [ ] 1–3 headline features present (or none if all minor fixes)
- [ ] Headlines written from the user's perspective
- [ ] No commit message syntax in output
- [ ] Internal-only changes omitted
- [ ] Breaking changes have explicit section
- [ ] All raw items accounted for (headline / list / intentionally omitted)
- [ ] No invented specifics
- [ ] Present tense throughout
- [ ] No reference-style compare links remain at the bottom of the file
Design Decisions & Alternatives
Why this workflow is designed the way it is, and what alternatives exist.
---
Why manual curated releases instead of release-please?
This workflow keeps release publication explicit:
- normal development continues on
main - no release PR is created
- no bot rewrites the changelog
- release happens only when a human prepares a curated changelog section and tags that commit
This preserves an intentional editing window without adding a second PR flow.
Choose release-please if you specifically want a bot-managed release PR and are comfortable with generated drafts and extra repository state.
---
Why not semantic-release?
semantic-release is strong when the goal is full automation:
- every qualifying merge to
maincan publish immediately - changelog content is generated from commit history
That is a poor fit when the team wants to decide when to release and rewrite the notes manually before publication.
Choose semantic-release if you want zero manual release coordination and are fine with every qualifying merge becoming a release candidate.
---
Why keep CHANGELOG.md as the source of truth?
Alternative: maintain GitHub Release notes separately from the changelog.
Problem: the two inevitably drift.
By extracting the GitHub Release body from CHANGELOG.md, the team only writes one release narrative and the published release always matches the repository history.
---
Why a local extract script instead of another GitHub Action?
A small repo-owned script is:
- easy to read
- easy to debug
- easy to adapt to the project's exact header format
It avoids depending on third-party release-note parsing behavior.
---
Why one release.yml?
A single release workflow is easier to explain and maintain.
It covers:
- GitHub Release publication
- optional package publishing
- optional binary uploads
Projects that need extra publish logic can append steps without changing the core release model.
---
Branch protection recommendations
For this workflow to function well:
- protect
main/master - require status checks such as tests and commitlint
- add PR reviews when the team wants mandatory human approval
- do not require special bot bypasses for release automation, because the release commit is human-authored
---
Why linked changelog headers?
Linked headers make the changelog directly navigable:
Unreleasedshows everything since the latest tag- each version header links to the exact compare view for that release
This removes the need for separate link definitions at the bottom of the file and keeps the release context visible at the point of reading.
#!/bin/bash
# scripts/extract-release-notes.sh
#
# Extracts a single version's section from CHANGELOG.md.
# Used by release.yml to publish GitHub Releases from manually curated changelog sections.
#
# Usage: ./scripts/extract-release-notes.sh 1.2.0
#
# Output: RELEASE_NOTES.md (in the current directory)
# Exit 1: if the version section is not found in CHANGELOG.md
set -euo pipefail
VERSION_TAG="${1:-${GITHUB_REF_NAME:-}}"
if [[ -z "$VERSION_TAG" ]]; then
echo "❌ No version tag provided." >&2
echo " Usage: $0 1.2.0" >&2
exit 1
fi
# Extract semver from tag: handles '1.2.0', 'myapp-1.2.0', and legacy 'v1.2.0'
VERSION=$(echo "$VERSION_TAG" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ -z "$VERSION" ]]; then
echo "❌ Could not extract semver from tag: $VERSION_TAG" >&2
exit 1
fi
CHANGELOG_FILE="${CHANGELOG_FILE:-CHANGELOG.md}"
OUTPUT_FILE="${OUTPUT_FILE:-RELEASE_NOTES.md}"
TITLE_FILE="${TITLE_FILE:-RELEASE_TITLE.txt}"
if [[ ! -f "$CHANGELOG_FILE" ]]; then
echo "❌ $CHANGELOG_FILE not found in $(pwd)" >&2
exit 1
fi
rm -f "$OUTPUT_FILE" "$TITLE_FILE"
python3 - "$CHANGELOG_FILE" "$VERSION" "$OUTPUT_FILE" "$TITLE_FILE" <<'PY'
import pathlib
import re
import sys
changelog_file, version, output_file, title_file = sys.argv[1:]
content = pathlib.Path(changelog_file).read_text(encoding="utf-8").splitlines()
header_pattern = re.compile(rf"^## \[{re.escape(version)}\]\(")
next_header_pattern = re.compile(r"^## \[")
capturing = False
section_lines = []
for line in content:
if not capturing:
if header_pattern.match(line):
capturing = True
continue
if next_header_pattern.match(line):
break
section_lines.append(line)
while section_lines and not section_lines[0].strip():
section_lines.pop(0)
while section_lines and not section_lines[-1].strip():
section_lines.pop()
if not section_lines:
sys.exit(0)
pathlib.Path(output_file).write_text("\n".join(section_lines) + "\n", encoding="utf-8")
title = ""
for line in section_lines:
if re.match(r"^\*\*[^*]", line):
title = re.sub(r"^\*\*", "", line)
title = re.sub(r"\*\*.*$", "", title)
break
pathlib.Path(title_file).write_text(title + ("\n" if title else ""), encoding="utf-8")
PY
# Fail loudly if nothing was extracted
if [[ ! -s "$OUTPUT_FILE" ]]; then
echo "❌ Version [${VERSION}] not found in ${CHANGELOG_FILE}." >&2
echo " Make sure the changelog has a section starting with:" >&2
echo " ## [${VERSION}](compare-url) (YYYY-MM-DD)" >&2
exit 1
fi
echo "✅ Extracted release notes for ${VERSION} → ${OUTPUT_FILE}"
# Extract the first bold line (**...**) as the release title
BOLD_LINE=$(grep -m 1 '^\*\*[^*]' "$OUTPUT_FILE" || true)
if [[ -n "$BOLD_LINE" ]]; then
# Strip leading/trailing ** markers
TITLE=$(echo "$BOLD_LINE" | sed 's/^\*\*//; s/\*\*.*//')
echo "$TITLE" > "$TITLE_FILE"
echo "✅ Extracted release title → ${TITLE_FILE}: ${TITLE}"
else
: > "$TITLE_FILE"
echo "ℹ️ No bold headline found; ${TITLE_FILE} left empty (release will use tag name)"
fi
# .github/workflows/commitlint-check.yml
# Optional: validates commit messages on PRs for non-Node projects
# that don't have local husky hooks.
name: Commitlint
on:
pull_request:
branches: [main]
jobs:
commitlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install commitlint
run: |
npm install --global \
@commitlint/cli \
@commitlint/config-conventional
- name: Validate PR commits
run: |
npx commitlint \
--from ${{ github.event.pull_request.base.sha }} \
--to ${{ github.event.pull_request.head.sha }} \
--verbose
# .github/workflows/release.yml
# Responsibility: Publish a GitHub Release from a manually curated changelog.
# Triggered only by pushing a release tag that points at a human-authored release commit.
name: Release
on:
push:
tags:
- '*.*.*'
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Parse tag
id: parse
env:
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
VERSION=$(echo "$REF_NAME" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+$')
if [[ -z "$VERSION" ]]; then
echo "Tag must end with a semantic version: $REF_NAME" >&2
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# Update this step to match the project's real version source.
- name: Validate package.json version
env:
EXPECTED_VERSION: ${{ steps.parse.outputs.version }}
run: |
set -euo pipefail
ACTUAL_VERSION=$(python3 -c 'import json; print(json.load(open("package.json", encoding="utf-8"))["version"])')
if [[ "$ACTUAL_VERSION" != "$EXPECTED_VERSION" ]]; then
echo "Tag version $EXPECTED_VERSION does not match package.json version $ACTUAL_VERSION" >&2
exit 1
fi
- name: Extract release notes from CHANGELOG.md
env:
REF_NAME: ${{ github.ref_name }}
CHANGELOG_FILE: CHANGELOG.md
run: |
chmod +x scripts/extract-release-notes.sh
./scripts/extract-release-notes.sh "$REF_NAME"
- name: Read release title
id: title
env:
REF_NAME: ${{ github.ref_name }}
run: |
if [[ -s RELEASE_TITLE.txt ]]; then
TITLE="${REF_NAME} - $(cat RELEASE_TITLE.txt)"
else
TITLE="${REF_NAME}"
fi
echo "name=$TITLE" >> "$GITHUB_OUTPUT"
- name: Create or update GitHub Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REF_NAME: ${{ github.ref_name }}
TITLE: ${{ steps.title.outputs.name }}
run: |
set -euo pipefail
if gh release view "$REF_NAME" >/dev/null 2>&1; then
gh release edit "$REF_NAME" \
--title "$TITLE" \
--notes-file RELEASE_NOTES.md
else
gh release create "$REF_NAME" \
--title "$TITLE" \
--notes-file RELEASE_NOTES.md
fi
Design Decisions & Alternatives
---
Why CodeQL over Semgrep, SonarQube, or Snyk?
CodeQL is GitHub's native static analysis tool, available for free on all public repositories and included in GitHub Advanced Security for private repos. It integrates directly with the Security tab, has no external service dependency, and is maintained by GitHub/Microsoft with rules for common CWEs.
Semgrep is excellent and has a generous free tier, but requires a separate account and service dependency. Better for custom rule authoring.
SonarQube / SonarCloud is comprehensive but adds significant setup complexity and has more aggressive gating that can frustrate teams new to security scanning.
Snyk focuses on dependency vulnerabilities (covered by dependency-review in this setup) rather than code-level issues.
Choose CodeQL if: you want the simplest security scanning setup with zero external dependencies. It's the right default for most projects.
---
Why run CodeQL on a weekly schedule?
A new CVE may be disclosed for a library that was safe when it was committed. A weekly run ensures the codebase is scanned against the latest vulnerability database even when there are no code changes.
The cron: '30 1 * * 1' schedule runs at 01:30 UTC Monday, offset from the top of the hour to reduce GitHub infrastructure load.
---
Why fail-on-severity: high in dependency-review?
Critical and high severity CVEs represent genuine risks (RCE, data exposure, authentication bypass). Blocking PRs on these is the right default.
Medium and low severity findings are informational but common enough that blocking on them creates alert fatigue. Start with high, then adjust as the team becomes familiar with the tool's output.
---
Why separate dependency-review from CodeQL?
They solve different problems:
- CodeQL: finds vulnerabilities in your code (logic bugs, injection, etc.)
- dependency-review: finds vulnerabilities in your dependencies (CVEs)
They run at different times:
- CodeQL: on push to main and weekly
- dependency-review: on PRs only (catches new dependencies before they merge)
Combining them would require one to run on a schedule that doesn't make sense for the other.
---
Why not include Trivy or similar container scanning?
Container scanning is only relevant for projects that build Docker images. Adding it to a base template would add noise for the majority of projects.
For projects that do use Docker, add Trivy as a separate step in the CI workflow or as a standalone workflow:
- name: Scan container image
uses: aquasecurity/trivy-action@master
with:
image-ref: 'my-image:latest'
severity: 'CRITICAL,HIGH'---
Why guide manual setup for secret scanning instead of automating it?
Secret scanning and push protection are repository settings, not workflow files. They can only be enabled via the GitHub UI or the REST API with admin credentials. Automating this would require the user to provide a personal access token with admin scope — a poor security/UX trade-off for a one-time setup step.
The manual path is: Settings → Security & analysis → Enable → done.
# .github/workflows/codeql.yml
# CodeQL static analysis — detects security vulnerabilities in source code.
# Supported languages: javascript-typescript, python, go, java-kotlin, csharp, cpp, ruby, swift
# Note: Rust is not yet supported by CodeQL.
name: CodeQL
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
# Run weekly even without pushes — catches newly-disclosed vulnerabilities in existing code
- cron: '30 1 * * 1' # Every Monday at 01:30 UTC
permissions:
actions: read
contents: read
security-events: write
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language:
- javascript-typescript
# Change to your project's language:
# - python
# - go
# - java-kotlin
# - csharp
# - cpp
# - ruby
# - swift
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# Optional: specify config file for custom queries
# config-file: .github/codeql/codeql-config.yml
# For compiled languages (Java, C/C++, C#), add your build command here:
# - name: Build
# run: make build
# For interpreted languages (JS, Python, Ruby), CodeQL builds automatically
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"
# .github/workflows/dependency-review.yml
# Blocks PRs that introduce dependencies with known security vulnerabilities.
# Works for all ecosystems: npm, pip, go, cargo, maven, nuget, etc.
# No language configuration needed — GitHub detects the ecosystem automatically.
name: Dependency Review
on:
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: write
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Dependency Review
uses: actions/dependency-review-action@v4
with:
# Fail on high and critical severity vulnerabilities
fail-on-severity: high
# Post a comment on the PR with vulnerability details
comment-summary-in-pr: always
# Allow specific advisories if needed (use GHSA IDs):
# allow-ghsas: GHSA-xxxx-xxxx-xxxx