diff --git a/.ci/scripts/pr_labels.py b/.ci/scripts/pr_labels.py new file mode 100644 index 00000000..24edc732 --- /dev/null +++ b/.ci/scripts/pr_labels.py @@ -0,0 +1,60 @@ +#!/bin/env python3 + +# This script is running with elevated privileges from the main branch against pull requests. + +import re +import sys +import tomllib +from pathlib import Path + +from git import Repo + + +def main(): + assert len(sys.argv) == 3 + + with open("pyproject.toml", "rb") as fp: + PYPROJECT_TOML = tomllib.load(fp) + BLOCKING_REGEX = re.compile(r"DRAFT|WIP|NO\s*MERGE|DO\s*NOT\s*MERGE|EXPERIMENT") + ISSUE_REGEX = re.compile(r"(?:fixes|closes)[\s:]+#(\d+)") + CHERRY_PICK_REGEX = re.compile(r"^\s*\(cherry picked from commit [0-9a-f]*\)\s*$") + try: + CHANGELOG_EXTS = { + f".{item['directory']}" for item in PYPROJECT_TOML["tool"]["towncrier"]["type"] + } + except KeyError: + CHANGELOG_EXTS = {"feature", "bugfix", "doc", "removal", "misc"} + + repo = Repo(".") + + base_commit = repo.commit(sys.argv[1]) + head_commit = repo.commit(sys.argv[2]) + + pr_commits = list(repo.iter_commits(f"{base_commit}..{head_commit}")) + + labels = { + "multi-commit": len(pr_commits) > 1, + "cherry-pick": False, + "no-issue": False, + "no-changelog": False, + "wip": False, + } + for commit in pr_commits: + labels["wip"] |= BLOCKING_REGEX.search(commit.summary) is not None + no_issue = ISSUE_REGEX.search(commit.message, re.IGNORECASE) is None + labels["no-issue"] |= no_issue + cherry_pick = CHERRY_PICK_REGEX.search(commit.message) is not None + labels["cherry-pick"] |= cherry_pick + changelog_snippets = [ + k + for k in commit.stats.files + if k.startswith("CHANGES/") and Path(k).suffix in CHANGELOG_EXTS + ] + labels["no-changelog"] |= not changelog_snippets + + print("ADD_LABELS=" + ",".join((k for k, v in labels.items() if v))) + print("REMOVE_LABELS=" + ",".join((k for k, v in labels.items() if not v))) + + +if __name__ == "__main__": + main() diff --git a/.ci/scripts/validate_commit_message.py b/.ci/scripts/validate_commit_message.py index 2bc6f02b..65f3f11e 100644 --- a/.ci/scripts/validate_commit_message.py +++ b/.ci/scripts/validate_commit_message.py @@ -1,24 +1,23 @@ # WARNING: DO NOT EDIT! # -# This file was generated by plugin_template, and is managed by bootstrap.py. Please use -# bootstrap.py to update this file. +# This file was generated by plugin_template and copied over to this repository :) # # For more info visit https://github.com/pulp/plugin_template -import glob -import os import re -import subprocess import sys - +from pathlib import Path +import subprocess +import os +import warnings from github import Github -KEYWORDS = ["fixes", "closes", "re", "ref"] -NO_ISSUE = "[noissue]" -CHANGELOG_EXTS = [".feature", ".bugfix", ".doc", ".removal", ".misc", ".deprecation", ".dev"] +CHANGELOG_EXTS = [".feature", ".bugfix", ".doc", ".removal", ".misc", ".deprecation"] +KEYWORDS = ["fixes", "closes"] sha = sys.argv[1] message = subprocess.check_output(["git", "log", "--format=%B", "-n 1", sha]).decode("utf-8") + g = Github(os.environ.get("GITHUB_TOKEN")) repo = g.get_repo("pulp/pulp-oci-images") @@ -27,41 +26,37 @@ def __check_status(issue): gi = repo.get_issue(int(issue)) if gi.pull_request: sys.exit(f"Error: issue #{issue} is a pull request.") - if gi.closed_at: + if gi.closed_at and "cherry picked from commit" not in message: + warnings.warn( + "When backporting, use the -x flag to append a line that says " + "'(cherry picked from commit ...)' to the original commit message." + ) sys.exit(f"Error: issue #{issue} is closed.") def __check_changelog(issue): - matches = glob.glob("CHANGES/{issue}.*".format(issue=issue)) + matches = list(Path("CHANGES").rglob(f"{issue}.*")) if len(matches) < 1: - sys.exit("Could not find changelog entry in CHANGES/ for {issue}.".format(issue=issue)) + sys.exit(f"Could not find changelog entry in CHANGES/ for {issue}.") for match in matches: - if os.path.splitext(match)[1] not in CHANGELOG_EXTS: - sys.exit("Invalid extension for changelog entry '{match}'.".format(match=match)) + if match.suffix not in CHANGELOG_EXTS: + sys.exit(f"Invalid extension for changelog entry '{match}'.") + if match.suffix == ".feature" and "cherry picked from commit" in message: + sys.exit(f"Can not backport '{match}' as it is a feature.") print("Checking commit message for {sha}.".format(sha=sha[0:7])) # validate the issue attached to the commit -if NO_ISSUE in message: - print("Commit {sha} has no issue attached. Skipping issue check".format(sha=sha[0:7])) -elif "Merge" in message and "cherry picked from commit" in message: - pass -else: - regex = r"(?:{keywords})[\s:]+#(\d+)".format(keywords=("|").join(KEYWORDS)) - pattern = re.compile(regex, re.IGNORECASE) +regex = r"(?:{keywords})[\s:]+#(\d+)".format(keywords=("|").join(KEYWORDS)) +pattern = re.compile(regex, re.IGNORECASE) - issues = pattern.findall(message) +issues = pattern.findall(message) - if issues: - for issue in pattern.findall(message): - __check_status(issue) - __check_changelog(issue) - else: - sys.exit( - "Error: no attached issues found for {sha}. If this was intentional, add " - " '{tag}' to the commit message.".format(sha=sha[0:7], tag=NO_ISSUE) - ) +if issues: + for issue in pattern.findall(message): + __check_status(issue) + __check_changelog(issue) print("Commit message for {sha} passed.".format(sha=sha[0:7])) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 575f3b95..dc4ccc01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,10 @@ name: pulp-oci-images CI on: pull_request: + paths: + - "images/**" + - ".github/**" + - ".ci/**" env: COLORTERM: 'yes' TERM: 'xterm-256color' diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml new file mode 100644 index 00000000..fe891cf7 --- /dev/null +++ b/.github/workflows/pr_checks.yml @@ -0,0 +1,66 @@ +# WARNING: DO NOT EDIT! +# +# This file was generated by plugin_template and copied over to this repository :) +# +# For more info visit https://github.com/pulp/plugin_template + +--- +name: "Pulp-OCI-Images PR static checks" +on: + pull_request_target: + types: ["opened", "synchronize", "reopened"] + +# This workflow runs with elevated permissions. +# Do not even think about running a single bit of code from the PR. +# Static analysis should be fine however. + +concurrency: + group: "${{ github.event.pull_request.number }}-${{ github.workflow }}" + cancel-in-progress: true + +jobs: + apply_labels: + runs-on: "ubuntu-latest" + name: "Label PR" + permissions: + pull-requests: "write" + steps: + - uses: "actions/checkout@v4" + with: + fetch-depth: 0 + - uses: "actions/setup-python@v5" + with: + python-version: "3.11" + - name: "Determine PR labels" + run: | + pip install GitPython==3.1.42 + git fetch origin ${{ github.event.pull_request.head.sha }} + python .ci/scripts/pr_labels.py "origin/${{ github.base_ref }}" "${{ github.event.pull_request.head.sha }}" >> "$GITHUB_ENV" + - uses: "actions/github-script@v7" + name: "Apply PR Labels" + with: + script: | + const { ADD_LABELS, REMOVE_LABELS } = process.env; + + if (REMOVE_LABELS.length) { + for await (const labelName of REMOVE_LABELS.split(",")) { + try { + await github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: labelName, + }); + } catch(err) { + } + } + } + if (ADD_LABELS.length) { + await github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ADD_LABELS.split(","), + }); + } +...