From de9399daef026e4ff9326e50c2a3f088336a7fb3 Mon Sep 17 00:00:00 2001 From: Brett Mastbergen Date: Mon, 13 Apr 2026 16:08:45 -0400 Subject: [PATCH] [ULS]: Add --bump mode to update_lt_spec.py for non-rebase spec updates Adds a --bump flag for updating the spec when commits have been added without rebasing to a new upstream kernel version. Bump mode (--bump): - Parses N from the current ciq_kernel-X.Y.Z-N tag and increments it - Updates %define pkgrelease from N to N+1 and resets %define buildid to .1, so the tarball name changes (e.g. linux-6.18.21-2.1.el9) - Prepends a new changelog entry above existing ones using commits since the last ciq_kernel tag as the log range; the changelog version uses .1 directly since buildid is always reset to .1 on bump - Optionally commits (--commit) and creates the new ciq_kernel-X.Y.Z-N+1 tag (--tag); --tag requires --commit, prints a warning if used without --bump, and is skipped with a warning if --commit made no changes Rebase mode now also resets %define pkgrelease back to 1%{?buildid} so the release counter returns to 1 for the new kernel version. New helpers in ciq_helpers.py: - parse_ciq_tag_release(): extracts N from ciq_kernel-X.Y.Z-N tags - parse_kernel_tag(): extended to accept ciq_kernel-X.Y.Z-N format - prepend_spec_changelog(): inserts a new entry at the top of %changelog - replace_spec_changelog(), prepend_spec_changelog(): raise ValueError instead of silently no-oping when %changelog is not found - read_spec_el_version(): reads %define values from spec lines; delegates to private _read_spec_define() which builds the regex from name + a value-capture pattern to avoid duplication - _set_spec_pkgrelease(): updates the numeric base of a %define pkgrelease line; shared by update_spec_file() and bump_spec_file() --- ciq_helpers.py | 82 ++++++++++++- update_lt_spec.py | 287 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 304 insertions(+), 65 deletions(-) diff --git a/ciq_helpers.py b/ciq_helpers.py index 95de468..7cf3029 100644 --- a/ciq_helpers.py +++ b/ciq_helpers.py @@ -357,16 +357,36 @@ def get_git_user(repo): return name, email +def parse_ciq_tag_release(tag): + """Extract the release counter N from a CIQ tag like 'ciq_kernel-6.18.21-1'. + + Returns the integer N. + Raises ValueError if the tag is not in ciq_kernel-X.Y.Z-N format. + """ + m = re.match(r"^ciq_kernel-\d+\.\d+\.\d+-(\d+)$", tag) + if not m: + raise ValueError( + f"Cannot parse CIQ release from tag: {tag!r} (expected 'ciq_kernel-X.Y.Z-N', e.g. 'ciq_kernel-6.18.21-1')" + ) + return int(m.group(1)) + + def parse_kernel_tag(tag): - """Validate and parse a kernel version tag like 'v6.12.74' or '6.12.74'. + """Validate and parse a kernel version tag. - Returns the version string without the 'v' prefix (e.g., '6.12.74'). + Accepts: 'v6.12.74', '6.12.74', or 'ciq_kernel-6.12.74-N'. + + Returns the version string (e.g., '6.12.74'). Raises ValueError if the tag format is invalid. """ - tag_without_v = tag.lstrip("v") + m = re.match(r"^ciq_kernel-(\d+\.\d+\.\d+)-\d+$", tag) + if m: + return m.group(1) + + tag_without_v = tag.removeprefix("v") tag_parts = tag_without_v.split(".") if len(tag_parts) != 3: - raise ValueError(f"Invalid kernel tag format: {tag} (expected vX.Y.Z or X.Y.Z, e.g., v6.12.74)") + raise ValueError(f"Invalid kernel tag format: {tag} (expected vX.Y.Z, X.Y.Z, or ciq_kernel-X.Y.Z-N)") try: for part in tag_parts: int(part) @@ -380,6 +400,7 @@ def replace_spec_changelog(spec_lines, new_changelog_lines): Preserves any trailing comment lines (starting with #) from the original changelog. Returns a new list of lines. + Raises ValueError if %changelog is not found. """ # Collect trailing comments from the original changelog section trailing_comments = [] @@ -393,12 +414,65 @@ def replace_spec_changelog(spec_lines, new_changelog_lines): # Build new spec, replacing everything from %changelog onward new_spec = [] + found = False for line in spec_lines: if line.startswith("%changelog"): + found = True new_spec.append(line) new_spec.extend(new_changelog_lines) new_spec.extend(trailing_comments) break new_spec.append(line) + if not found: + raise ValueError("Could not find %changelog section in spec file") + return new_spec + + +def prepend_spec_changelog(spec_lines, new_entry_lines): + """Prepend new_entry_lines at the top of the %changelog section. + + The existing changelog entries are preserved below the new entry. + Returns a new list of lines. + Raises ValueError if %changelog is not found. + """ + new_spec = [] + found = False + for i, line in enumerate(spec_lines): + if line.startswith("%changelog"): + found = True + new_spec.append(line) + new_spec.extend(new_entry_lines) + new_spec.extend(spec_lines[i + 1 :]) + break + new_spec.append(line) + + if not found: + raise ValueError("Could not find %changelog section in spec file") + + return new_spec + + +def _read_spec_define(spec_lines, name, value_pattern): + """Return the value of a %define directive from spec file lines. + + Builds a regex from name and value_pattern (a raw regex string matching the value), + scans spec_lines for the first match, and returns the captured value string. + Raises ValueError if the directive is not found. + """ + pattern = re.compile(rf"^%define {re.escape(name)}\s+({value_pattern})") + for line in spec_lines: + m = pattern.match(line) + if m: + return m.group(1) + raise ValueError(f"Could not find %define {name} in spec file") + + +def read_spec_el_version(spec_lines): + """Read the EL version number from spec file lines. + + Returns the el_version string (e.g., '9'). + Raises ValueError if not found. + """ + return _read_spec_define(spec_lines, "el_version", r"\d+") diff --git a/update_lt_spec.py b/update_lt_spec.py index 505eb7c..695f982 100755 --- a/update_lt_spec.py +++ b/update_lt_spec.py @@ -8,6 +8,7 @@ import argparse import os +import re import sys import time @@ -17,7 +18,44 @@ print("ERROR: GitPython is not installed. Install it with: pip install GitPython") sys.exit(1) -from ciq_helpers import get_git_user, last_git_tag, parse_kernel_tag, replace_spec_changelog +from ciq_helpers import ( + get_git_user, + last_git_tag, + parse_ciq_tag_release, + parse_kernel_tag, + prepend_spec_changelog, + read_spec_el_version, + replace_spec_changelog, +) + + +def _read_spec_file(spec_path): + try: + with open(spec_path, "r") as f: + return f.read().splitlines() + except IOError as e: + print(f"ERROR: Failed to read spec file {spec_path}: {e}") + sys.exit(1) + + +def _write_spec_file(spec_path, lines): + try: + with open(spec_path, "w") as f: + for line in lines: + f.write(line + "\n") + except IOError as e: + print(f"ERROR: Failed to write spec file {spec_path}: {e}") + sys.exit(1) + + +def _set_spec_pkgrelease(line, n): + """If line is a %define pkgrelease directive, set its numeric base to n. + + Returns the updated line, or the original line unchanged. + """ + if re.match(r"^%define\s+pkgrelease\s+\d+", line): + return re.sub(r"^(%define\s+pkgrelease\s+)\d+", rf"\g<1>{n}", line) + return line def calculate_lt_rebase_versions(kernel_version, buildid): @@ -72,27 +110,13 @@ def update_spec_file( upstream_tag: Git tag name (e.g., 'v6.12.77') srcgit: Git repository object """ - import re - - # Read the spec file - try: - with open(spec_path, "r") as f: - spec = f.read().splitlines() - except IOError as e: - print(f"ERROR: Failed to read spec file {spec_path}: {e}") - sys.exit(1) + spec = _read_spec_file(spec_path) # Extract el_version from spec file - el_version = None - for line in spec: - if line.startswith("%define el_version"): - match = re.search(r"%define el_version\s+(\d+)", line) - if match: - el_version = match.group(1) - break - - if not el_version: - print("ERROR: Could not find %define el_version in spec file") + try: + el_version = read_spec_el_version(spec) + except ValueError as e: + print(f"ERROR: {e}") sys.exit(1) # Construct dist string from el_version for changelog @@ -117,6 +141,8 @@ def update_spec_file( line = f"%define kernel_patch {kernel_patch}" elif line.startswith("%define buildid"): line = f"%define buildid {buildid}" + else: + line = _set_spec_pkgrelease(line, 1) updated_spec.append(line) # Build changelog entry lines @@ -141,26 +167,134 @@ def update_spec_file( "", ] - new_spec = replace_spec_changelog(updated_spec, changelog_lines) + try: + new_spec = replace_spec_changelog(updated_spec, changelog_lines) + except ValueError as e: + print(f"ERROR: {e}") + sys.exit(1) + + _write_spec_file(spec_path, new_spec) + + +def bump_spec_file( + spec_path, + kernel_version, + upstream_tag, + srcgit, +): + """Update the spec file for a non-rebase build: bump pkgrelease and prepend changelog. - # Write the updated spec file + Parses the release counter N from upstream_tag (ciq_kernel-X.Y.Z-N), increments it + to N+1, updates %define pkgrelease in the spec, and prepends a new changelog entry. + Uses upstream_tag..HEAD as the commit range. + + Arguments: + spec_path: Path to kernel.spec file + kernel_version: Kernel version string (e.g., '6.12.77') + upstream_tag: CIQ git tag (e.g., 'ciq_kernel-6.12.77-1'), used as range start + srcgit: Git repository object + + Returns the new pkgrelease base as an integer (e.g., 2). + """ + # Parse the release counter from the CIQ tag try: - with open(spec_path, "w") as f: - for line in new_spec: - f.write(line + "\n") - except IOError as e: - print(f"ERROR: Failed to write spec file {spec_path}: {e}") + current_n = parse_ciq_tag_release(upstream_tag) + except ValueError as e: + print(f"ERROR: {e}") + print(" Bump mode requires a ciq_kernel-X.Y.Z-N tag. Run rebase mode first.") + sys.exit(1) + + new_n = current_n + 1 + lt_tag_version = f"{kernel_version}-{new_n}" + print(f"Bumping pkgrelease base: {current_n} -> {new_n}") + + spec = _read_spec_file(spec_path) + + # Extract el_version from spec file + try: + el_version = read_spec_el_version(spec) + except ValueError as e: + print(f"ERROR: {e}") + sys.exit(1) + + dist = f".el{el_version}" + + # Get git user info + try: + name, email = get_git_user(srcgit) + except git.exc.GitCommandError as e: + print("ERROR: Failed to read git config. Please ensure user.name and user.email are configured.") + print(' Run: git config --global user.name "Your Name"') + print(' Run: git config --global user.email "your.email@example.com"') + print(f" Error details: {e}") + sys.exit(1) + + # Get commits since the last tag + try: + commit_logs = srcgit.git.log("--no-merges", "--pretty=format:-- %s (%an)", f"{upstream_tag}..HEAD") + except git.exc.GitCommandError as e: + print(f"ERROR: Failed to get git log from {upstream_tag}..HEAD: {e}") sys.exit(1) + if not commit_logs.strip(): + print(f"WARNING: No commits found since {upstream_tag}; changelog entry will have no commit lines") + + # Build new changelog entry + changelog_date = time.strftime("%a %b %d %Y") + changelog_lines = [ + f"* {changelog_date} {name} <{email}> - {lt_tag_version}.1{dist}", + ] + for log_line in commit_logs.split("\n"): + if log_line.strip(): + changelog_lines.append(log_line) + changelog_lines += [""] + + # Update spec: bump pkgrelease base and reset buildid to .1 + updated_spec = [] + for line in spec: + line = _set_spec_pkgrelease(line, new_n) + if line.startswith("%define buildid"): + line = "%define buildid .1" + updated_spec.append(line) + + # Prepend new entry above the existing changelog + try: + new_spec = prepend_spec_changelog(updated_spec, changelog_lines) + except ValueError as e: + print(f"ERROR: {e}") + sys.exit(1) + + _write_spec_file(spec_path, new_spec) + + return new_n + if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Update kernel.spec for LT kernel rebase") + parser = argparse.ArgumentParser( + description="Update kernel.spec for LT kernel rebases or bump the package release without changing the kernel version" + ) parser.add_argument("--srcgit", required=True, help="Location of srcgit repository") parser.add_argument("--spec-file", required=True, help="Path to kernel.spec file") - parser.add_argument("--buildid", default=".1", help="Build ID (default: .1)") + parser.add_argument("--buildid", default=".1", help="Build ID for rebase mode (default: .1)") parser.add_argument("--commit", action="store_true", help="Commit the spec file changes to git") + parser.add_argument( + "--bump", + action="store_true", + help="Bump mode: increment pkgrelease, reset buildid to .1, and prepend changelog (no kernel version change)", + ) + parser.add_argument( + "--tag", + action="store_true", + help="Create a new git tag after updating the spec (bump mode only); tag format: ciq_kernel-X.Y.Z-(N+1)", + ) args = parser.parse_args() + if args.tag and not args.bump: + print("WARNING: --tag is only valid with --bump, ignoring") + elif args.tag and not args.commit: + print("ERROR: --tag requires --commit (the spec must be committed before tagging)") + sys.exit(1) + # Initialize git repository srcgit_path = os.path.abspath(args.srcgit) try: @@ -181,51 +315,69 @@ def update_spec_file( print(f"Using last tag: {upstream_tag}") - # Validate tag format (should be like 'v6.12.74' or '6.12.74') + # Validate tag format (e.g., 'v6.12.74', '6.12.74', or 'ciq_kernel-6.12.74-N') try: kernel_version = parse_kernel_tag(upstream_tag) except ValueError as e: print(f"ERROR: {e}") sys.exit(1) - # Calculate version strings - full_kernel_version, tag_version, kernel_major_minor, kernel_patch, buildid, new_tag, major_version = ( - calculate_lt_rebase_versions(kernel_version, args.buildid) - ) - - print("\nLT Rebase Version Information:") - print(f" Full Kernel Version: {full_kernel_version}") - print(f" Kernel Major.Minor: {kernel_major_minor}") - print(f" Kernel Patch: {kernel_patch}") - print(f" Build ID: {buildid}") - print(f" Tag Version: {tag_version}") - print(f" New Tag: {new_tag}") - print(f" Major Version: {major_version}\n") - # Verify spec file exists spec_path = os.path.abspath(args.spec_file) if not os.path.exists(spec_path): print(f"ERROR: Spec file not found: {spec_path}") sys.exit(1) - # Update the spec file - print(f"Updating spec file: {spec_path}") - update_spec_file( - spec_path, - full_kernel_version, - kernel_major_minor, - kernel_patch, - buildid, - tag_version, - new_tag, - major_version, - upstream_tag, - srcgit, - ) - - print("Spec file updated successfully") + new_ciq_tag = None + + if args.bump: + print("\nBump mode: bumping pkgrelease and prepending changelog") + print(f" Kernel Version: {kernel_version}\n") + + print(f"Updating spec file: {spec_path}") + new_n = bump_spec_file( + spec_path, + kernel_version, + upstream_tag, + srcgit, + ) + print("Spec file updated successfully") + + new_ciq_tag = f"ciq_kernel-{kernel_version}-{new_n}" + commit_message = f"[CIQ] {new_ciq_tag} - updated spec" + else: + full_kernel_version, tag_version, kernel_major_minor, kernel_patch, buildid, new_tag, major_version = ( + calculate_lt_rebase_versions(kernel_version, args.buildid) + ) + + print("\nLT Rebase Version Information:") + print(f" Full Kernel Version: {full_kernel_version}") + print(f" Kernel Major.Minor: {kernel_major_minor}") + print(f" Kernel Patch: {kernel_patch}") + print(f" Build ID: {buildid}") + print(f" Tag Version: {tag_version}") + print(f" New Tag: {new_tag}") + print(f" Major Version: {major_version}\n") + + print(f"Updating spec file: {spec_path}") + update_spec_file( + spec_path, + full_kernel_version, + kernel_major_minor, + kernel_patch, + buildid, + tag_version, + new_tag, + major_version, + upstream_tag, + srcgit, + ) + print("Spec file updated successfully") + + commit_message = f"[CIQ] {upstream_tag} - updated spec" # Optionally commit the changes + committed = False if args.commit: print("Committing changes...") spec_path_rel = os.path.relpath(spec_path, srcgit.working_tree_dir) @@ -233,14 +385,27 @@ def update_spec_file( # Check if there are changes to commit if srcgit.is_dirty(path=spec_path_rel): - commit_message = f"[CIQ] {upstream_tag} - updated spec" try: srcgit.git.commit("-m", commit_message) print(f"Committed: {commit_message}") + committed = True except git.exc.GitCommandError as e: print(f"ERROR: Failed to commit changes: {e}") sys.exit(1) else: print("No changes to commit") + # Optionally create a git tag (bump mode only) + if args.bump and args.tag: + if not committed: + print(f"WARNING: Skipping tag {new_ciq_tag} — no commit was made") + else: + print(f"Creating tag {new_ciq_tag}...") + try: + srcgit.create_tag(new_ciq_tag) + print(f"Created tag: {new_ciq_tag}") + except git.exc.GitCommandError as e: + print(f"ERROR: Failed to create tag {new_ciq_tag}: {e}") + sys.exit(1) + print("\nDone!")