diff --git a/.github/workflows/deploy_tools_chart.yaml b/.github/workflows/deploy_tools_chart.yaml index 0a3ddcf6fc..b90b21d94b 100644 --- a/.github/workflows/deploy_tools_chart.yaml +++ b/.github/workflows/deploy_tools_chart.yaml @@ -21,4 +21,4 @@ jobs: openshift_token: ${{ secrets.OPENSHIFT_TOKEN }} insecure_skip_tls_verify: true - run: | - helm upgrade ccbc-tools helm/ccbc-tools --install --atomic -n ${{ secrets.NAMESPACE_PREFIX }}-tools --set namespacePrefix=${{ secrets.NAMESPACE_PREFIX }} + helm upgrade ccbc-tools helm/ccbc-tools --install --atomic -n ${{ secrets.NAMESPACE_PREFIX }}-tools --set namespacePrefix=${{ secrets.NAMESPACE_PREFIX }} --set deployer.githubToken=${{ secrets.TEKTON_GITHUB}} --set deployer.headerSecret=${{ secrets.JIRA_SECRET }} diff --git a/helm/ccbc-tools/templates/ci-cd/eventlistener.yaml b/helm/ccbc-tools/templates/ci-cd/eventlistener.yaml new file mode 100644 index 0000000000..aa734e2f20 --- /dev/null +++ b/helm/ccbc-tools/templates/ci-cd/eventlistener.yaml @@ -0,0 +1,23 @@ +apiVersion: triggers.tekton.dev/v1alpha1 +kind: EventListener +metadata: + name: jira-sprint-done-event-listener +spec: + triggers: + - bindings: + - kind: TriggerBinding + name: key + value: $(body.issue.key) + - kind: TriggerBinding + name: signature + value: '$(header[''Ccbc-Jira-Header''])' + interceptors: + - params: + - name: filter + value: 'header[''Ccbc-Jira-Header''] != null' + ref: + kind: ClusterInterceptor + name: cel + name: trigger-github + template: + ref: trigger-github-merge-and-release diff --git a/helm/ccbc-tools/templates/ci-cd/route.yaml b/helm/ccbc-tools/templates/ci-cd/route.yaml new file mode 100644 index 0000000000..81bf7f5af6 --- /dev/null +++ b/helm/ccbc-tools/templates/ci-cd/route.yaml @@ -0,0 +1,16 @@ +kind: Route +apiVersion: route.openshift.io/v1 +metadata: + name: ccbc-jira-sprint-done-el +spec: + host: ccbc-jira-sprint-done-el.apps.silver.devops.gov.bc.ca + to: + kind: Service + name: el-jira-sprint-done-event-listener + weight: 100 + port: + targetPort: http-listener + tls: + termination: edge + insecureEdgeTerminationPolicy: Redirect + wildcardPolicy: None diff --git a/helm/ccbc-tools/templates/ci-cd/secret.yaml b/helm/ccbc-tools/templates/ci-cd/secret.yaml new file mode 100644 index 0000000000..77956762e9 --- /dev/null +++ b/helm/ccbc-tools/templates/ci-cd/secret.yaml @@ -0,0 +1,8 @@ +kind: Secret +apiVersion: v1 +metadata: + name: trigger-deploy-secret +data: + headerSecret: {{ .Values.deployer.headerSecret | b64enc | quote }} + githubToken: {{ .Values.deployer.githubToken | b64enc | quote }} +type: Opaque diff --git a/helm/ccbc-tools/templates/ci-cd/task.yaml b/helm/ccbc-tools/templates/ci-cd/task.yaml new file mode 100644 index 0000000000..f96017e962 --- /dev/null +++ b/helm/ccbc-tools/templates/ci-cd/task.yaml @@ -0,0 +1,44 @@ +apiVersion: tekton.dev/v1beta1 +kind: Task +metadata: + name: trigger-github-merge-and-release +spec: + params: + - default: bcgov + description: Repo owner argument + name: arg1 + type: string + - default: CONN-CCBC-portal + description: Repo name argument + name: arg2 + type: string + - description: Branch name prefix (JIRA Key) + name: arg3 + type: string + - description: Received header signature key passed from EL + name: arg4 + type: string + steps: + - args: + - /workspace/source/lib/ci_cd/merge_process.py + - $(params.arg1) + - $(params.arg2) + - $(params.arg3) + - $(params.arg4) + command: + - python + env: + - name: GITHUB_TOKEN + valueFrom: + secretKeyRef: + key: githubToken + name: trigger-deploy-secret + - name: HEADER_SECRET + valueFrom: + secretKeyRef: + key: headerSecret + name: trigger-deploy-secret + image: >- + image-registry.openshift-image-registry.svc:5000/ff61fb-tools/python-3-11-with-requests + name: run-python + resources: {} diff --git a/helm/ccbc-tools/templates/ci-cd/triggertemplate.yaml b/helm/ccbc-tools/templates/ci-cd/triggertemplate.yaml new file mode 100644 index 0000000000..992ac2c547 --- /dev/null +++ b/helm/ccbc-tools/templates/ci-cd/triggertemplate.yaml @@ -0,0 +1,23 @@ +apiVersion: triggers.tekton.dev/v1alpha1 +kind: TriggerTemplate +metadata: + name: trigger-github-merge-and-release +spec: + params: + - description: The issue key + name: key + - description: The header key + name: signature + resourcetemplates: + - apiVersion: tekton.dev/v1beta1 + kind: TaskRun + metadata: + generateName: run-trigger-github-merge-and-release- + spec: + params: + - name: arg3 + value: $(tt.params.key) + - name: arg4 + value: $(tt.params.signature) + taskRef: + name: trigger-github-merge-and-release diff --git a/helm/ccbc-tools/templates/deployer/deployerRole.yaml b/helm/ccbc-tools/templates/deployer/deployerRole.yaml index 59c7788276..ccf3ab2a2d 100644 --- a/helm/ccbc-tools/templates/deployer/deployerRole.yaml +++ b/helm/ccbc-tools/templates/deployer/deployerRole.yaml @@ -214,5 +214,58 @@ rules: - update - patch - delete + - apiGroups: + - build.openshift.io + resources: + - buildconfigs + verbs: + - get + - list + - create + - update + - delete + - patch + - apiGroups: + - image.openshift.io + resources: + - imagestreams + verbs: + - get + - list + - create + - update + - delete + - patch + - apiGroups: + - tekton.dev + resources: + - tasks + - taskruns + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - triggers.tekton.dev + resources: + - eventlisteners + - interceptors + - triggers + - triggerbindings + - triggertemplates + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch {{ end }} diff --git a/helm/ccbc-tools/values.yaml b/helm/ccbc-tools/values.yaml index 2ee0618181..74aaed4db0 100644 --- a/helm/ccbc-tools/values.yaml +++ b/helm/ccbc-tools/values.yaml @@ -3,6 +3,8 @@ namespacePrefix: ~ deployer: serviceAccount: enabled: true + githubToken: '' # The GitHub token must be passed in via the deploy script + headerSecret: '' # The header secret must be passed in via the deploy script linter: serviceAccount: diff --git a/lib/ci_cd/merge_process.py b/lib/ci_cd/merge_process.py new file mode 100644 index 0000000000..827d49b6c1 --- /dev/null +++ b/lib/ci_cd/merge_process.py @@ -0,0 +1,150 @@ +import sys +import json +import os +import requests + +def find_pr_by_partial_branch(repo_owner, repo_name, branch_name): + # Make a GET request to the GitHub API with a partial branch name + response = requests.get(f"https://api.github.com/search/issues?q=repo:{repo_owner}/{repo_name}+head:{branch_name}+is:pr") + + # Check if the request was successful + if response.status_code != 200: + print("Failed to retrieve PRs: Repository or branch not found.") + sys.exit(1) + + try: + # Convert response text to JSON + data = response.json() + except json.JSONDecodeError as e: + print(f"Failed to decode JSON response: {e}") + sys.exit(1) + + # Filter out control characters from the JSON response + cleaned_response = {k: v.translate({0: None, 127: None}) if isinstance(v, str) else v for k, v in data.items()} + + # Extract the URL of the first pull request (if any) + try: + pr_api_url = cleaned_response["items"][0]["url"] + except (KeyError, IndexError): + print("No PR found for the specified branch.") + sys.exit(1) + + if pr_api_url: + return pr_api_url + else: + print("No PR found for the specified branch.") + +def update_pr_description(pr_url, token): + # Get the PR details + headers = {"Accept": "application/vnd.github.v3+json", "Authorization": f"token {token}"} + response = requests.get(pr_url, headers=headers) + + if response.status_code != 200: + print(f"Failed to retrieve PR details: {response.status_code}") + return + + pr_details = response.json() + + # Check if the PR body already contains the checkbox + checked_checkbox = "- [x] Check me to trigger auto merge process." + if checked_checkbox in pr_details["body"]: + print("Checkbox already exists in the PR description.") + else: + # Update the PR description + existing_checkbox = "- [ ] Check me to trigger auto merge process." + new_description = pr_details["body"].replace(existing_checkbox, "- [ ] Check me to trigger auto merge process. edited by script") + pr_details["body"] = new_description + + # Send the updated PR details + response = requests.patch(pr_url, headers=headers, json={"body": new_description}) + + if response.status_code == 200: + print("PR description updated successfully!") + else: + print(f"Failed to update PR description: {response.status_code}") + +def get_pull_request_id(pr_url, token): + # Extract pull request number from the URL + pr_number = pr_url.split("/")[-1] + + # GraphQL query to get pull request ID + query = """ + query GetPullRequestID { + repository(owner: "%s", name: "%s") { + pullRequest(number: %s) { + id + } + } + } + """ % (repo_owner, repo_name, pr_number) + + # Send GraphQL request + headers = {"Authorization": f"Bearer {token}"} + response = requests.post("https://api.github.com/graphql", json={"query": query}, headers=headers) + + if response.status_code != 200: + print(f"Failed to retrieve pull request ID: {response.status_code}") + return None + + data = response.json() + pull_request_id = data.get("data", {}).get("repository", {}).get("pullRequest", {}).get("id") + return pull_request_id + +def enable_auto_merge(pull_request_id, token): + # GraphQL mutation to enable auto merge + mutation = """ + mutation EnableAutoMerge { + enablePullRequestAutoMerge(input: {pullRequestId: "%s", mergeMethod: MERGE}) { + clientMutationId + } + } + """ % pull_request_id + + # Send GraphQL request + headers = {"Authorization": f"Bearer {token}"} + # response = requests.post("https://api.github.com/graphql", json={"query": mutation}, headers=headers) + + # if response.status_code != 200: + # print(f"Failed to enable auto merge: {response.status_code}") + # return + + print("Auto merge enabled successfully!") + +def check_header_secret(passed_value): + # Get the value of the environment variable named HEADER_SECRET + header_secret = os.environ.get('HEADER_SECRET') + + # Check if passed_value is equal to expected header_secret + if passed_value == header_secret: + return True + else: + return False + +if __name__ == "__main__": + if len(sys.argv) != 5: + print("Usage: python script.py ") + sys.exit(1) + + repo_owner = sys.argv[1] + repo_name = sys.argv[2] + branch_name = sys.argv[3] + passed_header = sys.argv[4] + token = os.environ.get('GITHUB_TOKEN') + + if not check_header_secret(passed_header): + print("Invalid header secret.") + sys.exit(1) + + print(repo_owner, repo_name, branch_name, passed_header, token) + + pr_url = find_pr_by_partial_branch(repo_owner, repo_name, branch_name) + + update_pr_description(pr_url, token) + + parts = pr_url.split("/") + repo_owner = parts[4] + repo_name = parts[5] + + pull_request_id = get_pull_request_id(pr_url, token) + if pull_request_id: + enable_auto_merge(pull_request_id, token)