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.
- auto_semver/__init__.py +5 -0
- auto_semver/adapters/__init__.py +4 -0
- auto_semver/adapters/git/__init__.py +6 -0
- auto_semver/adapters/git/base.py +22 -0
- auto_semver/adapters/git/constants.py +27 -0
- auto_semver/adapters/git/local.py +450 -0
- auto_semver/adapters/git/ops.py +17 -0
- auto_semver/adapters/git/promote.py +668 -0
- auto_semver/adapters/git/release_pr.py +668 -0
- auto_semver/adapters/git/verified.py +491 -0
- auto_semver/adapters/github/__init__.py +6 -0
- auto_semver/adapters/github/event.py +212 -0
- auto_semver/cli/__init__.py +6 -0
- auto_semver/cli/__main__.py +9 -0
- auto_semver/cli/bump.py +448 -0
- auto_semver/cli/finalize.py +223 -0
- auto_semver/cli/main.py +281 -0
- auto_semver/cli/promote.py +211 -0
- auto_semver/cli/setup.py +154 -0
- auto_semver/cli/utils.py +161 -0
- auto_semver/config/__init__.py +48 -0
- auto_semver/config/_models/__init__.py +35 -0
- auto_semver/config/_models/bump.py +22 -0
- auto_semver/config/_models/changelog.py +104 -0
- auto_semver/config/_models/commit_group.py +160 -0
- auto_semver/config/_models/commit_groups.py +63 -0
- auto_semver/config/_models/config.py +247 -0
- auto_semver/config/_models/lock_sync.py +48 -0
- auto_semver/config/_models/promotion.py +72 -0
- auto_semver/config/_models/pull_request.py +186 -0
- auto_semver/config/_models/release.py +31 -0
- auto_semver/config/config.py +128 -0
- auto_semver/config/constants.py +16 -0
- auto_semver/core/__init__.py +4 -0
- auto_semver/core/changelog/__init__.py +6 -0
- auto_semver/core/changelog/manager.py +289 -0
- auto_semver/core/commits/__init__.py +7 -0
- auto_semver/core/commits/grouper.py +276 -0
- auto_semver/core/commits/parser.py +164 -0
- auto_semver/core/pr/__init__.py +12 -0
- auto_semver/core/pr/builder.py +92 -0
- auto_semver/core/pr/github_builder.py +86 -0
- auto_semver/core/semver/__init__.py +8 -0
- auto_semver/core/semver/lock.py +200 -0
- auto_semver/core/semver/updater.py +75 -0
- auto_semver/core/semver/version.py +466 -0
- auto_semver/lock_sync/__init__.py +40 -0
- auto_semver/lock_sync/patch.py +206 -0
- auto_semver/lock_sync/pep440.py +56 -0
- auto_semver/lock_sync/registry.py +78 -0
- auto_semver/lock_sync/runner.py +100 -0
- auto_semver/lock_sync/strategies/__init__.py +17 -0
- auto_semver/lock_sync/strategies/npm.py +41 -0
- auto_semver/lock_sync/strategies/uv.py +41 -0
- auto_semver/lock_sync/strategy.py +59 -0
- auto_semver/lock_sync/sync.py +178 -0
- auto_semver/py.typed +0 -0
- auto_semver/setup/__init__.py +4 -0
- auto_semver/setup/check.py +93 -0
- auto_semver/setup/links.py +120 -0
- auto_semver/setup/scaffold.py +119 -0
- auto_semver/setup/scaffolds/__init__.py +3 -0
- auto_semver/setup/scaffolds/auto-semver.caller.yml +29 -0
- auto_semver/setup/scaffolds/auto_semver_config.yml +44 -0
- auto_semver/setup/scaffolds/promote.caller.yml +40 -0
- auto_semver/templates/__init__.py +26 -0
- auto_semver/templates/engine.py +595 -0
- auto_semver/templates/types.py +22 -0
- auto_semver/templates/utils.py +235 -0
- auto_semver_control-1.8.7.dev0.dist-info/METADATA +190 -0
- auto_semver_control-1.8.7.dev0.dist-info/RECORD +87 -0
- auto_semver_control-1.8.7.dev0.dist-info/WHEEL +5 -0
- auto_semver_control-1.8.7.dev0.dist-info/entry_points.txt +2 -0
- auto_semver_control-1.8.7.dev0.dist-info/licenses/LICENSE +674 -0
- auto_semver_control-1.8.7.dev0.dist-info/top_level.txt +2 -0
- runview/__init__.py +111 -0
- runview/bridge.py +43 -0
- runview/config.py +66 -0
- runview/file_sink.py +50 -0
- runview/record.py +42 -0
- runview/renderers/__init__.py +50 -0
- runview/renderers/base.py +76 -0
- runview/renderers/github.py +128 -0
- runview/renderers/plain.py +122 -0
- runview/renderers/rich_live.py +343 -0
- runview/reporter.py +178 -0
- 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.")
|
auto_semver/cli/setup.py
ADDED
|
@@ -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
|
auto_semver/cli/utils.py
ADDED
|
@@ -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
|
+
)
|