Skip to content

Commit

Permalink
feat: Check the PR milestone matches the upcoming draft release.
Browse files Browse the repository at this point in the history
When the PR is milestoned, we also check, but don't have access to the
draft release. Instead, we check against the local version, which we
check against the draft release in another check run.
  • Loading branch information
iphydf committed Nov 10, 2024
1 parent 97066a4 commit 81d0896
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 56 deletions.
1 change: 1 addition & 0 deletions .github/workflows/check-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ jobs:
- name: Check version against GitHub releases
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_MILESTONE: ${{ github.event.pull_request.milestone.title }}
run: $GITHUB_WORKSPACE/ci-tools/bin/check_release
12 changes: 12 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: checks

on:
pull_request:
branches: [master]
types: [opened, reopened, synchronize, milestoned]
pull_request_target:
branches: [master]

jobs:
checks:
uses: ./.github/workflows/check-release.yml
5 changes: 5 additions & 0 deletions .restyled.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
restylers:
- pyment:
enabled: false
- "*"
39 changes: 0 additions & 39 deletions .travis.yml

This file was deleted.

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Continuous integration tool repository

Version: 0.8.1

This repository contains tools for working with the TokTok Haskell
repositories. Its purpose is mainly to centralise the costly building of
external tools like `hlint` and `stylish-haskell`, which otherwise each Travis
Expand Down
119 changes: 102 additions & 17 deletions bin/check_release
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#!/usr/bin/env python3
import ast
import os
import re
import subprocess
import sys
from typing import cast
from typing import List
from typing import Optional

import requests

Expand All @@ -19,28 +21,78 @@ def github_repo() -> str:
capture_output=True).stdout.decode("utf-8").strip().split(":")[1])


def release_github() -> str:
def release_github() -> Optional[str]:
resp = requests.get(
f"{os.environ['GITHUB_API_URL']}/repos/{github_repo()}/releases",
auth=("", os.environ["GITHUB_TOKEN"]),
)

releases = resp.json()
if not releases:
print("WARNING: GitHub API produced empty response.")
print("WARNING: Skipping this check.")
sys.exit(0)
print("WARNING: GitHub API produced empty response for releases.")
print("WARNING: Aborting.")
sys.exit(1)

release = releases[0]
if not release["draft"]:
print("WARNING: Could not find the latest draft release.")
print("WARNING: Skipping this check.")
print("WARNING: Skipping the draft release check.")
print(f"Latest release found was {release['name']}")
sys.exit(0)
return cast(str, release["name"][1:])
return None

name = cast(str, release["name"])
if not name.startswith("v"):
print(f"WARNING: GitHub release {name} does not start with 'v'.")
print("WARNING: Aborting.")
sys.exit(1)
return name[1:] # Remove the 'v' prefix.


def parse_semver(version: str) -> Optional[tuple[int, int, int]]:
res = re.match(r"v(\d+)\.(\d+)\.(\d+)", version)
if not res:
return None
return int(res.group(1)), int(res.group(2)), int(res.group(3))


def print_semver(version: tuple[int, int, int]) -> str:
return f"{version[0]}.{version[1]}.{version[2]}"


def release_milestone() -> str:
resp = requests.get(
f"{os.environ['GITHUB_API_URL']}/repos/{github_repo()}/milestones",
auth=("", os.environ["GITHUB_TOKEN"]),
)

milestones = resp.json()
if not milestones:
print("WARNING: GitHub API produced empty response for milestones.")
print("WARNING: Aborting.")
sys.exit(1)

return print_semver(
sorted(v for v in tuple(
parse_semver(cast(str, ms["title"])) for ms in milestones)
if v)[0])


def release_pr_milestone() -> str:
if "PR_MILESTONE" not in os.environ:
print(
"WARNING: Could not find the milestone in the PR_MILESTONE environment variable."
)
print("WARNING: Skipping this check.")
sys.exit(1)
version = os.environ["PR_MILESTONE"]
if not version.startswith("v"):
print(f"WARNING: Milestone {version} does not start with 'v'.")
print("WARNING: Aborting.")
sys.exit(1)
return version[1:] # Remove the 'v' prefix.

def release_bazel(path: str) -> str:

def release_local(path: str) -> tuple[Optional[str], str]:
with open(os.path.join(path, "BUILD.bazel"), "r") as fh:
bzl = ast.parse(fh.read(), filename=path)
for stmt in bzl.body:
Expand All @@ -52,23 +104,56 @@ def release_bazel(path: str) -> str:
if (arg.arg == "version"
and isinstance(arg.value, ast.Constant)
and isinstance(arg.value.s, str)):
return arg.value.s
return arg.value.s, "BUILD.bazel"

# Check if configure.ac exists.
if os.path.exists(os.path.join(path, "configure.ac")):
return None, "configure.ac"

raise Exception(f"Could not find a haskell_library.version in {path}")
# Check if README.md contains "Version: x.y.z".
if os.path.exists(os.path.join(path, "README.md")):
with open(os.path.join(path, "README.md"), "r") as fh:
for line in fh:
res = re.match(r"Version: (.*)", line)
if res:
return res.group(1), "README.md"

raise Exception(f"Could not find a version in {path}")


def main(prog: str, args: List[str]) -> None:
path = args[0] if args else "."

gh_release = release_github()
bzl_release = release_bazel(path)

if gh_release == bzl_release:
print(f"PASS: Upcoming release version is {gh_release}")
else:
print(f"FAIL: GitHub draft release {gh_release} does not match "
f"BUILD.bazel {bzl_release}")
pr_release = release_pr_milestone()
ms_release = release_milestone()
local_release, local_origin = release_local(path)

print(f"GitHub release: {gh_release}")
print(f"Next GitHub Milestone release: {ms_release}")
print(f"PR Milestone release: {pr_release}")
print(f"Local release: {local_release} ({local_origin})")

if gh_release is None:
# Default to the milestone version if we can't read the draft release
# version. This happens when we call the workflow from a pull_request
# event as opposed to a pull_request_target event.
gh_release = pr_release

if local_release and gh_release != local_release:
print(f"\nFAIL: GitHub draft release {gh_release} does not match "
f"{local_origin} {local_release}")
sys.exit(1)
if ms_release != pr_release:
print(f"\nFAIL: Next GitHub Milestone release {ms_release} does not "
f"match PR milestone {pr_release}")
sys.exit(1)
if gh_release != pr_release:
print(f"\nFAIL: GitHub draft release {gh_release} does not match "
f"PR milestone {pr_release}")
sys.exit(1)

print(f"\nPASS: Upcoming release version is {gh_release}")


if __name__ == "__main__":
Expand Down

0 comments on commit 81d0896

Please sign in to comment.