Create GitHub pull requests from Port
This guide demonstrates how to open a pull-request in a GitHub repository from within Port using a Jenkins pipeline.
The workflow involves adding a resource block to a Terraform main.tf file and subsequently generating a PR for the modification on GitHub. In this specific instance, the added resource is a storage account in the Azure cloud.
Common use cases
- Automate infrastructure provisioning by creating pull requests for Terraform changes.
- Enable developers to request infrastructure resources through self-service actions.
- Maintain proper GitOps workflows with automated branch creation and PR generation.
- Streamline the process of adding new cloud resources to your infrastructure codebase.
Prerequisites
This guide assumes you have:
- You have a Port account and have completed the onboarding process.
- Jenkins with the Generic Webhook Trigger plugin installed
Implementation
Set up self-service action
-
Go to the Self-service page of your portal.
-
Click on the
+ New Actionbutton. -
Click on the
{...} Edit JSONbutton. -
Copy and paste the following JSON configuration into the editor.
Open GitHub Pr with Jenkins action(Click to expand)
PlaceholdersYOUR_JENKINS_URL- The URL of your Jenkins server.JOB_TOKEN- The token of the Jenkins job.
{
"identifier": "open_github_pr_with_jenkins",
"title": "Open GitHub PR with Jenkins",
"icon": "Microservice",
"description": "This action opens a PR after modifying a file using Jenkins",
"trigger": {
"type": "self-service",
"operation": "DAY-2",
"userInputs": {
"properties": {
"storage_name": {
"type": "string",
"title": "Storage Name"
},
"storage_location": {
"type": "string",
"title": "Storage Location"
}
},
"required": [],
"order": [
"storage_name",
"storage_location"
]
},
"blueprintIdentifier": "service"
},
"invocationMethod": {
"type": "JENKINS",
"url": "http://YOUR_JENKINS_URL/generic-webhook-trigger/invoke?token=<JOB_TOKEN>",
"agent": false,
"body": {
"{{ spreadValue() }}": "{{ .inputs }}",
"port_context": {
"runId": "{{ .run.id }}",
"blueprint": "{{ .action.blueprint }}",
"entity": "{{ .entity }}"
}
}
},
"requiredApproval": false
}Jenkins invocation typeLearn more about the Jenkins invocation type here.
-
Click
Save.
Now you should see the Open GitHub Pr with Jenkins action in the self-service page. 🎉
Configure Jenkins pipeline
Now we want to write the Jenkins pipeline that our action will trigger.
Set up credentials and tokens
-
First, let's obtain the necessary token and secrets:
-
Go to your GitHub tokens page, create a personal access token with
repoandadmin:orgscope, and copy it (this token is needed to create a pull-request from our pipeline).
-
To get your Port credentials, go to your Port application, click on the
...button in the top right corner, and selectCredentials. Here you can view and copy yourCLIENT_IDandCLIENT_SECRET:
-
-
Create the following as Jenkins Credentials:
-
Create the Port Credentials using the
Username with passwordkind and the idport-credentials.-
PORT_CLIENT_ID- Port Client ID. -
PORT_CLIENT_SECRET- Port Client Secret.
-
-
WEBHOOK_TOKEN- The webhook token so that the job can only be triggered if that token is supplied. -
GITHUB_TOKEN- The personal access token obtained from the previous step.
-
Create Terraform templates
We will now create a simple .tf file that will serve as a template for our new resource:
-
In your GitHub repository, create a file named
create-azure-storage.tfunder/templates/(it's path should be/templates/create-azure-storage.tf). -
Copy the following snippet and paste it in the file's contents:
create-azure-storage.tf
create-azure-storage.tf
resource "azurerm_storage_account" "storage_account" {
name = "{{ storage_name }}"
resource_group_name = "YourResourcesGroup" # replace this with one of your resource groups in your azure cloud account
location = "{{ storage_location }}"
account_tier = "Standard"
account_replication_type = "LRS"
account_kind = "StorageV2"
} -
Add the
main.tffile in the root of your repository.main.tf
main.tf# Configure the Azure provider
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0.2"
}
}
required_version = ">= 1.1.0"
}
provider "azurerm" {
features {}
}
Create the Jenkins pipeline
Now let's create the pipeline file:
-
Define variables for a pipeline: Define the STORAGE_NAME, STORAGE_LOCATION, REPO_URL and PORT_RUN_ID variables.
-
Token Setup: Define the token to match
JOB_TOKENas configured in your Port Action.
Our pipeline will consist of 3 steps for the selected service's repository:
-
Adding a resource block to the
main.tfusing the template and replacing its variables with the data from the action's input. -
Creating a pull request in the repository to add the new resource.
-
Reporting & logging the action result back to Port.
In your Jenkins pipeline, use the following snippet as its content:
Jenkins pipeline
import groovy.json.JsonSlurper
pipeline {
agent any
environment {
GITHUB_TOKEN = credentials("GITHUB_TOKEN")
NEW_BRANCH_PREFIX = 'infra/new-resource'
NEW_BRANCH_NAME = "${NEW_BRANCH_PREFIX}-${STORAGE_NAME}"
TEMPLATE_FILE = "templates/create-azure-storage.tf"
PORT_ACCESS_TOKEN = ""
REPO = ""
}
triggers {
GenericTrigger(
genericVariables: [
[key: 'STORAGE_NAME', value: '$.payload.properties.storage_name'],
[key: 'STORAGE_LOCATION', value: '$.payload.properties.storage_location'],
[key: 'REPO_URL', value: '$.payload.entity.properties.url'],
[key: 'PORT_RUN_ID', value: '$.context.runId']
],
causeString: 'Triggered by Port',
allowSeveralTriggersPerBuild: true,
regexpFilterExpression: '',
regexpFilterText: '',
printContributedVariables: true,
printPostContent: true
)
}
stages {
stage('Checkout') {
steps {
script {
def path = REPO_URL.substring(REPO_URL.indexOf("/") + 1);
def pathUrl = path.replace("/github.com/", "");
REPO = pathUrl
}
git branch: 'main', credentialsId: 'github', url: "git@github.com:${REPO}.git"
}
}
stage('Make Changes') {
steps {
script {
sh """cat ${TEMPLATE_FILE} | sed "s/{{ storage_name }}/${STORAGE_NAME}/g; s/{{ storage_location }}/${STORAGE_LOCATION}/g" >> main.tf"""
}
}
}
stage('Create Branch and Commit') {
steps {
script {
sh "git checkout -b ${NEW_BRANCH_NAME}"
sh "git commit -am 'Add a new resource block file'"
sh "git push origin ${NEW_BRANCH_NAME}"
}
}
}
stage('Create pull request') {
steps {
script {
repo = REPO
branch_name = NEW_BRANCH_NAME
base_branch = 'main'
title = 'New resource block ' + STORAGE_NAME
body = 'This pull request adds a new resource block to the project.'
createPullRequestCurl(repo, branch_name, base_branch, title, body)
}
}
}
stage('Get access token') {
steps {
withCredentials([usernamePassword(
credentialsId: 'port-credentials',
usernameVariable: 'PORT_CLIENT_ID',
passwordVariable: 'PORT_CLIENT_SECRET')]) {
script {
// Execute the curl command and capture the output
def result = sh(returnStdout: true, script: """
accessTokenPayload=\$(curl -X POST \
-H "Content-Type: application/json" \
-d '{"clientId": "${PORT_CLIENT_ID}", "clientSecret": "${PORT_CLIENT_SECRET}"}' \
-s "https://api.getport.io/v1/auth/access_token")
echo \$accessTokenPayload
""")
// Parse the JSON response using JsonSlurper
def jsonSlurper = new JsonSlurper()
def payloadJson = jsonSlurper.parseText(result.trim())
// Access the desired data from the payload
PORT_ACCESS_TOKEN = payloadJson.accessToken
}
}
}
}
stage('Notify Port') {
steps {
script {
def logs_report_response = sh(script: """
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${PORT_ACCESS_TOKEN}" \
-d '{"message": "Created GitHub PR for new terraform resource ${STORAGE_NAME}"}"}' \
"https://api.getport.io/v1/actions/runs/$PORT_RUN_ID/logs"
""", returnStdout: true)
println(logs_report_response)
}
}
}
stage('Update Run Status') {
steps {
script {
def status_report_response = sh(script: """
curl -X PATCH \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${PORT_ACCESS_TOKEN}" \
-d '{"status":"SUCCESS", "message": {"run_status": "Jenkins CI/CD Run completed successfully!"}}' \
"https://api.getport.io/v1/actions/runs/${PORT_RUN_ID}"
""", returnStdout: true)
println(status_report_response)
}
}
}
}
post {
failure {
// Update Port Run failed.
script {
def status_report_response = sh(script: """
curl -X PATCH \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${PORT_ACCESS_TOKEN}" \
-d '{"status":"FAILURE", "message": {"run_status": "Failed to create azure resource ${STORAGE_NAME}"}}' \
"https://api.getport.io/v1/actions/runs/${PORT_RUN_ID}"
""", returnStdout: true)
println(status_report_response)
}
}
// Clean after build
always {
cleanWs(cleanWhenNotBuilt: false,
deleteDirs: true,
disableDeferredWipeout: false,
notFailBuild: true,
patterns: [[pattern: '.gitignore', type: 'INCLUDE'],
[pattern: '.propsfile', type: 'EXCLUDE']])
}
}
}
def createPullRequestCurl(repo, headBranch, baseBranch, title, body) {
curlCommand = "curl -X POST https://api.github.com/repos/$repo/pulls -H 'Authorization: Bearer ${GITHUB_TOKEN}' -d '{ \"head\": \"$headBranch\", \"base\": \"$baseBranch\", \"title\": \"$title\", \"body\": \"$body\", \"draft\": false }'"
try {
response = sh(script: curlCommand)
if (response.contains('201 Created')) {
println "Pull request created successfully"
} else {
println "Failed to create pull request"
println response
}
} catch (Exception e) {
println "Error occurred during CURL request: ${e.getMessage()}"
}
}
All done! The action is ready to be executed 🚀
Let's test it!
Now let's test the action to ensure it works correctly:
-
Go to the Self-service page of your portal.
-
Click on the
Open GitHub PR with Jenkinsaction. -
Enter a name for your Azure storage account and a location.
-
Select any service from the list and click
Execute. -
A small popup will appear - click on
View detailsto see the action run details. -
Verify that the backend returned
Successand the pull-request was created successfully in your GitHub repository. -
Check your GitHub repository to confirm the new pull request has been created with the Terraform changes. All done! You can now create PRs for your services directly from Port 💪🏽
You may create a Jenkins pipeline to trigger the resource deployment on merging the PR. Checkout this example pipeline.
More relevant guides and examples: