From 81d08966bc7c32db7b5c9ae923962de4dd9ff42d Mon Sep 17 00:00:00 2001 From: iphydf Date: Sun, 10 Nov 2024 22:55:59 +0000 Subject: [PATCH] feat: Check the PR milestone matches the upcoming draft release. 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. --- .github/workflows/check-release.yml | 1 + .github/workflows/checks.yml | 12 +++ .restyled.yaml | 5 ++ .travis.yml | 39 --------- README.md | 2 + bin/check_release | 119 ++++++++++++++++++++++++---- 6 files changed, 122 insertions(+), 56 deletions(-) create mode 100644 .github/workflows/checks.yml create mode 100644 .restyled.yaml delete mode 100644 .travis.yml diff --git a/.github/workflows/check-release.yml b/.github/workflows/check-release.yml index d133f14..3998c6b 100644 --- a/.github/workflows/check-release.yml +++ b/.github/workflows/check-release.yml @@ -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 diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..8169421 --- /dev/null +++ b/.github/workflows/checks.yml @@ -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 diff --git a/.restyled.yaml b/.restyled.yaml new file mode 100644 index 0000000..9ccfcfb --- /dev/null +++ b/.restyled.yaml @@ -0,0 +1,5 @@ +--- +restylers: + - pyment: + enabled: false + - "*" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5cc7107..0000000 --- a/.travis.yml +++ /dev/null @@ -1,39 +0,0 @@ ---- -language: generic -dist: xenial -os: linux - -cache: - directories: - - "$HOME/.stack" - -script: - - export HOST=x86_64-linux - - mkdir -p ~/.local/bin - - export PATH=$HOME/.local/bin:$PATH - - travis_retry curl -L https://get.haskellstack.org/stable/linux-x86_64.tar.gz | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack' - - stack --no-terminal install hlint pandoc stylish-haskell happy - - travis_retry curl -L https://github.com/rubik/stack-hpc-coveralls/releases/download/v0.0.4.0/shc-linux-x64-8.0.1.tar.bz2 | tar -xj -C $HOME/.local/bin - - cp -a bin $HOME/.local/ - # TODO(iphydf): Figure out how to do this more generically. How were we supposed - # to know that this file is needed and can be found here? - - cp $HOME/.stack/snapshots/$HOST/*/8.8.2/share/$HOST-ghc-8.8.2/hlint-2.2.11/hlint.yaml $HOME/.local/bin/data/ - - (WORK=`pwd` && cd $HOME && tar zcf $WORK/ci-tools-$HOST-$TRAVIS_TAG.tar.gz -C $HOME - .local/bin - .stack/snapshots/$HOST/*/8.8.2/share/$HOST-ghc-8.8.2/happy-1.19.12 - .stack/snapshots/$HOST/*/8.8.2/share/$HOST-ghc-8.8.2/hlint-2.2.11 - .stack/snapshots/$HOST/*/8.8.2/share/$HOST-ghc-8.8.2/pandoc-2.9.1.1) - -deploy: - provider: releases - token: - secure: "W8KCVBdSuKq5p4wpHSMbkC+1R7yDuSK0RR8avlB+eqi3KeXIc/7Rya10vu62mZx86vVvkmwLcL9/gqKqDusoncbo/NVhdiReR8F52IDBl1amCqHcStFL1Wb9vN2hjX5a7X3IPan6i2ABKtuSmF62PwZro7GJhT6krVAjacrEyVMpE2amV3pVhX0RYMSz9Zc4oUNGC85+IsGvjlbH7pZ9Y/UbwRJP/BNAtkJFWrlZYR40iVEJWslhbb9t2ZLJbUwO7uqBMfGBqcA9JwtulKH+UdAoAVgBFUubF+Rf217FEwq1WtlPypXjy5ZHpogIwz2SvE4A41t9uychx9EGMv4R/vLCnElBU6fInEjGnrbGqxk6fEfLIOm7S2gcV1wdXEjkQFKnS6S7EnzI6DCm84mPQjYZ0Jq4nsm9a4yt8E7zMFHBOxoDm0XtQ/b36FY8UnoDXlWhxkctmy4H7Ta1IQQbjqrgkLCnTiouezJ16RoP3oyqW5liWTVB6xmTtOh+WY77nEVIHkT61/aCFytxv5j1r1VHLOvM7bsARrFergItN92UdMbI8uKnNbzZ49xQ2aNnZMn7ZiKKhHhzzQtK8PW8QXpMy1yhNlOmDIrURbOVTesHyp7+rYUVQsymy471wOnpPAjKsyz5ZOWCEo1pd8xEikRv/zwJvj4qkEO0ZHIF71c=" - file: ci-tools-$HOST-$TRAVIS_TAG.tar.gz - on: - repo: TokTok/ci-tools - tags: true - edge: true - -# Only build pull requests and releases, don't build master on pushes, -# except through api or cron. -if: type IN (pull_request, api, cron) OR tag IS present diff --git a/README.md b/README.md index ae12a46..0fc00d2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bin/check_release b/bin/check_release index 103b448..80787dd 100755 --- a/bin/check_release +++ b/bin/check_release @@ -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 @@ -19,7 +21,7 @@ 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"]), @@ -27,20 +29,70 @@ def release_github() -> str: 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: @@ -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__":