auto-semver-control 1.8.7.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. auto_semver/__init__.py +5 -0
  2. auto_semver/adapters/__init__.py +4 -0
  3. auto_semver/adapters/git/__init__.py +6 -0
  4. auto_semver/adapters/git/base.py +22 -0
  5. auto_semver/adapters/git/constants.py +27 -0
  6. auto_semver/adapters/git/local.py +450 -0
  7. auto_semver/adapters/git/ops.py +17 -0
  8. auto_semver/adapters/git/promote.py +668 -0
  9. auto_semver/adapters/git/release_pr.py +668 -0
  10. auto_semver/adapters/git/verified.py +491 -0
  11. auto_semver/adapters/github/__init__.py +6 -0
  12. auto_semver/adapters/github/event.py +212 -0
  13. auto_semver/cli/__init__.py +6 -0
  14. auto_semver/cli/__main__.py +9 -0
  15. auto_semver/cli/bump.py +448 -0
  16. auto_semver/cli/finalize.py +223 -0
  17. auto_semver/cli/main.py +281 -0
  18. auto_semver/cli/promote.py +211 -0
  19. auto_semver/cli/setup.py +154 -0
  20. auto_semver/cli/utils.py +161 -0
  21. auto_semver/config/__init__.py +48 -0
  22. auto_semver/config/_models/__init__.py +35 -0
  23. auto_semver/config/_models/bump.py +22 -0
  24. auto_semver/config/_models/changelog.py +104 -0
  25. auto_semver/config/_models/commit_group.py +160 -0
  26. auto_semver/config/_models/commit_groups.py +63 -0
  27. auto_semver/config/_models/config.py +247 -0
  28. auto_semver/config/_models/lock_sync.py +48 -0
  29. auto_semver/config/_models/promotion.py +72 -0
  30. auto_semver/config/_models/pull_request.py +186 -0
  31. auto_semver/config/_models/release.py +31 -0
  32. auto_semver/config/config.py +128 -0
  33. auto_semver/config/constants.py +16 -0
  34. auto_semver/core/__init__.py +4 -0
  35. auto_semver/core/changelog/__init__.py +6 -0
  36. auto_semver/core/changelog/manager.py +289 -0
  37. auto_semver/core/commits/__init__.py +7 -0
  38. auto_semver/core/commits/grouper.py +276 -0
  39. auto_semver/core/commits/parser.py +164 -0
  40. auto_semver/core/pr/__init__.py +12 -0
  41. auto_semver/core/pr/builder.py +92 -0
  42. auto_semver/core/pr/github_builder.py +86 -0
  43. auto_semver/core/semver/__init__.py +8 -0
  44. auto_semver/core/semver/lock.py +200 -0
  45. auto_semver/core/semver/updater.py +75 -0
  46. auto_semver/core/semver/version.py +466 -0
  47. auto_semver/lock_sync/__init__.py +40 -0
  48. auto_semver/lock_sync/patch.py +206 -0
  49. auto_semver/lock_sync/pep440.py +56 -0
  50. auto_semver/lock_sync/registry.py +78 -0
  51. auto_semver/lock_sync/runner.py +100 -0
  52. auto_semver/lock_sync/strategies/__init__.py +17 -0
  53. auto_semver/lock_sync/strategies/npm.py +41 -0
  54. auto_semver/lock_sync/strategies/uv.py +41 -0
  55. auto_semver/lock_sync/strategy.py +59 -0
  56. auto_semver/lock_sync/sync.py +178 -0
  57. auto_semver/py.typed +0 -0
  58. auto_semver/setup/__init__.py +4 -0
  59. auto_semver/setup/check.py +93 -0
  60. auto_semver/setup/links.py +120 -0
  61. auto_semver/setup/scaffold.py +119 -0
  62. auto_semver/setup/scaffolds/__init__.py +3 -0
  63. auto_semver/setup/scaffolds/auto-semver.caller.yml +29 -0
  64. auto_semver/setup/scaffolds/auto_semver_config.yml +44 -0
  65. auto_semver/setup/scaffolds/promote.caller.yml +40 -0
  66. auto_semver/templates/__init__.py +26 -0
  67. auto_semver/templates/engine.py +595 -0
  68. auto_semver/templates/types.py +22 -0
  69. auto_semver/templates/utils.py +235 -0
  70. auto_semver_control-1.8.7.dev0.dist-info/METADATA +190 -0
  71. auto_semver_control-1.8.7.dev0.dist-info/RECORD +87 -0
  72. auto_semver_control-1.8.7.dev0.dist-info/WHEEL +5 -0
  73. auto_semver_control-1.8.7.dev0.dist-info/entry_points.txt +2 -0
  74. auto_semver_control-1.8.7.dev0.dist-info/licenses/LICENSE +674 -0
  75. auto_semver_control-1.8.7.dev0.dist-info/top_level.txt +2 -0
  76. runview/__init__.py +111 -0
  77. runview/bridge.py +43 -0
  78. runview/config.py +66 -0
  79. runview/file_sink.py +50 -0
  80. runview/record.py +42 -0
  81. runview/renderers/__init__.py +50 -0
  82. runview/renderers/base.py +76 -0
  83. runview/renderers/github.py +128 -0
  84. runview/renderers/plain.py +122 -0
  85. runview/renderers/rich_live.py +343 -0
  86. runview/reporter.py +178 -0
  87. runview/summary.py +102 -0
