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__":