Skip to content

Commit

Permalink
Rewrite create release workflow
Browse files Browse the repository at this point in the history
 * Use Python when we can
 * Include a changelog
  • Loading branch information
ljodal committed Jul 6, 2022
1 parent 2bd2525 commit 9924a5f
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 41 deletions.
46 changes: 9 additions & 37 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
tags:
- 'v*'

env:
PYTHON_VERSION: "3.10"

jobs:
build-and-release:
runs-on: ubuntu-latest
Expand All @@ -15,44 +18,13 @@ jobs:
uses: actions/checkout@v3
- name: Setup project
uses: ./.github/actions/setup-project
- name: Run CI checks
run: poetry run make
- name: Build package
run: poetry build
- name: Create GitHub release
run: poetry run ./bin/create-github-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# - name: Publish package to PyPI
# run: poetry publish --username=__token__ --password="${{ secrets.PYPI_API_TOKEN }}"
- name: Create GitHub release
uses: actions/github-script@v6
with:
script: |
const fs = require("fs");
const tagName = context.ref.replace(/^refs\/tags\//, '');
const release = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: `Release ${tagName}`,
});
if (release.status < 200 || release.status >= 300) {
core.setFailed(`Could not create release for tag '${tagName}'`);
return;
}
async function uploadBuild(filename) {
const filepath = `./dist/${filename}`;
const response = await github.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: release.data.id,
name: filename,
data: fs.readFileSync(filepath),
});
return response.status >= 200 && response.status < 300;
}
const results = fs.readdirSync('./dist').map(uploadBuild);
const allUploaded = results.reduce((previous, current) => previous && current, true);
if (!allUploaded) {
core.setFailed(`Count not upload release assets for tag '${tagName}'`);
}
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.1.0-alpha.1] - 2022-07-06

### Added
- Initial release

[Unreleased]: https://github.com/kolonialno/oida/compare/v0.1.0-alpha.1...HEAD
[0.1.0-alpha.1]: https://github.com/kolonialno/oida/releases/tag/v0.1.0-alpha.1
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@ all : black mypy isort flake8 pytest

.PHONY: black
black:
black --check oida tests
black --check oida tests bin/*

.PHONY: mypy
mypy:
mypy
mypy bin/create-github-release

.PHONY: isort
isort:
isort --check-only oida tests
isort --check-only oida tests bin/*

.PHONY: flake8
flake8:
flake8 oida tests
flake8 oida tests bin/*

.PHONY: pytest
pytest:
Expand Down
144 changes: 144 additions & 0 deletions bin/create-github-release
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env python3

import argparse
import json
import os
import re
import subprocess
import sys
import traceback
from pathlib import Path
from typing import Any, Sequence, cast
from urllib.request import Request, urlopen

import keepachangelog # type: ignore


class GithubClient:
def __init__(self, *, repo: str, base_url: str, token: str) -> None:
self.repo = repo
self.token = token

def create_release(
self, tag: str, name: str, is_prerelease: bool, body: str
) -> int:
"""
Create a release and return its ID.
"""

data = self._request(
"POST",
f"https://api.github.com/repos/{self.repo}/releases",
body=json.dumps(
{"tag_name": tag, "name": name, "prerelease": is_prerelease}
).encode("utf-8"),
)
print(data)
return cast(int, data["id"])

def upload_release_asset(self, release_id: int, path: Path) -> None:
data = path.read_bytes()
content_length = str(len(data))
content_type = "application/zip" if path.stem == ".whl" else "application/gzip"

self._request(
"POST",
f"https://uploads.github.com/repos/{self.repo}/releases/{release_id}/assets?name={path.name}",
headers={"content-type": content_type, "content_length": content_length},
body=data,
)

def _request(
self,
method: str,
url: str,
*,
body: bytes,
headers: dict[str, str] | None = None,
) -> Any:
"""
Perform an HTTP request against the github API and return the decoded json.
"""

request = Request(method=method, url=url, data=body)
request.add_header("Authorization", f"Bearer {self.token}")
request.add_header("Accept", "application/vnd.github+json")
for name, value in (headers or {}).items():
request.add_header(name, value)

with urlopen(request) as response:
return json.loads(response.read().decode("utf-8"))


def get_version() -> tuple[str, bool]:
version = subprocess.check_output(
["poetry", "version", "--short"], encoding="utf-8"
).strip()
is_prerelease = not re.fullmatch(r"\d+\.\d+\.\d+", version)
return version, is_prerelease


def get_assets() -> list[Path]:
return list(Path("./dist").iterdir())


class EnvDefault(argparse.Action):
def __init__(self, fallback: str, **kwargs: Any) -> None:
default = os.environ.get(fallback)
required = default is None
super().__init__(default=default, required=required, **kwargs)

def __call__(
self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: str | Sequence[Any] | None,
option_string: str | None = None,
) -> None:
setattr(namespace, self.dest, values)


def format_changelog(version: str) -> str:
versions: dict[str, dict[str, Any]] = keepachangelog.to_dict("./CHANGELOG.md")
if (changelog := versions.get(version)) is None:
return ""

content = []
change_types = ["added", "changed", "deprecated", "removed", "fixed", "security"]
for change_type in change_types:
if (changes := changelog.get(change_type)) is None:
continue
content.append(f"## {change_type.capitalize()}")
for change in changes:
content.append(f"- {change}")

return "\n".join(content)


def main() -> None:
parser = argparse.ArgumentParser(
description="Create a GitHub release and upload build artifacts"
)
parser.add_argument("--repo", action=EnvDefault, fallback="GITHUB_REPOSITORY")
parser.add_argument("--base-url", action=EnvDefault, fallback="GITHUB_API_URL")
parser.add_argument("--token", action=EnvDefault, fallback="GITHUB_TOKEN")
parser.add_argument("--tag", action=EnvDefault, fallback="GITHUB_REF_NAME")
args = parser.parse_args()
print(args)

client = GithubClient(repo=args.repo, base_url=args.base_url, token=args.token)
version, is_prerelease = get_version()
changelog = format_changelog(version)

assets = get_assets()

release_id = client.create_release(args.tag, version, is_prerelease, changelog)
for asset in assets:
client.upload_release_asset(release_id, asset)


try:
main()
except Exception as exc:
traceback.print_exception(exc)
sys.exit(1)
17 changes: 16 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ flake8 = "^4.0.1"
pytest = "^7.1.2"
pep8-naming = "^0.13.0"
pytest-cov = "^3.0.0"
keepachangelog = "^1.0.0"

[tool.poetry.scripts]
oida = 'oida.console:main'
Expand Down

0 comments on commit 9924a5f

Please sign in to comment.