> For the complete documentation index, see llms.txt.
Skip to main content

Check out Port for yourself ➜ 

Scaffold repositories using Cookiecutter

Implement with AI

Send this guide to your coding agent.

Prerequisite: Install Port MCP

Open plan mode if your tool supports it; otherwise present the plan below filled in and wait for my approval. Implement this Port guide in my org via MCP:

https://docs.port.io/guides/all/scaffold-repositories-using-cookiecutter

Read the raw markdown version at https://docs.port.io/guides/all/scaffold-repositories-using-cookiecutter.md - it contains every tab and code block without page markup.

Goal: get the guide's core flow working end-to-end in my org; adapting it to fit my existing setup takes priority over matching the guide 1:1.

Plan:
1. Confirm MCP is connected, in the right org, with sufficient permissions.
2. If the guide offers alternative implementation paths (tabs), pick the one matching my installed integrations and tools, confirm it with me, and implement only that path.
3. Diff the guide's data model (blueprints, properties, relations, workflows, actions, agents, automations, integrations, webhook data sources, secrets) against mine.
4. Propose adaptations for gaps, reusing existing blueprints/relations over guide-named duplicates.
5. Flag what needs a UI click, credential, or secret from me, testing MCP capability empirically before ruling anything out. If the guide has a "Set up via API" section, use it for anything MCP can't do before treating a step as UI-only.
6. Stop on any blocker and give me options. Approving this plan authorizes the writes it lists; pause only for writes beyond what's listed.

Build:
- Extend blueprint schema additively when upserting; don't remove or overwrite existing properties, and treat type conflicts as a blocker, not an auto-fix.
- Never print secret values into the chat or logs; ask me to set them in Port, or write them via the secrets API without echoing them back.
- List any mock data in the plan, minimal and labeled mock; once approved, seed it without re-asking, and tell me what you seeded.
- For anything the guide writes downstream (e.g. a webhook target), use a real entity, not a mock.
- For pages/widgets, use the real page identifier from the app URL, not a guessed slug.
- When you hit a UI step confirmed (not assumed) unsupported via MCP and not covered by the guide's API sections, pause, give exact clicks, then resume via MCP.
- Validate and give links after each meaningful step (only a tool-returned URL, no guessed paths); don't proceed if the last run wasn't a success.

Done:
- Run the guide's "Let's test it" steps where possible (e.g. execute a workflow test run) and confirm the expected output exists in Port.
- Summarize adaptations, seeded data, what was mocked or skipped, remaining UI steps, and how to verify.

This guide demonstrates how to scaffold GitHub, Bitbucket, and Azure DevOps repositories from Port self-service actions using Cookiecutter templates.

Cookiecutter is an open-source project template engine. You can use the examples in this guide as a starting point, then replace the template URL with your own Cookiecutter template.

Common use cases

  • Give developers a self-service action for creating repositories from approved templates.
  • Standardize new repository structure across Git providers.
  • Register each scaffolded repository back into the Port context lake.

Prerequisites

  • Complete the Port onboarding process.
  • Install Cookiecutter on the runner or agent that executes the scaffold workflow.
  • Configure a Git provider account with permissions to create repositories and push code.
  • Configure Port API credentials. See the Port API token documentation.

Set up data model

The GitHub and Bitbucket examples use a microservice blueprint. The Azure DevOps example uses project and service blueprints.

If you already have matching blueprints from your Git provider integration, you can skip the relevant blueprint setup and adapt the action JSON to your existing blueprint identifiers.

Create the microservice blueprint

  1. Go to the Builder page.

  2. Click + Blueprint.

  3. Click Edit JSON.

  4. Copy and paste the following JSON configuration into the editor:

    Microservice blueprint (Click to expand)
    {
    "identifier": "microservice",
    "title": "Microservice",
    "icon": "Microservice",
    "schema": {
    "properties": {
    "description": {
    "title": "Description",
    "type": "string"
    },
    "url": {
    "title": "URL",
    "format": "url",
    "type": "string"
    }
    },
    "required": []
    },
    "mirrorProperties": {},
    "calculationProperties": {},
    "relations": {}
    }
  5. Click Save.

Create the Azure DevOps project blueprint

  1. Go to the Builder page.

  2. Click + Blueprint.

  3. Click Edit JSON.

  4. Copy and paste the following JSON configuration into the editor:

    Azure DevOps project blueprint (Click to expand)
    {
    "identifier": "project",
    "title": "Project",
    "icon": "AzureDevops",
    "schema": {
    "properties": {
    "state": {
    "title": "State",
    "type": "string",
    "icon": "AzureDevops",
    "description": "The current lifecycle state of the project."
    },
    "revision": {
    "title": "Revision",
    "type": "string",
    "icon": "AzureDevops",
    "description": "The revision number, indicating how many times the project configuration has been updated."
    },
    "visibility": {
    "title": "Visibility",
    "type": "string",
    "icon": "AzureDevops",
    "description": "Indicates whether the project is private or public"
    },
    "defaultTeam": {
    "title": "Default Team",
    "type": "string",
    "icon": "Team",
    "description": "Default team of the project"
    },
    "link": {
    "title": "Link",
    "type": "string",
    "format": "url",
    "icon": "AzureDevops",
    "description": "Link to the Azure DevOps project"
    }
    },
    "required": []
    },
    "mirrorProperties": {},
    "calculationProperties": {},
    "aggregationProperties": {},
    "relations": {}
    }
  5. Click Save.

