1
0
mirror of https://github.com/coreos/fedora-coreos-config.git synced 2026-02-05 09:45:30 +01:00
Files

225 lines
6.9 KiB
Plaintext
Raw Permalink Normal View History

#!/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
And also the RHCOS/SCOS versioning scheme such as:
9.8.20260125-0
'''
import argparse
import dotenv
import json
import os
import platform
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
FCOS_STREAM_TO_NUM = {
'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'
# Initialize all the components of our versions
x, y, z, n = (None, None, None, None)
# 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.
if y is None:
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
if z is None:
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':
n = get_next_iteration_from_builds(x, y, z)
else:
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}')
# 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()
parser.add_argument('--build-args', help="path to build-args.conf",
default='src/config/build-args.conf')
parser.add_argument('--workdir', help="path to cosa workdir")
parser.add_argument(
"--dev", action="store_true", help="generate a developer version"
)
return parser.parse_args()
def get_timestamp():
"""
Get the timestamp from either the lockfiles, or use the
current time if no lockfiles exist.
"""
# XXX: should sanity check that the lockfiles for all the basearches have
# matching timestamps
exts = ['json', 'yaml']
basearch = platform.machine()
for ext in exts:
try:
with open(f"src/config/manifest-lock.{basearch}.{ext}") as f:
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:
msg_src = "from datetime.now()"
dt = datetime.now()
eprint(f"timestamp: {dt.strftime('%Y%m%d')} ({msg_src})")
return dt
def split_base_version(base_version):
components = base_version.split('.')
if len(components) == 1:
return int(components[0]), None
else:
return int(components[0]), int(components[1])
def get_next_iteration_from_builds(x, y, z):
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']
last_version_tuple = parse_version(last_buildid)
if not last_version_tuple:
eprint(f"n: 0 (previous version {last_buildid} does not match scheme)")
return 0
if (x, y, z) != last_version_tuple[:3]:
eprint(f"n: 0 (previous version {last_buildid} x.y.z does not match)")
return 0
n = last_version_tuple[3] + 1
eprint(f"n: {n} (incremented from previous version {last_buildid})")
return n
def get_next_iteration_from_git(timestamp):
"""
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(
['git', 'rev-list', '--count', '--after', timestamp, 'HEAD'],
cwd="src/config", text=True
).strip()
eprint(
f"n: {commit_count_since_change} "
"(calculated using git commit history)"
)
return int(commit_count_since_change)
except subprocess.CalledProcessError as err:
msg = (
"Git command failed: unable to determine the next "
f"iteration value ({err})"
)
raise RuntimeError(msg) from err
def parse_version(version):
# 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)
if m is None:
return None
# 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
try:
time.strptime(m.group(timegroup), '%Y%m%d')
except ValueError:
return None
return tuple(map(int, m.groups()))
def eprint(*args):
print(*args, file=sys.stderr)
if __name__ == "__main__":
sys.exit(main())