Skip to main content

Check out Port for yourselfย 

Measuring pull request standards

This guide is aimed at helping engineering teams implement working agreements and measure pull request (PR) standards.
We will implement working agreements using Port's scorecards and measure PR standards using aggregation properties.
By the end of this guide, you will be able to track teams' performance based on pull requests metrics as shown here:

Overviewโ€‹

Effective collaboration and clear expectations are crucial for high-performing engineering teams.
Working agreements establish shared processes and standards, enhancing teamwork.

Measuring Pull request (PR) standards is essential for assessing code quality, review processes, and team efficiency.

By integrating working agreements with measurable PR metrics, teams can monitor adherence to best practices and continuously improve workflows.

We will discuss how to implement working agreements and measure PR standards using Port.

Metrics

Metrics are essential for assessing how well teams adhere to their working agreements.
They enable teams to track compliance, identify bottlenecks, and drive continuous improvement.

For detailed insights into key metrics like deployment frequency, lead time for changes, and change failure rate,
please refer to our DORA Metrics guide.

Prerequisitesโ€‹

  • Complete the Port onboarding process.
  • Access to a GitHub repository that is connected to Port via the onboarding process.

Working agreementsโ€‹

The following working agreements and PR checks have been implemented in our demo environment:

These checks are implemented using Port's scorecards.

Implementationโ€‹

This section will guide you through implementing the working agreements and PR checks in your Port environment. Follow the steps below:

  1. Add the properties to the pull request blueprint.

    • Go to the Builder in your Port portal.
    • Select the pull request blueprint.
    • Click on the ... button in the top right corner, and choose "Edit JSON".
    • Add the properties and mapping configurations as described below.
    • Save the changes.

    Expected JSON

    This is the expected JSON definition after adding the properties.


  1. Add the mapping configuration to the data source.

    • Go to the Data Sources in your Port portal.
    • Select the data source connected to your GitHub repository
    • Add the mapping configurations as described below.
    • Save the changes.
  2. Add the scorecard definitions to the pull request blueprint.

    • Go to the Builder in your Port portal.
    • Select the pull request blueprint.
    • Click on the Scorecard
    • Click on the + New Scorecard button.
    • Paste the scorecard definitions as described below.
    • Save the changes.

PR Description Cannot be Emptyโ€‹

Scorecard Definition

Scorecard definition (click to expand)
{
"identifier": "pr_descr_not_empty",
"title": "PR Description Cannot be Empty",
"description": "Ensures that the PR description is not empty.",
"level": "Bronze",
"query": {
"combinator": "and",
"conditions": [
{
"operator": "isNotEmpty",
"property": "prDescription"
}
]
}
}

Property

Add the prDescription property to the pull request blueprint:

Add property (click to expand)
"prDescription": {
"title": "PR Description",
"type": "string"
}

Mapping Configuration

Map the PR's body field from your data source to the prDescription property:

Mapping config (click to expand)
  - kind: pull-request
selector:
query: 'true'
port:
entity:
mappings:
identifier: .head.repo.name + (.id|tostring)
title: .title
blueprint: '"githubPullRequest"'
properties:
#other properties
prDescription: .body

PR Has Linked Issueโ€‹

Scorecard Definition

Scorecard definition (click to expand)
{
"identifier": "pr_has_issue_link",
"title": "PR Has Linked Issue",
"description": "Ensures that the PR is linked to an issue.",
"level": "Silver",
"query": {
"combinator": "and",
"conditions": [
{
"operator": "isNotEmpty",
"property": "issueUrl"
}
]
}
}

Property

Add the issueUrl property to the pull request blueprint:

Add property (click to expand)
"issueUrl": {
"title": "Issue URL",
"type": "string",
"format": "url"
}

Mapping Configuration

Map the issue URL from your data source to the issueurl property:

Mapping config (click to expand)
  - kind: pull-request
selector:
query: 'true'
port:
entity:
mappings:
identifier: .head.repo.name + (.id|tostring)
title: .title
blueprint: '"githubPullRequest"'
properties:
#other properties
issueUrl: .issue_url

PR Has No Unchecked Checkboxesโ€‹

Scorecard Definition

Scorecard definition (click to expand)
{
"identifier": "pr_no_unchecked_chk",
"title": "PR Has No Unchecked Checkboxes",
"description": "Ensures that there are no unchecked checkboxes in the PR description.",
"level": "Silver",
"query": {
"combinator": "and",
"conditions": [
{
"operator": "doesNotContains",
"property": "prDescription",
"value": "- [ ]"
}
]
}
}
Property addition