System blueprint

This guide requires the service blueprint to be created however it is a system blueprint provided by default in every Port organization. No setup is required.

Scaffold GitHub repositories

Set up Jenkins backend

  1. Create the following Jenkins credentials:

    • GITHUB_USERNAME - A GitHub user with access to the target organization.
    • GITHUB_TOKEN - A fine-grained personal access token with permissions to create repositories and push code.
    • PORT_CLIENT_ID - Your Port client ID.
    • PORT_CLIENT_SECRET - Your Port client secret.
  2. Use the Secret text credential type for each credential.

  3. Make sure the Jenkins agent can run git, curl, and cookiecutter.

Set up self-service action

Follow the steps below to create a self-service action that triggers the Jenkins pipeline.

  1. Go to the Self-service page in Port.

  2. Click on the + New Action button.

  3. Click on the {...} Edit JSON button.

  4. Copy and paste the following JSON configuration into the editor:

    Scaffold GitHub repository action (Click to expand)
    {
    "identifier": "microservice_scaffold",
    "title": "Scaffold Golang Microservice",
    "icon": "Go",
    "description": "Scaffold a new microservice from a Cookiecutter template",
    "trigger": {
    "type": "self-service",
    "operation": "CREATE",
    "userInputs": {
    "properties": {
    "repo_name": {
    "icon": "Microservice",
    "title": "Repo Name",
    "type": "string"
    },
    "github_org_name": {
    "icon": "Github",
    "title": "GitHub Org Name",
    "type": "string"
    }
    },
    "required": [
    "repo_name",
    "github_org_name"
    ]
    },
    "blueprintIdentifier": "microservice"
    },
    "invocationMethod": {
    "type": "WEBHOOK",
    "url": "https://<JENKINS_URL>/generic-webhook-trigger/invoke?token=<JOB_TOKEN>",
    "agent": false,
    "synchronized": false,
    "method": "POST",
    "body": {
    "action": "{{ .action.identifier[(\"microservice_\" | length):] }}",
    "resourceType": "run",
    "status": "TRIGGERED",
    "trigger": "{{ .trigger | {by, origin, at} }}",
    "context": {
    "entity": "{{.entity.identifier}}",
    "blueprint": "{{.action.blueprint}}",
    "runId": "{{.run.id}}"
    },
    "payload": {
    "entity": "{{ (if .entity == {} then null else .entity end) }}",
    "action": {
    "invocationMethod": {
    "type": "WEBHOOK",
    "agent": false,
    "url": "https://<JENKINS_URL>/generic-webhook-trigger/invoke?token=<JOB_TOKEN>",
    "synchronized": false,
    "method": "POST"
    },
    "trigger": "{{.trigger.operation}}"
    },
    "properties": {
    "{{if (.inputs | has(\"repo_name\")) then \"repo_name\" else null end}}": "{{.inputs.\"repo_name\"}}",
    "{{if (.inputs | has(\"github_org_name\")) then \"github_org_name\" else null end}}": "{{.inputs.\"github_org_name\"}}"
    },
    "censoredProperties": "{{.action.encryptedProperties}}"
    }
    }
    }
    }
  5. Click Save to create the action.

Create Jenkins pipeline

  1. Create a Jenkins pipeline.

  2. Enable a webhook trigger for the pipeline.

  3. Define pipeline variables named REPO_NAME, GITHUB_ORG_NAME, and RUN_ID.

    Jenkins variables for the GitHub scaffold pipeline
  4. Configure the Jenkins token to match the JOB_TOKEN value in your Port action.

  5. Add the following content to the pipeline:

    GitHub Jenkins pipeline script (Click to expand)
    import groovy.json.JsonSlurper

    pipeline {
    agent any

    environment {
    COOKIECUTTER_TEMPLATE = 'https://github.com/lacion/cookiecutter-golang'
    REPO_NAME = "${REPO_NAME}"
    GITHUB_ORG_NAME = "${GITHUB_ORG_NAME}"
    SCAFFOLD_DIR = "scaffold_${REPO_NAME}"
    PORT_ACCESS_TOKEN = ""
    PORT_BLUEPRINT_ID = "microservice"
    PORT_RUN_ID = "${RUN_ID}"
    }

    stages {
    stage('Get access token') {
    steps {
    script {
    withCredentials([
    string(credentialsId: 'PORT_CLIENT_ID', variable: 'PORT_CLIENT_ID'),
    string(credentialsId: 'PORT_CLIENT_SECRET', variable: 'PORT_CLIENT_SECRET')
    ]) {
    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.port.io/v1/auth/access_token")
    echo \$accessTokenPayload
    """)

    def jsonSlurper = new JsonSlurper()
    def payloadJson = jsonSlurper.parseText(result.trim())
    PORT_ACCESS_TOKEN = payloadJson.accessToken
    }
    }
    }
    }

    stage('Create GitHub repository') {
    steps {
    script {
    def logs_report_response = sh(script: """
    curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${PORT_ACCESS_TOKEN}" \
    -d '{"message": "Creating GitHub repository: ${REPO_NAME} in GitHub org: ${GITHUB_ORG_NAME}..."}' \
    "https://api.port.io/v1/actions/runs/${PORT_RUN_ID}/logs"
    """, returnStdout: true)

    println(logs_report_response)
    }
    script {
    withCredentials([string(credentialsId: 'GITHUB_TOKEN', variable: 'GITHUB_TOKEN')]) {
    sh """
    curl -i -H 'Authorization: token ${GITHUB_TOKEN}' \
    -d '{"name": "${REPO_NAME}", "private": true}' \
    https://api.github.com/orgs/${GITHUB_ORG_NAME}/repos
    """
    }
    }
    }
    }

    stage('Scaffold Cookiecutter template') {
    steps {
    script {
    def logs_report_response = sh(script: """
    curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${PORT_ACCESS_TOKEN}" \
    -d '{"message": "Scaffolding ${REPO_NAME}..."}' \
    "https://api.port.io/v1/actions/runs/${PORT_RUN_ID}/logs"
    """, returnStdout: true)

    println(logs_report_response)
    }
    script {
    withCredentials([
    string(credentialsId: 'GITHUB_USERNAME', variable: 'GITHUB_USERNAME'),
    string(credentialsId: 'GITHUB_TOKEN', variable: 'GITHUB_TOKEN')
    ]) {
    def yamlContent = """
    default_context:
    full_name: "Full Name"
    github_username: "githubuser"
    app_name: "${REPO_NAME}"
    project_short_description: "A Golang project."
    docker_hub_username: "dockerhubuser"
    docker_image: "dockerhubuser/alpine-base-image:latest"
    docker_build_image: "dockerhubuser/alpine-golang-buildimage"
    """
    writeFile(file: 'cookiecutter.yaml', text: yamlContent)

    sh("""
    rm -rf ${SCAFFOLD_DIR} ${REPO_NAME}
    git clone https://${GITHUB_USERNAME}:${GITHUB_TOKEN}@github.com/${GITHUB_ORG_NAME}/${REPO_NAME}

    cookiecutter ${COOKIECUTTER_TEMPLATE} --output-dir ${SCAFFOLD_DIR} --no-input --config-file cookiecutter.yaml -f

    rm -rf ${SCAFFOLD_DIR}/${REPO_NAME}/.git*
    cp -r ${SCAFFOLD_DIR}/${REPO_NAME}/* "${REPO_NAME}/"

    cd ${REPO_NAME}
    git config user.name "Jenkins Pipeline Bot"
    git config user.email "jenkins-pipeline[bot]@users.noreply.jenkins.com"
    git add .
    git commit -m "Scaffolded project ${REPO_NAME}"
    git push -u origin main
    cd ..

    rm -rf ${SCAFFOLD_DIR} ${REPO_NAME}
    """)
    }
    }
    }
    }

    stage('Create microservice entity') {
    steps {
    script {
    def logs_report_response = sh(script: """
    curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${PORT_ACCESS_TOKEN}" \
    -d '{"message": "Creating ${REPO_NAME} microservice Port entity..."}' \
    "https://api.port.io/v1/actions/runs/${PORT_RUN_ID}/logs"
    """, returnStdout: true)

    println(logs_report_response)
    }
    script {
    def status_report_response = sh(script: """
    curl --location --request POST "https://api.port.io/v1/blueprints/$PORT_BLUEPRINT_ID/entities?upsert=true&run_id=$PORT_RUN_ID&create_missing_related_entities=true" \
    --header "Authorization: Bearer $PORT_ACCESS_TOKEN" \
    --header "Content-Type: application/json" \
    --data-raw '{
    "identifier": "${REPO_NAME}",
    "title": "${REPO_NAME}",
    "properties": {
    "description": "${REPO_NAME} Golang project",
    "url": "https://github.com/${GITHUB_ORG_NAME}/${REPO_NAME}"
    },
    "relations": {}
    }'
    """, returnStdout: true)

    println(status_report_response)
    }
    }
    }

    stage('Update Port 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": "Scaffold Jenkins pipeline completed successfully!"}}' \
    "https://api.port.io/v1/actions/runs/${PORT_RUN_ID}"
    """, returnStdout: true)

    println(status_report_response)
    }
    }
    }
    }

    post {
    failure {
    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 scaffold ${REPO_NAME}"}}' \
    "https://api.port.io/v1/actions/runs/${PORT_RUN_ID}"
    """, returnStdout: true)

    println(status_report_response)
    }
    }

    always {
    cleanWs(cleanWhenNotBuilt: false,
    deleteDirs: true,
    disableDeferredWipeout: false,
    notFailBuild: true,
    patterns: [[pattern: '.gitignore', type: 'INCLUDE'],
    [pattern: '.propsfile', type: 'EXCLUDE']])
    }
    }
    }

Test the flow

  1. Go to the Self-service page in Port.

  2. Click the Scaffold Golang Microservice action.

  3. Enter the repository name and GitHub organization name.

  4. Click Execute.

  5. Wait for Jenkins to create the GitHub repository, scaffold the Cookiecutter template, and create the Port entity.

Scaffold Bitbucket repositories

Set up Jenkins backend

  1. Create the following Jenkins credentials:

    • BITBUCKET_USERNAME - A Bitbucket user with access to the target workspace and project.
    • BITBUCKET_APP_PASSWORD - A Bitbucket app password with Repositories:Read and Repositories:Write permissions.
    • PORT_CLIENT_ID - Your Port client ID.
    • PORT_CLIENT_SECRET - Your Port client secret.
  2. Use the Secret text credential type for each credential.

  3. Make sure the Jenkins agent can run git, curl, and cookiecutter.

Set up self-service action

Follow the steps below to create a self-service action that triggers the Jenkins pipeline.

  1. Go to the Self-service page in Port.

  2. Click on the + New Action button.

  3. Click on the {...} Edit JSON button.

  4. Copy and paste the following JSON configuration into the editor:

    Scaffold Bitbucket repository action (Click to expand)
    {
    "identifier": "microservice_scaffold_bitbucket",
    "title": "Scaffold Golang Microservice - Bitbucket",
    "icon": "Go",
    "description": "Create a repository for a new Golang microservice on Bitbucket",
    "trigger": {
    "type": "self-service",
    "operation": "CREATE",
    "userInputs": {
    "properties": {
    "repo_name": {
    "icon": "Microservice",
    "title": "Repo Name",
    "type": "string"
    },
    "bitbucket_workspace_name": {
    "icon": "BitBucket",
    "title": "Bitbucket Workspace Name",
    "type": "string"
    },
    "bitbucket_project_key": {
    "title": "Bitbucket Project Key",
    "icon": "BitBucket",
    "description": "Bitbucket project key symbol",
    "type": "string"
    }
    },
    "required": [
    "repo_name",
    "bitbucket_workspace_name",
    "bitbucket_project_key"
    ]
    },
    "blueprintIdentifier": "microservice"
    },
    "invocationMethod": {
    "type": "WEBHOOK",
    "url": "https://<JENKINS_URL>/generic-webhook-trigger/invoke?token=<JOB_TOKEN>",
    "agent": false,
    "synchronized": false,
    "method": "POST",
    "body": {
    "action": "{{ .action.identifier[(\"microservice_\" | length):] }}",
    "resourceType": "run",
    "status": "TRIGGERED",
    "trigger": "{{ .trigger | {by, origin, at} }}",
    "context": {
    "entity": "{{.entity.identifier}}",
    "blueprint": "{{.action.blueprint}}",
    "runId": "{{.run.id}}"
    },
    "payload": {
    "entity": "{{ (if .entity == {} then null else .entity end) }}",
    "action": {
    "invocationMethod": {
    "type": "WEBHOOK",
    "agent": false,
    "url": "https://<JENKINS_URL>/generic-webhook-trigger/invoke?token=<JOB_TOKEN>",
    "synchronized": false,
    "method": "POST"
    },
    "trigger": "{{.trigger.operation}}"
    },
    "properties": {
    "{{if (.inputs | has(\"repo_name\")) then \"repo_name\" else null end}}": "{{.inputs.\"repo_name\"}}",
    "{{if (.inputs | has(\"bitbucket_workspace_name\")) then \"bitbucket_workspace_name\" else null end}}": "{{.inputs.\"bitbucket_workspace_name\"}}",
    "{{if (.inputs | has(\"bitbucket_project_key\")) then \"bitbucket_project_key\" else null end}}": "{{.inputs.\"bitbucket_project_key\"}}"
    },
    "censoredProperties": "{{.action.encryptedProperties}}"
    }
    }
    }
    }
  5. Click Save to create the action.

Create Jenkins pipeline

  1. Create a Jenkins pipeline.

  2. Enable a webhook trigger for the pipeline.

  3. Define pipeline variables named REPO_NAME, BITBUCKET_WORKSPACE_NAME, BITBUCKET_PROJECT_KEY, and RUN_ID.

    Jenkins variables for the Bitbucket scaffold pipeline
  4. Configure the Jenkins token to match the JOB_TOKEN value in your Port action.

  5. Add the following content to the pipeline:

    Bitbucket Jenkins pipeline script (Click to expand)
    import groovy.json.JsonSlurper

    pipeline {
    agent any

    environment {
    COOKIECUTTER_TEMPLATE = 'https://github.com/lacion/cookiecutter-golang'
    REPO_NAME = "${REPO_NAME}"
    BITBUCKET_WORKSPACE_NAME = "${BITBUCKET_WORKSPACE_NAME}"
    BITBUCKET_PROJECT_KEY = "${BITBUCKET_PROJECT_KEY}"
    SCAFFOLD_DIR = "scaffold_${REPO_NAME}"
    PORT_ACCESS_TOKEN = ""
    PORT_BLUEPRINT_ID = "microservice"
    PORT_RUN_ID = "${RUN_ID}"
    }

    stages {
    stage('Get access token') {
    steps {
    script {
    withCredentials([
    string(credentialsId: 'PORT_CLIENT_ID', variable: 'PORT_CLIENT_ID'),
    string(credentialsId: 'PORT_CLIENT_SECRET', variable: 'PORT_CLIENT_SECRET')
    ]) {
    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.port.io/v1/auth/access_token")
    echo \$accessTokenPayload
    """)

    def jsonSlurper = new JsonSlurper()
    def payloadJson = jsonSlurper.parseText(result.trim())
    PORT_ACCESS_TOKEN = payloadJson.accessToken
    }
    }
    }
    }

    stage('Create Bitbucket repository') {
    steps {
    script {
    def logs_report_response = sh(script: """
    curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${PORT_ACCESS_TOKEN}" \
    -d '{"message": "Creating Bitbucket repository: ${REPO_NAME} in workspace: ${BITBUCKET_WORKSPACE_NAME}, project: ${BITBUCKET_PROJECT_KEY}..."}' \
    "https://api.port.io/v1/actions/runs/${PORT_RUN_ID}/logs"
    """, returnStdout: true)

    println(logs_report_response)
    }
    script {
    withCredentials([
    string(credentialsId: 'BITBUCKET_USERNAME', variable: 'BITBUCKET_USERNAME'),
    string(credentialsId: 'BITBUCKET_APP_PASSWORD', variable: 'BITBUCKET_APP_PASSWORD')
    ]) {
    sh """
    curl -i -u ${BITBUCKET_USERNAME}:${BITBUCKET_APP_PASSWORD} \
    -d '{"is_private": true, "scm": "git", "project": {"key": "${BITBUCKET_PROJECT_KEY}"}}' \
    https://api.bitbucket.org/2.0/repositories/${BITBUCKET_WORKSPACE_NAME}/${REPO_NAME}
    """
    }
    }
    }
    }

    stage('Scaffold Cookiecutter template') {
    steps {
    script {
    def logs_report_response = sh(script: """
    curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${PORT_ACCESS_TOKEN}" \
    -d '{"message": "Scaffolding ${REPO_NAME}..."}' \
    "https://api.port.io/v1/actions/runs/${PORT_RUN_ID}/logs"
    """, returnStdout: true)

    println(logs_report_response)
    }
    script {
    withCredentials([
    string(credentialsId: 'BITBUCKET_USERNAME', variable: 'BITBUCKET_USERNAME'),
    string(credentialsId: 'BITBUCKET_APP_PASSWORD', variable: 'BITBUCKET_APP_PASSWORD')
    ]) {
    def yamlContent = """
    default_context:
    full_name: "Full Name"
    github_username: "bitbucketuser"
    app_name: "${REPO_NAME}"
    project_short_description: "A Golang project."
    docker_hub_username: "dockerhubuser"
    docker_image: "dockerhubuser/alpine-base-image:latest"
    docker_build_image: "dockerhubuser/alpine-golang-buildimage"
    """
    writeFile(file: 'cookiecutter.yaml', text: yamlContent)

    sh("""
    rm -rf ${SCAFFOLD_DIR} ${REPO_NAME}
    git clone https://${BITBUCKET_USERNAME}:${BITBUCKET_APP_PASSWORD}@bitbucket.org/${BITBUCKET_WORKSPACE_NAME}/${REPO_NAME}.git

    cookiecutter ${COOKIECUTTER_TEMPLATE} --output-dir ${SCAFFOLD_DIR} --no-input --config-file cookiecutter.yaml -f

    rm -rf ${SCAFFOLD_DIR}/${REPO_NAME}/.git*
    cp -r ${SCAFFOLD_DIR}/${REPO_NAME}/* "${REPO_NAME}/"

    cd ${REPO_NAME}
    git config user.name "Jenkins Pipeline Bot"
    git config user.email "jenkins-pipeline[bot]@users.noreply.jenkins.com"
    git add .
    git commit -m "Scaffolded project ${REPO_NAME}"
    git push -u origin master
    cd ..

    rm -rf ${SCAFFOLD_DIR} ${REPO_NAME}
    """)
    }
    }
    }
    }

    stage('Create microservice entity') {
    steps {
    script {
    def logs_report_response = sh(script: """
    curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${PORT_ACCESS_TOKEN}" \
    -d '{"message": "Creating ${REPO_NAME} microservice Port entity..."}' \
    "https://api.port.io/v1/actions/runs/${PORT_RUN_ID}/logs"
    """, returnStdout: true)

    println(logs_report_response)
    }
    script {
    def status_report_response = sh(script: """
    curl --location --request POST "https://api.port.io/v1/blueprints/$PORT_BLUEPRINT_ID/entities?upsert=true&run_id=$PORT_RUN_ID&create_missing_related_entities=true" \
    --header "Authorization: Bearer $PORT_ACCESS_TOKEN" \
    --header "Content-Type: application/json" \
    --data-raw '{
    "identifier": "${REPO_NAME}",
    "title": "${REPO_NAME}",
    "properties": {
    "description": "${REPO_NAME} Golang project",
    "url": "https://bitbucket.org/${BITBUCKET_WORKSPACE_NAME}/${REPO_NAME}/src"
    },
    "relations": {}
    }'
    """, returnStdout: true)

    println(status_report_response)
    }
    }
    }

    stage('Update Port 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": "Scaffold Jenkins pipeline completed successfully!"}}' \
    "https://api.port.io/v1/actions/runs/${PORT_RUN_ID}"
    """, returnStdout: true)

    println(status_report_response)
    }
    }
    }
    }

    post {
    failure {
    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 scaffold ${REPO_NAME}"}}' \
    "https://api.port.io/v1/actions/runs/${PORT_RUN_ID}"
    """, returnStdout: true)

    println(status_report_response)
    }
    }

    always {
    cleanWs(cleanWhenNotBuilt: false,
    deleteDirs: true,
    disableDeferredWipeout: false,
    notFailBuild: true,
    patterns: [[pattern: '.gitignore', type: 'INCLUDE'],
    [pattern: '.propsfile', type: 'EXCLUDE']])
    }
    }
    }

Test the flow

  1. Go to the Self-service page in Port.

  2. Click the Scaffold Golang Microservice - Bitbucket action.

  3. Enter the repository name, Bitbucket workspace name, and Bitbucket project key.

  4. Click Execute.

  5. Wait for Jenkins to create the Bitbucket repository, scaffold the Cookiecutter template, and create the Port entity.

Scaffold Azure DevOps repositories

Set up Azure DevOps backend

  1. Create an Azure DevOps repository called python_scaffolder in your Azure DevOps organization and project.

  2. Configure a service connection to Azure DevOps.

  3. Use port_trigger for both WebHook Name and Service connection name.

Set up self-service action

Follow the steps below to create a self-service action that triggers the Azure DevOps pipeline.

  1. Go to the Self-service page in Port.

  2. Click on the + New Action button.

  3. Click on the {...} Edit JSON button.

  4. Copy and paste the following JSON configuration into the editor:

    Scaffold Azure DevOps repository action (Click to expand)
    {
    "identifier": "azure_scaffolder",
    "title": "Scaffold Azure Repository",
    "icon": "Azure",
    "description": "Scaffold a new repository in Azure DevOps",
    "trigger": {
    "type": "self-service",
    "operation": "CREATE",
    "userInputs": {
    "properties": {
    "service_name": {
    "title": "Service Name",
    "description": "The new service's name",
    "type": "string"
    },
    "azure_organization": {
    "icon": "DefaultProperty",
    "type": "string",
    "title": "Azure Organization",
    "description": "The Azure DevOps organization name",
    "default": "<YOUR_DEFAULT_AZURE_DEVOPS_ORGANIZATION_NAME>"
    },
    "description": {
    "type": "string",
    "title": "Description",
    "description": "Description of the scaffold"
    },
    "azure_project": {
    "icon": "DefaultProperty",
    "title": "Azure Project",
    "type": "string",
    "description": "Your Azure DevOps project ID",
    "blueprint": "project",
    "sort": {
    "property": "$identifier",
    "order": "ASC"
    },
    "format": "entity"
    }
    },
    "required": [
    "service_name",
    "azure_organization",
    "azure_project"
    ],
    "order": [
    "service_name",
    "azure_organization",
    "azure_project",
    "description"
    ]
    },
    "blueprintIdentifier": "service"
    },
    "invocationMethod": {
    "type": "AZURE_DEVOPS",
    "webhook": "port_trigger",
    "org": "<AZURE_DEVOPS_ORGANIZATION_NAME>",
    "payload": {
    "properties": {
    "service_name": "{{.inputs.\"service_name\"}}",
    "azure_organization": "{{.inputs.\"azure_organization\"}}",
    "description": "{{.inputs.\"description\"}}",
    "azure_project": "{{.inputs.\"azure_project\"}}"
    },
    "port_context": {
    "blueprint": "{{.action.blueprint}}",
    "runId": "{{.run.id}}",
    "trigger": "{{ .trigger }}"
    }
    }
    },
    "requiredApproval": false
    }
  5. Click Save to create the action.

Create Azure DevOps pipeline

  1. In your python_scaffolder repository, create a file called azure-pipelines.yml in the root directory.

  2. Copy and paste the following YAML configuration into the file:

    Azure DevOps pipeline YAML (Click to expand)
    trigger: none

    pool:
    vmImage: "ubuntu-latest"

    variables:
    RUN_ID: "${{ parameters.port_trigger.port_context.runId }}"
    BLUEPRINT_ID: "${{ parameters.port_trigger.port_context.blueprint }}"
    SERVICE_NAME: "${{ parameters.port_trigger.properties.service_name }}"
    DESCRIPTION: "${{ parameters.port_trigger.properties.description }}"
    AZURE_ORGANIZATION: "${{ parameters.port_trigger.properties.azure_organization }}"
    AZURE_PROJECT: "${{ parameters.port_trigger.properties.azure_project.title }}"
    PROJECT_ID: "${{ parameters.port_trigger.properties.azure_project.identifier }}"

    resources:
    webhooks:
    - webhook: port_trigger
    connection: port_trigger

    stages:
    - stage: fetch_port_access_token
    jobs:
    - job: fetch_port_access_token
    steps:
    - script: |
    sudo apt-get update
    sudo apt-get install -y jq
    - script: |
    accessToken=$(curl -X POST \
    -H 'Content-Type: application/json' \
    -d '{"clientId": "$(PORT_CLIENT_ID)", "clientSecret": "$(PORT_CLIENT_SECRET)"}' \
    -s 'https://api.port.io/v1/auth/access_token' | jq -r '.accessToken')
    echo "##vso[task.setvariable variable=accessToken;isOutput=true]$accessToken"
    displayName: Fetch access token
    name: getToken

    - stage: scaffold
    dependsOn:
    - fetch_port_access_token
    jobs:
    - job: scaffold
    variables:
    COOKIECUTTER_TEMPLATE_URL: "https://github.com/brettcannon/python-azure-web-app-cookiecutter"
    steps:
    - script: |
    sudo apt-get update
    sudo apt-get install -y jq
    sudo pip install cookiecutter -q
    - script: |
    PAYLOAD="{\"name\":\"$SERVICE_NAME\",\"project\":{\"id\":\"$PROJECT_ID\"}}"

    echo "SERVICE_NAME: $SERVICE_NAME"
    echo "AZURE_ORGANIZATION: $AZURE_ORGANIZATION"
    echo "PROJECT_ID: $PROJECT_ID"
    echo "PAYLOAD: $PAYLOAD"

    if [[ -z "$PERSONAL_ACCESS_TOKEN" ]]; then
    echo "PERSONAL_ACCESS_TOKEN is not set or is empty."
    exit 1
    else
    echo "PERSONAL_ACCESS_TOKEN is set."
    fi

    CREATE_REPO_RESPONSE=$(curl -s -u :$PERSONAL_ACCESS_TOKEN \
    -X POST "https://dev.azure.com/$AZURE_ORGANIZATION/$PROJECT_ID/_apis/git/repositories?api-version=7.0" \
    -H "Content-Type: application/json" \
    -d "$PAYLOAD")

    echo "CREATE_REPO_RESPONSE: $CREATE_REPO_RESPONSE"

    PROJECT_URL=$(echo $CREATE_REPO_RESPONSE | jq -r .webUrl)

    if [[ -z "$PROJECT_URL" ]] || [[ "$PROJECT_URL" == "null" ]]; then
    echo "Failed to create Azure DevOps repository."
    exit 1
    fi

    echo "##vso[task.setvariable variable=PROJECT_URL;isOutput=true]$PROJECT_URL"

    COOKIECUTTER_NAME=$(echo "$SERVICE_NAME" | tr '_' '-')
    echo "Original SERVICE_NAME: $SERVICE_NAME"
    echo "Sanitized COOKIECUTTER_NAME: $COOKIECUTTER_NAME"

    cat <<EOF > cookiecutter.yaml
    default_context:
    site_name: "$COOKIECUTTER_NAME"
    python_version: "3.6.0"
    EOF
    cookiecutter $COOKIECUTTER_TEMPLATE_URL --no-input --config-file cookiecutter.yaml --output-dir scaffold_out

    if [[ "$COOKIECUTTER_NAME" != "$SERVICE_NAME" ]]; then
    echo "Renaming Cookiecutter output directory to match repository name..."
    mv "scaffold_out/$COOKIECUTTER_NAME" "scaffold_out/$SERVICE_NAME"
    fi

    echo "Initializing new repository..."
    git config --global user.email "scaffolder@email.com"
    git config --global user.name "Mighty Scaffolder"
    git config --global init.defaultBranch "main"

    cd "scaffold_out/$SERVICE_NAME"
    git init
    git add .
    git commit -m "Initial commit"

    ENCODED_PROJECT=$(echo "$AZURE_PROJECT" | sed 's/ /%20/g')
    git remote add origin "https://$PERSONAL_ACCESS_TOKEN@dev.azure.com/$AZURE_ORGANIZATION/$ENCODED_PROJECT/_git/$SERVICE_NAME"

    git config --global http.lowSpeedLimit 1000
    git config --global http.lowSpeedTime 300

    git push -u origin --all
    env:
    PERSONAL_ACCESS_TOKEN: $(PERSONAL_ACCESS_TOKEN)
    displayName: "Create repository in Azure DevOps"
    name: scaffold

    - stage: upsert_entity
    dependsOn:
    - fetch_port_access_token
    - scaffold
    jobs:
    - job: upsert_entity
    variables:
    accessToken: $[ stageDependencies.fetch_port_access_token.fetch_port_access_token.outputs['getToken.accessToken'] ]
    PROJECT_URL: $[ stageDependencies.scaffold.scaffold.outputs['scaffold.PROJECT_URL'] ]
    steps:
    - script: |
    sudo apt-get update
    sudo apt-get install -y jq
    - script: |
    curl -X POST \
    -H 'Content-Type: application/json' \
    -H 'Authorization: Bearer $(accessToken)' \
    -d '{
    "identifier": "${{ variables.SERVICE_NAME }}",
    "title": "${{ variables.SERVICE_NAME }}",
    "properties": {
    "description": "${{ variables.DESCRIPTION }}",
    "url": "$(PROJECT_URL)"
    },
    "relations": {
    "project": "${{ variables.PROJECT_ID }}"
    }
    }' \
    "https://api.port.io/v1/blueprints/${{ variables.BLUEPRINT_ID }}/entities?upsert=true&run_id=${{ variables.RUN_ID }}&create_missing_related_entities=true"

    - stage: update_run_status
    dependsOn:
    - upsert_entity
    - fetch_port_access_token
    - scaffold
    jobs:
    - job: update_run_status
    variables:
    accessToken: $[ stageDependencies.fetch_port_access_token.fetch_port_access_token.outputs['getToken.accessToken'] ]
    PROJECT_URL: $[ stageDependencies.scaffold.scaffold.outputs['scaffold.PROJECT_URL'] ]
    steps:
    - script: |
    sudo apt-get update
    sudo apt-get install -y jq
    - script: |
    curl -X PATCH \
    -H 'Content-Type: application/json' \
    -H 'Authorization: Bearer $(accessToken)' \
    -d '{"status":"SUCCESS", "message": {"run_status": "Scaffold ${{ variables.SERVICE_NAME }} finished successfully!\\n Project URL: $(PROJECT_URL)" }}' \
    "https://api.port.io/v1/actions/runs/${{ variables.RUN_ID }}"

    - stage: update_run_status_failed
    dependsOn:
    - upsert_entity
    - fetch_port_access_token
    - scaffold
    condition: failed()
    jobs:
    - job: update_run_status_failed
    variables:
    accessToken: $[ stageDependencies.fetch_port_access_token.fetch_port_access_token.outputs['getToken.accessToken'] ]
    steps:
    - script: |
    curl -X PATCH \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $accessToken" \
    -d '{"status":"FAILURE", "message": {"run_status": "Scaffold '"$SERVICE_NAME"' failed" }}' \
    "https://api.port.io/v1/actions/runs/$RUN_ID"
  3. Configure the pipeline in Azure DevOps:

    • Go to Pipelines.
    • Click Create Pipeline.
    • Select Azure Repos Git.
    • Choose the python_scaffolder repository.
    • Click Save from the Run dropdown menu.
  4. Create the following secret variables in your pipeline:

    • PERSONAL_ACCESS_TOKEN - An Azure DevOps personal access token with Code Full and Release Read, write & execute permissions.
    • PORT_CLIENT_ID - Your Port client ID.
    • PORT_CLIENT_SECRET - Your Port client secret.

Test the flow

  1. Go to the Self-service page in Port.

  2. Click the Scaffold Azure Repository action.

  3. Fill in the required details:

    • Service Name.
    • Azure Organization.
    • Azure Project.
    • Description.
  4. Click Execute.

  5. Wait for Azure DevOps to create the repository, scaffold the Cookiecutter template, and create the Port entity.

Need help?

Having issues with Azure DevOps integration or pipelines? See the Azure DevOps Troubleshooting Guide for step-by-step help.

More relevant guides