@@ -0,0 +1,211 @@
1
+ # Copyright (c) 2025-2026 Guy Erreich
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+ """
5
+ CLI command for manually promoting versions between branches.
6
+
7
+ This command allows users to promote versions manually, with validation
8
+ against the configured promotion rules.
9
+ """
10
+
11
+ import logging
12
+
13
+ from auto_semver.adapters.git import GitOps
14
+ from auto_semver.cli.utils import build_promotion_metadata_hook, promotion_prefer_source_paths
15
+ from auto_semver.config import Config
16
+ from auto_semver.core.semver import Version
17
+ from runview import get_summary, log_group, status
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def run(
23
+ *,
24
+ gitops: GitOps,
25
+ config: Config,
26
+ to_branch: str,
27
+ from_branch: str | None = None,
28
+ from_tag: str | None = None,
29
+ dry_run: bool = False,
30
+ ) -> None:
31
+ """
32
+ Promote a version from one branch to another directly (merge + tag).
33
+
34
+ Args:
35
+ gitops (GitOps): Git operations handler.
36
+ config (Config): Loaded configuration object.
37
+ to_branch (str): Target branch name.
38
+ from_branch (str | None): Source branch name. Optional if tag is provided.
39
+ from_tag (str | None): Specific tag to promote. If None, uses latest from branch.
40
+ dry_run (bool): If True, validate only without performing git operations.
41
+
42
+ Raises:
43
+ ValueError: If promotion is not allowed or fails.
44
+ """
45
+ logger.info(f"Initiating manual promotion to {to_branch}")
46
+ summary = get_summary()
47
+ summary.set("command", "promote")
48
+ summary.set("dry-run", "yes" if dry_run else "no")
49
+
50
+ with log_group("Validate"):
51
+ with status("Resolving source version..."):
52
+ try:
53
+ version, source_branch = _get_source_version(gitops, config, from_tag, from_branch)
54
+ except Exception as e:
55
+ raise ValueError(f"Failed to get version from source: {e}") from e
56
+
57
+ # Validate promotion is allowed
58
+ promotion_rule = config.data.validate_promotion(
59
+ from_branch=source_branch,
60
+ to_branch=to_branch,
61
+ require_auto_promote=False,
62
+ )
63
+
64
+ logger.info(
65
+ f"Promotion validated: {promotion_rule.from_branch} → {promotion_rule.to_branch}"
66
+ )
67
+ logger.info(f"Found version {version} to promote")
68
+ summary.set("branches", f"{source_branch} -> {to_branch}")
69
+
70
+ _validate_target_version(gitops, to_branch, version)
71
+
72
+ if dry_run:
73
+ logger.info("🧪 Dry run mode: Skipping promotion.")
74
+ summary.set("outcome", "dry-run")
75
+ return
76
+
77
+ # Calculate promoted version
78
+ target_suffix = config.data.suffixes.get(to_branch, "")
79
+ promoted_version = Version(
80
+ major=version.major,
81
+ minor=version.minor,
82
+ patch=version.patch,
83
+ suffix=target_suffix if target_suffix else None,
84
+ )
85
+ summary.set("version", f"{version} -> {promoted_version}")
86
+ logger.info(f"Promoting version {version} → {promoted_version}")
87
+
88
+ with log_group("Merge/tag"):
89
+ try:
90
+ # Use tag as source if available, otherwise use branch
91
+ merge_source = from_tag if from_tag else source_branch
92
+ use_source_tag = bool(from_tag)
93
+
94
+ metadata_hook = build_promotion_metadata_hook(
95
+ config=config,
96
+ source_branch=source_branch,
97
+ target_branch=to_branch,
98
+ gitops=gitops,
99
+ )
100
+
101
+ with status("Merging and tagging..."):
102
+ gitops.auto_promote(
103
+ source_branch=merge_source,
104
+ target_branch=to_branch,
105
+ version=str(promoted_version),
106
+ source_version=str(version),
107
+ is_source_tag=use_source_tag,
108
+ post_merge_hook=metadata_hook,
109
+ prefer_source_paths=promotion_prefer_source_paths(config),
110
+ )
111
+
112
+ logger.info(f"✅ Promotion completed successfully: {source_branch} → {to_branch}")
113
+ logger.info(f"Tagged {to_branch} with {promoted_version}")
114
+
115
+ except Exception as e:
116
+ raise ValueError(f"Failed to promote: {e}") from e
117
+
118
+
119
+ def _get_source_version(
120
+ gitops: GitOps,
121
+ config: Config,
122
+ from_tag: str | None,
123
+ from_branch: str | None,
124
+ ) -> tuple[Version, str]:
125
+ """
126
+ Retrieve the source version and branch.
127
+
128
+ Args:
129
+ gitops: Git operations handler.
130
+ config: Configuration object.
131
+ from_tag: Optional tag to promote from.
132
+ from_branch: Optional branch to promote from.
133
+
134
+ Returns:
135
+ tuple[Version, str]: The version and the source branch name.
136
+
137
+ Raises:
138
+ ValueError: If version cannot be determined or source branch cannot be inferred.
139
+ """
140
+ if from_tag:
141
+ logger.info(f"Using specified tag: {from_tag}")
142
+
143
+ version: Version | None
144
+ # Try to parse version from tag name first
145
+ try:
146
+ version = Version.parse(from_tag)
147
+ logger.info(f"Parsed version from tag name: {version}")
148
+ except ValueError:
149
+ logger.warning(
150
+ f"Could not parse version from tag name '{from_tag}'. Falling back to lockfile."
151
+ )
152
+ version = gitops.get_lock_version_from_tag(tag_name=from_tag)
153
+
154
+ if not version:
155
+ raise ValueError(f"No version found for tag '{from_tag}'.")
156
+
157
+ suffix = version.suffix or ""
158
+ # Find branch that matches the version's suffix
159
+ from_branch = None
160
+ for branch, s in config.data.suffixes.items():
161
+ if s == suffix:
162
+ from_branch = branch
163
+ break
164
+
165
+ if not from_branch:
166
+ raise ValueError(
167
+ f"Could not determine source branch from version suffix '{suffix}' "
168
+ f"in tag '{from_tag}'."
169
+ )
170
+ logger.info(f"Inferred source branch '{from_branch}' from tag '{from_tag}'")
171
+ return version, from_branch
172
+
173
+ if from_branch:
174
+ logger.info(f"Using latest version from branch: {from_branch}")
175
+ branch_version = gitops.get_lock_version_from_branch(branch_name=from_branch)
176
+ if not branch_version:
177
+ raise ValueError(
178
+ f"No version found for branch '{from_branch}'. Ensure the branch is tagged."
179
+ )
180
+ return branch_version, from_branch
181
+
182
+ raise ValueError("Either --from-branch or --from-tag must be provided.")
183
+
184
+
185
+ def _validate_target_version(gitops: GitOps, to_branch: str, source_version: Version) -> None:
186
+ """
187
+ Ensure the source version is greater than the target branch version.
188
+
189
+ Args:
190
+ gitops: Git operations handler.
191
+ to_branch: Target branch name.
192
+ source_version: The version being promoted.
193
+
194
+ Raises:
195
+ ValueError: If source version is not greater than target version.
196
+ """
197
+ try:
198
+ target_version = gitops.get_lock_version_from_branch(branch_name=to_branch)
199
+ if target_version:
200
+ logger.info(f"Current version on {to_branch}: {target_version}")
201
+ if source_version <= target_version:
202
+ raise ValueError(
203
+ f"Cannot promote version {source_version} to {to_branch} because it is not greater "
204
+ f"than the current version {target_version}."
205
+ )
206
+ else:
207
+ logger.info(f"No existing version found on {to_branch}. Proceeding.")
208
+ except Exception as e:
209
+ if isinstance(e, ValueError) and "Cannot promote" in str(e):
210
+ raise
211
+ logger.warning(f"Could not verify target branch version: {e}. Proceeding with caution.")
@@ -0,0 +1,154 @@
1
+ # Copyright (c) 2025-2026 Guy Erreich
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+ """CLI command to onboard a repository onto Action-Semver-Control."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import webbrowser
10
+ from pathlib import Path
11
+
12
+ from auto_semver.setup.links import (
13
+ SETUP_DOC_URL,
14
+ app_registration_url,
15
+ workflow_bump_deep_link,
16
+ workflow_promote_deep_link,
17
+ )
18
+ from auto_semver.setup.scaffold import (
19
+ RepoRef,
20
+ detect_repo,
21
+ scaffold_files,
22
+ set_repo_secret,
23
+ set_repo_variable,
24
+ verify_gh_authenticated,
25
+ )
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ def run(
31
+ *,
32
+ owner: str | None = None,
33
+ repo: str | None = None,
34
+ default_branch: str | None = None,
35
+ app_id: str | None = None,
36
+ client_id: str | None = None,
37
+ private_key_path: Path | None = None,
38
+ dry_run: bool = False,
39
+ open_browser: bool = False,
40
+ skip_secrets: bool = False,
41
+ skip_scaffold: bool = False,
42
+ ) -> None:
43
+ """Guide setup of GitHub App credentials and consumer workflow files.
44
+
45
+ Args:
46
+ owner: GitHub repository owner (defaults to origin remote).
47
+ repo: GitHub repository name (defaults to origin remote).
48
+ default_branch: Base branch for deep links and scaffolding context.
49
+ app_id: Deprecated alias for client_id (kept for CLI compatibility).
50
+ client_id: GitHub App Client ID (preferred; from app settings).
51
+ private_key_path: Path to the App private key PEM file.
52
+ dry_run: Print actions without writing secrets or files.
53
+ open_browser: Open the prefilled App registration URL in a browser.
54
+ skip_secrets: Skip writing GH_APP_CLIENT_ID and GH_APP_PRIVATE_KEY.
55
+ skip_scaffold: Skip writing config and workflow files.
56
+ """
57
+ repo_ref = _resolve_repo(owner=owner, repo=repo, default_branch=default_branch)
58
+ reg_url = app_registration_url()
59
+
60
+ print("Action-Semver-Control setup")
61
+ print("===========================")
62
+ print()
63
+ print("1. Register a GitHub App (Contents + Pull requests: read/write, webhooks off):")
64
+ print(f" {reg_url}")
65
+ print()
66
+ install_target = f"{repo_ref.owner}/{repo_ref.repo}"
67
+ print("2. Install the app on this repository:")
68
+ print(f" https://github.com/settings/apps (Install App → {install_target})")
69
+ print()
70
+ print("3. Generate a private key and note the App **Client ID** (not the numeric App ID).")
71
+ print()
72
+
73
+ if open_browser and not dry_run:
74
+ webbrowser.open(reg_url)
75
+
76
+ if not skip_secrets:
77
+ verify_gh_authenticated()
78
+ resolved_client_id = (client_id or app_id or _prompt("GitHub App Client ID")).strip()
79
+ key_path = private_key_path or Path(_prompt("Path to private key .pem file"))
80
+ private_key = key_path.read_text(encoding="utf-8")
81
+ set_repo_variable(
82
+ owner=repo_ref.owner,
83
+ repo=repo_ref.repo,
84
+ name="GH_APP_CLIENT_ID",
85
+ value=resolved_client_id,
86
+ dry_run=dry_run,
87
+ )
88
+ set_repo_secret(
89
+ owner=repo_ref.owner,
90
+ repo=repo_ref.repo,
91
+ name="GH_APP_PRIVATE_KEY",
92
+ value=private_key,
93
+ dry_run=dry_run,
94
+ )
95
+ print("Repository variable GH_APP_CLIENT_ID and secret GH_APP_PRIVATE_KEY configured.")
96
+ else:
97
+ print("Skipped credential configuration (--skip-secrets).")
98
+
99
+ if not skip_scaffold:
100
+ written = scaffold_files(Path.cwd(), dry_run=dry_run)
101
+ if written:
102
+ print("Scaffolded files:")
103
+ for path in written:
104
+ print(f" - {path}")
105
+ else:
106
+ print("No new files scaffolded (targets already exist).")
107
+ else:
108
+ print("Skipped file scaffolding (--skip-scaffold).")
109
+
110
+ bump_link = workflow_bump_deep_link(
111
+ owner=repo_ref.owner,
112
+ repo=repo_ref.repo,
113
+ branch=repo_ref.default_branch,
114
+ )
115
+ promote_link = workflow_promote_deep_link(
116
+ owner=repo_ref.owner,
117
+ repo=repo_ref.repo,
118
+ branch=repo_ref.default_branch,
119
+ )
120
+
121
+ print()
122
+ print("Optional: add workflows via the GitHub UI instead of local scaffold:")
123
+ print(f" Bump: {bump_link}")
124
+ print(f" Promote: {promote_link}")
125
+ print()
126
+ print(f"Full guide: {SETUP_DOC_URL}")
127
+ print()
128
+ print("Next: merge a PR to dev/staging/master to trigger the first semver bump.")
129
+
130
+
131
+ def _resolve_repo(
132
+ *,
133
+ owner: str | None,
134
+ repo: str | None,
135
+ default_branch: str | None,
136
+ ) -> RepoRef:
137
+ if owner and repo:
138
+ return RepoRef(owner=owner, repo=repo, default_branch=default_branch or "master")
139
+ detected = detect_repo()
140
+ if default_branch:
141
+ return RepoRef(
142
+ owner=detected.owner,
143
+ repo=detected.repo,
144
+ default_branch=default_branch,
145
+ )
146
+ return detected
147
+
148
+
149
+ def _prompt(label: str) -> str:
150
+ value = input(f"{label}: ").strip()
151
+ if not value:
152
+ msg = f"{label} is required"
153
+ raise ValueError(msg)
154
+ return value
@@ -0,0 +1,161 @@
1
+ # Copyright (c) 2025-2026 Guy Erreich
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+ """CLI utility functions for auto_semver."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from collections.abc import Callable
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING
12
+
13
+ from auto_semver.adapters.github import GitHubEvent
14
+ from auto_semver.config import Config
15
+ from auto_semver.config.constants import PR_HIDDEN_MARKER
16
+ from auto_semver.core.changelog.manager import ChangelogManager
17
+ from auto_semver.core.semver import SemverLock, Version
18
+ from auto_semver.core.semver.updater import VersionFileUpdater
19
+ from auto_semver.lock_sync import sync_package_locks
20
+
21
+ if TYPE_CHECKING:
22
+ from auto_semver.adapters.git import GitOps
23
+
24
+ logger = logging.getLogger(__package__)
25
+
26
+
27
+ def promotion_prefer_source_paths(config: Config) -> list[str]:
28
+ """Paths that diverge by design during promotion and may take the source side."""
29
+ paths: list[str] = [SemverLock.path]
30
+ if config.data.changelog and config.data.changelog.file:
31
+ paths.append(str(config.data.changelog.file))
32
+ paths.extend(str(path) for path in config.data.version_files)
33
+ return paths
34
+
35
+
36
+ def apply_promotion_metadata(
37
+ *,
38
+ config: Config,
39
+ source_branch: str,
40
+ source_version: str,
41
+ target_version: str,
42
+ target_branch: str,
43
+ merge_sha: str,
44
+ repo_root: Path,
45
+ sync_locks: bool = True,
46
+ ) -> None:
47
+ """Rewrite changelog, version files, and lock for a promoted target branch."""
48
+ rule = config.data.find_promotion_rule(from_branch=source_branch, to_branch=target_branch)
49
+ if rule is None:
50
+ logger.warning(
51
+ "Promotion metadata for %s → %s does not match any promotions[] rule in config",
52
+ source_branch,
53
+ target_branch,
54
+ )
55
+
56
+ if config.data.changelog and config.data.changelog.file:
57
+ try:
58
+ manager = ChangelogManager.from_config(config)
59
+ manager.update_version_in_header(old_version=source_version, new_version=target_version)
60
+ except Exception as err:
61
+ logger.warning("Failed to update changelog header during promotion: %s", err)
62
+
63
+ promoted = Version.parse(target_version)
64
+ for file_path in config.data.version_files:
65
+ VersionFileUpdater(file_path=str(file_path), version=promoted).update()
66
+
67
+ if sync_locks:
68
+ sync_package_locks(repo_root=repo_root, config=config.data.lock_sync)
69
+
70
+ try:
71
+ lock = SemverLock.load_from_file()
72
+ lock.as_promotion_baseline(
73
+ source_branch=source_branch,
74
+ target_branch=target_branch,
75
+ version=promoted,
76
+ merge_sha=merge_sha,
77
+ )
78
+ lock.save_to_file()
79
+ except FileNotFoundError:
80
+ logger.warning("No lockfile found during promotion metadata update")
81
+
82
+
83
+ def build_promotion_metadata_hook(
84
+ *,
85
+ config: Config,
86
+ source_branch: str,
87
+ target_branch: str,
88
+ gitops: GitOps,
89
+ sync_locks: bool = True,
90
+ ) -> Callable[[str, str], None]:
91
+ """Build a post-merge hook that applies dev/rc metadata on the target branch.
92
+
93
+ Args:
94
+ config: Loaded configuration.
95
+ source_branch: Branch the version is leaving.
96
+ target_branch: Branch receiving the promotion.
97
+ gitops: Git operations for the current repo.
98
+ sync_locks: When false, leave lockfiles for the runner CLI step.
99
+ """
100
+
101
+ def hook(source_version: str, target_version: str) -> None:
102
+ apply_promotion_metadata(
103
+ config=config,
104
+ source_branch=source_branch,
105
+ source_version=source_version,
106
+ target_version=target_version,
107
+ target_branch=target_branch,
108
+ merge_sha=gitops.repo.head.commit.hexsha,
109
+ repo_root=Path(gitops.repo.working_tree_dir or "."),
110
+ sync_locks=sync_locks,
111
+ )
112
+
113
+ return hook
114
+
115
+
116
+ def is_finalized(*, config: Config, event: GitHubEvent) -> bool:
117
+ """
118
+ Check if the PR was generated by the script.
119
+
120
+ If the PR is auto generated by the script then we should start the finalization workflow
121
+ else its a bump workflow.
122
+
123
+ Args:
124
+ config (Config): Config object.
125
+ event (GitHubEvent): GitHubEvent object.
126
+
127
+ """
128
+ logger.debug("Checking if the merged PR matches the finalized state")
129
+
130
+ body: str = event.get_body()
131
+ labels: list[str] = event.get_labels_safe()
132
+
133
+ expected_labels: list[str] = config.data.pull_request.labels
134
+
135
+ reasons = []
136
+
137
+ # Labels
138
+ missing_labels = set(expected_labels) - set(labels)
139
+ if missing_labels:
140
+ reasons.append(f"Missing labels: {sorted(missing_labels)}")
141
+ reasons.append(f" All PR labels: {sorted(labels)}")
142
+ reasons.append(f" Expected labels: {sorted(expected_labels)}")
143
+
144
+ # Identity: marker + labels only. Title templates may include {{date}} and other
145
+ # runtime vars that cannot be re-rendered identically at finalize time.
146
+
147
+ # Body
148
+ if PR_HIDDEN_MARKER not in body:
149
+ reasons.append("Body mismatch:")
150
+ reasons.append(" Hidden marker is not found in the body content.")
151
+ reasons.append(" The body was not generated by semver auto.")
152
+
153
+ if reasons:
154
+ logger.info("Detected bump workflow (not a finalize release PR)")
155
+ for reason in reasons:
156
+ logger.debug(reason)
157
+ return False
158
+
159
+ logger.info("Detected finalize workflow")
160
+
161
+ return True
@@ -0,0 +1,48 @@
1
+ # Copyright (c) 2025-2026 Guy Erreich
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+ """Public config package entry point.
5
+
6
+ Import ``Config`` and schema types from here — never from ``config._models``.
7
+ ``_models`` is a private implementation detail of this package.
8
+ """
9
+
10
+ from auto_semver.config._models import (
11
+ BranchName,
12
+ BumpConfig,
13
+ ChangelogConfig,
14
+ Commit,
15
+ CommitGroup,
16
+ CommitGroupConfig,
17
+ CommitGroups,
18
+ CommitGroupsConfig,
19
+ ConfigData,
20
+ LockSyncConfig,
21
+ PromotionRule,
22
+ PullRequestConfig,
23
+ RegexPattern,
24
+ ReleaseConfig,
25
+ )
26
+ from auto_semver.config._models.changelog import ChangelogTemplateVars
27
+ from auto_semver.config._models.pull_request import PullRequestTemplateVars
28
+ from auto_semver.config.config import Config
29
+
30
+ __all__ = [
31
+ "BranchName",
32
+ "BumpConfig",
33
+ "ChangelogConfig",
34
+ "ChangelogTemplateVars",
35
+ "Commit",
36
+ "CommitGroup",
37
+ "CommitGroupConfig",
38
+ "CommitGroups",
39
+ "CommitGroupsConfig",
40
+ "Config",
41
+ "ConfigData",
42
+ "LockSyncConfig",
43
+ "PromotionRule",
44
+ "PullRequestConfig",
45
+ "PullRequestTemplateVars",
46
+ "RegexPattern",
47
+ "ReleaseConfig",
48
+ ]
@@ -0,0 +1,35 @@
1
+ # Copyright (c) 2025-2026 Guy Erreich
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+ from auto_semver.config._models.bump import BumpConfig
5
+ from auto_semver.config._models.changelog import ChangelogConfig
6
+ from auto_semver.config._models.commit_group import (
7
+ Commit,
8
+ CommitGroup,
9
+ CommitGroupConfig,
10
+ CommitGroups,
11
+ RegexPattern,
12
+ )
13
+ from auto_semver.config._models.commit_groups import CommitGroupsConfig
14
+ from auto_semver.config._models.config import ConfigData
15
+ from auto_semver.config._models.lock_sync import LockSyncConfig
16
+ from auto_semver.config._models.promotion import BranchName, PromotionRule
17
+ from auto_semver.config._models.pull_request import PullRequestConfig
18
+ from auto_semver.config._models.release import ReleaseConfig
19
+
20
+ __all__ = [
21
+ "BranchName",
22
+ "BumpConfig",
23
+ "ChangelogConfig",
24
+ "Commit",
25
+ "CommitGroup",
26
+ "CommitGroupConfig",
27
+ "CommitGroups",
28
+ "CommitGroupsConfig",
29
+ "ConfigData",
30
+ "LockSyncConfig",
31
+ "PromotionRule",
32
+ "PullRequestConfig",
33
+ "RegexPattern",
34
+ "ReleaseConfig",
35
+ ]
@@ -0,0 +1,22 @@
1
+ # Copyright (c) 2025-2026 Guy Erreich
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+ """Version bump mode configuration."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Literal
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ class BumpConfig(BaseModel):
14
+ """Controls how version numbers increment between releases."""
15
+
16
+ mode: Literal["classic", "cumulative"] = Field(
17
+ default="classic",
18
+ description=(
19
+ "classic: semver (minor resets patch). "
20
+ "cumulative: patch accumulates fixes, minor accumulates features without patch reset"
21
+ ),
22
+ )