diff --git a/.github/workflows/feature-pr.yml b/.github/workflows/feature-pr.yml index ffbb123..fcee49e 100644 --- a/.github/workflows/feature-pr.yml +++ b/.github/workflows/feature-pr.yml @@ -1,7 +1,6 @@ # -# @Project: @cldmv/git-embedded -# @Filename: /.github/workflows/feature-pr.yml -# @Date: 2026-07-18 15:49:12 -07:00 (1784414952) +# @Project: @cldmv/.github +# @Filename: /examples/individual-repo-workflows/release-flow-v4/feature-pr.yml # @Author: Nate Corcoran # @Email: # @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. @@ -9,34 +8,28 @@ # Individual repo: .github/workflows/feature-pr.yml # -# v4 ergonomics: auto-opens (and refreshes) a PR from a code-side branch to -# the right integration branch on every push. +# v4 ergonomics: auto-opens (and refreshes) a PR from a code-side branch to the +# right integration branch on every push. # # Mapping (matches CLDMV/.github docs/conventions/branch-naming.md): # feat/*, feature/*, fix/*, release/*, chore/*, refactor/*, # docs/*, ci/*, perf/*, test/*, style/* β†’ next # hotfix/* β†’ hotfixes # -# Reserved branches NOT auto-PR'd: dependabot/* and copilot/* (they manage -# their own PRs); badges, gh-pages (bot-only); master/main (the target). +# Reserved branches NOT auto-PR'd: dependabot/* and copilot/* (they manage their +# own PRs); badges, gh-pages (bot-only); master/main (the target). # -# On first push: creates the PR with a categorized changelog body (same -# format the v4 release-PR machinery generates). On subsequent pushes: -# refreshes the existing PR's body with the latest categorized commits. -# Uses the shared get-commit-range + generate-comprehensive-changelog -# actions for the format, so consumer PRs look identical to release PRs -# in structure (Breaking Changes / Features / Bug Fixes / Other Changes / -# Contributors). -# -# Skipped automatically: bot pushes (your bot App's login / github-actions[bot]) -# and any push whose head commit starts with 'chore: bump version'. +# Thin caller: all job logic (target detection, changelog body, PR create/ +# refresh) lives in the reusable, pinned at @v4. Bumping the pin carries fixes +# without editing this file. The `push` trigger and its branch-prefix list stay +# here (GitHub requires the trigger local, and the list is per-repo config). name: πŸ”€ Feature PR (v4) on: push: branches: # CUSTOMIZE: prune this list to whichever branch prefixes your - # repo uses. Must align with the `case` statement below. + # repo uses. Must align with the `case` statement in the reusable. - 'feat/**' - 'feature/**' - 'fix/**' @@ -50,215 +43,17 @@ on: - 'style/**' - 'hotfix/**' -permissions: - contents: read - pull-requests: write - -# Serialize per-branch so a flurry of pushes doesn't race the -# "does a PR already exist?" check. concurrency: group: feature-pr-${{ github.repository }}-${{ github.ref }} cancel-in-progress: false jobs: open-pr: - # Loop guard: replace 'cldmv-bot[bot]' with your bot App's login. - if: | - github.actor != 'cldmv-bot[bot]' && - github.actor != 'github-actions[bot]' && - !startsWith(github.event.head_commit.message, 'chore: bump version') - runs-on: ubuntu-latest - steps: - - name: Determine target branch - id: target - shell: bash - run: | - branch="${GITHUB_REF#refs/heads/}" - echo "branch=$branch" >> "$GITHUB_OUTPUT" - # CUSTOMIZE: adjust the case arms to match your branch - # conventions. Anything not matched is silently skipped - # (so master/main, badges, gh-pages, dependabot/*, etc. - # are safe regardless of what fires the workflow). - # The flow_label sorts first in the PR's label list - # (the leading `!` precedes every letter alphabetically) - # so a glance at any PR's badges reveals which lane it's in. - # Lane (target) AND declared type both come from the branch - # prefix β€” the v4 convention requires a typed prefix, so a - # `docs/*` branch is a docs change, `fix/*` a fix, etc. The flow - # label is `! β†’ ` so it reflects what the PR actually - # is, not a blanket "feature". (Previously every next-lane branch - # got `! feature β†’ next`, mislabelling docs/fix/chore PRs.) - case "$branch" in - hotfix/*) target="hotfixes"; type="hotfix" ;; - feat/*|feature/*) target="next"; type="feature" ;; - fix/*) target="next"; type="fix" ;; - docs/*) target="next"; type="docs" ;; - chore/*) target="next"; type="chore" ;; - refactor/*) target="next"; type="refactor" ;; - ci/*) target="next"; type="ci" ;; - perf/*) target="next"; type="perf" ;; - test/*) target="next"; type="test" ;; - style/*) target="next"; type="style" ;; - release/*) target="next"; type="release" ;; - *) - target=""; type=""; flow_label="" - echo "::notice::Branch '$branch' does not match any auto-PR pattern; skipping." - ;; - esac - # Leading `!` sorts the flow label first in the PR's badge list, - # so a glance reveals both the change type and its lane. - if [ -n "$target" ]; then - flow_label="! ${type} β†’ ${target}" - fi - echo "target=$target" >> "$GITHUB_OUTPUT" - echo "flow_label=$flow_label" >> "$GITHUB_OUTPUT" - - - name: Create App token - id: app-token - if: steps.target.outputs.target != '' - 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: Check for existing PR - id: existing - if: steps.target.outputs.target != '' - shell: bash - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - run: | - pr=$(gh pr list --repo "$GITHUB_REPOSITORY" \ - --head "${{ steps.target.outputs.branch }}" \ - --base "${{ steps.target.outputs.target }}" \ - --state open \ - --json number --jq '.[0].number // ""') - echo "number=$pr" >> "$GITHUB_OUTPUT" - if [ -n "$pr" ]; then - echo "::notice::Existing PR #$pr will be refreshed." - fi - - - name: Checkout (full history for git log) - if: steps.target.outputs.target != '' - uses: CLDMV/.github/.github/actions/common/steps/checkout-code@v4 - with: - fetch-depth: 0 - - - name: Fetch target branch ref - if: steps.target.outputs.target != '' - shell: bash - run: | - git fetch --quiet origin "${{ steps.target.outputs.target }}" - - - name: Get categorized commits (base..head) - id: commits - if: steps.target.outputs.target != '' - uses: CLDMV/.github/.github/actions/git/steps/get-commit-range@v4 - with: - base-ref: origin/${{ steps.target.outputs.target }} - head-ref: HEAD - - - name: Detect feature commits in range - id: feat - if: steps.target.outputs.target != '' - shell: bash - env: - COMMITS: ${{ steps.commits.outputs.commits }} - run: | - # `type: feature` is applied when the range contains a feature, - # mirroring the changelog's own "Features" section: get-commit- - # range tags `feat:` (and content-categorized add/new) commits - # as category "feature". Reuses the already-computed commits. - has_feature=false - if printf '%s' "$COMMITS" | jq -e 'any(.[]; .category == "feature")' >/dev/null 2>&1; then - has_feature=true - fi - echo "has_feature=$has_feature" >> "$GITHUB_OUTPUT" - echo "πŸ“Š feature detected in range: $has_feature" - - - name: Generate categorized changelog body - id: changelog - if: steps.target.outputs.target != '' - uses: CLDMV/.github/.github/actions/git/steps/generate-comprehensive-changelog@v4 - with: - commits: ${{ steps.commits.outputs.commits }} - commit-range: ${{ steps.commits.outputs.commit-range }} - env: - GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - - - name: Save body to file - if: steps.target.outputs.target != '' - shell: bash - env: - BODY: ${{ steps.changelog.outputs.changelog-content }} - run: | - printf '%s' "$BODY" > /tmp/pr-body.md - - - name: Create PR (first push) - if: steps.target.outputs.target != '' && steps.existing.outputs.number == '' - shell: bash - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - HEAD_COMMIT_MSG: ${{ github.event.head_commit.message }} - HEAD_BRANCH: ${{ steps.target.outputs.branch }} - BASE_BRANCH: ${{ steps.target.outputs.target }} - run: | - # Title = head commit's first line β€” preserves the - # conventional-commit prefix the release-PR title-normalizer - # and commit-type aggregator expect. - title=$(printf '%s\n' "$HEAD_COMMIT_MSG" | head -1) - pr_url=$(gh pr create \ - --repo "$GITHUB_REPOSITORY" \ - --base "$BASE_BRANCH" \ - --head "$HEAD_BRANCH" \ - --title "$title" \ - --body-file /tmp/pr-body.md) - echo "::notice::Opened $pr_url" - # Apply the flow label (sorts first in the PR's badge list). - # `|| true` so a missing label in the repo (catalog not yet - # synced) doesn't fail the workflow. - if [ -n "${{ steps.target.outputs.flow_label }}" ]; then - gh pr edit "$pr_url" --add-label "${{ steps.target.outputs.flow_label }}" || true - fi - # Apply `type: feature` when the range implements a feature. - if [ "${{ steps.feat.outputs.has_feature }}" = "true" ]; then - gh pr edit "$pr_url" --add-label "type: feature" || true - fi - { - echo "### πŸ”€ Auto-opened PR" - echo "" - echo "$pr_url" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Refresh existing PR body - if: steps.target.outputs.target != '' && steps.existing.outputs.number != '' - shell: bash - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - PR_NUMBER: ${{ steps.existing.outputs.number }} - run: | - gh pr edit "$PR_NUMBER" \ - --repo "$GITHUB_REPOSITORY" \ - --body-file /tmp/pr-body.md - # Re-apply the flow label so a manual removal doesn't - # strand the PR without its lane indicator. - if [ -n "${{ steps.target.outputs.flow_label }}" ]; then - gh pr edit "$PR_NUMBER" \ - --repo "$GITHUB_REPOSITORY" \ - --add-label "${{ steps.target.outputs.flow_label }}" || true - fi - # Apply `type: feature` when the range implements a feature. - if [ "${{ steps.feat.outputs.has_feature }}" = "true" ]; then - gh pr edit "$PR_NUMBER" \ - --repo "$GITHUB_REPOSITORY" \ - --add-label "type: feature" || true - fi - echo "::notice::Refreshed PR #${PR_NUMBER} body" - { - echo "### πŸ”€ Refreshed PR body" - echo "" - echo "PR #${PR_NUMBER}" - } >> "$GITHUB_STEP_SUMMARY" + permissions: + contents: read + pull-requests: write + uses: CLDMV/.github/.github/workflows/workflow-feature-pr.yml@v4 + secrets: + # Map your repo/org secrets to the expected names. + 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 index 79383d5..830cad6 100644 --- a/.github/workflows/hotfix-redirector.yml +++ b/.github/workflows/hotfix-redirector.yml @@ -1,6 +1,6 @@ # -# @Project: @cldmv/git-embedded -# @Filename: /.github/workflows/hotfix-redirector.yml +# @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: @@ -13,55 +13,39 @@ # # 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`). +# 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. 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. +# Thin caller: all job logic (token, checkout, git identity, redirect action) +# lives in the reusable, pinned at @v4. Bumping the pin carries new requirements +# (e.g. the checkout + git identity the cherry-pick path needs) without editing +# this file. 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. +# SECURITY NOTE: pull_request_target runs in the BASE repo's context with WRITE +# permissions + secrets. The reusable checks out `hotfixes` (a trusted base-repo +# branch, NOT the PR head/fork) and only cherry-picks/pushes against it. # -# `opened` only (NOT `edited`): if a maintainer manually re-targets the PR, -# we must not fight them by redirecting again. +# `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 + permissions: + contents: write + pull-requests: write + uses: CLDMV/.github/.github/workflows/workflow-hotfix-redirector.yml@v4 + secrets: + # Map your repo/org secrets to the expected names. + 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/hotfixes-release.yml b/.github/workflows/hotfixes-release.yml index d7cfeea..a228e45 100644 --- a/.github/workflows/hotfixes-release.yml +++ b/.github/workflows/hotfixes-release.yml @@ -1,6 +1,6 @@ # -# @Project: @cldmv/git-embedded -# @Filename: /.github/workflows/hotfixes-release.yml +# @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: @@ -12,153 +12,44 @@ # 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`. +# (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. # -# 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). +# Thin caller: all job logic (plan / create / refresh) lives in the reusable, +# pinned at @v4. Bumping the pin carries fixes without editing this file. +# CUSTOMIZE `package_name` / `build_command` to match your package (same values +# as your next-release.yml). name: πŸš‘ Hotfixes Release (v4) on: push: branches: [hotfixes] - -permissions: - contents: write - pull-requests: write + workflow_dispatch: # manual kick β€” e.g. to open/refresh the PR for content already on `hotfixes` 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 }} + release: + permissions: + contents: write + pull-requests: write + uses: CLDMV/.github/.github/workflows/workflow-hotfixes-release.yml@v4 + with: + package_name: "@cldmv/git-embedded" + build_command: "echo 'βœ“ no build step'" + secrets: + # Map your repo/org secrets to the expected names. + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + # Optional release-PR notifier webhooks β€” each is independently + # opt-in: leave one unset and that channel is silently skipped. + # Delete the lines you don't use. + 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-release.yml b/.github/workflows/next-release.yml index bf0c701..5d4a9f8 100644 --- a/.github/workflows/next-release.yml +++ b/.github/workflows/next-release.yml @@ -1,6 +1,6 @@ # -# @Project: @cldmv/git-embedded -# @Filename: /.github/workflows/next-release.yml +# @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: @@ -17,19 +17,19 @@ # 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). +# Thin caller: all job logic (plan / create / refresh) lives in the reusable, +# pinned at @v4. Bumping the pin carries fixes without editing this file. +# 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 (optional; +# defaults to `npm run build:ci`) name: πŸš€ Next Release (v4) on: push: branches: [next] - -permissions: - contents: write - pull-requests: write + workflow_dispatch: # manual kick β€” e.g. to open/refresh the PR for content already on `next` # Serialize: each run re-resolves the current PR state, so queueing (not # cancelling) avoids a create/refresh race when pushes land back-to-back. @@ -38,141 +38,24 @@ concurrency: 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 }} + release: + permissions: + contents: write + pull-requests: write + uses: CLDMV/.github/.github/workflows/workflow-next-release.yml@v4 + with: + package_name: "@cldmv/git-embedded" + build_command: "echo 'βœ“ no build step'" + secrets: + # Map your repo/org secrets to the expected names. + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + # Optional release-PR notifier webhooks β€” each is independently + # opt-in: leave one unset and that channel is silently skipped. + # Delete the lines you don't use. + 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 index f19a665..87ba088 100644 --- a/.github/workflows/next-reset.yml +++ b/.github/workflows/next-reset.yml @@ -1,6 +1,6 @@ # -# @Project: @cldmv/git-embedded -# @Filename: /.github/workflows/next-reset.yml +# @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: @@ -16,184 +16,28 @@ # - `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. +# * hotfix release (hotfixes β†’ master): MERGE master into `next` instead, +# so next's accumulated feature work is preserved (Β§7.2, option B). # -# 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. +# Thin caller: all job logic (the wait-for-tags gate + the branch sync) lives in +# the reusable, pinned at @v4. Bumping the pin carries fixes without editing +# this file. 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 }} + sync: + permissions: + contents: write + uses: CLDMV/.github/.github/workflows/workflow-next-reset.yml@v4 + secrets: + # Map your repo/org secrets to the expected names. + 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/pr-title-normalizer.yml b/.github/workflows/pr-title-normalizer.yml index f7dd668..adba973 100644 --- a/.github/workflows/pr-title-normalizer.yml +++ b/.github/workflows/pr-title-normalizer.yml @@ -1,6 +1,6 @@ # -# @Project: @cldmv/git-embedded -# @Filename: /.github/workflows/pr-title-normalizer.yml +# @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: @@ -9,56 +9,37 @@ # 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.) +# 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 β€” the underlying +# action shipped in v3.3.0; it owns all skip logic: bot authors, the +# long-running release PRs, titles already starting with `release:`, and titles +# that already conform.) +# +# Thin caller: all job logic lives in the reusable, pinned at @v4. Bumping the +# pin carries fixes without editing this file. 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. +# SECURITY NOTE: pull_request_target runs in the BASE repo's context with WRITE +# permissions and access to secrets. SAFE β€” the reusable's path is API-only (it +# never checks out or executes PR content). 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 }} + permissions: + contents: read + pull-requests: write + uses: CLDMV/.github/.github/workflows/workflow-pr-title-normalizer.yml@v4 + secrets: + # Map your repo/org secrets to the expected names. + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }}