blob: 9271303afbe9f171b53f470e5defaccd5d9d7160 [file] [log] [blame]
# Copyright 2023 The Dart Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from recipe_engine.post_process import (MustRun, DropExpectation)
from PB.recipes.dart.release.release import Release
import datetime
import json
import re
DEPS = [
'dart',
'depot_tools/git',
'depot_tools/gsutil',
'recipe_engine/bcid_reporter',
'recipe_engine/buildbucket',
'recipe_engine/context',
'recipe_engine/cipd',
'recipe_engine/file',
'recipe_engine/path',
'recipe_engine/properties',
'recipe_engine/raw_io',
'recipe_engine/runtime',
'recipe_engine/service_account',
'recipe_engine/step',
'recipe_engine/time',
]
RELEASES = {
'stable': 'stable',
'beta': 'testing',
'dev': 'unstable',
}
PROPERTIES = Release
PYTHON_VERSION_COMPATIBILITY = 'PY3'
def _install_reprepro(api):
api.step('install reprepro dependencies', [
'sudo', 'apt-get', 'install', '-y', 'libarchive-dev', 'libassuan-dev',
'libdb-dev', 'libdb5.3-dev', 'libgpg-error-dev', 'libgpgme-dev'
])
reprepro_dir = api.path['cleanup'].join('reprepro')
# We pin a custom reprepro with special support for multiple versions. The git
# hash is the exact version we have been relying on locally and protects us
# against the third party repository.
api.git.checkout(
url='https://github.com/ionos-enterprise/reprepro',
ref='b37d8daba6bfb4c20241cf623a24e64532dd8868',
dir_path=reprepro_dir,
submodules=False,
set_got_revision=False,
submodule_update_recursive=False)
with api.context(cwd=reprepro_dir):
api.step('configure reprepro', ['./configure'])
api.step('make reprepro', ['make', '-j8'])
reprepro = reprepro_dir.join('reprepro')
return reprepro
def _get_latest_release(api, channel):
version_url = f'gs://dart-archive/channels/{channel}/release/latest/VERSION'
stdout = api.gsutil.cat(
version_url,
name=f'get latest {channel} version',
stdout=api.raw_io.output_text()).stdout
version = json.loads(stdout)['version']
return api.dart.Version(version=version)
def _available_versions(api, channel):
horizon = api.dart.Version(version='2.0.0')
if channel != 'stable':
stable = _get_latest_release(api, 'stable')
latest = _get_latest_release(api, channel)
horizon = stable if stable < latest else latest
releases_url = f'gs://dart-archive/channels/{channel}/release/'
listing = api.gsutil.list(
releases_url,
name=f'list {channel} releases',
stdout=api.raw_io.output_text(add_output_log=True))
versions = []
for url in listing.stdout.splitlines():
name = url.split('/')[6]
if api.dart.is_version(name):
version = api.dart.Version(version=name)
if horizon <= version:
versions.append(version)
return versions
def _download(api, channel, debs_dir):
provenance_horizon = api.dart.Version(version={
'dev': '3.1.0-145.0.dev',
'beta': '3.1.0-163.1.beta',
'stable': '3.0.3',
}[channel])
api.file.ensure_directory('mkdir debs', debs_dir)
for version in _available_versions(api, channel):
# TODO: Detect all the available architectures.
deb_name = f'dart_{version}-1_amd64.deb'
deb_file = debs_dir.join(deb_name)
api.dart.download_and_verify(
deb_name,
'dart-archive',
f'channels/{channel}/release/{version}/linux_packages/{deb_name}',
deb_file,
'misc_software://dart/sdk/debian',
no_verify=version < provenance_horizon)
def _repository(api, reprepro, version, release, base_dir, debs_dir,
repository_dir):
conf_dir = base_dir.join('conf')
api.file.ensure_directory('mkdir conf', conf_dir)
distributions_file = conf_dir.join('distributions')
distributions = f'''Origin: Google LLC
Label: Google
Suite: {release}
Codename: {release}
Version: 1.0
Components: main
Description: Google dart-linux software repository
Architectures: amd64
'''
api.file.write_text('write conf/distributions', distributions_file,
distributions)
debs = api.file.listdir(
'list debian packages',
debs_dir,
test_data=[
f'dart_{version}-1_amd64.deb',
f'dart_{version}-1_amd64.deb.intoto.jsonl'
])
debs = [deb for deb in debs if str(deb).endswith('.deb')]
api.step('reprepro', [
reprepro, '--outdir', repository_dir, '--basedir', base_dir, 'includedeb',
release
] + debs)
def _sign(api, version, release_file):
# Delete the previous signing request to prevent signing the old version.
api.gsutil(['rm', '-r', 'gs://dart-debian-signing/**'],
name='delete contents of dart-debian-signing bucket',
ok_ret='any')
listing = api.gsutil.list(
'gs://dart-debian-signing/',
name='list dart-debian-signing contents',
stdout=api.raw_io.output_text(add_output_log=True)).stdout
api.step.empty(
'confirm the dart-debian-signing bucket is non-empty',
status=api.step.INFRA_FAILURE if listing else api.step.SUCCESS)
# Prepare the signing request in cloud storage along with provenance proving
# the Release file was uploaded by this builder.
api.gsutil.upload(
release_file, 'dart-debian-signing', 'Release', name='upload Release')
sha256 = api.file.file_hash(release_file)
api.bcid_reporter.report_gcs(sha256, 'gs://dart-debian-signing/Release')
# Trigger debian signing by committing to a repository polled by the service.
debian_signing_dir = api.path['cleanup'].join('debian-signing')
api.git.checkout(
url='https://dart-internal.googlesource.com/debian-signing',
ref='main',
dir_path=debian_signing_dir)
with api.context(cwd=debian_signing_dir):
api.git('config', 'user.name', 'Dart CI', name='configure user.name')
api.git(
'config',
'user.email',
api.buildbucket.swarming_task_service_account,
name='configure user.email')
api.git('commit', '--allow-empty', '-m',
f'Trigger {version} debian signing')
api.git(
'push', 'origin', 'HEAD:refs/heads/main', name='trigger debian signing')
# Wait for the signed Release to appear in cloud storage.
attempts = 0
subdirectory = None
signature_object = None
provenance_object = None
while True:
listing = api.gsutil.list(
'gs://dart-debian-signing/**',
name='await debian signing',
stdout=api.raw_io.output_text(add_output_log=True))
for url in listing.stdout.splitlines():
object_path = url[len('gs://dart-debian-signing/'):]
if object_path.startswith('prod/dart/signing/debian/continuous/'):
if object_path.endswith('/Release.asc'):
signature_object = object_path
if object_path.endswith('.intoto.jsonl'):
provenance_object = object_path
if signature_object and provenance_object:
break
attempts += 1
if 15 <= attempts:
api.step.empty(
'timed out waiting for debian signing', status=api.step.INFRA_FAILURE)
api.time.sleep(60)
# Download the signed Release and verify its provenance.
api.dart.download_and_verify(
'Release.asc',
'dart-debian-signing',
signature_object,
f'{release_file}.gpg',
'misc_software://kokoro/test/generic_builder/allow_l2',
provenance_object=provenance_object)
# TODO: Clean up the bucket when this recipe goes into production.
#api.gsutil(['rm', '-r', 'gs://dart-debian-signing/**'],
# name='clean up dart-debian-signing bucket')
def _upload(api, repository_dir):
# TODO(b/231124800): Replace download.dartlang.org with a secure bucket.
api.gsutil(
[
'-h', 'Cache-Control:private, max-age=0, no-cache', '-m', 'rsync',
'-R', repository_dir, 'gs://download.dartlang.org/linux/debian/'
],
name='publish debian repository',
dry_run=api.runtime.is_experimental,
)
def RunSteps(api, properties):
assert not api.buildbucket.builder_name.endswith(
'-try'), 'tryjob is not supported'
api.bcid_reporter.report_stage('start')
version = properties.version
assert version, 'the recipe requires a version'
channel = api.dart.Version(version=version).fields['CHANNEL']
release = RELEASES[channel]
base_dir = api.path['cleanup']
debs_dir = base_dir.join('debs')
repository_dir = base_dir.join('repository')
api.bcid_reporter.report_stage('fetch')
reprepro = _install_reprepro(api)
_download(api, channel, debs_dir)
api.bcid_reporter.report_stage('compile')
_repository(api, reprepro, version, release, base_dir, debs_dir,
repository_dir)
api.bcid_reporter.report_stage('upload')
release_file = repository_dir.join('dists').join(release).join('Release')
api.file.listdir('list repository', repository_dir, recursive=True)
api.file.read_text('read Release', release_file)
api.file.read_text(
'read Packages',
repository_dir.join('dists').join(release).join('main').join(
'binary-amd64').join('Packages'))
_sign(api, version, release_file)
_upload(api, repository_dir)
api.bcid_reporter.report_stage('upload-complete')
def GenTests(api):
build = api.buildbucket.ci_build_message(
project='dart-internal',
builder='sign-debian',
git_repo='https://dart.googlesource.com/sdk',
git_ref='refs/heads/dev',
revision='aa0ef37c3022a866a939f2040f8b9cf7b7b18d29')
api.buildbucket.update_backend_service_account(build, 'service@example.com')
versions = '''
gs://dart-archive/channels/dev/release/latest/
gs://dart-archive/channels/dev/release/3.0.0-55.0.dev/
gs://dart-archive/channels/dev/release/3.1.0-145.0.dev/
gs://dart-archive/channels/dev/release/3.1.0-155.0.dev/
gs://dart-archive/channels/dev/release/12345/
'''[1:]
yield api.test(
'sign',
api.properties(Release(version='3.1.0-155.0.dev')),
api.buildbucket.build(build),
api.step_data(
'gsutil get latest stable version',
stdout=api.raw_io.output_text('{"version": "3.0.5"}')),
api.step_data(
'gsutil get latest dev version',
stdout=api.raw_io.output_text('{"version": "3.1.0-155.0.dev"}')),
api.step_data(
'gsutil list dev releases', stdout=api.raw_io.output_text(versions)),
api.step_data(
'verify dart_3.1.0-145.0.dev-1_amd64.deb provenance',
stdout=api.raw_io.output_text('{"allowed": true}')),
api.step_data(
'verify dart_3.1.0-155.0.dev-1_amd64.deb provenance',
stdout=api.raw_io.output_text('{"allowed": true}')),
api.step_data(
'gsutil await debian signing (2)',
stdout=api.raw_io.output_text('''
gs://dart-debian-signing/Release
gs://dart-debian-signing/Release.attestation
gs://dart-debian-signing/Release.intoto.jsonl
gs://dart-debian-signing/prod/dart/signing/debian/continuous/3/20230616-035526/artifacts/Release.asc
gs://dart-debian-signing/prod/dart/signing/debian/continuous/3/20230616-035526/d0395538-7394-4cce-a1d4-7182ad02e19b.intoto.jsonl
'''[1:])),
api.step_data(
'verify Release.asc provenance',
stdout=api.raw_io.output_text('{"allowed": true}')),
api.post_process(MustRun, 'snoop: report_stage (5)'),
)
yield api.test(
'sign-timeout',
api.properties(Release(version='3.1.0-155.0.dev')),
api.buildbucket.build(build),
api.step_data(
'gsutil get latest stable version',
stdout=api.raw_io.output_text('{"version": "3.0.5"}')),
api.step_data(
'gsutil get latest dev version',
stdout=api.raw_io.output_text('{"version": "3.1.0-155.0.dev"}')),
api.step_data(
'gsutil list dev releases', stdout=api.raw_io.output_text(versions)),
api.step_data(
'verify dart_3.1.0-145.0.dev-1_amd64.deb provenance',
stdout=api.raw_io.output_text('{"allowed": true}')),
api.step_data(
'verify dart_3.1.0-155.0.dev-1_amd64.deb provenance',
stdout=api.raw_io.output_text('{"allowed": true}')),
api.post_process(DropExpectation),
status='INFRA_FAILURE',
)