diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..585ad76 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,79 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/automation/dependabot.yml +# @Date: 2026-05-26 00:00:00 -07:00 (1782460800) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/dependabot.yml +# +# Dependabot configuration tuned for the v4 staging-branch release flow. +# +# How it fits with v4: +# - All Dependabot PRs target `next` so they pool with every other +# contributor change and batch into the next release. +# - `dependabot-auto-merge.yml` (in this same folder) auto-merges +# patch/minor bumps into `next` after CI passes β€” zero-touch pooling. +# Delete that workflow if you'd rather review each bump by hand. +# - Security updates are detected by `hotfix-redirector.yml` +# (release-flow-v4/) by GHSA references in the PR body and auto-promoted +# from `next` β†’ `hotfixes` so they ship via the hotfix lane, not the +# next-batch release. No special routing needed in this config. +# +# Customize per repo: +# - Add or remove `package-ecosystem` blocks for your stack (gomod, pip, +# bundler, gradle, maven, cargo, docker, etc.). +# - Adjust `directory` if your manifests don't live at the repo root. +# - Tighten `open-pull-requests-limit` if Dependabot's noise is too much. +# - Add `allow` / `ignore` rules for specific packages. +# - Add `groups` to bundle related bumps into a single PR. + +version: 2 +updates: + # GitHub Actions: keep pinned action SHAs / version tags fresh. + - package-ecosystem: "github-actions" + directory: "/" + target-branch: "next" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + commit-message: + prefix: "deps" + # Grouped PRs cut noise: one PR per (security | patch | minor) bundle + # per week instead of N separate PRs. Security PRs still get retargeted + # to `hotfixes` by hotfix-redirector.yml when GHSA refs appear in the + # body β€” bundling N GHSA fixes into one PR is fine, the redirector + # only needs one match to retarget. + groups: + security: + applies-to: security-updates + patterns: ["*"] + patch: + applies-to: version-updates + update-types: ["patch"] + minor: + applies-to: version-updates + update-types: ["minor"] + + # NPM: package.json + package-lock.json updates. + # Delete this block if your repo isn't a Node project. + - package-ecosystem: "npm" + directory: "/" + target-branch: "next" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + commit-message: + prefix: "deps" + groups: + security: + applies-to: security-updates + patterns: ["*"] + patch: + applies-to: version-updates + update-types: ["patch"] + minor: + applies-to: version-updates + update-types: ["minor"] diff --git a/.github/workflows/branch-retention.yml b/.github/workflows/branch-retention.yml new file mode 100644 index 0000000..14df9e4 --- /dev/null +++ b/.github/workflows/branch-retention.yml @@ -0,0 +1,38 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/automation/branch-retention.yml +# @Date: 2026-05-20 00:00:00 -07:00 (1779606000) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/branch-retention.yml +# +# On PR merge: most branches deleted immediately; release/* keeps last 5, +# hotfix/* keeps last 3. master/main/badges/gh-pages never touched. +# +# v4 flow: feature PRs merge into `next` and hotfix PRs into `hotfixes` +# (not directly into master). next/hotfixes are in the branches: filter +# below so this workflow fires on those PR closures too β€” otherwise +# feat/* / fix/* / chore/* etc. would pile up on origin indefinitely. +# (Repos that haven't adopted v4 just won't see those branches; the +# extra entries in the filter are harmless.) +name: 🌿 Branch Retention + +on: + pull_request: + types: [closed] + branches: [master, main, next, hotfixes] + +permissions: + contents: write + pull-requests: read + +jobs: + retain: + if: github.event.pull_request.merged == true + uses: CLDMV/.github/.github/workflows/reusable-branch-retention.yml@v4 + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..06e3e5d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,302 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/core-cicd/ci.yml +# @Date: 2026-05-20 00:00:00 -07:00 (1779606000) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/ci.yml +name: πŸ§ͺ CI Tests & Build + +on: + # Note: do NOT add `paths:` / `paths-ignore:` at the trigger level. Doing + # that makes GitHub skip the workflow entirely for docs-only changes, which + # means `Required PR Check` never posts and the ruleset blocks the merge. + # The reusable workflow's `paths-gate` job does the same job from inside, + # and exposes a `docs_only` output so this workflow can still green-light + # the required check for docs-only PRs (see `required-check` below). The + # ignore globs themselves are passed via the `paths_ignore:` input below + # β€” override there if your repo needs different rules. + # + # `push` fires for branches in this repo only (forks push to their own remote, + # not ours). Branch protection on the PR reads the status check from the + # commit SHA, so this single trigger covers both pre-PR pushes and PR head + # updates without duplicating runs. + push: + # Bot-managed branches (badges, gh-pages) carry no source to test. + branches-ignore: [badges, gh-pages] + # `pull_request` covers two cases: + # - Fork PRs (push doesn't fire upstream for fork commits). + # - Release PRs from `next` / `hotfixes` β†’ `master`. Their head SHA is + # a bot `chore: bump version` commit that workflow-ci.yml's + # `commit-gate` job filters out on the push path, so without the + # pull_request fallback the release PR's `Required PR Check` + # status never gets posted and the ruleset blocks the merge. + # `branches:` includes the v4 integration branches so PRs targeting + # `next` / `hotfixes` get CI too β€” feature PRs from forks would + # otherwise get nothing. Non-fork feature PRs still skip the + # pull_request `ci` job (push covers them); see the `if:` on the job. + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: [master, main, next, hotfixes] + workflow_dispatch: + inputs: + debug: + description: "Enable debug logging for troubleshooting" + type: boolean + required: false + default: false + node_version: + description: "Node.js version to use (default: lts/*)" + type: string + required: false + default: "lts/*" + min_node_version: + description: "Minimum Node.js version for matrix testing (default: 20, oldest non-EOL)" + type: string + required: false + default: "20" + max_node_major: + description: "Override max Node.js major version (default: 22)" + type: string + required: false + default: "22" + lts_only_matrix: + description: "Only include even-numbered (LTS) Node.js major versions in the test matrix" + type: boolean + required: false + default: false + package_manager: + description: "Package manager (npm or yarn)" + type: string + required: false + default: "npm" + test_environment: + description: "Environment for tests (affects NODE_ENV and NODE_OPTIONS --conditions flag)" + type: string + required: false + default: "development" + # ── Coverage badge ─────────────────────────────────────────────── + enable_coverage_badge: + description: "Run the coverage + badge-push job after CI passes" + type: boolean + required: false + default: true + coverage_command: + description: "Command to run tests and generate coverage data" + type: string + required: false + default: "npm run ci:coverage" + coverage_summary_path: + description: "Path to the coverage-summary.json produced by Jest / c8" + type: string + required: false + default: "coverage/coverage-summary.json" + badges_branch: + description: "Branch where the badge JSON is published" + type: string + required: false + default: "badges" + badge_filename: + description: "Filename for the badge JSON committed to the badges branch" + type: string + required: false + default: "coverage.json" + upload_coverage_artifact: + description: "Upload the full coverage/ directory as a workflow artifact" + type: boolean + required: false + default: true + # ── Type check ────────────────────────────────────────────────── + type_check_command: + description: "Command to run type checking" + type: string + required: false + default: "npm run test:types" + skip_type_check: + description: "Skip the type-check step in the coverage-badge job" + type: boolean + required: false + default: false + default_branch: + description: "Default branch name β€” badge is only pushed on pushes to this branch" + type: string + required: false + default: "master" + enable_coverage_pr_comment: + description: "Inject a coverage badge into the PR description on pull request events" + type: boolean + required: false + default: true + +# Cancel superseded runs on feature branches; keep every master/main run as the +# permanent green record. Keyed on github.ref so push and pull_request events +# for the same branch share a group (the `if:` on the ci job already prevents +# non-fork PR sync from running, but the shared group guards against edge +# cases). +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main' }} + +# Workflow-level: matches the broadest write surface the called +# `workflow-ci.yml` reaches across its branches: +# - coverage-badge: contents:write (push to `badges` branch) +# - coverage-pr-comment: pull-requests:write (edit PR description body) +# Jobs that don't need write (CI matrix, commit-gate, the mirror below) +# inherit but never exercise the surface. The mirror job overrides to +# `permissions: {}` since it's pure shell. +permissions: + contents: write + pull-requests: write + +jobs: + ci: + name: πŸ—οΈ Continuous Integration + # Run on pull_request when: + # - The PR is from a fork (push doesn't fire upstream for fork commits). + # - The PR is a v4 release PR β€” head ref is `next` or `hotfixes` + # targeting `master`/`main`. Push-event CI on the head SHA is + # unreliable for these because workflow-ci.yml's `commit-gate` + # filters out the bot's `chore: bump version` commit, so without + # this fallback the release PR's `Required PR Check` never posts. + # Other (in-repo, non-release) PRs skip β€” the push event on the head + # branch already ran CI and posted status to the SHA. + if: | + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.fork == true || + github.event.pull_request.head.ref == 'next' || + github.event.pull_request.head.ref == 'hotfixes' + uses: CLDMV/.github/.github/workflows/workflow-ci.yml@v4 + with: + package_name: "@cldmv/git-embedded" # Required: replace with your NPM package name + # Globs that should NOT trigger the heavy CI matrix. When every changed + # file matches one of these, `docs_only=true` flows out of the reusable + # and `required-check` below posts a green Required PR Check without + # running CI. The default in the reusable matches these β€” override only + # if your repo needs different rules. + paths_ignore: | + **.md + docs/** + *.md + LICENSE + .gitignore + debug: ${{ github.event.inputs.debug == 'true' }} + node_version: ${{ github.event.inputs.node_version || 'lts/*' }} + min_node_version: ${{ github.event.inputs.min_node_version || '20' }} + max_node_major: ${{ github.event.inputs.max_node_major || '22' }} + # Full matrix on feature branches (catch per-version bugs pre-merge); + # LTS-only on master/main as the final confidence check / audit record. + lts_only_matrix: ${{ github.event.inputs.lts_only_matrix == 'true' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }} + package_manager: ${{ github.event.inputs.package_manager || 'npm' }} + test_command: "npm test" # Use defaults: NODE_ENV=development, NODE_OPTIONS=--conditions=development + # test_command: "NODE_OPTIONS='--conditions=slothlet-dev' npm test" # Override NODE_OPTIONS only + # test_command: "NODE_ENV=test npm test" # Override NODE_ENV only + # test_command: "NODE_ENV=test NODE_OPTIONS='--conditions=slothlet-dev' npm test" # Override both + test_environment: ${{ github.event.inputs.test_environment || 'development' }} # Alternative to setting in test_command + build_command: "echo 'βœ“ no build step'" + skip_performance_tests: false + skip_matrix_tests: false + + # ── Coverage badge ───────────────────────────────────────────────────── + # Runs after a successful CI build; pushes a Shields.io-compatible badge + # JSON to the `badges` branch (signed commit via bot GPG). + # Only runs on direct pushes to default_branch β€” PRs and feature branches + # are automatically skipped so coverage always reflects merged master code. + # Requires: the coverage_command produces coverage/coverage-summary.json + enable_coverage_badge: ${{ github.event.inputs.enable_coverage_badge != 'false' }} + default_branch: ${{ github.event.inputs.default_branch || 'master' }} # Badge only pushed when a push lands on this branch + coverage_command: ${{ github.event.inputs.coverage_command || 'npm run ci:coverage' }} + coverage_summary_path: ${{ github.event.inputs.coverage_summary_path || 'coverage/coverage-summary.json' }} + badges_branch: ${{ github.event.inputs.badges_branch || 'badges' }} + badge_filename: ${{ github.event.inputs.badge_filename || 'coverage.json' }} + upload_coverage_artifact: ${{ github.event.inputs.upload_coverage_artifact != 'false' }} + + # ── Type check (runs inside the coverage-badge job) ──────────────────── + type_check_command: ${{ github.event.inputs.type_check_command || 'npm run test:types' }} + skip_type_check: ${{ github.event.inputs.skip_type_check == 'true' }} + + # ── PR coverage badge ───────────────────────────────────────────────── + # Injects a Shields.io badge + breakdown table directly into the PR body + # on every push to the PR branch. Only fires on pull_request events; + # skipped automatically on push and workflow_dispatch. No files committed. + enable_coverage_pr_comment: ${{ github.event.inputs.enable_coverage_pr_comment != 'false' }} + + # Authentication & Bot Configuration + # The workflow supports automatic App token detection for enhanced permissions and proper attribution: + # - WITH App secrets: Operations attributed to CLDMV bot, enhanced permissions for workflow repositories + # - WITHOUT App secrets: Falls back to GitHub Actions bot with standard permissions + # Note: CI workflow currently only runs build/test jobs, but App secrets are included for consistency + # To set up App authentication, add these secrets to your repository settings: + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + # Optional: CLDMV Bot credentials for enhanced permissions and proper attribution + # If not provided, will use default GITHUB_TOKEN with GitHub Actions bot attribution + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + # Required when enable_coverage_badge: true + BOT_NAME: ${{ secrets.CLDMV_BOT_NAME }} + BOT_EMAIL: ${{ secrets.CLDMV_BOT_EMAIL }} + BOT_GPG_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_GPG_PRIVATE_KEY }} + BOT_GPG_PASSPHRASE: ${{ secrets.CLDMV_BOT_GPG_PASSPHRASE }} + + # βœ… Stable check that mirrors the `ci` result so branch protection has a + # single, predictable status name to require. The push event runs on the + # same SHA that becomes the PR head, so the status attaches to the PR + # automatically β€” no `pull_request` round-trip needed for non-fork + # non-release PRs. + required-check: + name: βœ… Required PR Check + needs: ci + # Mirror the `ci` job's gating exactly. The four cases that run: + # 1. push events (job needs CI run) + # 2. fork PRs (push doesn't cover forks) + # 3. release PRs from `next` β†’ master/main (push covers SHA but commit-gate skips chore-bump) + # 4. release PRs from `hotfixes` β†’ master/main (same reason) + # In-repo feature PRs targeting `next` / `hotfixes` skip on + # pull_request β€” push on the head branch already posted the status + # on the SHA, and mirroring here would overwrite it. + if: | + always() && ( + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.fork == true || + github.event.pull_request.head.ref == 'next' || + github.event.pull_request.head.ref == 'hotfixes' + ) + runs-on: ubuntu-latest + # Pure shell mirror β€” no GitHub API access. Strip the workflow's + # write defaults to zero for this job. + permissions: {} + steps: + - name: Mirror reusable result + env: + IS_MASTER_SYNC: ${{ needs.ci.outputs.is_master_sync }} + DOCS_ONLY: ${{ needs.ci.outputs.docs_only }} + CI_RESULT: ${{ needs.ci.result }} + run: | + echo "ci.result=$CI_RESULT docs_only=$DOCS_ONLY is_master_sync=$IS_MASTER_SYNC" + # next/hotfixes was force-synced to master β€” head SHA matches the + # default branch, nothing new to test, green-light without running CI. + if [ "$IS_MASTER_SYNC" = "true" ]; then + echo "Branch tip matches master β€” Required PR Check passes without running CI." + exit 0 + fi + # Docs-only PR β€” the reusable skipped the heavy chain and exported + # docs_only=true. Green-light Required PR Check so the ruleset + # doesn't block a docs change. + if [ "$DOCS_ONLY" = "true" ]; then + echo "Docs-only change β€” Required PR Check passes without running CI." + exit 0 + fi + if [ "$CI_RESULT" = "success" ]; then + echo "Reusable CI passed." + exit 0 + elif [ "$CI_RESULT" = "failure" ] || [ "$CI_RESULT" = "cancelled" ]; then + echo "Reusable CI did not pass." + exit 1 + else + # covers 'skipped' or undefined; force red to avoid silent green + echo "Reusable CI produced no pass/fail; treating as failure." + exit 1 + fi diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..faed0f4 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,70 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/security/codeql.yml +# @Date: 2026-05-20 00:00:00 -07:00 (1779606000) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/codeql.yml +# +# REQUIRED REPO SETTING β€” CodeQL must be in "Advanced" mode for this workflow +# to upload SARIF. If the repo has CodeQL "Default setup" enabled (the +# GitHub-managed alternative), upload runs fail with: +# +# "Code Scanning could not process the submitted SARIF file: CodeQL +# analyses from advanced configurations cannot be processed when the +# default setup is enabled" +# +# The org-bootstrap-repo action automatically disables default setup +# (overwrite-with-warn) on every fanout run, so a freshly-bootstrapped +# repo lands in the right state by default. If you want to KEEP default +# setup (the GitHub-managed config) instead of this workflow, DELETE +# this codeql.yml file β€” with the conflict gone, the bootstrap leaves +# default setup alone on subsequent runs. +# +# Manual fix when running outside the bootstrap: +# Settings β†’ Code security and analysis β†’ Code scanning β†’ CodeQL +# analysis β†’ βš™οΈ β†’ Switch to advanced. +name: πŸ” CodeQL + +on: + push: + branches: [master, main] + # Same fork-PR consideration as ci.yml: pull_request fires for forks; SARIF + # upload to base-repo Security tab fails with read-only token. Acceptable β€” + # push-to-master analysis after merge catches anything missed. DO NOT use + # pull_request_target (runs base-repo workflow with secrets against fork + # code; dangerous). + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + # Include the v4 integration branches (`next`, `hotfixes`) so feature + # and hotfix PRs trigger CodeQL. Without these, branch protection + # rulesets that require the CodeQL check on `next`/`hotfixes` will + # sit on "waiting for results" indefinitely. Branches that don't + # exist in a given repo simply never trigger the workflow β€” harmless + # for repos that haven't adopted the v4 staging-branch flow. + branches: [master, main, next, hotfixes] + schedule: + - cron: "37 14 * * 1" # weekly Monday 14:37 UTC; GitHub updates queries over time + +permissions: + security-events: write + contents: read + actions: read + +concurrency: + group: codeql-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main' }} + +jobs: + analyze: + uses: CLDMV/.github/.github/workflows/reusable-codeql.yml@v4 + with: + languages: "javascript-typescript" + # Override defaults if needed: + # queries: "security-extended,security-and-quality" + # paths_ignore: "node_modules/,dist/,coverage/,**/test/**" + # config_file: ".github/codeql-config.yml" + # build_mode: "autobuild" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..654e8a3 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,54 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/automation/dependabot-auto-merge.yml +# @Date: 2026-05-20 00:00:00 -07:00 (1779606000) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/dependabot-auto-merge.yml +# +# Auto-approves + queues auto-merge for Dependabot patch/minor bumps after +# CI passes. Major bumps are left for a human. +# +# Default in v4: ON. To opt out, delete this file β€” Dependabot PRs still +# flow into `next` (via dependabot.yml) but require a manual merge click. +# +# How v4 routing works: +# - dependabot.yml sets `target-branch: next`, so Dependabot opens PRs +# against `next`. This workflow auto-merges those PRs into `next` after +# CI; they batch into the next release like every other change. +# - For security advisories, hotfix-redirector.yml (release-flow-v4/) +# detects GHSA references in the PR body and retargets the PR from +# `next` β†’ `hotfixes` *before* this workflow runs, so security updates +# auto-merge into the hotfix lane instead of waiting for the next batch. +# +# Required setup (one-time per repo): +# 1. Settings β†’ Pull Requests β†’ "Allow auto-merge" β†’ ON +# (enabled automatically by `release-flow-v4/v4-bootstrap.yml`) +# 2. Branch protection on `next` and `hotfixes` with required CI status +# checks β€” the action refuses to merge into an unprotected branch. +# Both are validated by the action; the workflow fails loudly if missing. +name: πŸ€– Dependabot Auto-Merge + +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + +permissions: + contents: write + pull-requests: write + +jobs: + automerge: + # Pre-filter at workflow level so this doesn't spin up for every PR. + if: github.event.pull_request.user.login == 'dependabot[bot]' + uses: CLDMV/.github/.github/workflows/reusable-dependabot-auto-merge.yml@v4 + with: + bump_types: "patch,minor" + merge_method: "squash" + # also_for_actors: "renovate[bot]" # extend if you adopt Renovate + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..82a55c9 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,34 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/security/dependency-review.yml +# @Date: 2026-05-20 00:00:00 -07:00 (1779606000) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/dependency-review.yml +name: πŸ”’ Dependency Review + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: [master, main] + +permissions: + contents: read + pull-requests: write + +jobs: + review: + uses: CLDMV/.github/.github/workflows/reusable-dependency-review.yml@v4 + with: + fail_on_severity: "moderate" + # Per-repo license policy override: + # deny_licenses: "AGPL-3.0,LGPL-3.0" # block copyleft for an Apache-2.0 repo + # Bot App credentials. When set, the dependency-review PR comment is + # posted by the consumer's bot App instead of github-actions[bot]. + # Both lines are optional; remove them to fall back to GITHUB_TOKEN. + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/hotfix-redirector.yml b/.github/workflows/hotfix-redirector.yml new file mode 100644 index 0000000..c45bdbd --- /dev/null +++ b/.github/workflows/hotfix-redirector.yml @@ -0,0 +1,67 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/release-flow-v4/hotfix-redirector.yml +# @Date: 2026-05-22 00:00:00 -07:00 (1779778800) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/hotfix-redirector.yml +# +# v4 hotfix lane: retarget hotfix/security PRs to the `hotfixes` branch. +# +# Two paths trigger a redirect (CLDMV/.github docs/conventions/release-flow-v4.md Β§5.2, Β§6.5): +# 1. Head branch matches `hotfix/*` or `security/*` (human-driven hotfix flow). +# 2. Author is `dependabot[bot]` AND the PR body references a GHSA security +# advisory (Dependabot's security-update PRs flow into the hotfix lane; +# routine version bumps stay on `next`). +# +# The redirect-hotfix-pr action owns all detection logic β€” it skips non-matching +# bot PRs, non-matching heads, and PRs already on `hotfixes`, and posts a +# one-time explanatory comment with the appropriate reason. +name: πŸ”€ Hotfix PR Redirector (v4) + +# SECURITY NOTE: pull_request_target runs in the BASE repo's context with +# WRITE permissions + secrets. SAFE here because it is API-only β€” the +# redirect-hotfix-pr action never checks out or executes PR content. +# DO NOT add a checkout step. +# +# `opened` only (NOT `edited`): if a maintainer manually re-targets the PR, +# we must not fight them by redirecting again. +on: + pull_request_target: + types: [opened] + +permissions: + contents: read + pull-requests: write + +concurrency: + group: hotfix-redirector-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + redirect: + name: "πŸ”€ Redirect to hotfixes" + runs-on: ubuntu-latest + steps: + - name: Create App token + id: app-token + uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4 + with: + client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + env: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + + - name: Redirect hotfix/security PR to hotfixes + uses: CLDMV/.github/.github/actions/github/steps/redirect-hotfix-pr@v4 + with: + pr-number: ${{ github.event.pull_request.number }} + github-token: ${{ steps.app-token.outputs.token }} + head-ref: ${{ github.event.pull_request.head.ref }} + base-ref: ${{ github.event.pull_request.base.ref }} + user-type: ${{ github.event.pull_request.user.type }} + target-base: hotfixes diff --git a/.github/workflows/hotfixes-release.yml b/.github/workflows/hotfixes-release.yml new file mode 100644 index 0000000..eee6f79 --- /dev/null +++ b/.github/workflows/hotfixes-release.yml @@ -0,0 +1,164 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/release-flow-v4/hotfixes-release.yml +# @Date: 2026-05-22 00:00:00 -07:00 (1779778800) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/hotfixes-release.yml +# +# v4 hotfix lane: maintain the ONE persistent `hotfixes β†’ master` release PR. +# +# Mirror of next-release.yml but for the `hotfixes` integration branch +# (CLDMV/.github docs/conventions/release-flow-v4.md Β§5.4, Β§6.2). Fires on +# every push to `hotfixes` (hotfix/security PR squash-merges land here), and +# resolves-or-creates the persistent `hotfixes β†’ master` release PR. Patches +# the current release independently of whatever is pending on `next`. +# +# Same model as the next lane: the version bump rides on `hotfixes` as a +# `chore: bump version` commit, carried to master through the squash (Β§8.1). +name: πŸš‘ Hotfixes Release (v4) + +on: + push: + branches: [hotfixes] + +permissions: + contents: write + pull-requests: write + +concurrency: + group: hotfixes-release-${{ github.repository }} + cancel-in-progress: false + +jobs: + plan: + # Loop guard: skip the bot's own chore-bump pushes and reset pushes. + # Replace `cldmv-bot[bot]` with your bot App's login if different. + if: | + github.actor != 'cldmv-bot[bot]' && + github.actor != 'github-actions[bot]' && + !startsWith(github.event.head_commit.message, 'chore: bump version') + name: "πŸ” Plan (detect changes + resolve PR)" + runs-on: ubuntu-latest + outputs: + has-changes: ${{ steps.detect.outputs.has-changes }} + pr-number: ${{ steps.resolve.outputs.pr-number }} + steps: + - name: Create App token + id: app-token + uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4 + with: + client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + env: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + + - name: Checkout hotfixes + uses: CLDMV/.github/.github/actions/common/steps/checkout-code@v4 + with: + ref: hotfixes + fetch-depth: 0 + + - name: Detect master..hotfixes changes + id: detect + shell: bash + run: | + git fetch origin master --quiet + count=$(git rev-list --count origin/master..HEAD) + echo "πŸ“Š commits on hotfixes not yet on master: $count" + if [ "$count" -gt 0 ]; then + echo "has-changes=true" >> "$GITHUB_OUTPUT" + else + echo "has-changes=false" >> "$GITHUB_OUTPUT" + echo "ℹ️ hotfixes is in sync with master β€” nothing to release." + fi + + - name: Resolve persistent hotfixesβ†’master PR + id: resolve + if: steps.detect.outputs.has-changes == 'true' + shell: bash + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + pr=$(gh pr list --repo "$GITHUB_REPOSITORY" --head hotfixes --base master \ + --state open --json number --jq '.[0].number // ""') + echo "pr-number=$pr" >> "$GITHUB_OUTPUT" + if [ -n "$pr" ]; then + echo "πŸ” existing hotfix release PR: #$pr β€” will refresh" + else + echo "πŸ†• no hotfix release PR yet β€” will create" + fi + + create: + name: "πŸ†• Create hotfix release PR" + needs: plan + if: needs.plan.outputs.has-changes == 'true' && needs.plan.outputs.pr-number == '' + runs-on: ubuntu-latest + steps: + - name: Create App token + id: app-token + uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4 + with: + client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + env: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + + # Customize `package-name` + `build-command` to match this repo β€” + # see notes in next-release.yml. + - name: Create release PR + uses: CLDMV/.github/.github/actions/github/jobs/create-release-pr@v4 + with: + package-name: "@cldmv/git-embedded" + build-command: "echo 'βœ“ no build step'" + github-token: ${{ steps.app-token.outputs.token }} + + refresh: + name: "πŸ” Refresh hotfix release PR #${{ needs.plan.outputs.pr-number }}" + needs: plan + if: needs.plan.outputs.has-changes == 'true' && needs.plan.outputs.pr-number != '' + runs-on: ubuntu-latest + steps: + - name: Create App token + id: app-token + uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4 + with: + client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + env: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + + - name: Refresh release PR + id: refresh + uses: CLDMV/.github/.github/actions/github/jobs/update-release-pr@v4 + with: + head-ref: hotfixes + pr-number: ${{ needs.plan.outputs.pr-number }} + package-name: "@cldmv/git-embedded" + build-command: "echo 'βœ“ no build step'" + github-token: ${{ steps.app-token.outputs.token }} + + # Optional: release-PR notifier. See next-release.yml for the + # rationale. Delete the step to opt out entirely; leave a webhook + # secret unset to opt out of that one channel. + - name: Notify on release-PR version bump + if: steps.refresh.outputs.version-changed == 'true' + uses: CLDMV/.github/.github/actions/community/jobs/release-notifier@v4 + with: + event_kind: release_pr + pr_number: ${{ needs.plan.outputs.pr-number }} + version: ${{ steps.refresh.outputs.new-version }} + github_token: ${{ steps.app-token.outputs.token }} + env: + DISCORD_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.DISCORD_RELEASE_PR_PUBLIC_WEBHOOK }} + DISCORD_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.DISCORD_RELEASE_PR_PRIVATE_WEBHOOK }} + SLACK_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.SLACK_RELEASE_PR_PUBLIC_WEBHOOK }} + SLACK_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.SLACK_RELEASE_PR_PRIVATE_WEBHOOK }} + GENERIC_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.GENERIC_RELEASE_PR_PUBLIC_WEBHOOK }} + GENERIC_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.GENERIC_RELEASE_PR_PRIVATE_WEBHOOK }} diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..3e8b8f0 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,44 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/automation/labeler.yml +# @Date: 2026-05-20 00:00:00 -07:00 (1779606000) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/labeler.yml +# +# Path-based PR auto-labeler. Uses CLDMV's org-default labeler.default.yml; +# override per-repo by adding .github/labeler.yml in this repo (same shape). +# +# Labels applied additively β€” never removes labels added by humans or other +# automation. +# +# Batch 5.2 from tmp/plan-future-workflows.md. +name: 🏷️ PR Labeler + +# SECURITY NOTE: This workflow uses pull_request_target so it can apply labels +# to fork PRs. pull_request_target runs in the BASE repo's context with WRITE +# permissions and access to secrets. This is SAFE for THIS workflow because: +# - We never checkout the PR head ref +# - We never run code from the PR (no `run:` step uses PR data) +# - We only call REST APIs to read the file list and post labels +# DO NOT add a checkout step or any step that executes PR-supplied content +# (build commands, scripts, test runs, etc.) to this workflow. +on: + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + pull-requests: write + +jobs: + label: + uses: CLDMV/.github/.github/workflows/reusable-pr-labeler.yml@v4 + # Optional. Without these, labels are attributed to github-actions[bot]. + # With these, they're attributed to your CLDMV bot App. + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/master-commit-audit.yml b/.github/workflows/master-commit-audit.yml new file mode 100644 index 0000000..4be375f --- /dev/null +++ b/.github/workflows/master-commit-audit.yml @@ -0,0 +1,63 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/release-companions/master-commit-audit.yml +# @Date: 2026-05-20 00:00:00 -07:00 (1779606000) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/master-commit-audit.yml +# +# Post-merge safety net: when any commit lands on the default branch, verify +# its subject matches the expected release-flow patterns. On miss, auto-file +# a GitHub Issue (deduped by SHA) so the alert is persistent and assignable +# β€” not just a red ❌ that dies in inbox. +# +# Catches: release-workflow title-generation regressions, branch-protection +# bypasses, unexpected bot commits, direct emergency pushes. +# +# Batch 5.1 from tmp/plan-future-workflows.md. +name: 🧾 Master Commit Audit + +on: + push: + branches: [master, main] + +permissions: + contents: read + issues: write + +jobs: + audit: + runs-on: ubuntu-latest + steps: + # Optional. Without these, the audit issue is filed by + # github-actions[bot]. With them, the issue is filed by your bot App. + - name: Create App token (falls back to GITHUB_TOKEN) + id: app-token + uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4 + with: + client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + env: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + + - name: Audit commit subject + uses: CLDMV/.github/.github/actions/git/jobs/audit-commit-subject@v4 + with: + commit_sha: ${{ github.sha }} + # Allow release commits (from the release workflow), maintenance + # commits, and standard merge commits. Customize per repo if + # your conventions differ. + allowed_patterns: | + ^release: v\d+\.\d+\.\d+( \(#\d+\))?$ + ^chore(\([^)]+\))?: .+ + ^Merge pull request #\d+ from .+ + # Canonical label names from CLDMV/.github's data/github-labels.json + # (note the space after each colon). Replace with names that exist + # in your repo's label catalog. + issue_labels: "type: ci,priority: high" + # issue_assignee: "shinrai" # uncomment to auto-assign + github_token: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/next-release.yml b/.github/workflows/next-release.yml new file mode 100644 index 0000000..915b5d7 --- /dev/null +++ b/.github/workflows/next-release.yml @@ -0,0 +1,178 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/release-flow-v4/next-release.yml +# @Date: 2026-05-22 00:00:00 -07:00 (1779778800) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/next-release.yml +# +# v4 core: maintain the ONE persistent `next β†’ master` release PR for this repo. +# +# Fires on every push to `next` (contributor PR squash-merges land here). +# Resolves the existing release PR and refreshes it, or creates it the first +# time `next` diverges from master. The release PR batches all accumulated +# feature commits into a single release β€” that batching is v4's whole point +# (see CLDMV/.github docs/conventions/release-flow-v4.md Β§5.3, Β§6.1). +# +# The version bump rides on `next` as a `chore: bump version` commit pushed +# by the release-PR machinery; it's carried to master through the squash +# (Β§8.1 β€” master accepts changes only via PR squash, and the publish flow +# reads package.json as-is). +name: πŸš€ Next Release (v4) + +on: + push: + branches: [next] + +permissions: + contents: write + pull-requests: write + +# Serialize: each run re-resolves the current PR state, so queueing (not +# cancelling) avoids a create/refresh race when pushes land back-to-back. +concurrency: + group: next-release-${{ github.repository }} + cancel-in-progress: false + +jobs: + plan: + # Loop guard: the refresh/create steps push a `chore: bump version` + # commit to `next` (as the bot), and next-reset.yml force-pushes + # `next` (as the bot). Neither should re-trigger a release-PR refresh. + # Replace `cldmv-bot[bot]` with your bot App's login if different. + if: | + github.actor != 'cldmv-bot[bot]' && + github.actor != 'github-actions[bot]' && + !startsWith(github.event.head_commit.message, 'chore: bump version') + name: "πŸ” Plan (detect changes + resolve PR)" + runs-on: ubuntu-latest + outputs: + has-changes: ${{ steps.detect.outputs.has-changes }} + pr-number: ${{ steps.resolve.outputs.pr-number }} + steps: + - name: Create App token + id: app-token + uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4 + with: + client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + env: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + + - name: Checkout next + uses: CLDMV/.github/.github/actions/common/steps/checkout-code@v4 + with: + ref: next + fetch-depth: 0 + + - name: Detect master..next changes + id: detect + shell: bash + run: | + git fetch origin master --quiet + count=$(git rev-list --count origin/master..HEAD) + echo "πŸ“Š commits on next not yet on master: $count" + if [ "$count" -gt 0 ]; then + echo "has-changes=true" >> "$GITHUB_OUTPUT" + else + echo "has-changes=false" >> "$GITHUB_OUTPUT" + echo "ℹ️ next is in sync with master β€” nothing to release." + fi + + - name: Resolve persistent nextβ†’master PR + id: resolve + if: steps.detect.outputs.has-changes == 'true' + shell: bash + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + # The persistent release PR is the open PR with head=next, + # base=master. There is at most one (concurrency-serialized). + pr=$(gh pr list --repo "$GITHUB_REPOSITORY" --head next --base master \ + --state open --json number --jq '.[0].number // ""') + echo "pr-number=$pr" >> "$GITHUB_OUTPUT" + if [ -n "$pr" ]; then + echo "πŸ” existing release PR: #$pr β€” will refresh" + else + echo "πŸ†• no release PR yet β€” will create" + fi + + create: + name: "πŸ†• Create release PR" + needs: plan + if: needs.plan.outputs.has-changes == 'true' && needs.plan.outputs.pr-number == '' + runs-on: ubuntu-latest + steps: + - name: Create App token + id: app-token + uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4 + with: + client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + env: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + + # Runs on the `next` ref β†’ create-release-pr opens next β†’ master and + # pushes the chore-bump commit to next. Customize: + # - `package-name` β†’ your npm package (or any unique identifier) + # - `build-command` β†’ your build script, or a stub like + # `echo 'βœ“ no build step'` for a meta package + - name: Create release PR + uses: CLDMV/.github/.github/actions/github/jobs/create-release-pr@v4 + with: + package-name: "@cldmv/git-embedded" + build-command: "echo 'βœ“ no build step'" + github-token: ${{ steps.app-token.outputs.token }} + + refresh: + name: "πŸ” Refresh release PR #${{ needs.plan.outputs.pr-number }}" + needs: plan + if: needs.plan.outputs.has-changes == 'true' && needs.plan.outputs.pr-number != '' + runs-on: ubuntu-latest + steps: + - name: Create App token + id: app-token + uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4 + with: + client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + env: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + + - name: Refresh release PR + id: refresh + uses: CLDMV/.github/.github/actions/github/jobs/update-release-pr@v4 + with: + head-ref: next + pr-number: ${{ needs.plan.outputs.pr-number }} + package-name: "@cldmv/git-embedded" + build-command: "echo 'βœ“ no build step'" + github-token: ${{ steps.app-token.outputs.token }} + + # Optional: release-PR notifier. Fires only when the target + # version actually changes (PR open or version-bump shift), not + # on the changelog-only refreshes that run on every push. Each + # secret is independently opt-in: leave a webhook unset and that + # channel is silently skipped. Delete this step to opt out + # entirely. + - name: Notify on release-PR version bump + if: steps.refresh.outputs.version-changed == 'true' + uses: CLDMV/.github/.github/actions/community/jobs/release-notifier@v4 + with: + event_kind: release_pr + pr_number: ${{ needs.plan.outputs.pr-number }} + version: ${{ steps.refresh.outputs.new-version }} + github_token: ${{ steps.app-token.outputs.token }} + env: + DISCORD_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.DISCORD_RELEASE_PR_PUBLIC_WEBHOOK }} + DISCORD_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.DISCORD_RELEASE_PR_PRIVATE_WEBHOOK }} + SLACK_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.SLACK_RELEASE_PR_PUBLIC_WEBHOOK }} + SLACK_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.SLACK_RELEASE_PR_PRIVATE_WEBHOOK }} + GENERIC_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.GENERIC_RELEASE_PR_PUBLIC_WEBHOOK }} + GENERIC_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.GENERIC_RELEASE_PR_PRIVATE_WEBHOOK }} diff --git a/.github/workflows/next-reset.yml b/.github/workflows/next-reset.yml new file mode 100644 index 0000000..d47f046 --- /dev/null +++ b/.github/workflows/next-reset.yml @@ -0,0 +1,199 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/release-flow-v4/next-reset.yml +# @Date: 2026-05-22 00:00:00 -07:00 (1779778800) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/next-reset.yml +# +# v4 core: after a release lands on master, re-sync the integration branches +# (CLDMV/.github docs/conventions/release-flow-v4.md Β§6.3, Β§7). +# +# - `hotfixes` is ALWAYS force-reset to master HEAD after any release. +# - `next` depends on which lane released: +# * normal release (next β†’ master, or a v3-style feat β†’ master): +# force-reset `next` to master HEAD (Β§7.1). +# * hotfix release (hotfixes β†’ master): MERGE master into `next` +# instead, so next's accumulated feature work is preserved (Β§7.2, +# option B). The merge is a no-op (204) when next has no extra work. +# +# The released lane is detected from the PR head ref behind the squash +# commit's trailing "(#N)". +# +# wait-for-tags gate: a release also fires update-major-version-tags, which +# rolls the major tags. Jobs resolve `uses: ...@vN` at job start, so without +# this gate the sync job can run the PREVIOUS release's action code. The gate +# polls the RELEASED major's tag β€” parsed from the `release: vX.Y.Z` commit +# β€” until it matches the release commit. +# +# Self-healing: no-ops pre-cutover (neither integration branch exists), but +# post-cutover it RECREATES a branch that went missing β€” e.g. branch-retention +# deleting `next` as a merged PR head. force-reset-branch creates the ref +# when it's absent. +name: ♻️ Next/Hotfixes Reset (v4) + +on: + push: + branches: [master, main] + +permissions: + contents: write + +concurrency: + group: next-reset-${{ github.repository }} + cancel-in-progress: false + +jobs: + wait-for-tags: + # Only fire on a release commit (the squash-merge of a release PR). + if: startsWith(github.event.head_commit.message, 'release:') + name: "⏳ Wait for the released major tag to roll forward" + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Poll the released major tag until it matches the release commit + shell: bash + env: + TARGET_SHA: ${{ github.sha }} + REPO: ${{ github.repository }} + COMMIT_MSG: ${{ github.event.head_commit.message }} + run: | + echo "πŸ” Release commit: $TARGET_SHA" + # Parse the released MAJOR from the `release: vX.Y.Z` subject + # and poll THAT tag (e.g. @v4 for v4.x). update-major-version- + # tags rolls @v to the release commit; a hardcoded @v3 + # would never match on a major bump (which creates @v4). + major=$(printf '%s' "$COMMIT_MSG" | grep -oiE 'release:[^0-9]*v?[0-9]+' | grep -oE '[0-9]+$' | head -1) + if [ -z "$major" ]; then + echo "⚠️ Could not parse a major version from the commit subject β€” skipping the gate." + exit 0 + fi + tag="v${major}" + echo "⏳ Gating on @${tag}…" + max_attempts=24 # 24 * 5s = 120s + for attempt in $(seq 1 $max_attempts); do + sha=$(git ls-remote "https://github.com/${REPO}.git" "refs/tags/${tag}^{}" 2>/dev/null | awk '{print $1}') + [ -z "$sha" ] && sha=$(git ls-remote "https://github.com/${REPO}.git" "refs/tags/${tag}" 2>/dev/null | awk '{print $1}') + echo "Attempt $attempt/$max_attempts: @${tag} β†’ ${sha:-}" + if [ "$sha" = "$TARGET_SHA" ]; then + echo "βœ… @${tag} matches the release commit β€” safe to proceed" + exit 0 + fi + [ "$attempt" -lt "$max_attempts" ] && sleep 5 + done + echo "⚠️ Timed out waiting for @${tag} β€” proceeding anyway." + + sync-branches: + name: "♻️ Sync next + hotfixes to master" + needs: wait-for-tags + if: startsWith(github.event.head_commit.message, 'release:') + runs-on: ubuntu-latest + steps: + - name: Create App token + id: app-token + uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4 + with: + client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + # Pushing/merging master's tree (which includes + # .github/workflows/**) requires contents + workflows write. + permission_contents: "true" + permission_workflows: "true" + env: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + + - name: Checkout master + uses: CLDMV/.github/.github/actions/common/steps/checkout-code@v4 + with: + fetch-depth: 0 + + - name: Determine released lane + id: lane + shell: bash + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + COMMIT_MSG: ${{ github.event.head_commit.message }} + run: | + # The squash commit ends with "(#N)" β€” the merged PR number. + prnum=$(printf '%s' "$COMMIT_MSG" | grep -oE '#[0-9]+' | tail -1 | tr -d '#') + head="" + if [ -n "$prnum" ]; then + head=$(gh pr view "$prnum" --repo "$GITHUB_REPOSITORY" \ + --json headRefName --jq '.headRefName' 2>/dev/null || echo "") + fi + echo "released PR #${prnum:-?} head ref: ${head:-}" + if [ "$head" = "hotfixes" ]; then + echo "lane=hotfix" >> "$GITHUB_OUTPUT" + else + echo "lane=other" >> "$GITHUB_OUTPUT" + fi + + - name: Guard β€” is this repo on v4? (do next/hotfixes exist?) + id: guard + shell: bash + run: | + next_exists=false; hotfixes_exists=false + if git ls-remote --exit-code --heads origin next >/dev/null 2>&1; then + next_exists=true + git fetch origin next:refs/remotes/origin/next --quiet || true + fi + if git ls-remote --exit-code --heads origin hotfixes >/dev/null 2>&1; then + hotfixes_exists=true + git fetch origin hotfixes:refs/remotes/origin/hotfixes --quiet || true + fi + # "v4 adopted" = at least one integration branch exists. Keeps + # the reset a no-op on pre-cutover repos (neither exists) while + # letting it RECREATE a branch that went missing post-cutover + # (e.g. one was deleted as a merged PR head). force-reset-branch + # creates if absent. + v4_adopted=false + { [ "$next_exists" = true ] || [ "$hotfixes_exists" = true ]; } && v4_adopted=true + { + echo "next-exists=$next_exists" + echo "hotfixes-exists=$hotfixes_exists" + echo "v4-adopted=$v4_adopted" + } >> "$GITHUB_OUTPUT" + echo "ℹ️ next=$next_exists hotfixes=$hotfixes_exists v4-adopted=$v4_adopted lane=${{ steps.lane.outputs.lane }}" + + # hotfixes always tracks master after a release β€” created if missing. + - name: Ensure hotfixes = master HEAD (reset; create if missing) + if: steps.guard.outputs.v4-adopted == 'true' + uses: CLDMV/.github/.github/actions/git/steps/force-reset-branch@v4 + with: + target-branch: hotfixes + source-ref: master + github-token: ${{ steps.app-token.outputs.token }} + + # Normal release β†’ next is force-reset (its work just shipped), and + # recreated if it was deleted on merge. + - name: Ensure next = master HEAD (normal release; create if missing) + if: steps.guard.outputs.v4-adopted == 'true' && steps.lane.outputs.lane != 'hotfix' + uses: CLDMV/.github/.github/actions/git/steps/force-reset-branch@v4 + with: + target-branch: next + source-ref: master + github-token: ${{ steps.app-token.outputs.token }} + + # Hotfix release + next still exists β†’ merge master into next to + # preserve its accumulated feature work (Β§7.2 option B). + - name: Merge master into next (hotfix release; next exists) + if: steps.guard.outputs.v4-adopted == 'true' && steps.lane.outputs.lane == 'hotfix' && steps.guard.outputs.next-exists == 'true' + uses: CLDMV/.github/.github/actions/github/steps/merge-master-into-branch@v4 + with: + target-branch: next + source-ref: master + github-token: ${{ steps.app-token.outputs.token }} + + # Hotfix release but next is MISSING (deleted) β†’ recreate it at master + # HEAD; there's no accumulated work to preserve. + - name: Recreate next at master HEAD (hotfix release; next missing) + if: steps.guard.outputs.v4-adopted == 'true' && steps.lane.outputs.lane == 'hotfix' && steps.guard.outputs.next-exists == 'false' + uses: CLDMV/.github/.github/actions/git/steps/force-reset-branch@v4 + with: + target-branch: next + source-ref: master + github-token: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/pr-title-normalizer.yml b/.github/workflows/pr-title-normalizer.yml new file mode 100644 index 0000000..25c8d85 --- /dev/null +++ b/.github/workflows/pr-title-normalizer.yml @@ -0,0 +1,64 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/release-flow-v4/pr-title-normalizer.yml +# @Date: 2026-05-22 00:00:00 -07:00 (1779778800) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/pr-title-normalizer.yml +# +# Normalize contributor PR titles to Conventional Commits format, derived +# from the highest-priority commit in the PR. The release flow expects this +# shape, so a v4 repo wants this enabled. (Also backportable to v3 repos β€” +# it wires the normalize-pr-title action, shipped in v3.3.0; the action owns +# all skip logic: bot authors, the long-running release PRs, titles already +# starting with `release:`, and titles that already conform.) +name: 🏷️ PR Title Normalizer + +# SECURITY NOTE: pull_request_target runs in the BASE repo's context with +# WRITE permissions and access to secrets. SAFE for THIS workflow because it +# is API-only β€” the normalize-pr-title action never checks out the PR head +# and never executes PR content. DO NOT add a checkout step. +# +# Triggers on opened + synchronize only (NOT edited): a maintainer hand- +# editing the title must not kick off a re-normalize loop. +on: + pull_request_target: + types: [opened, synchronize] + +permissions: + contents: read + pull-requests: write + +# Collapse a burst of pushes to one normalize run per PR; the newest push +# carries the authoritative commit set, so cancelling an in-flight run is fine. +concurrency: + group: pr-title-normalizer-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + normalize: + name: "✏️ Normalize title" + runs-on: ubuntu-latest + steps: + - name: Create App token (falls back to GITHUB_TOKEN) + id: app-token + uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4 + with: + client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + env: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + + - name: Normalize PR title + uses: CLDMV/.github/.github/actions/github/steps/normalize-pr-title@v4 + with: + pr-number: ${{ github.event.pull_request.number }} + github-token: ${{ steps.app-token.outputs.token }} + base-ref: ${{ github.event.pull_request.base.ref }} + head-ref: ${{ github.event.pull_request.head.ref }} + user-type: ${{ github.event.pull_request.user.type }} + user-login: ${{ github.event.pull_request.user.login }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..a214219 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,125 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/core-cicd/publish.yml +# @Date: 2026-05-20 00:00:00 -07:00 (1779606000) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/publish.yml +name: πŸ“¦ Release and Publish + +on: + push: + branches: [master, main] + paths-ignore: + - "**.md" + - ".github/ISSUE_TEMPLATE/**" + - ".github/PULL_REQUEST_TEMPLATE/**" + workflow_dispatch: + inputs: + debug: + description: "Enable debug logging for troubleshooting" + type: boolean + required: false + default: false + dry_run: + description: "Dry run mode - validate everything but don't publish or create releases" + type: boolean + required: false + default: false + node_version: + description: "Node.js version to use (default: lts/*)" + type: string + required: false + default: "lts/*" + package_manager: + description: "Package manager (npm or yarn)" + type: string + required: false + default: "npm" + test_environment: + description: "Environment for tests (affects NODE_ENV and NODE_OPTIONS --conditions flag)" + type: string + required: false + default: "development" + version: + description: "Version to publish (auto-detected from package.json if not provided)" + type: string + required: false + default: "" + publish_to_npm: + description: "Publish to NPM registry" + type: boolean + required: false + default: true + publish_to_github_packages: + description: "Publish to GitHub Packages registry" + type: boolean + required: false + default: true + min_node_version: + description: "Minimum Node.js version for matrix testing (enables matrix when set)" + type: string + required: false + default: "20" + max_node_major: + description: "Override max Node.js major version (default: 22)" + type: string + required: false + default: "22" + use_gpg: + description: "Enable GPG signing (if GPG secrets provided)" + type: boolean + required: false + default: false + +# NEVER cancel an in-flight publish β€” half-published versions are nasty to +# clean up. Concurrent publishes for the same ref queue instead so they +# serialize naturally. +concurrency: + group: publish-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + publish-package: + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + permissions: + contents: write + packages: write + id-token: write + uses: CLDMV/.github/.github/workflows/workflow-publish.yml@v4 + with: + package_name: "@cldmv/git-embedded" # Required: replace with your NPM package name + debug: ${{ github.event.inputs.debug == 'true' }} + dry_run: ${{ github.event.inputs.dry_run == 'true' }} + node_version: ${{ github.event.inputs.node_version || 'lts/*' }} + package_manager: ${{ github.event.inputs.package_manager || 'npm' }} + version: ${{ github.event.inputs.version || '' }} + publish_to_npm: ${{ github.event.inputs.publish_to_npm != 'false' }} + publish_to_github_packages: ${{ github.event.inputs.publish_to_github_packages != 'false' }} + publish_command: "" + github_packages_publish_command: "" + min_node_version: ${{ github.event.inputs.min_node_version || '20' }} + max_node_major: ${{ github.event.inputs.max_node_major || '22' }} + test_command: "npm test" # Use defaults: NODE_ENV=development, NODE_OPTIONS=--conditions=development + # test_command: "NODE_OPTIONS='--conditions=slothlet-dev' npm test" # Override NODE_OPTIONS only + # test_command: "NODE_ENV=test npm test" # Override NODE_ENV only + # test_command: "NODE_ENV=test NODE_OPTIONS='--conditions=slothlet-dev' npm test" # Override both + test_environment: ${{ github.event.inputs.test_environment || 'development' }} # Alternative to setting in test_command + build_command: "npm run build:ci" + is_prerelease: false + release_source_only: false + create_documentation: true + skip_performance_tests: false + skip_matrix_tests: false + use_gpg: ${{ github.event.inputs.use_gpg == 'true' }} + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + TAGGER_NAME: ${{ secrets.CLDMV_BOT_NAME }} + TAGGER_EMAIL: ${{ secrets.CLDMV_BOT_EMAIL }} + GPG_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.CLDMV_BOT_GPG_PASSPHRASE }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..bdcabbd --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,46 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/automation/stale.yml +# @Date: 2026-05-20 00:00:00 -07:00 (1779606000) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/stale.yml +# +# First-run guidance: on a repo with an existing backlog, the first +# scheduled run can mark a LOT of issues stale at once (notification storm). +# Recommended: enable with `dry_run: true` first, dispatch manually to +# preview, then flip to live. The dispatch input below makes this easy. +# +# Batch 2.3 from tmp/plan-future-workflows.md. +name: πŸ‚ Stale Issues & PRs + +on: + schedule: + - cron: "13 5 * * *" # daily 05:13 UTC (off-the-hour to avoid GH cron stampede) + workflow_dispatch: + inputs: + dry_run: + description: "Preview only β€” no changes will be made" + type: boolean + default: false + +permissions: + issues: write + pull-requests: write + +jobs: + sweep: + uses: CLDMV/.github/.github/workflows/reusable-stale.yml@v4 + with: + dry_run: ${{ github.event.inputs.dry_run == 'true' }} + # Override timers if needed: + # days_before_issue_stale: 60 + # days_before_issue_close: 14 + # days_before_pr_stale: 30 + # days_before_pr_close: 7 + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/tag-health.yml b/.github/workflows/tag-health.yml new file mode 100644 index 0000000..80701a5 --- /dev/null +++ b/.github/workflows/tag-health.yml @@ -0,0 +1,64 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/release-companions/tag-health.yml +# @Date: 2026-05-20 00:00:00 -07:00 (1779606000) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/tag-health.yml +# +# Wakes the reusable-tag-health.yml workflow on a weekly schedule. The +# reusable already implements validation, bot-signature fixes, unsigned-tag +# fixes, orphaned-release recovery, orphaned-tag relocation, and rolling +# major/minor tag maintenance β€” but it's dormant by default. This template +# is what triggers it. +# +# Batch 3.1 from tmp/plan-future-workflows.md. +name: πŸ₯ Tag Health + +on: + schedule: + # Weekly Sunday 04:04 UTC. Off-the-hour to dodge the GitHub :00-cron + # stampede; weekly cadence because tag drift accumulates slowly. + - cron: "4 4 * * 0" + workflow_dispatch: + inputs: + debug: + description: "Enable debug logging for troubleshooting" + type: boolean + required: false + default: false + create_documentation: + description: "Update VERSION_TAGS.md if rolling tags moved" + type: boolean + required: false + default: false + use_gpg: + description: "Enable GPG signing for any tags the sweep creates/recreates" + type: boolean + required: false + default: true + +permissions: + contents: write + +jobs: + health: + uses: CLDMV/.github/.github/workflows/reusable-tag-health.yml@v4 + with: + debug: ${{ github.event.inputs.debug == 'true' }} + # Full unified sweep: validates, fixes bot signatures, fixes + # unsigned tags, recovers orphaned releases, relocates orphaned + # tags, and updates rolling major/minor refs. + run_unified_tag_health: true + create_documentation: ${{ github.event.inputs.create_documentation == 'true' }} + use_gpg: ${{ github.event.inputs.use_gpg != 'false' }} + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + TAGGER_NAME: ${{ secrets.CLDMV_BOT_NAME }} + TAGGER_EMAIL: ${{ secrets.CLDMV_BOT_EMAIL }} + GPG_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.CLDMV_BOT_GPG_PASSPHRASE }} diff --git a/.github/workflows/update-major-version-tags.yml b/.github/workflows/update-major-version-tags.yml new file mode 100644 index 0000000..cabcb26 --- /dev/null +++ b/.github/workflows/update-major-version-tags.yml @@ -0,0 +1,87 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/core-cicd/update-major-version-tags.yml +# @Date: 2026-05-20 00:00:00 -07:00 (1779606000) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/update-major-version-tags.yml +name: 🏷️ Update Major Version Tags + +on: + release: + types: [published] + workflow_dispatch: + inputs: + debug: + description: "Enable debug logging for troubleshooting" + type: boolean + required: false + default: false + create_documentation: + description: "Whether to create/update VERSION_TAGS.md documentation" + type: boolean + required: false + default: false + use_gpg: + description: "Enable GPG signing (if GPG secrets provided)" + type: boolean + required: false + default: true + # Tag health configuration + max_tags: + description: "Maximum number of tags to process (safety limit)" + required: false + default: "100" + max_major_versions: + description: "Maximum number of major versions to process" + required: false + default: "10" + max_minor_versions: + description: "Maximum number of minor versions per major to process" + required: false + default: "10" + bot_patterns: + description: "JSON array of bot name patterns to identify bot signatures" + required: false + default: '["CLDMV Bot", "cldmv-bot", "github-actions[bot]"]' + include_patterns: + description: "JSON array of tag patterns to include (e.g. ['v*', 'release-*'])" + required: false + default: '["v*"]' + exclude_patterns: + description: "JSON array of tag patterns to exclude" + required: false + default: "[]" + +jobs: + update-tags: + # Skip release events fired without a tag_name (e.g. "untagged-" runs + # the bot or a prior code path can produce). The reusable workflow has its + # own tag-readiness polling for forward-facing prevention; this guard + # protects against legacy / external sources of untagged release events. + # Batch 1.2 from tmp/plan-future-workflows.md. + if: github.event_name != 'release' || github.event.release.tag_name != '' + uses: CLDMV/.github/.github/workflows/workflow-update-major-version-tags.yml@v4 + permissions: + contents: write + with: + debug: ${{ github.event.inputs.debug == 'true' }} + create_documentation: ${{ github.event.inputs.create_documentation == 'true' }} + use_gpg: ${{ github.event.inputs.use_gpg != 'false' }} + max_tags: ${{ github.event.inputs.max_tags || '100' }} + max_major_versions: ${{ github.event.inputs.max_major_versions || '10' }} + max_minor_versions: ${{ github.event.inputs.max_minor_versions || '10' }} + bot_patterns: ${{ github.event.inputs.bot_patterns || '["CLDMV Bot", "cldmv-bot", "github-actions[bot]"]' }} + include_patterns: ${{ github.event.inputs.include_patterns || '["v*"]' }} + exclude_patterns: ${{ github.event.inputs.exclude_patterns || '[]' }} + secrets: + # Map your repo/org secrets to the expected names + TAGGER_NAME: ${{ secrets.CLDMV_BOT_NAME }} + TAGGER_EMAIL: ${{ secrets.CLDMV_BOT_EMAIL }} + GPG_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.CLDMV_BOT_GPG_PASSPHRASE }} + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/v4-bootstrap.yml b/.github/workflows/v4-bootstrap.yml new file mode 100644 index 0000000..d8c86c2 --- /dev/null +++ b/.github/workflows/v4-bootstrap.yml @@ -0,0 +1,106 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/release-flow-v4/v4-bootstrap.yml +# @Date: 2026-05-26 00:00:00 -07:00 (1780124400) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/v4-bootstrap.yml +# +# Per-repo v4 bootstrap β€” thin wrapper around the shared +# `org-bootstrap-repo@v4` action. Run once per repo from the Actions tab +# (or, for org-wide rollout, prefer `local-org-onboarding.yml` in +# CLDMV/.github which fans out across many repos in parallel). +# +# What gets applied (overwrite-with-warn β€” divergences are surfaced in the +# run summary): +# - `next` + `hotfixes` branches created from master HEAD if missing +# - repo settings: allow_auto_merge=true, delete_branch_on_merge=false, +# allow_squash_merge=true, allow_merge_commit=true, +# allow_rebase_merge=false, allow_update_branch=true; plus PR-merge +# dialog defaults (merge_commit_title / squash_merge_commit_title = +# PR_TITLE, merge_commit_message / squash_merge_commit_message = +# PR_BODY) so the resulting commit captures the PR title + body +# verbatim (release-PR body = the categorized changelog β†’ lands on +# master). Per-branch ruleset allowed_merge_methods picks the method. +# - security toggles: dependabot alerts + security updates, secret +# scanning + push protection, private vulnerability reporting +# - rulesets: replaces the three rulesets (Protect Master/Next/Hotfixes) +# with the org canonical defaults +# +# What is NOT applied (GitHub doesn't expose it via REST / GraphQL / gh CLI +# β€” confirmed against community/community#188598; the bootstrap surfaces +# this as a 'Manual one-time toggles' line in the run summary): +# - Settings β†’ General β†’ Pull Requests β†’ "Auto-close issues with merged +# linked pull requests" (recommended ON). Toggle in the repo UI once. +# +# Idempotent β€” re-running is safe. Default `dry_run: true` previews +# everything before applying. +# +# Full design: CLDMV/.github docs/conventions/release-flow-v4.md +# Migration checklist: CLDMV/.github docs/migration/v3-to-v4.md +name: πŸš€ v4 Bootstrap + +on: + workflow_dispatch: + inputs: + dry_run: + description: "Dry-run: preview every mutation without firing it. Default `true` β€” set to `false` to actually apply changes." + type: boolean + required: false + default: true + code_security: + description: "Code Security policy. off = disable. public-only = enable only if this repo is public (free). all = enable (paid on private)." + type: choice + required: false + default: "off" + options: + - "off" + - "public-only" + - "all" + secret_protection: + description: "Secret Protection (scanning + push protection) policy. Same shape as code_security." + type: choice + required: false + default: "off" + options: + - "off" + - "public-only" + - "all" + steps: + description: "Subset of phases to run, comma-separated." + required: false + default: "branches,settings,security,rulesets" + +permissions: + contents: read + +jobs: + bootstrap: + name: "πŸš€ Bootstrap v4 (this repo)" + runs-on: ubuntu-latest + steps: + - name: Create App token + id: app-token + # Full-permission App token β€” bootstrap needs administration:write + # for security toggles + ruleset import, plus contents:write for + # branch creation. + uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4 + with: + client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + env: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + + - name: Bootstrap + uses: CLDMV/.github/.github/actions/github/jobs/org-bootstrap-repo@v4 + with: + # target_repo defaults to GITHUB_REPOSITORY (this repo). + github_token: ${{ steps.app-token.outputs.token }} + dry_run: ${{ github.event.inputs.dry_run }} + steps: ${{ github.event.inputs.steps }} + code_security: ${{ github.event.inputs.code_security }} + secret_protection: ${{ github.event.inputs.secret_protection }} diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml new file mode 100644 index 0000000..b414467 --- /dev/null +++ b/.github/workflows/welcome.yml @@ -0,0 +1,38 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/automation/welcome.yml +# @Date: 2026-05-20 00:00:00 -07:00 (1779606000) +# @Author: Nate Corcoran +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/welcome.yml +# +# Batch 5.3 from tmp/plan-future-workflows.md. +name: πŸ‘‹ Welcome Contributor + +# SECURITY NOTE: pull_request_target runs in the BASE repo's context with +# WRITE permissions and access to secrets. SAFE for THIS workflow because: +# - We never checkout the PR head ref +# - We never run code from the PR (no `run:` step uses PR data) +# - We only call REST APIs to read prior interactions and post a comment +# DO NOT add a checkout step or any step that executes PR-supplied content. +on: + issues: + types: [opened] + pull_request_target: + types: [opened] + +permissions: + issues: write + pull-requests: write + +jobs: + welcome: + uses: CLDMV/.github/.github/workflows/reusable-welcome.yml@v4 + # Optional. Without these, the welcome comment is posted by + # github-actions[bot]. With these, it's posted by your CLDMV bot App. + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }}