CHERRY-PICK: prereq to fixing the aws flags for garage shenanigans
For the user manual, we don't delete things that are missing when doing
aws s3 sync. This doesn't seem wise. If you delete a page in the manual,
it will stay public and visible.
This adds `--delete` to the `aws s3 sync` commands to fix this issue.
Closes #396
Change-Id: I6d7fb97bcdab96c0115d6c66fea0310125207df4
(cherry picked from commit 16df34b295)
312 lines
9.3 KiB
Plaintext
312 lines
9.3 KiB
Plaintext
import json
|
|
import subprocess
|
|
import itertools
|
|
import textwrap
|
|
import logging
|
|
from pathlib import Path
|
|
import tempfile
|
|
import hashlib
|
|
import datetime
|
|
|
|
from . import environment
|
|
from .environment import RelengEnvironment
|
|
from . import keys
|
|
from . import docker
|
|
from .version import VERSION, RELEASE_NAME, MAJOR, OFFICIAL_RELEASE
|
|
from .gitutils import verify_are_on_tag, git_preconditions
|
|
from . import release_notes
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
$RAISE_SUBPROC_ERROR = True
|
|
$XONSH_SHOW_TRACEBACK = True
|
|
|
|
GCROOTS_DIR = Path('./release/gcroots')
|
|
BUILT_GCROOTS_DIR = Path('./release/gcroots-build')
|
|
DRVS_TXT = Path('./release/drvs.txt')
|
|
ARTIFACTS = Path('./release/artifacts')
|
|
MANUAL = Path('./release/manual')
|
|
|
|
RELENG_MSG = "Release created with releng/create_release.xsh"
|
|
|
|
BUILD_CORES = 16
|
|
MAX_JOBS = 2
|
|
|
|
|
|
def setup_creds(env: RelengEnvironment):
|
|
key = keys.get_ephemeral_key(env)
|
|
$AWS_SECRET_ACCESS_KEY = key.secret_key
|
|
$AWS_ACCESS_KEY_ID = key.id
|
|
$AWS_DEFAULT_REGION = 'garage'
|
|
$AWS_ENDPOINT_URL = environment.S3_ENDPOINT
|
|
|
|
|
|
def official_release_commit_tag(force_tag=False):
|
|
print('[+] Setting officialRelease in version.json and tagging')
|
|
prev_branch = $(git symbolic-ref --short HEAD).strip()
|
|
|
|
git switch --detach
|
|
|
|
# Must be done in two parts due to buffering (opening the file immediately
|
|
# would truncate it).
|
|
new_version_json = $(jq --indent 4 '.official_release = true' version.json)
|
|
with open('version.json', 'w') as fh:
|
|
fh.write(new_version_json)
|
|
git add version.json
|
|
|
|
message = f'release: {VERSION} "{RELEASE_NAME}"\n\nRelease produced with releng/create_release.xsh'
|
|
git commit -m @(message)
|
|
git tag @(['-f'] if force_tag else []) -a -m @(message) @(VERSION)
|
|
|
|
with open('releng/prev-git-branch.txt', 'w') as fh:
|
|
fh.write(prev_branch)
|
|
|
|
return prev_branch
|
|
|
|
|
|
def merge_to_release(prev_branch):
|
|
git switch @(prev_branch)
|
|
# Create a merge back into the release branch so that git tools understand
|
|
# that the release branch contains the tag, without the release commit
|
|
# actually influencing the tree.
|
|
merge_msg = textwrap.dedent("""\
|
|
release: merge release {VERSION} back to mainline
|
|
|
|
This merge commit returns to the previous state prior to the release but leaves the tag in the branch history.
|
|
{RELENG_MSG}
|
|
""").format(VERSION=VERSION, RELENG_MSG=RELENG_MSG)
|
|
git merge -m @(merge_msg) -s ours @(VERSION)
|
|
|
|
|
|
def realise(paths: list[str]):
|
|
args = [
|
|
'--realise',
|
|
'--max-jobs',
|
|
MAX_JOBS,
|
|
'--cores',
|
|
BUILD_CORES,
|
|
'--log-format',
|
|
'bar-with-logs',
|
|
'--add-root',
|
|
BUILT_GCROOTS_DIR
|
|
]
|
|
nix-store @(args) @(paths)
|
|
|
|
|
|
def eval_jobs(build_profile):
|
|
nej_output = $(nix-eval-jobs --workers 4 --gc-roots-dir @(GCROOTS_DIR) --force-recurse --flake f'.#release-jobs.{build_profile}')
|
|
return [json.loads(s) for s in nej_output.strip().split('\n')]
|
|
|
|
|
|
def upload_drv_paths_and_outputs(env: RelengEnvironment, paths: list[str]):
|
|
proc = subprocess.Popen([
|
|
'nix',
|
|
'copy',
|
|
'-v',
|
|
'--to',
|
|
env.cache_store_uri(),
|
|
'--stdin',
|
|
],
|
|
stdin=subprocess.PIPE,
|
|
env=__xonsh__.env.detype(),
|
|
)
|
|
|
|
proc.stdin.write('\n'.join(itertools.chain(paths, x + '^*' for x in paths)).encode())
|
|
proc.stdin.close()
|
|
rv = proc.wait()
|
|
if rv != 0:
|
|
raise subprocess.CalledProcessError(rv, proc.args)
|
|
|
|
|
|
def make_manifest(builds_by_system):
|
|
|
|
def manifest_line(system, out):
|
|
return f' {system} = "{out}";'
|
|
|
|
manifest_text = textwrap.dedent("""\
|
|
# This file was generated by releng/create_release.xsh in Lix
|
|
{{
|
|
{lines}
|
|
}}
|
|
""").format(lines='\n'.join(manifest_line(s, p) for (s, p) in builds_by_system.items()))
|
|
|
|
return manifest_text
|
|
|
|
|
|
def make_git_tarball(to: Path):
|
|
git archive --verbose --prefix=lix-@(VERSION)/ --format=tar.gz -o @(to) @(VERSION)
|
|
|
|
|
|
def confirm(prompt, expected):
|
|
resp = input(prompt)
|
|
|
|
if resp != expected:
|
|
raise ValueError('Unconfirmed')
|
|
|
|
|
|
def sha256_file(f: Path):
|
|
hasher = hashlib.sha256()
|
|
|
|
with open(f, 'rb') as h:
|
|
while data := h.read(1024 * 1024):
|
|
hasher.update(data)
|
|
|
|
return hasher.hexdigest()
|
|
|
|
|
|
def extract_builds_by_system(eval_result):
|
|
# This could be a dictionary comprehension, but we want to be absolutely
|
|
# sure we don't have duplicates.
|
|
ret = {}
|
|
for attr in eval_result:
|
|
if attr['attrPath'][0] != 'build':
|
|
continue
|
|
assert attr['system'] not in ret
|
|
ret[attr['system']] = attr['outputs']['out']
|
|
return ret
|
|
|
|
|
|
def make_artifacts_dir(eval_result, d: Path):
|
|
d.mkdir(exist_ok=True, parents=True)
|
|
version_dir = d / 'lix' / f'lix-{VERSION}'
|
|
version_dir.mkdir(exist_ok=True, parents=True)
|
|
|
|
tarballs_drv = next(p for p in eval_result if p['attr'] == 'tarballs')
|
|
cp --no-preserve=mode -r @(tarballs_drv['outputs']['out'])/* @(version_dir)
|
|
|
|
builds_by_system = extract_builds_by_system(eval_result)
|
|
|
|
# FIXME: upgrade-nix searches for manifest.nix at root, which is rather annoying
|
|
with open(d / 'manifest.nix', 'w') as h:
|
|
h.write(make_manifest(builds_by_system))
|
|
|
|
with open(version_dir / 'manifest.nix', 'w') as h:
|
|
h.write(make_manifest(builds_by_system))
|
|
|
|
print('[+] Make sources tarball')
|
|
|
|
filename = f'lix-{VERSION}.tar.gz'
|
|
git_tarball = version_dir / filename
|
|
make_git_tarball(git_tarball)
|
|
|
|
file_hash = sha256_file(git_tarball)
|
|
|
|
print(f'Hash: {file_hash}')
|
|
with open(version_dir / f'{filename}.sha256', 'w') as h:
|
|
h.write(file_hash)
|
|
|
|
|
|
def prepare_release_notes():
|
|
rl_path = release_notes.build_release_notes_to_file()
|
|
|
|
commit_msg = textwrap.dedent("""\
|
|
release: release notes for {VERSION}
|
|
|
|
{RELENG_MSG}
|
|
""").format(VERSION=VERSION, RELENG_MSG=RELENG_MSG)
|
|
|
|
git add @(rl_path) @(release_notes.SUMMARY)
|
|
git rm --ignore-unmatch 'doc/manual/rl-next/*.md'
|
|
|
|
git commit -m @(commit_msg)
|
|
|
|
|
|
def upload_artifacts(env: RelengEnvironment, noconfirm=False, no_check_git=False, force_push_tag=False):
|
|
if not no_check_git:
|
|
verify_are_on_tag()
|
|
git_preconditions()
|
|
assert 'AWS_SECRET_ACCESS_KEY' in __xonsh__.env
|
|
|
|
tree @(ARTIFACTS)
|
|
|
|
env_part = f'environment {env.name}'
|
|
not noconfirm and confirm(
|
|
f'Would you like to release {ARTIFACTS} as {VERSION} in {env.colour(env_part)}? Type "I want to release this to {env.name}" to confirm\n',
|
|
f'I want to release this to {env.name}'
|
|
)
|
|
|
|
docker_images = list((ARTIFACTS / f'lix/lix-{VERSION}').glob(f'lix-{VERSION}-docker-image-*.tar.gz'))
|
|
assert docker_images
|
|
|
|
print('[+] Upload to cache')
|
|
with open(DRVS_TXT) as fh:
|
|
upload_drv_paths_and_outputs(env, [x.strip() for x in fh.readlines() if x])
|
|
|
|
print('[+] Upload docker images')
|
|
for target in env.docker_targets:
|
|
docker.upload_docker_images(target, docker_images)
|
|
|
|
print('[+] Upload to release bucket')
|
|
aws s3 cp --recursive @(ARTIFACTS)/ @(env.releases_bucket)/
|
|
print('[+] Upload manual')
|
|
upload_manual(env)
|
|
|
|
prev_branch = None
|
|
try:
|
|
with open('releng/prev-git-branch.txt', 'r') as fh:
|
|
prev_branch = fh.read().strip()
|
|
except FileNotFoundError:
|
|
log.warn('Cannot find previous git branch file, skipping pushing git objects')
|
|
|
|
if prev_branch:
|
|
print('[+] git push to the repo')
|
|
# We have to push the ref to gerrit for review at least such that the
|
|
# commit is known, before we can push it as a tag.
|
|
if env.git_repo_is_gerrit:
|
|
git push @(env.git_repo) f'{prev_branch}:refs/for/{prev_branch}'
|
|
else:
|
|
git push @(env.git_repo) f'{prev_branch}:{prev_branch}'
|
|
|
|
print('[+] git push tag')
|
|
git push @(['-f'] if force_push_tag else []) @(env.git_repo) f'{VERSION}:refs/tags/{VERSION}'
|
|
|
|
|
|
def do_tag_merge(force_tag=False, no_check_git=False):
|
|
if not no_check_git:
|
|
git_preconditions()
|
|
prev_branch = official_release_commit_tag(force_tag=force_tag)
|
|
merge_to_release(prev_branch)
|
|
git switch --detach @(VERSION)
|
|
|
|
|
|
def build_manual(eval_result):
|
|
(drv, manual) = next((x['drvPath'], x['outputs']['doc']) for x in eval_result if x['attr'] == 'build.x86_64-linux')
|
|
print('[+] Building manual')
|
|
realise([drv])
|
|
|
|
cp --no-preserve=mode -T -vr @(manual)/share/doc/nix/manual @(MANUAL)
|
|
|
|
|
|
def upload_manual(env: RelengEnvironment):
|
|
if OFFICIAL_RELEASE:
|
|
version = MAJOR
|
|
else:
|
|
version = 'nightly'
|
|
|
|
print('[+] aws s3 sync manual')
|
|
aws s3 sync --delete @(MANUAL)/ @(env.docs_bucket)/manual/lix/@(version)/
|
|
if OFFICIAL_RELEASE:
|
|
aws s3 sync --delete @(MANUAL)/ @(env.docs_bucket)/manual/lix/stable/
|
|
|
|
|
|
def build_artifacts(build_profile, no_check_git=False):
|
|
rm -rf release/
|
|
if not no_check_git:
|
|
verify_are_on_tag()
|
|
git_preconditions()
|
|
|
|
print('[+] Evaluating')
|
|
eval_result = eval_jobs(build_profile)
|
|
drv_paths = [x['drvPath'] for x in eval_result]
|
|
|
|
print('[+] Building')
|
|
realise(drv_paths)
|
|
build_manual(eval_result)
|
|
|
|
with open(DRVS_TXT, 'w') as fh:
|
|
# don't bother putting the release tarballs themselves because they are duplicate and huge
|
|
fh.write('\n'.join(x['drvPath'] for x in eval_result if x['attr'] != 'lix-release-tarballs'))
|
|
|
|
make_artifacts_dir(eval_result, ARTIFACTS)
|
|
print(f'[+] Done! See {ARTIFACTS}')
|