2024-02-20 17:12:19 +00:00
|
|
|
#!/usr/bin/python3 -u
|
|
|
|
|
|
|
|
|
|
# This file originally lived in
|
|
|
|
|
# https://github.com/coreos/fedora-coreos-releng-automation. See that repo for
|
|
|
|
|
# archeological git research.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
Implements the Fedora CoreOS versioning scheme as per:
|
|
|
|
|
https://github.com/coreos/fedora-coreos-tracker/issues/81
|
|
|
|
|
https://github.com/coreos/fedora-coreos-tracker/issues/211
|
2026-01-28 03:24:08 +00:00
|
|
|
And also the RHCOS/SCOS versioning scheme such as:
|
|
|
|
|
9.8.20260125-0
|
2024-02-20 17:12:19 +00:00
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
import argparse
|
2025-12-12 18:09:53 +00:00
|
|
|
import dotenv
|
2024-02-20 17:12:19 +00:00
|
|
|
import json
|
|
|
|
|
import os
|
2025-11-13 09:23:17 +00:00
|
|
|
import platform
|
2024-02-20 17:12:19 +00:00
|
|
|
import re
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
import time
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
# https://github.com/coreos/fedora-coreos-tracker/issues/211#issuecomment-543547587
|
2026-01-28 03:24:08 +00:00
|
|
|
FCOS_STREAM_TO_NUM = {
|
2024-02-20 17:12:19 +00:00
|
|
|
'next': 1,
|
|
|
|
|
'testing': 2,
|
|
|
|
|
'stable': 3,
|
|
|
|
|
'next-devel': 10,
|
|
|
|
|
'testing-devel': 20,
|
|
|
|
|
'rawhide': 91,
|
|
|
|
|
'branched': 92,
|
|
|
|
|
'bodhi-updates-testing': 93,
|
|
|
|
|
'bodhi-updates': 94,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
args = parse_args()
|
|
|
|
|
if args.workdir is not None:
|
|
|
|
|
os.chdir(args.workdir)
|
|
|
|
|
assert os.path.isdir('builds'), 'Missing builds/ dir'
|
|
|
|
|
|
2026-01-28 03:24:08 +00:00
|
|
|
# Initialize all the components of our versions
|
2026-01-29 09:28:14 +00:00
|
|
|
x, y, z, n = (None, None, None, None)
|
2026-01-28 03:24:08 +00:00
|
|
|
|
|
|
|
|
# Pick up values from our build-args.
|
|
|
|
|
config = dotenv.dotenv_values(args.build_args)
|
|
|
|
|
|
|
|
|
|
# Grab the current datetime object representing the timestamp
|
|
|
|
|
# for the timestamp component of our version.
|
|
|
|
|
dt = get_timestamp()
|
|
|
|
|
|
|
|
|
|
# The base version in FCOS is a single number (i.e. 43) while
|
|
|
|
|
# in RHCOS/SCOS it's two numbers separated by . (i.e. 9.8 or 10.0)
|
|
|
|
|
x, y = split_base_version(config['VERSION'])
|
|
|
|
|
|
|
|
|
|
# The y component in FCOS is the timestamp, while in RHCOS/SCOS
|
|
|
|
|
# it's the z component. We'll convert to a YYYYMMDD formatted string.
|
2026-01-29 09:28:14 +00:00
|
|
|
if y is None:
|
2026-01-28 03:24:08 +00:00
|
|
|
y = int(dt.strftime('%Y%m%d'))
|
|
|
|
|
else:
|
|
|
|
|
z = int(dt.strftime('%Y%m%d'))
|
|
|
|
|
|
|
|
|
|
# At this point if z isn't defined then we're FCOS
|
2026-01-29 09:28:14 +00:00
|
|
|
if z is None:
|
2026-01-28 03:24:08 +00:00
|
|
|
z = FCOS_STREAM_TO_NUM[config['STREAM']]
|
|
|
|
|
|
|
|
|
|
# For !FCOS and in dev mode we'll default to getting the build ID
|
|
|
|
|
# n component by incrementing on top of the last build. For FCOS
|
|
|
|
|
# not in dev mode we'll calculate the n by looking at git history.
|
|
|
|
|
if args.dev or config['ID'] != 'fedora':
|
2026-01-13 09:30:42 +00:00
|
|
|
n = get_next_iteration_from_builds(x, y, z)
|
|
|
|
|
else:
|
2026-01-28 03:24:08 +00:00
|
|
|
n = get_next_iteration_from_git(str(dt))
|
|
|
|
|
|
|
|
|
|
# On !FCOS the delimeter for the `n` component is a -
|
|
|
|
|
n_delimiter = '.' if config['ID'] == 'fedora' else '-'
|
|
|
|
|
|
|
|
|
|
# Now we can compute the final version. Note we prepend the
|
|
|
|
|
# `dev` string for the n component if --dev was passed.
|
|
|
|
|
dev = 'dev' if args.dev else ''
|
|
|
|
|
new_version = f'{x}.{y}.{z}{n_delimiter}{dev}{n}'
|
|
|
|
|
eprint(f'VERSIONARY: selected new version for build: {new_version}')
|
2024-02-20 17:12:19 +00:00
|
|
|
|
|
|
|
|
# sanity check the new version by trying to re-parse it
|
|
|
|
|
assert parse_version(new_version) is not None
|
|
|
|
|
print(new_version)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_args():
|
|
|
|
|
parser = argparse.ArgumentParser()
|
2026-01-28 03:24:08 +00:00
|
|
|
parser.add_argument('--build-args', help="path to build-args.conf",
|
|
|
|
|
default='src/config/build-args.conf')
|
2024-02-20 17:12:19 +00:00
|
|
|
parser.add_argument('--workdir', help="path to cosa workdir")
|
2026-01-13 09:30:42 +00:00
|
|
|
parser.add_argument(
|
|
|
|
|
"--dev", action="store_true", help="generate a developer version"
|
|
|
|
|
)
|
2024-02-20 17:12:19 +00:00
|
|
|
return parser.parse_args()
|
|
|
|
|
|
|
|
|
|
|
2026-01-28 03:24:08 +00:00
|
|
|
def get_timestamp():
|
2024-02-20 17:12:19 +00:00
|
|
|
"""
|
2026-01-28 03:24:08 +00:00
|
|
|
Get the timestamp from either the lockfiles, or use the
|
|
|
|
|
current time if no lockfiles exist.
|
2024-02-20 17:12:19 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# XXX: should sanity check that the lockfiles for all the basearches have
|
|
|
|
|
# matching timestamps
|
|
|
|
|
exts = ['json', 'yaml']
|
2025-11-13 09:23:17 +00:00
|
|
|
basearch = platform.machine()
|
2024-02-20 17:12:19 +00:00
|
|
|
for ext in exts:
|
|
|
|
|
try:
|
2025-11-13 09:23:17 +00:00
|
|
|
with open(f"src/config/manifest-lock.{basearch}.{ext}") as f:
|
2024-02-20 17:12:19 +00:00
|
|
|
lockfile = yaml.safe_load(f)
|
|
|
|
|
generated = lockfile.get('metadata', {}).get('generated')
|
|
|
|
|
if not generated:
|
|
|
|
|
raise Exception("Missing 'metadata.generated' key "
|
|
|
|
|
f"from {lockfile}")
|
|
|
|
|
dt = datetime.strptime(generated, '%Y-%m-%dT%H:%M:%SZ')
|
|
|
|
|
msg_src = "from lockfile"
|
|
|
|
|
break
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
continue
|
|
|
|
|
else:
|
2026-01-28 03:24:08 +00:00
|
|
|
msg_src = "from datetime.now()"
|
2024-02-20 17:12:19 +00:00
|
|
|
dt = datetime.now()
|
|
|
|
|
|
2026-01-28 03:24:08 +00:00
|
|
|
eprint(f"timestamp: {dt.strftime('%Y%m%d')} ({msg_src})")
|
|
|
|
|
return dt
|
2024-02-20 17:12:19 +00:00
|
|
|
|
|
|
|
|
|
2026-01-28 03:24:08 +00:00
|
|
|
def split_base_version(base_version):
|
|
|
|
|
components = base_version.split('.')
|
|
|
|
|
if len(components) == 1:
|
2026-01-29 09:28:14 +00:00
|
|
|
return int(components[0]), None
|
2026-01-28 03:24:08 +00:00
|
|
|
else:
|
|
|
|
|
return int(components[0]), int(components[1])
|
2024-02-20 17:12:19 +00:00
|
|
|
|
|
|
|
|
|
2026-01-13 09:30:42 +00:00
|
|
|
def get_next_iteration_from_builds(x, y, z):
|
2024-02-20 17:12:19 +00:00
|
|
|
try:
|
|
|
|
|
with open('builds/builds.json') as f:
|
|
|
|
|
builds = json.load(f)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
builds = {'builds': []}
|
|
|
|
|
|
|
|
|
|
if len(builds['builds']) == 0:
|
|
|
|
|
eprint("n: 0 (no previous builds)")
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
last_buildid = builds['builds'][0]['id']
|
2026-01-28 03:24:08 +00:00
|
|
|
last_version_tuple = parse_version(last_buildid)
|
|
|
|
|
if not last_version_tuple:
|
2024-02-20 17:12:19 +00:00
|
|
|
eprint(f"n: 0 (previous version {last_buildid} does not match scheme)")
|
|
|
|
|
return 0
|
|
|
|
|
|
2026-01-28 03:24:08 +00:00
|
|
|
if (x, y, z) != last_version_tuple[:3]:
|
2024-02-20 17:12:19 +00:00
|
|
|
eprint(f"n: 0 (previous version {last_buildid} x.y.z does not match)")
|
|
|
|
|
return 0
|
|
|
|
|
|
2026-01-28 03:24:08 +00:00
|
|
|
n = last_version_tuple[3] + 1
|
2024-02-20 17:12:19 +00:00
|
|
|
eprint(f"n: {n} (incremented from previous version {last_buildid})")
|
|
|
|
|
return n
|
|
|
|
|
|
|
|
|
|
|
2026-01-28 03:24:08 +00:00
|
|
|
def get_next_iteration_from_git(timestamp):
|
2026-01-13 09:30:42 +00:00
|
|
|
"""
|
|
|
|
|
Compute the next iteration number based on git commit history.
|
|
|
|
|
|
|
|
|
|
Given the Y component of the version (YYYY-MM-DD HH:MM:SS date),
|
|
|
|
|
this counts all commits from the start of that date up to HEAD.
|
|
|
|
|
This guarantees that multiple builds on the same day each receive a
|
|
|
|
|
unique `.n` value, even if several changes occur.
|
|
|
|
|
|
|
|
|
|
See: https://github.com/coreos/fedora-coreos-tracker/issues/2015
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Count commits after that point
|
|
|
|
|
commit_count_since_change = subprocess.check_output(
|
2026-01-28 03:24:08 +00:00
|
|
|
['git', 'rev-list', '--count', '--after', timestamp, 'HEAD'],
|
2026-01-13 09:30:42 +00:00
|
|
|
cwd="src/config", text=True
|
|
|
|
|
).strip()
|
2026-01-13 11:00:46 +00:00
|
|
|
eprint(
|
2026-01-13 09:30:42 +00:00
|
|
|
f"n: {commit_count_since_change} "
|
|
|
|
|
"(calculated using git commit history)"
|
|
|
|
|
)
|
2026-01-28 03:24:08 +00:00
|
|
|
return int(commit_count_since_change)
|
2026-01-13 09:30:42 +00:00
|
|
|
except subprocess.CalledProcessError as err:
|
|
|
|
|
msg = (
|
|
|
|
|
"Git command failed: unable to determine the next "
|
|
|
|
|
f"iteration value ({err})"
|
|
|
|
|
)
|
|
|
|
|
raise RuntimeError(msg) from err
|
|
|
|
|
|
|
|
|
|
|
2024-02-20 17:12:19 +00:00
|
|
|
def parse_version(version):
|
2026-01-28 03:24:08 +00:00
|
|
|
# Note that (?:pattern) os a non-matching group in python regex so
|
|
|
|
|
# it won't show up in the matched m.groups()
|
|
|
|
|
m = re.match(r'^([0-9]+)\.([0-9]+)\.([0-9]+)(?:\.|-)(?:dev)?([0-9]+)$', version)
|
2024-02-20 17:12:19 +00:00
|
|
|
if m is None:
|
|
|
|
|
return None
|
2026-01-28 03:24:08 +00:00
|
|
|
# sanity-check date. The time could be in the y component or z component
|
|
|
|
|
# so we have to look for it in either.
|
|
|
|
|
timegroup = 2
|
|
|
|
|
if len(m.group(3)) == 8:
|
|
|
|
|
timegroup = 3
|
2024-02-20 17:12:19 +00:00
|
|
|
try:
|
2026-01-28 03:24:08 +00:00
|
|
|
time.strptime(m.group(timegroup), '%Y%m%d')
|
2024-02-20 17:12:19 +00:00
|
|
|
except ValueError:
|
|
|
|
|
return None
|
2026-01-28 03:24:08 +00:00
|
|
|
return tuple(map(int, m.groups()))
|
2024-02-20 17:12:19 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def eprint(*args):
|
|
|
|
|
print(*args, file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
sys.exit(main())
|