From 7b607ea932ab4847c2bffef4ea477c5e62f5a6be Mon Sep 17 00:00:00 2001 From: Jacob Elias Date: Tue, 11 Jun 2024 18:27:05 -0500 Subject: [PATCH] feat: v1 tag-service and release flow for proxyd --- .circleci/continue_config.yml | 51 +++++++++++- .github/workflows/tag-service.yml | 55 +++++++++++++ ops/tag-service/.gitignore | 1 + ops/tag-service/README.md | 21 +++++ ops/tag-service/requirements.txt | 2 + ops/tag-service/tag-service.py | 124 ++++++++++++++++++++++++++++++ ops/tag-service/tag-tool.py | 93 ++++++++++++++++++++++ 7 files changed, 343 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/tag-service.yml create mode 100644 ops/tag-service/.gitignore create mode 100644 ops/tag-service/README.md create mode 100644 ops/tag-service/requirements.txt create mode 100755 ops/tag-service/tag-service.py create mode 100644 ops/tag-service/tag-tool.py diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index 89d79a7..0f56986 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -407,16 +407,59 @@ workflows: docker_name: proxyd docker_tags: <>,<> docker_context: . - - docker-publish: + # - docker-publish: + # filters: + # tags: + # only: /^proxyd\/v.*/ + # branches: + # ignore: /.*/ + # name: proxyd-docker-publish + # docker_name: proxyd + # docker_tags: <>,<> + # context: + # - oplabs-gcr-release + # requires: + # - proxyd-docker-build + + release: + when: + not: + equal: [ scheduled_pipeline, << pipeline.trigger_source >> ] + jobs: + - hold: + type: approval + filters: + tags: + only: /^(proxyd|ufm-[a-z0-9\-]*|op-[a-z0-9\-]*)\/v.*/ + branches: + ignore: /.*/ + - docker-build: + name: op-ufm-docker-release + filters: + tags: + only: /^op-ufm\/v.*/ + branches: + ignore: /.*/ + docker_name: op-ufm + docker_tags: <> + publish: true + release: true + context: + - oplabs-gcr-release + requires: + - hold + - docker-build: + name: proxyd-docker-release filters: tags: only: /^proxyd\/v.*/ branches: ignore: /.*/ - name: proxyd-docker-publish docker_name: proxyd - docker_tags: <>,<> + docker_tags: <> + publish: true + release: true context: - oplabs-gcr-release requires: - - proxyd-docker-build + - hold diff --git a/.github/workflows/tag-service.yml b/.github/workflows/tag-service.yml new file mode 100644 index 0000000..60baa6f --- /dev/null +++ b/.github/workflows/tag-service.yml @@ -0,0 +1,55 @@ +name: Tag Service + +on: + workflow_dispatch: + inputs: + bump: + description: 'How much to bump the version by' + required: true + type: choice + options: + - major + - minor + - patch + - prerelease + - finalize-prerelease + service: + description: 'Which service to release' + required: true + type: choice + options: + - op-ufm + - proxyd + prerelease: + description: Increment major/minor/patch as prerelease? + required: false + type: boolean + default: false + +jobs: + release: + runs-on: ubuntu-latest + environment: op-stack-production + steps: + - uses: actions/checkout@v4 + - name: Fetch tags + run: git fetch --tags origin --force + - name: Setup Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install deps + run: pip install -r requirements.txt + working-directory: ops/tag-service + - run: ops/tag-service/tag-service.py --bump="$BUMP" --service="$SERVICE" + env: + INPUT_GITHUB_TOKEN: ${{ github.token }} + BUMP: ${{ github.event.inputs.bump }} + SERVICE: ${{ github.event.inputs.service }} + if: ${{ github.event.inputs.prerelease == 'false' }} + - run: ops/tag-service/tag-service.py --bump="$BUMP" --service="$SERVICE" --pre-release + env: + INPUT_GITHUB_TOKEN: ${{ github.token }} + BUMP: ${{ github.event.inputs.bump }} + SERVICE: ${{ github.event.inputs.service }} + if: ${{ github.event.inputs.prerelease == 'true' }} diff --git a/ops/tag-service/.gitignore b/ops/tag-service/.gitignore new file mode 100644 index 0000000..f5e96db --- /dev/null +++ b/ops/tag-service/.gitignore @@ -0,0 +1 @@ +venv \ No newline at end of file diff --git a/ops/tag-service/README.md b/ops/tag-service/README.md new file mode 100644 index 0000000..b1fdc89 --- /dev/null +++ b/ops/tag-service/README.md @@ -0,0 +1,21 @@ +# Tag Service +Tag Service is a Github action which builds new tags and applies them to services in the monorepo. +It accepts: +* Service name +* Bump Amount [major, minor, patch] +* Prerelease and Finalize-Prerelease (to add/remove `rc` versions) + +It can be triggered from the Github Actions panel in the monorepo + +# Tag Tool +Tag Tool is a minimal rewrite of the Tag Service to let operators prepare and commit tags from commandline +It accepts: +* Service name +* Bump Amount [major, minor, patch, prerelease, finalize-prerelease] + +Tag Tool is meant to be run locally, and *does not* perform any write operations. Instead, it prints the git commands to console for the operator to use. + +Additionally, a special service name "op-stack" is available, which will bump versions for `op-node`, `op-batcher` and `op-proposer` from the highest semver amongst them. + +To run Tag Tool locally, the only dependency is `pip install semver` + diff --git a/ops/tag-service/requirements.txt b/ops/tag-service/requirements.txt new file mode 100644 index 0000000..c01ed2c --- /dev/null +++ b/ops/tag-service/requirements.txt @@ -0,0 +1,2 @@ +click==8.1.3 +semver==3.0.0-dev4 diff --git a/ops/tag-service/tag-service.py b/ops/tag-service/tag-service.py new file mode 100755 index 0000000..93c9e63 --- /dev/null +++ b/ops/tag-service/tag-service.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +import logging.config +import os +import re +import subprocess +import sys + +import click +import semver + +# Minimum version numbers for packages migrating from legacy versioning. +MIN_VERSIONS = { + 'proxyd': '3.16.0', +} + +VALID_BUMPS = ('major', 'minor', 'patch', 'prerelease', 'finalize-prerelease') + +MESSAGE_TEMPLATE = '[tag-service-release] Tag {service} at {version}' + +LOGGING_CONFIG = { + 'version': 1, + 'disable_existing_loggers': True, + 'formatters': { + 'standard': { + 'format': '%(asctime)s [%(levelname)s]: %(message)s' + }, + }, + 'handlers': { + 'default': { + 'level': 'INFO', + 'formatter': 'standard', + 'class': 'logging.StreamHandler', + 'stream': 'ext://sys.stderr' + }, + }, + 'loggers': { + '': { + 'handlers': ['default'], + 'level': 'INFO', + 'propagate': False + }, + } +} + +logging.config.dictConfig(LOGGING_CONFIG) +log = logging.getLogger(__name__) + + +@click.command() +@click.option('--bump', required=True, type=click.Choice(VALID_BUMPS)) +@click.option('--service', required=True, type=click.Choice(list(MIN_VERSIONS.keys()))) +@click.option('--pre-release/--no-pre-release', default=False) +def tag_version(bump, service, pre_release): + tags = subprocess.run(['git', 'tag', '--list'], capture_output=True, check=True) \ + .stdout.decode('utf-8').splitlines() + + # Filter out tags that don't match the service name, and tags + # for prerelease versions. + version_pattern = f'^{service}/v\\d+\\.\\d+\\.\\d+(-rc\\.\\d+)?$' + svc_versions = [t.replace(f'{service}/v', '') for t in tags if re.match(version_pattern, t)] + svc_versions = sorted(svc_versions, key=lambda v: semver.Version.parse(v), reverse=True) + + if pre_release and bump == 'prerelease': + raise Exception('Cannot use --bump=prerelease with --pre-release') + + if pre_release and bump == 'finalize-prerelease': + raise Exception('Cannot use --bump=finalize-prerelease with --pre-release') + + if len(svc_versions) == 0: + latest_version = MIN_VERSIONS[service] + else: + latest_version = svc_versions[0] + + latest_version = semver.Version.parse(latest_version) + + log.info(f'Latest version: v{latest_version}') + + if bump == 'major': + bumped = latest_version.bump_major() + elif bump == 'minor': + bumped = latest_version.bump_minor() + elif bump == 'patch': + bumped = latest_version.bump_patch() + elif bump == 'prerelease': + bumped = latest_version.bump_prerelease() + elif bump == 'finalize-prerelease': + bumped = latest_version.finalize_version() + else: + raise Exception('Invalid bump type: {}'.format(bump)) + + if pre_release: + bumped = bumped.bump_prerelease() + + new_version = 'v' + str(bumped) + new_tag = f'{service}/{new_version}' + + log.info(f'Bumped version: {new_version}') + + log.info('Configuring git') + # The below env vars are set by GHA. + gh_actor = os.environ['GITHUB_ACTOR'] + gh_token = os.environ['INPUT_GITHUB_TOKEN'] + gh_repo = os.environ['GITHUB_REPOSITORY'] + origin_url = f'https://{gh_actor}:${gh_token}@github.com/{gh_repo}.git' + subprocess.run(['git', 'config', 'user.name', gh_actor], check=True) + subprocess.run(['git', 'config', 'user.email', f'{gh_actor}@users.noreply.github.com'], check=True) + subprocess.run(['git', 'remote', 'set-url', 'origin', origin_url], check=True) + + log.info(f'Creating tag: {new_tag}') + subprocess.run([ + 'git', + 'tag', + '-a', + new_tag, + '-m', + MESSAGE_TEMPLATE.format(service=service, version=new_version) + ], check=True) + + log.info('Pushing tag to origin') + subprocess.run(['git', 'push', 'origin', new_tag], check=True) + + +if __name__ == '__main__': + tag_version() diff --git a/ops/tag-service/tag-tool.py b/ops/tag-service/tag-tool.py new file mode 100644 index 0000000..f3fd847 --- /dev/null +++ b/ops/tag-service/tag-tool.py @@ -0,0 +1,93 @@ +import argparse +import subprocess +import re +import semver + +SERVICES = [ + 'ci-builder', + 'ci-builder-rust', + 'chain-mon', + 'op-node', + 'op-batcher', + 'op-challenger', + 'op-dispute-mon', + 'op-proposer', + 'da-server', + 'proxyd', + 'op-heartbeat', + 'op-contracts', + 'test', + 'op-stack', # special case for tagging op-node, op-batcher, and op-proposer together + 'op-conductor', +] +VERSION_PATTERN = '^{service}/v\\d+\\.\\d+\\.\\d+(-rc\\.\\d+)?$' +GIT_TAG_COMMAND = 'git tag -a {tag} -m "{message}"' +GIT_PUSH_COMMAND = 'git push origin {tag}' + +def new_tag(service, version, bump): + if bump == 'major': + bumped = version.bump_major() + elif bump == 'minor': + bumped = version.bump_minor() + elif bump == 'patch': + bumped = version.bump_patch() + elif bump == 'prerelease': + bumped = version.bump_prerelease() + elif bump == 'finalize-prerelease': + bumped = version.finalize_version() + else: + raise Exception('Invalid bump type: {}'.format(bump)) + return f'{service}/v{bumped}' + +def latest_version(service): + # Get the list of tags from the git repository. + tags = subprocess.run(['git', 'tag', '--list', f'{service}/v*'], capture_output=True, check=True) \ + .stdout.decode('utf-8').splitlines() + # Filter out tags that don't match the service name, and tags for prerelease versions. + svc_versions = sorted([t.replace(f'{service}/v', '') for t in tags]) + if len(svc_versions) == 0: + raise Exception(f'No tags found for service: {service}') + return svc_versions[-1] + +def latest_among_services(services): + latest = '0.0.0' + for service in services: + candidate = latest_version(service) + if semver.compare(candidate, latest) > 0: + latest = candidate + return latest + +def main(): + parser = argparse.ArgumentParser(description='Create a new git tag for a service') + parser.add_argument('--service', type=str, help='The name of the Service') + parser.add_argument('--bump', type=str, help='The type of bump to apply to the version number') + parser.add_argument('--message', type=str, help='Message to include in git tag', default='[tag-tool-release]') + args = parser.parse_args() + + service = args.service + + if service == 'op-stack': + latest = latest_among_services(['op-node', 'op-batcher', 'op-proposer']) + else: + latest = latest_version(service) + + bumped = new_tag(service, semver.VersionInfo.parse(latest), args.bump) + + print(f'latest tag: {latest}') + print(f'new tag: {bumped}') + print('run the following commands to create the new tag:\n') + # special case for tagging op-node, op-batcher, and op-proposer together. All three would share the same semver + if args.service == 'op-stack': + print(GIT_TAG_COMMAND.format(tag=bumped.replace('op-stack', 'op-node'), message=args.message)) + print(GIT_PUSH_COMMAND.format(tag=bumped.replace('op-stack', 'op-node'))) + print(GIT_TAG_COMMAND.format(tag=bumped.replace('op-stack', 'op-batcher'), message=args.message)) + print(GIT_PUSH_COMMAND.format(tag=bumped.replace('op-stack', 'op-batcher'))) + print(GIT_TAG_COMMAND.format(tag=bumped.replace('op-stack', 'op-proposer'), message=args.message)) + print(GIT_PUSH_COMMAND.format(tag=bumped.replace('op-stack', 'op-proposer'))) + else: + print(GIT_TAG_COMMAND.format(tag=bumped, message=args.message)) + print(GIT_PUSH_COMMAND.format(tag=bumped)) + +if __name__ == "__main__": + main() +