Hierarchical Branch Development And Release Workflow
nats-sinks uses a hierarchical branch workflow. The main branch is the
public release integration branch. It should only change when maintainers
explicitly decide to release and merge the current release development branch.
Ordinary development happens below the release branch:
- one
release-vX.Y.Zbranch exists for the upcoming release, - each feature or managed issue gets its own issue branch from that release branch,
- each bug found while developing a feature gets its own bug branch from the feature branch,
- solved bug branches merge back into their feature branch,
- completed feature branches merge back into the release branch,
- the release branch merges into
mainonly for an explicit release.
This workflow protects users who install from PyPI, GitHub Releases, Read the Docs, or GitHub Pages. It also gives maintainers a clear audit trail: every released change can be traced to an issue, a branch, a pull request, local test evidence, documentation updates, release notes, and, when relevant, GitHub Actions release validation.
Ordinary branch pushes are intentionally quiet. GitHub Actions should not start
after every small commit. Validation is started deliberately when a maintainer
is ready to merge a feature branch into the release branch or when the release
branch is ready to merge into main.
Required Flow
flowchart TD
Main[main]
Release[release-vX.Y.Z]
Issue[GitHub issue]
Feature[issue-N-short-name]
BugIssue[GitHub bug issue]
Bug[bug-N-short-name]
LocalChecks[Local checks and evidence]
FeaturePR[PR: issue branch to release branch]
ReleasePR[PR: release branch to main]
ReleaseChecks[Manual release validation]
Tag[Tag main]
Publish[GitHub Release, PyPI, docs]
Main --> Release
Issue --> Feature
Release --> Feature
Feature --> BugIssue
BugIssue --> Bug
Feature --> Bug
Bug --> Feature
Feature --> LocalChecks
LocalChecks --> FeaturePR
FeaturePR --> Release
Release --> ReleasePR
ReleasePR --> ReleaseChecks
ReleaseChecks --> Main
Main --> Tag
Tag --> Publish
The release boundary is intentionally conservative:
- Start every release from
mainwith a branch namedrelease-vX.Y.Z. - Start every feature or managed issue from the release branch with a branch
named
issue-N-short-descriptionorfeature-N-short-description. - If a defect is found during feature development, create a bug report and a
branch named
bug-N-short-descriptionfrom the active feature branch. - Add the smallest focused regression test before fixing a bug.
- Merge the bug branch back into the feature branch after the test evidence proves the fix.
- Close branch-local development bug issues after their bug branch has merged into the feature branch and the issue contains sanitized evidence.
- Merge completed feature branches into the release branch after local checks, documentation, changelog updates, and issue evidence are complete.
- Keep ordinary branch pushes quiet; do not run GitHub Actions after every small branch update.
- Run manual release validation only when the release branch is ready to merge
into
main, or when a maintainer explicitly requests validation for an important feature merge. - Merge the release branch into
mainonly when the maintainer explicitly decides to release. - Create the release tag from
main, not from any release, feature, or bug branch.
Branch Names
Use clear branch prefixes so humans and automation can understand the merge target:
| Prefix | Base Branch | Merge Target | Use |
|---|---|---|---|
release-vX.Y.Z |
main |
main |
Release integration for a specific version, for example release-v0.4.1. |
issue-N-short-name |
release-vX.Y.Z |
release-vX.Y.Z |
Normal issue implementation work. |
feature-N-short-name |
release-vX.Y.Z |
release-vX.Y.Z |
Feature work when the feature wording is clearer than issue. |
bug-N-short-name |
Active issue or feature branch | Active issue or feature branch | Defects found during development of that feature. |
hotfix-N-short-name |
main or a release branch |
main or release branch |
Urgent release-bound fixes that still require review. |
Avoid branch names that reveal private customer names, IP addresses, internal systems, sensitive subjects, or operational details. Use public-safe issue numbers and short neutral descriptions.
Pull Request Targets
Pull request bases must follow the branch hierarchy:
| Current Branch | Pull Request Base | When To Open |
|---|---|---|
bug-N-* |
the active issue-N-* or feature-N-* branch |
After the failing regression test and fix are ready for review. |
issue-N-* or feature-N-* |
release-vX.Y.Z |
After local checks, documentation, changelog, and issue evidence are complete. |
release-vX.Y.Z |
main |
Only when the maintainer explicitly decides to release. |
Use Related #123 in pull request descriptions for managed release-bound
issues. Avoid Closes #123, Fixes #123, or Resolves #123 unless the issue
is intentionally closed before release. Release automation closes eligible
release-bound feature and bug issues after the associated GitHub Release exists
and the required evidence is present.
Pull requests should carry the same searchable labels as the managed source
issue. The local helper copies labels onto the PR by default. It detects issue
numbers from branch names such as issue-123-short-name, from dedicated
Related #123 lines in the pull request body, and from explicit --issue
arguments.
This keeps GitHub issue and PR views aligned for release labels, sink labels,
bug labels, documentation labels, and the completed lifecycle label. The
official GitHub Issue Priority field is not copied because it is issue-field
metadata rather than a PR label.
Development-only bug reports created while working on a feature may be closed when the bug branch has merged back into the feature branch. These bugs never reached a released artifact, so their closure boundary is the feature branch that contains the fix. The bug report must still include the failing test, the fix approach, the successful test evidence, and a sanitized close-out comment.
Local Pull Request Helper
Pull requests are normally created from a maintainer workstation:
scripts/check-gh-auth.sh
scripts/open-release-pr.sh --repo ProjectCuillin/nats-sinks --base release-v0.4.1
The helper refuses to run from main. It pushes the current branch, creates or
updates a draft pull request, and uses a release-control checklist in the pull
request body. Draft pull requests keep review intent visible while avoiding
validation churn on every small branch push.
Use an explicit --base whenever the current branch is not a release branch.
For example:
# Bug branch back into the feature branch.
scripts/open-release-pr.sh \
--repo ProjectCuillin/nats-sinks \
--base issue-129-websocket-transport
# Feature branch back into the release branch.
scripts/open-release-pr.sh \
--repo ProjectCuillin/nats-sinks \
--base release-v0.4.1
# Feature branch back into the release branch. Ready non-main PRs are eligible
# for guarded auto-approval and issue-label synchronization by default.
scripts/open-release-pr.sh \
--repo ProjectCuillin/nats-sinks \
--base release-v0.4.1 \
--issue 123 \
--ready
# Release branch into main when the release is explicitly approved.
scripts/open-release-pr.sh --repo ProjectCuillin/nats-sinks --base main
When a branch is ready for release-bound validation, mark the pull request ready in GitHub and dispatch validation deliberately:
scripts/run-release-validation.sh --repo ProjectCuillin/nats-sinks
That helper starts the manual CI, Docs, and CodeQL workflows for the
current branch. Use it only when the branch is ready for merge or release
validation.
Pull Request Merge Comments
Every pull request merge must have a short public comment before the merge happens. The comment should tell reviewers what was tested, which local or manual checks passed, and whether any checks were intentionally skipped. This keeps the release trail understandable without requiring access to private chat history or local terminal output.
Use the guarded merge helper instead of calling gh pr merge directly:
cat > .local/pr-merge-comments/pr-123.md <<'EOF'
## Test Evidence
- `python -m pytest tests/unit/test_pr_merge_comment.py -q` passed.
- `scripts/check.sh` passed in the local release workspace.
## Merge Note
This PR updates local workflow automation only. Runtime NATS delivery,
commit-then-ACK behavior, sink writes, and package imports are unchanged.
EOF
python scripts/merge-pr-with-comment.py \
--repo ProjectCuillin/nats-sinks \
--pr 123 \
--comment-file .local/pr-merge-comments/pr-123.md \
--delete-branch
The helper validates the comment first, posts it to the pull request, and only
then invokes gh pr merge. If the comment is empty, lacks a test-evidence
heading, contains common secret patterns, or cannot be posted, the merge is not
started. Use --dry-run to verify the planned action without writing to
GitHub:
python scripts/merge-pr-with-comment.py \
--repo ProjectCuillin/nats-sinks \
--pr 123 \
--comment-file .local/pr-merge-comments/pr-123.md \
--dry-run
The helper supports the normal GitHub merge methods:
python scripts/merge-pr-with-comment.py --pr 123 --comment-file .local/pr-merge-comments/pr-123.md --method merge
python scripts/merge-pr-with-comment.py --pr 123 --comment-file .local/pr-merge-comments/pr-123.md --method squash
python scripts/merge-pr-with-comment.py --pr 123 --comment-file .local/pr-merge-comments/pr-123.md --method rebase
Merge comments are public. Do not include passwords, tokens, certificates, private endpoints, IP addresses, Oracle wallet paths, payload dumps, classified examples, customer names, or local environment details.
Guarded Non-Main Auto-Approval
Issue, feature, and bug pull requests can be approved automatically when they
target another work branch, such as a release branch or a parent feature
branch. This is intended for pull requests raised by the local maintainer
workflow itself. It keeps the development branch moving while preserving the
protected release boundary around main.
The guardrail is strict:
- automated approval is allowed only when the pull request base is not
main; - release pull requests into
mainare never auto-approved; - draft pull requests are not approved by default;
- the helper can require the pull request author to match the current GitHub identity;
- local checks, issue evidence, documentation, and changelog updates are still required before merge.
Use the dedicated helper to validate the rule directly:
scripts/approve-non-main-pr.sh \
--repo ProjectCuillin/nats-sinks \
--pr 123 \
--expected-author louwersj \
--dry-run
Example successful dry-run output:
PR #123 is eligible for non-main auto-approval.
The same helper refuses release-bound pull requests:
Refusing to auto-approve a pull request targeting main.
scripts/open-release-pr.sh calls this helper automatically after creating or
updating a ready pull request whose base branch is not main. Use
--no-auto-approve-non-main or
NATS_SINKS_AUTO_APPROVE_NON_MAIN_PR=false when a branch should stay
unapproved for manual inspection. If --auto-approve-non-main is explicitly
passed for a PR targeting main, the script exits before approval. GitHub may
not allow or count a self-approval under branch protection rules, depending on
repository settings. In that case the helper leaves the pull request open and
prints a warning unless approval was explicitly requested. Use a separate
reviewer or bot identity when true automatic approval is required. This helper
is a convenience for non-main development flow, not a substitute for
maintainer release approval.
Pull Request Label Synchronization
scripts/open-release-pr.sh calls scripts/sync-pr-labels.py by default so
managed issue labels and pull request labels stay in sync. This is useful when
maintainers filter work by release, sink, bug, documentation, security, or
completed status.
The label sync helper is intentionally conservative:
- it copies labels only from GitHub Issues that are explicitly or safely detected;
- it treats only branch names, explicit
--issuearguments, and dedicatedRelated #123lines as source issue references; - it validates issue and pull request numbers before calling GitHub;
- it rejects label names containing ASCII control characters before printing or applying them;
- it removes stale project-managed pull request labels by default while preserving manual labels that automation does not own;
- it does not copy secrets, payloads, subjects, or any issue-field values;
- it leaves the official GitHub Issue
Priorityfield on the issue instead of inventing a PR priority label.
Use an explicit issue argument when a branch covers work that cannot be derived from the branch name:
scripts/open-release-pr.sh \
--repo ProjectCuillin/nats-sinks \
--base release-v0.4.1 \
--issue 123
For diagnostic or manual use, run the helper directly:
python scripts/sync-pr-labels.py \
--repo ProjectCuillin/nats-sinks \
--pr 456 \
--issue 123 \
--dry-run
Example output:
would add PR labels: enhancement, release-v0.4.1, completed
Would copy 3 label(s) from issue(s) #123 to PR #456.
If a pull request has stale project-managed labels, a dry-run also shows the planned removal:
would remove stale PR labels: release-unscheduled
Disable label synchronization only for exceptional maintenance work:
scripts/open-release-pr.sh \
--repo ProjectCuillin/nats-sinks \
--base release-v0.4.1 \
--no-copy-issue-labels-to-pr
Disable stale-label removal only during deliberate repository maintenance:
scripts/open-release-pr.sh \
--repo ProjectCuillin/nats-sinks \
--base release-v0.4.1 \
--no-remove-stale-pr-labels
The repository also includes a manual .github/workflows/auto-pr.yml workflow
for token-gated pull request creation. It is not triggered by branch pushes.
The workflow requires a repository secret named NATS_SINKS_PR_BOT_TOKEN. If
the secret is not configured, the workflow exits successfully with a notice and
does not create a pull request.
The release pull request gate workflows are intentionally narrower than normal
CI. Pull Request Governance and Dependency Review run when a pull request
to main is marked ready, reopened, or updated. That keeps ordinary work
branch pushes quiet while ensuring the final release pull request always has
fresh required checks on its latest commit.
Main Branch Protection
GitHub branch protection is the hard enforcement layer. The repository should
protect main with these settings:
- require a pull request before merging,
- require conversation resolution,
- require the release pull request governance check,
- require dependency review,
- block force pushes,
- block branch deletion,
- include administrators in the rule.
This is intentionally a solo-maintainer policy. GitHub does not allow a user to
approve their own pull request, so requiring one approving review or CODEOWNER
review would prevent every release while the repository has only one
write-access maintainer. The release safety boundary is therefore the pull
request, the automated release-gate checks, the required release evidence
comment, and the release tag validation that refuses to publish anything not
already merged into main.
Apply or refresh that policy with:
scripts/check-gh-auth.sh
scripts/apply-branch-protection.sh --repo ProjectCuillin/nats-sinks
The authenticated GitHub account must have permission to administer branch protection. The script does not read or print tokens.
Release Tags Must Come From Main
The release workflow validates that a tag such as v0.4.1 points at a commit
already merged into main. If a maintainer accidentally tags a release branch
before merging it, the release workflow fails before publishing to PyPI.
sequenceDiagram
participant Maintainer
participant Feature as issue-N branch
participant ReleaseBranch as release-vX.Y.Z
participant Main as main
participant Checks as Manual Validation
participant Release as Release Workflow
Maintainer->>Feature: Commit small quiet changes
Maintainer->>Feature: Run local tests and collect evidence
Feature->>ReleaseBranch: Merge feature PR
Maintainer->>ReleaseBranch: Prepare release notes and final checks
Maintainer->>Checks: Dispatch CI, Docs, and CodeQL
Checks->>ReleaseBranch: Report release-validation checks
ReleaseBranch->>Main: Merge approved release PR
Maintainer->>Main: Tag vX.Y.Z
Main->>Release: Publish package, SBOM, checksums, docs, and GitHub Release
What To Do If Main Is Changed Directly
A direct commit to main is treated as a process failure, even if the code is
technically correct. The maintainer should:
- stop release preparation,
- verify branch protection is still enabled,
- document the exception in the relevant GitHub issue or release notes,
- create follow-up work if automation or permissions allowed the bypass,
- continue future work from the release branch hierarchy.
The goal is not bureaucracy. The goal is a public, reviewable release trail for a package that is intended for operational and mission-support environments.