Skip to content
Snippets Groups Projects
Commit 17830b25 authored by Marco Clemencic's avatar Marco Clemencic
Browse files

Add citation instructions and helper to prepare new releases

See merge request !1494
parents b608885e ddc95eb7
No related branches found
No related tags found
1 merge request!1494Add citation instructions and helper to prepare new releases
Pipeline #6355382 passed
cff-version: 1.2.0
title: Gaudi
message: >-
If you use this software, please cite it using the
metadata from this file.
type: software
authors:
- name: LHCb Collaboration
- name: ATLAS Collaboration
identifiers:
- type: doi
value: 10.5281/zenodo.3660963
repository-code: 'https://gitlab.cern.ch/gaudi/Gaudi'
url: 'https://cern.ch/gaudi'
abstract: >-
Gaudi is a software architecture and framework that can be
used to facilitate the development of data processing
applications for High Energy Physics experiments.
keywords:
- Gaudi
- LHCb
- ATLAS
- CERN
- HEP
- Software Framework
license: Apache-2.0
version: v37r0
date-released: '2023-09-14'
#!/usr/bin/env python3
#####################################################################################
# (c) Copyright 2023 CERN for the benefit of the LHCb and ATLAS collaborations #
# #
# This software is distributed under the terms of the Apache version 2 licence, #
# copied verbatim in the file "LICENSE". #
# #
# In applying this licence, CERN does not waive the privileges and immunities #
# granted to it by virtue of its status as an Intergovernmental Organization #
# or submit itself to any jurisdiction. #
#####################################################################################
import datetime
import re
import sys
from collections.abc import Callable, Iterable
from difflib import unified_diff
from subprocess import run
from typing import Union
import click
def normalize_version(version: str) -> tuple[str, str]:
"""
Convert a version in format "vXrY" or "X.Y" in the pair ("X.Y", "vXrY").
>>> normalize_version("v37r0")
('37.0', 'v37r0')
>>> normalize_version("37.0.1")
('37.0.1', 'v37r0p1')
"""
# extract the digits
numbers = re.findall(r"\d+", version)
return (
".".join(numbers),
"".join("{}{}".format(*pair) for pair in zip("vrpt", numbers)),
)
class Fields:
"""
Helper to carry the allowed fields for formatting replacement strings.
>>> f = Fields("v37r1", datetime.date(2023, 9, 25))
>>> f
Fields('37.1', datetime.date(2023, 9, 25))
>>> f.data
{'cmake_version': '37.1', 'tag_version': 'v37r1', 'date': datetime.date(2023, 9, 25)}
"""
def __init__(self, version: str, date: datetime.date):
cmake_version, tag_version = normalize_version(version)
self._data = dict(
cmake_version=cmake_version,
tag_version=tag_version,
date=date,
)
def __repr__(self):
return (
f"Fields({repr(self._data['cmake_version'])}, {repr(self._data['date'])})"
)
@property
def data(self):
return self._data
class ReplacementRule:
"""
Helper to replace lines with patterns or applying functions.
>>> r = ReplacementRule(r"^version: ", "version: {cmake_version}")
>>> f = Fields("v1r1", datetime.date(2023, 9, 25))
>>> r("nothing to change\\n", f)
'nothing to change\\n'
>>> r("version: 1.0\\n", f)
'version: 1.1\\n'
"""
def __init__(
self,
pattern: Union[str, re.Pattern],
replace: Union[str, Callable[[str, Fields], str]],
):
self.pattern = re.compile(pattern)
if isinstance(replace, str):
replace = f"{replace.rstrip()}\n"
self.replace = lambda _line, fields: replace.format(**fields.data)
else:
self.replace = replace
def __call__(self, line: str, fields: Fields) -> str:
if self.pattern.match(line):
return self.replace(line, fields)
return line
class FileUpdater:
def __init__(
self, filename: str, rules: Iterable[Union[ReplacementRule, tuple[str, str]]]
):
self.filename = filename
self.rules = [
r if isinstance(r, ReplacementRule) else ReplacementRule(*r) for r in rules
]
def _apply_rules(self, line: str, fields: Fields) -> str:
for rule in self.rules:
line = rule(line, fields)
return line
def __call__(self, fields: Fields) -> tuple[str, list[str], list[str]]:
with open(self.filename) as f:
old = f.readlines()
return self.filename, old, [self._apply_rules(line, fields) for line in old]
def update_changelog(fields: Fields) -> tuple[str, list[str], list[str]]:
"""
Special updater to fill draft changelog entry.
"""
latest_tag = run(
["git", "describe", "--tags", "--abbrev=0"], capture_output=True, text=True
).stdout.strip()
# This formats the git log as a rough markdown list
# - collect the log formatting it such that we can machine parse it
changes_txt = run(
["git", "log", "--first-parent", "--format=%s<=>%b|", f"{latest_tag}.."],
capture_output=True,
text=True,
).stdout
# - removing trailing separator and make it a single line
changes_txt = " ".join(changes_txt.strip().rstrip("|").splitlines())
# - normalize issues and merge requests links
changes = (
changes_txt.replace("Closes #", "gaudi/Gaudi#")
.replace("See merge request ", "")
.split("|")
)
# - split the messages and format the list
changes = [
f"- {msg.strip()} ({', '.join(refs.split())})\n"
if refs.strip()
else f"- {msg.strip()}\n"
for change in changes
for msg, refs in [change.split("<=>", 1)]
]
filename = "CHANGELOG.md"
with open(filename) as f:
old = f.readlines()
for idx, line in enumerate(old):
if line.startswith("## ["):
break
data = old[:idx]
data.extend(
[
"## [{tag_version}](https://gitlab.cern.ch/gaudi/Gaudi/-/releases/{tag_version}) - {date}\n".format(
**fields.data
),
"\n",
"### Changed\n",
"### Added\n",
"### Fixed\n",
"\n",
]
)
data.extend(changes)
data.extend(["\n", "\n"])
data.extend(old[idx:])
return filename, old, data
@click.command()
@click.argument("version", type=str)
@click.argument(
"date",
type=click.DateTime(("%Y-%m-%d",)),
metavar="[DATE]",
default=datetime.datetime.now(),
)
@click.option(
"--dry-run",
"-n",
default=False,
is_flag=True,
help="only show what would change, but do not modify the files",
)
def update_version(version: str, date: datetime.datetime, dry_run: bool):
"""
Helper to easily update the project version number in all needed files.
"""
fields = Fields(version, date.date())
click.echo(
"Bumping version to {cmake_version} (tag: {tag_version})".format(**fields.data)
)
for updater in [
FileUpdater(
"CMakeLists.txt",
[(r"^project\(Gaudi VERSION", "project(Gaudi VERSION {cmake_version}")],
),
FileUpdater(
"CITATION.cff",
[
(r"^version: ", "version: {tag_version}"),
(r"^date-released: ", "date-released: '{date}'"),
],
),
FileUpdater(
"docs/source/conf.py",
[
(r"^version = ", "version = {cmake_version}"),
(r"^release = ", "release = {tag_version}"),
],
),
update_changelog,
]:
filename, old, new = updater(fields)
if old != new:
if dry_run:
sys.stdout.writelines(
unified_diff(
old,
new,
fromfile=f"a/{filename}",
tofile=f"b/{filename}",
)
)
else:
click.echo(f"updated {filename}")
with open(filename, "w") as f:
f.writelines(new)
if __name__ == "__main__":
update_version()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment