mirror of
https://github.com/fmtlib/fmt.git
synced 2026-06-15 08:26:13 +08:00
Adds .github/workflows/release.yml, triggered on `release: created`, which:
- checks out the release's target_commitish,
- builds the source zip via CMake/CPack (`package_source`),
- uploads the zip to the draft release,
- calls slsa-framework/slsa-github-generator to produce a
SLSA v1.0 *.intoto.jsonl provenance file and attach it to the
same draft release.
After CI completes, the draft has both the zip and the provenance attached,
and the maintainer reviews and publishes as before.
Updates support/release.py to stop building and uploading the zip locally;
that work has moved to CI so the SLSA provenance attests to the actual
build environment that produced the artifact, not to a hash observed
after the fact. The script still builds docs locally because the
subsequent mkdocs deploy step depends on them.
180 lines
5.5 KiB
Python
Executable File
180 lines
5.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
"""Make a release.
|
|
|
|
Usage:
|
|
release.py [<branch>]
|
|
|
|
For the release command $FMT_TOKEN should contain a GitHub personal access token
|
|
obtained from https://github.com/settings/tokens.
|
|
"""
|
|
|
|
from __future__ import print_function
|
|
import datetime, docopt, errno, fileinput, json, os
|
|
import re, shutil, sys
|
|
from subprocess import check_call
|
|
import urllib.request
|
|
|
|
|
|
class Git:
|
|
def __init__(self, dir):
|
|
self.dir = dir
|
|
|
|
def call(self, method, args, **kwargs):
|
|
return check_call(['git', method] + list(args), **kwargs)
|
|
|
|
def add(self, *args):
|
|
return self.call('add', args, cwd=self.dir)
|
|
|
|
def checkout(self, *args):
|
|
return self.call('checkout', args, cwd=self.dir)
|
|
|
|
def clean(self, *args):
|
|
return self.call('clean', args, cwd=self.dir)
|
|
|
|
def clone(self, *args):
|
|
return self.call('clone', list(args) + [self.dir])
|
|
|
|
def commit(self, *args):
|
|
return self.call('commit', args, cwd=self.dir)
|
|
|
|
def pull(self, *args):
|
|
return self.call('pull', args, cwd=self.dir)
|
|
|
|
def push(self, *args):
|
|
return self.call('push', args, cwd=self.dir)
|
|
|
|
def reset(self, *args):
|
|
return self.call('reset', args, cwd=self.dir)
|
|
|
|
def update(self, *args):
|
|
clone = not os.path.exists(self.dir)
|
|
if clone:
|
|
self.clone(*args)
|
|
return clone
|
|
|
|
|
|
def clean_checkout(repo, branch):
|
|
repo.clean('-f', '-d')
|
|
repo.reset('--hard')
|
|
repo.checkout(branch)
|
|
|
|
|
|
class Runner:
|
|
def __init__(self, cwd):
|
|
self.cwd = cwd
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
kwargs['cwd'] = kwargs.get('cwd', self.cwd)
|
|
check_call(args, **kwargs)
|
|
|
|
|
|
def create_build_env():
|
|
"""Create a build environment."""
|
|
class Env:
|
|
pass
|
|
env = Env()
|
|
env.fmt_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
env.build_dir = 'build'
|
|
env.fmt_repo = Git(os.path.join(env.build_dir, 'fmt'))
|
|
return env
|
|
|
|
|
|
if __name__ == '__main__':
|
|
args = docopt.docopt(__doc__)
|
|
env = create_build_env()
|
|
fmt_repo = env.fmt_repo
|
|
|
|
branch = args.get('<branch>')
|
|
if branch is None:
|
|
branch = 'master'
|
|
if not fmt_repo.update('-b', branch, 'git@github.com:fmtlib/fmt'):
|
|
clean_checkout(fmt_repo, branch)
|
|
|
|
# Update the date in the changelog and extract the version and the first
|
|
# section content.
|
|
changelog = 'ChangeLog.md'
|
|
changelog_path = os.path.join(fmt_repo.dir, changelog)
|
|
is_first_section = True
|
|
first_section = []
|
|
for i, line in enumerate(fileinput.input(changelog_path, inplace=True)):
|
|
if i == 0:
|
|
version = re.match(r'# (.*) - TBD', line).group(1)
|
|
line = '# {} - {}\n'.format(
|
|
version, datetime.date.today().isoformat())
|
|
elif not is_first_section:
|
|
pass
|
|
elif line.startswith('#'):
|
|
is_first_section = False
|
|
else:
|
|
first_section.append(line)
|
|
sys.stdout.write(line)
|
|
if first_section[0] == '\n':
|
|
first_section.pop(0)
|
|
|
|
ns_version = None
|
|
base_h_path = os.path.join(fmt_repo.dir, 'include', 'fmt', 'base.h')
|
|
for line in fileinput.input(base_h_path):
|
|
m = re.match(r'\s*inline namespace v(.*) .*', line)
|
|
if m:
|
|
ns_version = m.group(1)
|
|
break
|
|
major_version = version.split('.')[0]
|
|
if not ns_version or ns_version != major_version:
|
|
raise Exception(f'Version mismatch {ns_version} != {major_version}')
|
|
|
|
# Workaround GitHub-flavored Markdown treating newlines as <br>.
|
|
changes = ''
|
|
code_block = False
|
|
stripped = False
|
|
for line in first_section:
|
|
if re.match(r'^\s*```', line):
|
|
code_block = not code_block
|
|
changes += line
|
|
stripped = False
|
|
continue
|
|
if code_block:
|
|
changes += line
|
|
continue
|
|
if line == '\n' or re.match(r'^\s*\|.*', line):
|
|
if stripped:
|
|
changes += '\n'
|
|
stripped = False
|
|
changes += line
|
|
continue
|
|
if stripped:
|
|
line = ' ' + line.lstrip()
|
|
changes += line.rstrip()
|
|
stripped = True
|
|
|
|
fmt_repo.checkout('-B', 'release')
|
|
fmt_repo.add(changelog)
|
|
fmt_repo.commit('-m', 'Update version')
|
|
|
|
# Build the docs locally; the source zip is now built and attached to the
|
|
# release in CI by .github/workflows/release.yml, which also generates a
|
|
# SLSA provenance attestation for it.
|
|
run = Runner(fmt_repo.dir)
|
|
run('cmake', '.')
|
|
run('make', 'doc')
|
|
|
|
# Create a draft release on GitHub. The release workflow triggers on
|
|
# `release: created`, builds the source zip from `target_commitish`, and
|
|
# attaches the zip plus *.intoto.jsonl provenance to this draft. After
|
|
# reviewing the draft, the maintainer clicks Publish to finalize.
|
|
fmt_repo.push('origin', 'release')
|
|
auth_headers = {'Authorization': 'token ' + os.getenv('FMT_TOKEN')}
|
|
req = urllib.request.Request(
|
|
'https://api.github.com/repos/fmtlib/fmt/releases',
|
|
data=json.dumps({'tag_name': version,
|
|
'target_commitish': 'release',
|
|
'body': changes, 'draft': True}).encode('utf-8'),
|
|
headers=auth_headers, method='POST')
|
|
with urllib.request.urlopen(req) as response:
|
|
if response.status != 201:
|
|
raise Exception(f'Failed to create a release ' +
|
|
'{response.status} {response.reason}')
|
|
|
|
short_version = '.'.join(version.split('.')[:-1])
|
|
check_call(['./mkdocs', 'deploy', short_version])
|