feat: v1 tag-service and release flow for proxyd

This commit is contained in:
Jacob Elias 2024-06-11 18:27:05 -05:00
parent adec715b0e
commit 7b607ea932
7 changed files with 343 additions and 4 deletions

@ -407,16 +407,59 @@ workflows:
docker_name: proxyd docker_name: proxyd
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>> docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
docker_context: . docker_context: .
- docker-publish: # - docker-publish:
# filters:
# tags:
# only: /^proxyd\/v.*/
# branches:
# ignore: /.*/
# name: proxyd-docker-publish
# docker_name: proxyd
# docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
# 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: <<pipeline.git.revision>>
publish: true
release: true
context:
- oplabs-gcr-release
requires:
- hold
- docker-build:
name: proxyd-docker-release
filters: filters:
tags: tags:
only: /^proxyd\/v.*/ only: /^proxyd\/v.*/
branches: branches:
ignore: /.*/ ignore: /.*/
name: proxyd-docker-publish
docker_name: proxyd docker_name: proxyd
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>> docker_tags: <<pipeline.git.revision>>
publish: true
release: true
context: context:
- oplabs-gcr-release - oplabs-gcr-release
requires: requires:
- proxyd-docker-build - hold

55
.github/workflows/tag-service.yml vendored Normal file

@ -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' }}

1
ops/tag-service/.gitignore vendored Normal file

@ -0,0 +1 @@
venv

21
ops/tag-service/README.md Normal file

@ -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`

@ -0,0 +1,2 @@
click==8.1.3
semver==3.0.0-dev4

124
ops/tag-service/tag-service.py Executable file

@ -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()

@ -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()