diff --git a/.github/actions/merge/Dockerfile b/.github/actions/merge/Dockerfile deleted file mode 100644 index 2049e57..0000000 --- a/.github/actions/merge/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM python:3.12 AS builder - -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - libcairo2-dev libgirepository1.0-dev - -ADD requirements.txt . - -RUN python -m venv /venv && \ - /venv/bin/python -m pip install -r requirements.txt PyGObject==3.50.0 - -FROM python:3.12-slim -ENV PATH="/venv/bin:$PATH" - -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - git gh jq libcairo2 libgirepository-1.0-1 gir1.2-json-1.0 && \ - apt-get clean && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /venv /venv -COPY . /app -WORKDIR /app - -CMD ["/app/entrypoint.py"] diff --git a/.github/actions/merge/entrypoint.py b/.github/actions/merge/entrypoint.py deleted file mode 100755 index c842e85..0000000 --- a/.github/actions/merge/entrypoint.py +++ /dev/null @@ -1,336 +0,0 @@ -#!/usr/bin/env python3 - -import glob -import json -import os -import re -import subprocess -import sys -import tempfile -import time - -import gi -import github -import pygit2 -import yaml -from gql import Client, gql -from gql.transport.requests import RequestsHTTPTransport - -gi.require_version("Json", "1.0") -from gi.repository import Json # noqa: E402 - - -def set_protected_branch(token, repo, branch): - transport = RequestsHTTPTransport( - url="https://api.github.com/graphql", - headers={"Authorization": f"Bearer {token}"}, - ) - client = Client(transport=transport, fetch_schema_from_transport=False) - - gql_get_repo_id = gql( - """ - query get_repo_id($repo: String!) { - repository(name: $repo, owner: "flathub") { - id - } - } - """ - ) - - gql_add_branch_protection = gql( - """ - mutation add_branch_protection($repositoryID: ID!, $pattern: String!) { - createBranchProtectionRule( - input: { - allowsDeletions: false - allowsForcePushes: false - dismissesStaleReviews: false - isAdminEnforced: false - pattern: $pattern - repositoryId: $repositoryID - requiresApprovingReviews: true - requiredApprovingReviewCount: 0 - requiresCodeOwnerReviews: false - requiresStatusChecks: true - requiresStrictStatusChecks: true - restrictsReviewDismissals: false - requiredStatusCheckContexts: ["builds/x86_64"] - } - ) { - branchProtectionRule { - id - } - } - } - """ - ) - - repo_id = client.execute(gql_get_repo_id, variable_values={"repo": repo}) - repo_id = repo_id["repository"]["id"] - - result = client.execute( - gql_add_branch_protection, - variable_values={"repositoryID": repo_id, "pattern": branch}, - ) - return result - - -def detect_appid(dirname): - files = [] - ret = (None, None) - appid = None - - for ext in ("yml", "yaml", "json"): - files.extend(glob.glob(f"{dirname}/*.{ext}")) - - for filename in files: - print(f"Parsing {filename}") - if os.path.isfile(filename): - ext = filename.split(".")[-1] - - with open(filename) as f: - if ext in ("yml", "yaml"): - manifest = yaml.safe_load(f) - if "app-id" in manifest: - appid = manifest["app-id"] - elif "id" in manifest: - appid = manifest["id"] - else: - parser = Json.Parser() - if parser.load_from_file(filename): - root_node = parser.get_root() - if root_node.get_node_type() == Json.NodeType.OBJECT: - json_object = root_node.get_object() - if json_object.has_member("id"): - appid = json_object.get_string_member("id") - elif json_object.has_member("app-id"): - appid = json_object.get_string_member("app-id") - - if not appid: - continue - - if appid: - manifest_file = os.path.basename(filename) - if os.path.splitext(manifest_file)[0] != appid: - print(f"Skipping {manifest_file}, does not match appid {appid}") - continue - ret = (manifest_file, appid) - - return ret - - -def main(): - github_token = os.environ.get("GITHUB_TOKEN") - if not github_token: - print("GITHUB_TOKEN environment variable is not set") - sys.exit(1) - - github_event_path = os.environ.get("GITHUB_EVENT_PATH") - with open(github_event_path) as f: - github_event = json.load(f) - - if github_event["action"] != "created": - print("The event is not a comment") - sys.exit(0) - - if "pull_request" not in github_event["issue"]: - print("The issue is not a pull request") - sys.exit(0) - - github_comment = github_event["comment"]["body"] - - if not github_comment.startswith("/merge"): - print("The comment does not start with '/merge'") - sys.exit(0) - - command_pattern = re.compile(r"^/merge(?::([\w.-]+))? head=([a-fA-F0-9]{40})(.*)$") - matched = command_pattern.search(github_comment) - if not matched: - print( - "The comment is not a valid '/merge' command.\n" - "Format: '/merge: " - "head= " - "'" - ) - sys.exit(1) - - branch_match = matched.group(1) or "master" - if branch_match in ("master", "beta"): - target_repo_default_branch = branch_match - else: - target_repo_default_branch = f"branch/{branch_match}" - - print(f"Got target branch {target_repo_default_branch} from comment") - - pr_head_sha = str(matched.group(2)) - print(f"Got PR HEAD SHA from comment: {pr_head_sha}") - - rest_comment = matched.group(3) - - # https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/iam-configuration-reference/username-considerations-for-external-authentication#about-username-normalization - # > Usernames for user accounts on GitHub can only contain alphanumeric characters and dashes - # > If the username is longer than 39 characters (including underscore and short code), - # > the provisioning attempt will fail with a 400 error. - additional_colbs = [m[1:] for m in re.findall(r"@[a-zA-Z0-9-]{1,39}", rest_comment)] - - print(f"Got additional collaborators {additional_colbs} from comment") - - gh = github.Github(github_token) - org = gh.get_organization("flathub") - - admins = org.get_team_by_slug("admins") - reviewers = org.get_team_by_slug("reviewers") - comment_author = gh.get_user(github_event["comment"]["user"]["login"]) - - flathub = org.get_repo("flathub") - - pr_id = int(github_event["issue"]["number"]) - pr = flathub.get_pull(pr_id) - pr_branch = pr.head.label.split(":")[1] - fork_url = pr.head.repo.clone_url - pr_author = pr.user.login - - if not admins.has_in_members(comment_author) and not reviewers.has_in_members( - comment_author - ): - print(f"{comment_author} is not a reviewer") - sys.exit(1) - - tmpdir = tempfile.TemporaryDirectory() - print(f"Cloning {fork_url} (branch: {pr_branch})") - clone = pygit2.clone_repository(fork_url, tmpdir.name, checkout_branch=pr_branch) - clone_head_sha = str(clone.head.target) - print(f"Clone HEAD SHA: {clone_head_sha}") - clone.submodules.update(init=True) - - assert flathub.get_pull(pr_id).state == "open" - - assert flathub.get_pull(pr_id).head.sha == pr_head_sha, ( - f"Current PR HEAD SHA {flathub.get_pull(pr_id).head.sha} does not match approved SHA" - ) - - assert clone_head_sha == pr_head_sha - - manifest_file, appid = detect_appid(tmpdir.name) - if manifest_file is None or appid is None: - print("Failed to detect appid") - sys.exit(1) - - print(f"Detected {appid} as appid from {manifest_file}") - - try: - org.get_repo(appid) - print( - f"Repository {appid} already exists in the flathub organisation, exiting." - ) - sys.exit(1) - except github.GithubException as err: - if err.status == 404: - print( - f"Repository {appid} does not exist in the flathub organisation, continuing." - ) - pass - else: - print(f"Unexpected error while checking for repository {appid}: {err}") - raise - - print("Creating new repo on Flathub") - repo = org.create_repo(appid) - repo_name = repo.name - time.sleep(5) - repo.edit( - homepage=f"https://flathub.org/apps/details/{appid}", - delete_branch_on_merge=True, - ) - - print("Adding flathub remote") - clone.remotes.create( - "flathub", f"https://x-access-token:{github_token}@github.com/flathub/{appid}" - ) - - print("Pushing changes to the new Flathub repo") - git_push = ( - f"cd {tmpdir.name} && git push flathub {pr_branch}:{target_repo_default_branch}" - ) - ret = subprocess.run( - git_push, - shell=True, - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - if ret.stdout: - print(f"Git push stdout:\n{ret.stdout.decode().strip()}") - if ret.stderr: - print(f"Git push stderr:\n{ret.stderr.decode().strip()}") - - repo.remove_from_collaborators("flathubbot") - - print("Setting protected branches") - for branch in ("master", "main", "stable", "branch/*", "beta", "beta/*"): - set_protected_branch(github_token, appid, branch) - - remote_branch_obj = repo.get_branch(target_repo_default_branch) - remote_head_sha = str(remote_branch_obj.commit.sha) - - assert flathub.get_pull(pr_id).state == "open" - - print(f"Remote HEAD SHA: {remote_head_sha}") - assert pr_head_sha == remote_head_sha - - assert remote_branch_obj.protected is True, ( - f"Branch '{target_repo_default_branch}' is not protected" - ) - - print(f"Adding {pr_author} to collaborators") - repo.add_to_collaborators(pr_author, permission="push") - - print("Adding 'trusted-maintainers' to collaborators") - trusted_maintainers = org.get_team_by_slug("trusted-maintainers") - trusted_maintainers.update_team_repository(repo, "push") - - if repo_name.startswith("org.kde."): - print("Adding KDE maintainers to collaborators") - kde_maintainers = org.get_team_by_slug("KDE") - kde_maintainers.update_team_repository(repo, "push") - - if repo_name.startswith("org.gnome.") and repo_name.count(".") == 2: - print("Adding GNOME maintainers to collaborators") - gnome_maintainers = org.get_team_by_slug("GNOME") - gnome_maintainers.update_team_repository(repo, "push") - - for user in additional_colbs: - try: - print(f"Adding mentioned {user} to collaborators") - repo.add_to_collaborators(user, permission="push") - except github.GithubException as err: - print(f"Adding mentioned {user} failed") - print(err) - pass - - assert flathub.get_pull(pr_id).state == "open" - - final_colbs = [user.login for user in repo.get_collaborators(affiliation="outside")] - print(f"External colloborators added: {final_colbs}") - - close_comment = ( - f"A repository for this submission has been created: {repo.html_url} and it will be published to Flathub in 4-5 hours.", - "\n", - f"You will receive an [invite]({repo.html_url}/invitations) to be a collaborator to the above repository. Please make sure to enable 2FA on GitHub and accept the invite within one week.", - "\n", - "Please go through the [App maintenance guide](https://docs.flathub.org/docs/for-app-authors/maintenance/) if you have never maintained an app on Flathub before.", - "\n", - "If you're the original developer (or an authorized party), please [verify your app](https://docs.flathub.org/docs/for-app-authors/verification) to let users know it's coming from you.", - "\n", - "Please follow the [Flathub blog](https://docs.flathub.org/blog) for the latest announcements.", - "\n", - "Thanks!", - ) - - print("Closing the pull request") - pr.create_issue_comment("\n".join(close_comment)) - pr.edit(state="closed") - - -if __name__ == "__main__": - main() diff --git a/.github/actions/merge/requirements.in b/.github/actions/merge/requirements.in deleted file mode 100644 index cc15727..0000000 --- a/.github/actions/merge/requirements.in +++ /dev/null @@ -1,4 +0,0 @@ -gql[requests] -pygit2 -PyGithub -PyYAML diff --git a/.github/actions/merge/requirements.txt b/.github/actions/merge/requirements.txt deleted file mode 100644 index 73c3970..0000000 --- a/.github/actions/merge/requirements.txt +++ /dev/null @@ -1,65 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile .github/actions/merge/requirements.in -# -anyio==4.4.0 - # via gql -backoff==2.2.1 - # via gql -certifi==2024.7.4 - # via requests -cffi==1.16.0 - # via - # cryptography - # pygit2 - # pynacl -charset-normalizer==3.3.2 - # via requests -cryptography==44.0.1 - # via pyjwt -deprecated==1.2.14 - # via pygithub -gql[requests]==3.5.0 - # via -r requirements.in -graphql-core==3.2.3 - # via gql -idna==3.7 - # via - # anyio - # requests - # yarl -multidict==6.0.5 - # via yarl -pycparser==2.22 - # via cffi -pygit2==1.15.0 - # via -r requirements.in -pygithub==2.3.0 - # via -r requirements.in -pyjwt[crypto]==2.8.0 - # via pygithub -pynacl==1.5.0 - # via pygithub -pyyaml==6.0.1 - # via -r requirements.in -requests==2.32.4 - # via - # gql - # pygithub - # requests-toolbelt -requests-toolbelt==1.0.0 - # via gql -sniffio==1.3.1 - # via anyio -typing-extensions==4.12.2 - # via pygithub -urllib3==2.2.2 - # via - # pygithub - # requests -wrapt==1.16.0 - # via deprecated -yarl==1.9.4 - # via gql diff --git a/.github/workflows/build-merge.yml b/.github/workflows/build-merge.yml deleted file mode 100644 index be29b62..0000000 --- a/.github/workflows/build-merge.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: 'Build image for merge workflow' - -on: - push: - branches: master - paths: - - ".github/actions/merge/*" - - .github/workflows/merge.yml - - .github/workflows/build-merge.yml - workflow_dispatch: - -jobs: - docker: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - # 4.2.2 - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - persist-credentials: false - - - name: Set up Docker Buildx - # 3.10.0 - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 - - - name: Login to ghcr.io - # 3.4.0 - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push - id: docker_build - # 6.15.0 - uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 - with: - context: .github/actions/merge - file: .github/actions/merge/Dockerfile - tags: ghcr.io/flathub/actions/merge:latest - push: true - cache-from: type=gha - cache-to: type=gha,mode=max