If you haven't added the prDescription property and its relative mapping config, please refer to the PR Description Cannot be Empty section.

PR Requires Reviewersโ€‹

Scorecard Definition

Scorecard definition (click to expand)
{
"identifier": "pr_has_reviewers_req",
"title": "PR Requires Reviewers",
"description": "Ensures that the PR has at least one reviewer requested.",
"level": "Bronze",
"query": {
"combinator": "and",
"conditions": [
{
"operator": "isNotEmpty",
"property": "reviewers"
}
]
}
}

Property

Add the reviewers property to the pull request blueprint if it doesn't exist:

Add property (click to expand)
"reviewers": {
"title": "Reviewers",
"type": "array"
}

Mapping Configuration

Map the list of reviewers from your data source to the reviewers property:

Mapping config (click to expand)
  - kind: pull-request
selector:
query: 'true'
port:
entity:
mappings:
identifier: .head.repo.name + (.id|tostring)
title: .title
blueprint: '"githubPullRequest"'
properties:
#other properties
reviewers: '[.requested_reviewers[].login]'

PR Is Linked to a Milestoneโ€‹

Scorecard Definition

Scorecard definition (click to expand)
{
"identifier": "pr_linked_milestone",
"title": "PR Is Linked to a Milestone",
"description": "Ensures that the PR is linked to a milestone.",
"level": "Gold",
"query": {
"combinator": "and",
"conditions": [
{
"operator": "isNotEmpty",
"property": "milestone"
}
]
}
}

Property

Add property (click to expand)

Add the milestone property to the pull request blueprint:

"milestone": {
"title": "Milestone",
"type": "object"
}

Mapping Configuration

Mapping config (click to expand)

Map the milestone information from your data source to the milestone property.

  - kind: pull-request
selector:
query: 'true'
port:
entity:
mappings:
identifier: .head.repo.name + (.id|tostring)
title: .title
blueprint: '"githubPullRequest"'
properties:
#other properties
milestone: .milestone

PR Changed X Files or Lessโ€‹

Scorecard Definition

This agreement has multiple levels based on the number of files changed.

Scorecard definition (click to expand)
Bronze level scorecard definition (click to expand)
{
"identifier": "pr_file_limit_bronze",
"title": "PR Can't Have More than 15 Changed Files",
"description": "Ensures that the PR does not have more than 15 changed files.",
"level": "Bronze",
"query": {
"combinator": "and",
"conditions": [
{
"operator": "<=",
"property": "changedFiles",
"value": 15
}
]
}
}
Silver level scorecard definition (click to expand)
{
"identifier": "pr_file_limit_silver",
"title": "PR Can't Have More than 10 Changed Files",
"description": "Ensures that the PR does not have more than 10 changed files.",
"level": "Silver",
"query": {
"combinator": "and",
"conditions": [
{
"operator": "<=",
"property": "changedFiles",
"value": 10
}
]
}
}
Gold level scorecard definition (click to expand)
{
"identifier": "pr_file_limit_gold",
"title": "PR Can't Have More than 5 Changed Files",
"description": "Ensures that the PR does not have more than 5 changed files.",
"level": "Gold",
"query": {
"combinator": "and",
"conditions": [
{
"operator": "<=",
"property": "changedFiles",
"value": 5
}
]
}
}
value adjustment

You can adjust the value based on your team's requirements.

Property

Add the changedFiles property to the pull request blueprint:

Add property (click to expand)
    "changedFiles": {
"title": "Changed Files",
"type": "number"
}

Mapping Configuration

Map the number of changed_files from the data source to the changedFiles property:

Mapping config (click to expand)
  - kind: pull-request
selector:
query: 'true'
port:
entity:
mappings:
identifier: .head.repo.name + (.id|tostring)
title: .title
blueprint: '"githubPullRequest"'
properties:
#other properties
changedFiles: .changed_files

PR Has Been Open for X Daysโ€‹

Scorecard Definition

Scorecard definition (click to expand)
{
"identifier": "pr_open_for_less_than_10_days",
"title": "PR Has Been Open for Less Than 10 Days",
"description": "Ensures that the PR has not been open for more than 10 days.",
"level": "Silver",
"query": {
"combinator": "and",
"conditions": [
{
"operator": "<=",
"property": "days_old",
"value": 10
},
{
"operator": "=",
"property": "status",
"value": "open"
}
]
}
}

Property

Add a calculation property days_old to compute how many days the PR has been open:

