From 74f8be72c965d25f41ff42479971d547f1f92e71 Mon Sep 17 00:00:00 2001 From: Angel Sanadinov Date: Sun, 17 Dec 2023 19:50:27 +0100 Subject: [PATCH] Replace Travis CI with github actions --- .github/workflows/build.yml | 35 +++++++ .github/workflows/publish.yml | 42 ++++++++ .github/workflows/release.yml | 48 +++++++++ .travis.yml | 14 --- README.md | 2 +- mix.exs | 2 +- release.py | 190 ++++++++++++++++++++++++++++++++++ 7 files changed, 317 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release.yml delete mode 100644 .travis.yml create mode 100755 release.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..b844c70 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +name: asciichart Build + +on: [push, pull_request] + +env: + MIX_ENV: test + +jobs: + elixir: + runs-on: ubuntu-latest + name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} + strategy: + matrix: + otp: ['24.3'] + elixir: ['1.13', '1.14', '1.15'] + + steps: + - uses: actions/checkout@v3 + + - uses: erlef/setup-beam@v1 + with: + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} + + - name: Run QA + run: | + mix deps.get + mix deps.compile + mix test + mix coveralls.json + + - name: Push Coverage Result + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..5992956 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,42 @@ +name: asciichart Publish + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + workflow_dispatch: + +env: + ELIXIR_VERSION: "1.15" + OTP_VERSION: "24.3" + HEX_API_KEY: ${{ secrets.HEX_API_KEY }} + +jobs: + elixir: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.ELIXIR_VERSION }} + otp-version: ${{ env.OTP_VERSION }} + + - name: Build and publish + run: | + mix deps.get + mix deps.compile + mix hex.publish --yes + + release: + runs-on: ubuntu-latest + needs: + - elixir + steps: + - name: Release + uses: softprops/action-gh-release@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1829735 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +name: asciichart Release + +on: + workflow_dispatch: + inputs: + next_version: + description: 'Next release version' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major + verbose: + description: 'Enable debug logging during release' + required: false + type: boolean + +jobs: + release: + runs-on: ubuntu-latest + + if: github.ref == 'refs/heads/master' + + steps: + - uses: actions/checkout@v4 + with: + ssh-key: ${{secrets.RELEASE_KEY}} + ref: master + + - name: Setup Python 3 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Release project + run: | + pip install semver + git config --global user.name ${{github.actor}} + git config --global user.email ${{github.actor}}@users.noreply.github.com + ./release.py $VERBOSE --next $NEXT_VERSION + git push + git push --tags + + env: + NEXT_VERSION: ${{ inputs.next_version }} + VERBOSE: ${{ inputs.verbose && '--verbose' || '' }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 46a863a..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: elixir - -elixir: - - 1.7.1 - -otp_release: - - 21.0 - -branches: - only: - - master - -script: - - "MIX_ENV=test mix do deps.get, test && mix compile && mix coveralls.travis" diff --git a/README.md b/README.md index 7628e1a..2ea558e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # asciichart -[![Hex version badge](https://img.shields.io/hexpm/v/asciichart.svg)](https://hex.pm/packages/asciichart) [![Travis](https://travis-ci.org/sndnv/asciichart.svg?branch=master)](https://travis-ci.org/sndnv/asciichart) [![Coverage Status](https://coveralls.io/repos/github/sndnv/asciichart/badge.svg?branch=master)](https://coveralls.io/github/sndnv/asciichart?branch=master) [![license](https://img.shields.io/github/license/sndnv/asciichart.svg)]() +[![Hex version badge](https://img.shields.io/hexpm/v/asciichart.svg)](https://hex.pm/packages/asciichart) [![codecov](https://codecov.io/gh/sndnv/asciichart/graph/badge.svg?token=EVAYGZRRZB)](https://codecov.io/gh/sndnv/asciichart) [![license](https://img.shields.io/github/license/sndnv/asciichart.svg)]() Terminal ASCII line charts in Elixir with no dependencies. diff --git a/mix.exs b/mix.exs index 49bc487..aeac033 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ defmodule Asciichart.MixProject do [ app: :asciichart, version: "1.1.1-SNAPSHOT", - elixir: "~> 1.15", + elixir: "~> 1.7", start_permanent: Mix.env() == :prod, package: package(), deps: deps(), diff --git a/release.py b/release.py new file mode 100755 index 0000000..f8a7697 --- /dev/null +++ b/release.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 + +import argparse +import logging +import re +import semver +import subprocess +import sys + +DESCRIPTION = ('Bumps all versions across the project to the next release version, ' + 'commits and tags the changes, and updates to the next snapshot version') + + +def require_no_changes(): + build_result = subprocess.run(['git', 'status', '--porcelain'], capture_output=True, text=True) + output = build_result.stdout.split('\n') + build_result.stderr.split('\n') + output = list(filter(None, output)) + + if len(output) == 0: + logging.debug('No changes found in repo') + else: + logging.error('Release failed - uncommitted changes found in repo [\n {}\n]'.format('\n '.join(output))) + sys.exit(1) + + +def require_version_updated(next_version, updated_version): + if updated_version == next_version: + logging.debug('Version change applied') + else: + logging.error( + 'Release failed - expected updated version to be [{}] but [{}] found'.format( + next_version, + updated_version + ) + ) + sys.exit(1) + + +def get_version_from(version_file, with_version_regex): + with open(version_file) as f: + content = f.readlines() + pattern = re.compile(with_version_regex) + match = next(filter(lambda m: m is not None, map(lambda line: pattern.match(line), content)), None) + if match: + logging.debug( + 'Loaded version [{}] from file [{}] with regex [{}]'.format( + match.group(1), + version_file, + with_version_regex + ) + ) + return match.group(1).replace('+', '-') + else: + logging.error('Release failed - could not find version in [{}]'.format(version_file)) + sys.exit(1) + + +def get_current_version(version_files): + versions = {} + for version_file, version_regex in version_files.items(): + versions[version_file] = get_version_from(version_file=version_file, with_version_regex=version_regex) + + unique_versions = list(set(versions.values())) + + if len(unique_versions) != 1: + logging.error( + 'Release failed - found [{}] different versions ({}) in [\n {}\n]'.format( + len(unique_versions), + ', '.join(unique_versions), + '\n '.join(map(lambda e: '[{}] in {}'.format(e[1], e[0]), versions.items())) + ) + ) + sys.exit(1) + else: + return list(versions.values())[0] + + +def get_next_version(current_version, next_version): + current = semver.Version.parse(current_version) + + next = { + 'patch': current.bump_patch() if not current_version.endswith('-SNAPSHOT') else semver.Version.parse( + current_version.replace('-SNAPSHOT', '')), + 'minor': current.bump_minor(), + 'major': current.bump_major(), + }.get(next_version.lower()) + + return str(next or semver.Version.parse(next_version)) + + +def apply_next_version_to(version_file, current_version, next_version, with_version_regex): + with open(version_file, 'r') as f: + content = f.readlines() + pattern = re.compile(with_version_regex) + actual_current_version = current_version.replace('-', '+') if 'setup.py' in version_file else current_version + actual_next_version = next_version.replace('-', '+') if 'setup.py' in version_file else next_version + updated = list( + map( + lambda line: line.replace(actual_current_version, actual_next_version) if pattern.match(line) else line, + content + ) + ) + + with open(version_file, 'w') as f: + f.write(''.join(updated)) + + +def apply_next_version(version_files, current_version, next_version): + for version_file, version_regex in version_files.items(): + apply_next_version_to( + version_file=version_file, + current_version=current_version, + next_version=next_version, + with_version_regex=version_regex + ) + + +def exec_git_command(command): + if subprocess.run(command).returncode == 0: + logging.debug('Executed git command [{}]'.format(' '.join(command))) + else: + logging.error('Release failed - could not execute git command [{}]'.format(' '.join(command))) + sys.exit(1) + + +def commit_version_files(version_files, next_version): + for version_file in version_files: + exec_git_command(command=['git', 'add', version_file]) + exec_git_command(command=['git', 'commit', '-m', 'Updating version to {}'.format(next_version)]) + + +def create_tag(next_version): + exec_git_command(command=['git', 'tag', 'v{}'.format(next_version)]) + + +def main(): + version_regex = '\\d+\\.\\d+\\.\\d+.*' + + version_files = { + 'mix.exs': '\\s*version: "({})",$'.format(version_regex), + } + + parser = argparse.ArgumentParser(description=DESCRIPTION) + + parser.add_argument( + '-n', '--next', + required=False, + default='patch', + help='select next release version; can be either one of [major|minor|patch] or an explicit version (ex: 1.5.0)' + ) + + parser.add_argument( + '-v', '--verbose', + action='store_true', + help='enable debug logging' + ) + + args = parser.parse_args() + + logging.basicConfig( + format='[%(asctime)-15s] [%(levelname)s] [%(name)-5s]: %(message)s', + level=logging.getLevelName(logging.DEBUG if args.verbose else logging.INFO) + ) + + require_no_changes() + + current_version = get_current_version(version_files=version_files) + next_version = get_next_version(current_version=current_version, next_version=args.next) + + apply_next_version(version_files=version_files, current_version=current_version, next_version=next_version) + + updated_version = get_current_version(version_files=version_files) + + require_version_updated(next_version=next_version, updated_version=updated_version) + + commit_version_files(version_files=version_files, next_version=next_version) + create_tag(next_version=next_version) + + next_snapshot_version = '{}-SNAPSHOT'.format(get_next_version(current_version=next_version, next_version='patch')) + apply_next_version(version_files=version_files, current_version=next_version, next_version=next_snapshot_version) + + updated_snapshot_version = get_current_version(version_files=version_files) + + require_version_updated(next_version=next_snapshot_version, updated_version=updated_snapshot_version) + + commit_version_files(version_files=version_files, next_version=next_snapshot_version) + + +if __name__ == '__main__': + main()