diff --git a/.github/workflows/branch-retention.yml b/.github/workflows/branch-retention.yml new file mode 100644 index 0000000..98eae99 --- /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 CLDMV +# @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 index 3bc475c..ee4db28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,114 +1,312 @@ # -# @Project: @cldmv/wisp -# @Filename: /.github/workflows/ci.yml -# @Date: 2025-10-30 15:13:04 -07:00 (1761862384) -# @Author: Nate Hyson +# @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: -# ----- -# @Last modified by: Nate Hyson (Shinrai@users.noreply.github.com) -# @Last modified time: 2025-10-30 15:13:44 -07:00 (1761862424) -# ----- -# @Copyright: Copyright (c) 2013-2025 Catalyzed Motivation Inc. All rights reserved. +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. # - # Individual repo: .github/workflows/ci.yml name: πŸ§ͺ CI Tests & Build on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - branches: [master, main] - paths-ignore: - - "**.md" - - "docs/**" - - "*.md" - - "LICENSE" - - ".gitignore" - push: - branches: ["**"] - paths-ignore: - - "**.md" - - "docs/**" - - "*.md" - - "LICENSE" - - ".gitignore" - 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" - package_manager: - description: "Package manager (npm or yarn)" - type: string - required: false - default: "npm" + # 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: true + 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 - uses: CLDMV/.github/.github/workflows/workflow-ci.yml@v1 - with: - package_name: "@cldmv/slothlet" # Required: Your NPM package name - debug: ${{ github.event.inputs.debug == 'true' }} - node_version: ${{ github.event.inputs.node_version || 'lts/*' }} - min_node_version: ${{ github.event.inputs.min_node_version || '16.14' }} - max_node_major: ${{ github.event.inputs.max_node_major || '22' }} - package_manager: ${{ github.event.inputs.package_manager || 'npm' }} - test_command: "npm test" - build_command: "npm run build:ci" - skip_performance_tests: false - skip_matrix_tests: false + 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/wisp" # Required: wisp's 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' }} + # LTS-only matrix (even majors: 20, 22, 24, …) on every event. Odd majors + # (21, 23, …) are non-LTS interim releases, and the native-binding test + # toolchain (vitest 4 / rolldown / vite 8) excludes them via `engines` + # (`^20.19.0 || >=22.12.0`), so a "full matrix" on them only re-discovers a + # known toolchain gap ("Cannot find native binding") rather than a real + # per-version regression. workflow_dispatch can still opt out (set false). + lts_only_matrix: ${{ github.event.inputs.lts_only_matrix != 'false' }} + package_manager: ${{ github.event.inputs.package_manager || 'npm' }} + test_command: "npm test" # mocha (test/**/*.mjs) + test_environment: ${{ github.event.inputs.test_environment || 'development' }} # Alternative to setting in test_command + # wisp's "build" is type-declaration generation. `build:types` (tsc) both + # emits types/ AND type-checks (tsc fails on type errors), so it carries + # the type check on every matrix node. Not `build:ci` β€” that also re-runs + # mocha, which `test_command` above already ran. + build_command: "npm run build:types" + 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 + # wisp has no coverage tooling (mocha only, no `ci:coverage` script, no + # coverage-summary.json, and no `badges` branch), so the coverage-badge + # job is disabled. Enable it once a coverage command is added. + 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. + # Disabled for the same reason as enable_coverage_badge above β€” no + # coverage data is produced, so there is nothing to inject into the PR. + 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_ID: ${{ secrets.CLDMV_BOT_APP_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + # 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 runs on BOTH push and PR and mirrors ci result - required-check: - name: βœ… Required PR Check - needs: ci - # run on both push and PR so the context exists on the PR head SHA - if: always() - runs-on: ubuntu-latest - steps: - - name: Mirror reusable result - run: | - echo "ci.result=${{ needs.ci.result }}" - if [ "${{ needs.ci.result }}" = "success" ]; then - echo "Reusable CI passed." - exit 0 - elif [ "${{ needs.ci.result }}" = "failure" ] || [ "${{ needs.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 + # βœ… 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/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000..f8ac698 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,60 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/security/cla.yml +# @Date: 2026-05-20 00:00:00 -07:00 (1779606000) +# @Author: Nate Corcoran CLDMV +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/cla.yml +# +# Per-CLA-version signing with per-repo override support. Each commit author +# must either: +# - Be in the org (silent pass via /orgs/CLDMV/members lookup) +# - Be in the exempt-bots list +# - Already have a signature record at the active (scope, version) in the +# central ledger repo (default: CLDMV/.cla-signatures) +# - Reply on this PR with the exact required text +# +# Default vs. override scope: +# - DEFAULT (this repo has NO root-level CLA.md): the bot uses the org-wide +# CLA at cla-versions/v.md in the ledger. Signing once covers every +# CLDMV repo that uses the default until the major.minor is bumped. +# - OVERRIDE (this repo HAS a root-level CLA.md): the bot enforces the +# consumer-repo text and reads the version from its header. Signatures +# live under signatures//overrides///v/ and +# are scoped to this repo only. +# +# Required setup: +# - Bot App must have `Organization permissions β†’ Members: read` for the +# org-member exemption. +# - Bot App must have `Repository contents: write` on the ledger repo. +# - Optional `CLDMV_CLA_BOT_APP_CLIENT_ID` / `CLDMV_CLA_BOT_APP_PRIVATE_KEY` +# org secrets override the general bot identity for CLA actions only. +name: πŸ“œ CLA + +on: + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review] + issue_comment: + types: [created] + +permissions: + contents: read + pull-requests: write + statuses: write + issues: write + +jobs: + cla: + uses: CLDMV/.github/.github/workflows/reusable-cla.yml@v4 + with: + cla_version: "1.0" + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + CLA_BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_CLA_BOT_APP_CLIENT_ID }} + CLA_BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_CLA_BOT_APP_PRIVATE_KEY }} + TAGGER_NAME: ${{ secrets.CLDMV_BOT_NAME }} + TAGGER_EMAIL: ${{ secrets.CLDMV_BOT_EMAIL }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..5dc8e7a --- /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 CLDMV +# @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..1f8e1ae --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,56 @@ +# +# @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 CLDMV +# @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 defaults to "merge" β€” Dependabot PRs target next / hotfixes, + # whose rulesets are merge-only. Override only if your branches differ. + # merge_method: "merge" + # 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..47e56c3 --- /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 CLDMV +# @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..c246478 --- /dev/null +++ b/.github/workflows/hotfix-redirector.yml @@ -0,0 +1,71 @@ +# +# @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 its base isn't Dependabot's routine +# target-branch (default "next") β€” GitHub always overrides +# dependabot.yml's target-branch for security updates, so a base landing +# on the default branch instead of "next" is itself the signal +# (Dependabot's PR body doesn't reliably embed the GHSA id, so a literal +# GHSA reference is only a secondary check). 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..9794b46 --- /dev/null +++ b/.github/workflows/hotfixes-release.yml @@ -0,0 +1,169 @@ +# +# @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] + workflow_dispatch: # manual kick β€” e.g. to open/refresh the PR for content already on `hotfixes` + +permissions: + contents: write + pull-requests: write + +concurrency: + group: hotfixes-release-${{ github.repository }} + cancel-in-progress: false + +jobs: + plan: + # Loop guard: skip only the release machinery's own `chore: bump version` + # push (create/update-release-pr writes it back to `hotfixes`). Do NOT gate + # on github.actor β€” dependabot security PRs are auto-merged into `hotfixes` + # BY the bot, so gating the bot out meant the hotfixesβ†’master release PR + # was never created (every push here is a bot merge). Reset force-pushes are + # harmless: the detect step below no-ops when in sync with master. + # workflow_dispatch bypasses the guard (event_name != 'push'). + if: | + github.event_name != 'push' || + !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/wisp" + build-command: "npm run build:types" + 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/wisp" + build-command: "npm run build:types" + 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..17e3b04 --- /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 CLDMV +# @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..1115757 --- /dev/null +++ b/.github/workflows/master-commit-audit.yml @@ -0,0 +1,52 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/release-companions/master-commit-audit.yml +# @Date: 2026-05-31 00:00:00 -07:00 (1780210800) +# @Author: Nate Corcoran CLDMV +# @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. +# +# Thin caller: steps, the action ref, and the canonical commit-subject pattern +# set all live in reusable-master-commit-audit.yml@v4 (the patterns come from +# the audit-commit-subject action's default). Nothing here can drift. +name: 🧾 Master Commit Audit + +on: + push: + branches: [master, main] + +permissions: + contents: read + issues: write + +jobs: + audit: + uses: CLDMV/.github/.github/workflows/reusable-master-commit-audit.yml@v4 + with: + # allowed_patterns omitted β†’ inherit the canonical default + # (release + chore + merge). Uncomment ONLY if this repo lands other + # commit shapes directly on the default branch: + # allowed_patterns: | + # ^release: v\d+\.\d+\.\d+( - .+?)?( \(#\d+\))?$ + # ^chore(\([^)]+\))?: .+ + # ^Merge pull request #\d+ from .+ + # ^feat(\([^)]+\))?: .+ + issue_labels: "type: ci,priority: high" + # issue_assignee: "shinrai" # uncomment to auto-assign + # Optional bot App credentials β€” when set, the audit issue is filed by + # the consumer's bot App instead of github-actions[bot]. Remove both + # lines 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/next-release.yml b/.github/workflows/next-release.yml new file mode 100644 index 0000000..74cea9a --- /dev/null +++ b/.github/workflows/next-release.yml @@ -0,0 +1,181 @@ +# +# @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] + workflow_dispatch: # manual kick β€” e.g. to open/refresh the PR for content already on `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: skip only the release machinery's own `chore: bump version` + # push (create/update-release-pr writes it back to `next`). Do NOT gate on + # github.actor β€” dependabot security PRs are auto-merged BY the bot, and + # excluding the bot actor meant bot-merged PRs never refreshed the release + # PR (fatal on `hotfixes`, where every merge is a bot auto-merge). Reset + # force-pushes are harmless: the detect step below no-ops when in sync with + # master. workflow_dispatch bypasses the guard (event_name != 'push'). + if: | + github.event_name != 'push' || + !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/wisp" + build-command: "npm run build:types" + 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/wisp" + build-command: "npm run build:types" + 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 index 8e37188..f2aaca4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,107 +1,137 @@ # -# @Project: @cldmv/wisp -# @Filename: /.github/workflows/publish.yml -# @Date: 2025-10-30 15:13:04 -07:00 (1761862384) -# @Author: Nate Hyson +# @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: -# ----- -# @Last modified by: Nate Hyson (Shinrai@users.noreply.github.com) -# @Last modified time: 2025-10-30 15:14:07 -07:00 (1761862447) -# ----- -# @Copyright: Copyright (c) 2013-2025 Catalyzed Motivation Inc. All rights reserved. +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. # - # Individual repo: .github/workflows/publish.yml name: πŸ“¦ Release and Publish on: - pull_request: - types: [closed] - branches: [master, main] - 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" - 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 + 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.pull_request.merged == true || github.event_name == 'workflow_dispatch' - uses: CLDMV/.github/.github/workflows/workflow-publish.yml@v1 - with: - package_name: "@cldmv/slothlet" # Required: 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 || '16.14' }} - max_node_major: ${{ github.event.inputs.max_node_major || '22' }} - test_command: "npm test" - 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_ID: ${{ secrets.CLDMV_BOT_APP_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 }} + 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/wisp" # Required: wisp's 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" # mocha (test/**/*.mjs) + test_environment: ${{ github.event.inputs.test_environment || 'development' }} # Alternative to setting in test_command + # build:types (tsc) emits the published types/ dir and type-checks it. + build_command: "npm run build:types" + # --- Satellite packages (optional) ---------------------------------------- + # Publish extra packages carved from this build's output (e.g. locale JSON, + # generated types) at the SAME version/commit as the core, each with its own + # @scope/name@version tag + GitHub Release. Uncomment to opt in. + # extra_packages: a JSON [{ name, dir }] array, OR a glob (single '*' in the + # final path segment, e.g. "dist-packages/*"). Empty = disabled. + # build_subpackages_command: runs after build_command to produce + # dist-packages// β€” omit if build_command already does the carve. + # First publish of each new @scope/name needs a one-time token publish + + # trusted-publisher setup. Full details + the contract: + # docs/conventions/satellite-packages.md + # extra_packages: "dist-packages/*" + # build_subpackages_command: "npm run build:subpackages" + # -------------------------------------------------------------------------- + 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/release-notify.yml b/.github/workflows/release-notify.yml new file mode 100644 index 0000000..6552044 --- /dev/null +++ b/.github/workflows/release-notify.yml @@ -0,0 +1,37 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/release-companions/release-notify.yml +# @Date: 2026-05-26 00:00:00 -07:00 (1780124400) +# @Author: Nate Corcoran CLDMV +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/release-notify.yml +# +# Fires on `release: published` and dispatches the release announcement to +# any enabled webhook. No config file β€” each channel is just a secret: +# +# DISCORD_RELEASES_PUBLIC_WEBHOOK / DISCORD_RELEASES_PRIVATE_WEBHOOK +# SLACK_RELEASES_PUBLIC_WEBHOOK / SLACK_RELEASES_PRIVATE_WEBHOOK +# GENERIC_RELEASES_PUBLIC_WEBHOOK / GENERIC_RELEASES_PRIVATE_WEBHOOK +# +# Visibility is determined automatically from the repo: GitHub `public` β†’ +# PUBLIC, `private` or `internal` β†’ PRIVATE. Set the org-level secret in +# CLDMV for the default URL; set a repo-level secret with the same name to +# override (or to an empty string to mute that channel for this repo). +name: πŸ“£ Release Notify + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + notify: + # Defensive: skip untagged releases (mirrors Batch 1.2's filter) + if: github.event.release.tag_name != '' + uses: CLDMV/.github/.github/workflows/reusable-release-notifier.yml@v4 + secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index f09953c..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,124 +0,0 @@ -# -# @Project: @cldmv/wisp -# @Filename: /.github/workflows/release.yml -# @Date: 2025-10-30 15:13:04 -07:00 (1761862384) -# @Author: Nate Hyson -# @Email: -# ----- -# @Last modified by: Nate Hyson (Shinrai@users.noreply.github.com) -# @Last modified time: 2025-10-30 15:14:12 -07:00 (1761862452) -# ----- -# @Copyright: Copyright (c) 2013-2025 Catalyzed Motivation Inc. All rights reserved. -# - - -# Individual repo: .github/workflows/release.yml -# -# This workflow intelligently detects release commits in the commit history -# and creates release PRs. It runs on human commits to branches (except main/master) -# and uses smart commit analysis to determine if a release should be created. -# Bot commits are ignored to prevent infinite loops. -# -name: πŸš€ Create Release PR - -on: - push: - branches-ignore: [master, main] - paths-ignore: - - "**.md" - - "docs/**" - 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 create PR or make changes" - 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" - package_manager: - description: "Package manager (npm or yarn)" - type: string - required: false - default: "npm" - version_bump: - description: "Type of version bump (major, minor, patch). Leave empty for auto-detection from commit message." - type: string - required: false - default: "" - version: - description: "Specific version for release (auto-calculated if not provided)" - type: string - required: false - default: "" - is_prerelease: - description: "Whether this is a prerelease" - type: boolean - required: false - default: false - create_documentation: - description: "Whether to create/update VERSION_TAGS.md" - type: boolean - required: false - default: true - -jobs: - create-release-pr: - # Prevent infinite loops: Don't run on bot commits or if PR already exists - if: | - github.actor != 'cldmv-bot[bot]' && - github.actor != 'github-actions[bot]' && - !startsWith(github.event.head_commit.message, 'chore: bump version') - uses: CLDMV/.github/.github/workflows/workflow-release.yml@v1 - with: - package_name: "@cldmv/slothlet" # Required: 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/*' }} - min_node_version: ${{ github.event.inputs.min_node_version || '16.14' }} - max_node_major: ${{ github.event.inputs.max_node_major || '22' }} - package_manager: ${{ github.event.inputs.package_manager || 'npm' }} - test_command: "npm test" - build_command: "npm run build:ci" - version_bump: ${{ github.event.inputs.version_bump || '' }} - version: ${{ github.event.inputs.version || '' }} - is_prerelease: ${{ github.event.inputs.is_prerelease == 'true' }} - release_source_only: false - create_documentation: ${{ github.event.inputs.create_documentation != 'false' }} - skip_performance_tests: false - skip_matrix_tests: 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 - # To set up App authentication, add these secrets to your repository settings: - secrets: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - # CLDMV Bot credentials for enhanced permissions and proper attribution - # Uncomment these lines to use your bot instead of github-actions[bot] - BOT_APP_ID: ${{ secrets.CLDMV_BOT_APP_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} - # Optional: GPG signing credentials (only needed if use_gpg: true) - 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/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..3c4561b --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,56 @@ +# +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/security/scorecard.yml +# @Date: 2026-05-31 00:00:00 -07:00 (1780210800) +# @Author: Nate Corcoran CLDMV +# @Email: +# @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. +# + +# Individual repo: .github/workflows/scorecard.yml +# +# OpenSSF Scorecard β€” scans the repo against ~18 security best-practice checks +# and produces a 0-10 score. Thin caller: the steps and the SHA-pinned +# scorecard-action version live in reusable-scorecard.yml@v4, so the action +# version can't drift in this copy (it just calls the org reusable). Triggers +# stay here, per OpenSSF's recommended setup. +name: πŸ”¬ OpenSSF Scorecard + +on: + branch_protection_rule: + schedule: + - cron: "32 7 * * 1" # weekly Monday 07:32 UTC + push: + branches: [master, main] + workflow_dispatch: + +# Caller must grant what the reusable needs β€” notably id-token: write for the +# OpenSSF transparency-log publish. +# +# No workflow-level `permissions:` here β€” grant on the `analyze` job below +# instead. scorecard-action's publish step verifies that write permissions +# were granted JOB-scoped, not workflow-wide (matching OSSF's own example: +# https://github.com/ossf/scorecard-action#example-workflow). A workflow-level +# grant satisfies GitHub's own reusable-workflow permission rules fine, but +# still trips scorecard-action's own check β€” the rejection ("workflow +# verification failed: global perm is set to write: permission for X is set +# to write") means "granted globally," not "forbidden." +# +# Do NOT add security-events: write here while publish_results: true below. +# scorecard-action's publish step rejects submissions from a workflow whose +# token has security-events write access (it verifies the caller can't have +# tampered with results before they hit the public transparency log). That +# trade-off means the reusable's own SARIF-to-Security-tab upload step has no +# permission to run in this configuration; the public OpenSSF badge is the +# thing this default enables, so consumers take that trade-off by default. +# Only add security-events: write back (job-scoped) if publish_results is set +# to false instead (SARIF-to-Security-tab upload, no public badge). +jobs: + analyze: + permissions: + id-token: write + contents: read + actions: read + uses: CLDMV/.github/.github/workflows/reusable-scorecard.yml@v4 + with: + publish_results: true # set false for private repos / to skip the public badge diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..717393c --- /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 CLDMV +# @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..6f1245b --- /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 CLDMV +# @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 index 33aeb2e..cabcb26 100644 --- a/.github/workflows/update-major-version-tags.yml +++ b/.github/workflows/update-major-version-tags.yml @@ -1,14 +1,18 @@ +# +# @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] - push: - branches: - - main - - master - workflow_dispatch: inputs: debug: @@ -54,7 +58,13 @@ on: jobs: update-tags: - uses: CLDMV/.github/.github/workflows/workflow-update-major-version-tags.yml@v1 + # 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: @@ -73,5 +83,5 @@ jobs: TAGGER_EMAIL: ${{ secrets.CLDMV_BOT_EMAIL }} GPG_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.CLDMV_BOT_GPG_PASSPHRASE }} - BOT_APP_ID: ${{ secrets.CLDMV_BOT_APP_ID }} + 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..c22571d --- /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 CLDMV +# @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 }}