Add property (click to expand)
"days_old": {
"title": "Days Old",
"icon": "DefaultProperty",
"calculation": "(now / 86400) - (.properties.updatedAt | capture(\"(?<date>\\\\d{4}-\\\\d{2}-\\\\d{2})\") | .date | strptime(\"%Y-%m-%d\") | mktime / 86400) | floor",
"type": "number"
}
Required properties

Ensure that createdAt and mergedAt properties are correctly mapped from your data source.

PR Batch Size Calculationโ€‹

Scorecard Definition

This agreement has levels based on the batch size.

Scorecard definition (click to expand)
Bronze level scorecard definition (click to expand)
{
"identifier": "pr_batch_size_bronze",
"title": "PR Cannot Have Large or Gigantic Batch Size",
"description": "Ensures that the PR does not have a Large or Gigantic batch size.",
"level": "Bronze",
"query": {
"combinator": "or",
"conditions": [
{
"operator": "!=",
"property": "batchSize",
"value": "Large"
},
{
"operator": "!=",
"property": "batchSize",
"value": "Gigantic"
}
]
}
}
Silver level scorecard definition (click to expand)
{
"identifier": "pr_batch_size_silver",
"title": "PR Has Medium Batch Size",
"description": "Ensures that the PR has a Medium batch size.",
"level": "Silver",
"query": {
"combinator": "and",
"conditions": [
{
"operator": "=",
"property": "batchSize",
"value": "Medium"
}
]
}
}
Gold level scorecard definition (click to expand)
{
"identifier": "pr_batch_size_gold",
"title": "PR Has Small Batch Size",
"description": "Ensures that the PR has a Small batch size.",
"level": "Gold",
"query": {
"combinator": "and",
"conditions": [
{
"operator": "=",
"property": "batchSize",
"value": "Small"
}
]
}
}

Property

Add a property batchSize to categorize the PR's batch size:

Add property (click to expand)
"batchSize": {
"title": "Batch Size",
"type": "string",
"enum": [
"Small",
"Medium",
"Large",
"Gigantic"
],
"enumColors": {
"Small": "lightGray",
"Medium": "orange",
"Large": "yellow",
"Gigantic": "red"
}
}

Mapping Configuration

Map the PR's additions, deletions, and changedFiles properties to the batchSize property:

Mapping config (click to expand)
    - kind: pull-request
selector:
query: 'true'
port:
entity:
mappings:
identifier: .head.repo.name + (.id|tostring)
title: .title
blueprint: '"githubPullRequest"'
properties:
#other properties
batchSize: >
if (.commits <= 3 and (.additions + .deletions) <= 50 and
.changed_files <= 3) then "Small" elif (.commits <= 10 and
(.additions + .deletions) <= 200 and .changed_files <= 7) then
"Medium" elif (.commits <= 20 and (.additions + .deletions) <= 500
and .changed_files <= 15) then "Large" else "Gigantic" end
Batch size calculation

You can adjust the thresholds based on your team's requirements.

Add total LOC changed propertyโ€‹

Add the calculation property totalLocChanged on the pull request blueprint:

Calculation property (click to expand)
"totalLocChanged": {
"title": "Total LOC Changed",
"type": "number",
"calculation": ".properties.additions + .properties.deletions"
}

Pull request metrics aggregationโ€‹

To measure PR standards effectively, add aggregation properties on the service blueprint. This will allow us to capture important metrics such as:

  • PR Average Duration (Service Level)
  • PRs Opened (Service & Organization Level)
  • PRs Merged (Service & Organization Level)
  • Average Commits per PR (Service & Organization Level)
  • Average Lines of Code (LOC) Changed (Service & Organization Level)

Aggregation on higher hierarchy
  • To aggregate on an organization level, apply the same settings to a higher hierarchy.
  • If you want to see it on a team level, you need to make sure services are related to teams and add the same aggregations to team
Adding Aggregation to Blueprints

To add aggregation properties to your blueprints, follow these steps:

  1. Go to the Builder in your Port portal.
  2. Locate and select your blueprint.
  3. Click the {...} button in the top right corner, and choose Edit JSON.
  4. Insert the respective aggregation or calculation properties under the aggregationProperties or calculationProperties section in the blueprint's JSON schema.
  5. Save your changes to apply the new aggregation configuration.

Add the following aggregation property to the service blueprint:

Aggregation property (click to expand)
"averagePrDuration": {
"title": "Average PR Duration",
"type": "number",
"target": "githubPullRequest",
"calculationSpec": {
"func": "average",
"averageOf": "week",
"calculationBy": "property",
"property": "days_old"
},
"query": {
"combinator": "and",
"rules": [
{
"property": "mergedAt",
"operator": "isNotEmpty"
}
]
}
}

By implementing these aggregation properties, you can effectively measure and monitor PR standards at both the service and organization levels.

Visualizationโ€‹

By leveraging Port's Dashboards, you can create custom dashboards to track the pr metrics and monitor your team's performance over time.

Dashboard setupโ€‹

  1. Go to your software catalog.
  2. Click on the + New button in the left sidebar.
  3. Select New dashboard.
  4. Name the dashboard (PR Metrics), choose an icon if desired, and click Create.

This will create a new empty dashboard. Let's get ready-to-add widgets

Adding widgetsโ€‹

Average Pr Merged per month (click to expand)
  1. Click + Widget and select Number Chart.

  2. Title: Average Pr's Merged per month, (add the GitPullRequest icon).

  3. Select Aggregate by property and choose Service as the Blueprint.

  4. Choose Merged PRs as the Property.

  5. Select average for the Function and choose month for Average of.

  6. Select Created At for Measure time by.

  7. Select custom as the Unit and input Pull Request merged per month as the Custom unit.

  8. Click Save.

Total Pr's Merged (click to expand)
  1. Click + Widget and select Number Chart.

  2. Title: Total Pr's Merged , (add the GitPullRequest icon).

  3. Select Aggregate by property and choose Service as the Blueprint.

  4. Choose Merged PRs as the Property.

  5. Select sum for the Function.

  6. Click Save.

Mean Time to Merge (Days) (click to expand)
  1. Click + Widget and select Number Chart.

  2. Title: Mean Time to Merge (Days), (add the Merge icon).

  3. Select Display single property and choose Service the Entity.

  4. Choose Averge Time to Merge as the Property.

  5. Click Save.

Total Weekly Pr commits (click to expand)
  1. Click + Widget and select Number Chart.

  2. Title: Total Weekly Pr commits, (add the GitPullRequest icon).

  3. Select Aggregate by property and choose Service as the Blueprint.

  4. Choose Averge Commits per PRs as the Property.

  5. Select average for the Function and choose week for Average of.

  6. Select Created At for Measure time by.

  7. Select custom as the Unit and input Weekly as the Custom unit.

  8. Click Save.

Service Scorecard Performance by Team (click to expand)
  1. Click + Widget and select Table.

  2. Title the widget Team Service Scorecard Performance.

  3. Choose the Service blueprint.

  4. Click Save to add the widget to the dashboard.

  5. Click on the ... button in the top right corner of the table and select Customize table.

  6. In the top right corner of the table, click on Manage Properties and add the following properties:

    • Title: The name of each service.
    • PR Metrics: PR Metrics scorecard aggregation on service.
    • Owning Team: The team that owns the service.
  7. Click on the Group by any Column on the top right conner and select Owning Team.

  8. Click on the save icon in the top right corner of the widget to save the customized table.

Notification automationโ€‹

Add this automation feature to notify a Slack channel when a scorecard value changes

Automation for sending Slack notifications on scorecard value change (click to expand)
{
"identifier": "scorecardValueChanged",
"title": "Notify Slack on Scorecard Value Change",
"icon": "Slack",
"description": "Sends a Slack message when the scorecard value changes.",
"trigger": {
"type": "automation",
"event": {
"type": "ENTITY_UPDATED",
"blueprintIdentifier": "githubPullRequest"
},
"condition": {
"type": "JQ",
"expressions": [
".diff.after.scorecardsStats != .diff.before.scorecardsStats"
],
"combinator": "and"
}
},
"invocationMethod": {
"type": "WEBHOOK",
"url": "{{ .event.diff.after.properties.serviceSlackUrl }}",
"agent": false,
"synchronized": true,
"body": {
"channel": "{{ .event.diff.after.properties.serviceSlackChannel }}",
"text": "*Scorecard value changed for PR <{{ .event.diff.after.properties.link }}|{{ .event.diff.after.title }}>*\n\n*Title:* {{ .event.diff.after.title }}\n\n*Old Scorecard Value:* {{ .event.diff.before.scorecardsStats }}\n\n*New Scorecard Value:* {{ .event.diff.after.scorecardsStats }}\n\n*Link:* <{{ .event.diff.after.properties.link }}|View PR>\n\n"
}
},
"publish": true
}

Adding Automations

To add new automations, follow the steps outlined in the Automation Setup section of this guide.
and remember to set the serviceSlackUrl and serviceSlackChannel properties on the service blueprint.