Skip to content

Commit

Permalink
[STRATCONN-3623] - Adds github actions workflow to label PRs (#1905)
Browse files Browse the repository at this point in the history
* Adds PR labeler workflow

* rename token

* update secret name

* fix script path

* change path

* Delete scripts folder

* dummy commit

* update registration regex

* Add comments on checks
  • Loading branch information
varadarajan-tw authored Mar 5, 2024
1 parent 8d3a139 commit 53b4441
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 0 deletions.
55 changes: 55 additions & 0 deletions .github/workflows/label-prs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# This workflow labels PRs based on the files that were changed. It uses a custom script to this
# instead of actions/labeler as few of the tags are more than just file changes.

name: Label PRs
on:
pull_request:

jobs:
pr-labeler:
runs-on: ubuntu-20.04
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Compute Labels
id: compute-labels
uses: actions/github-script@v7
with:
# Required for the script to access team membership information.
# Scope: members:read and contentes:read permission on the organization.
github-token: ${{ secrets.GH_PAT_MEMBER_AND_PULL_REQUEST_READONLY }}
script: |
const script = require('./scripts/compute-labels.js')
await script({github, context, core})
- name: Apply Labels
uses: actions/github-script@v7
env:
labelsToAdd: '${{ steps.compute-labels.outputs.add }}'
labelsToRemove: '${{ steps.compute-labels.outputs.remove }}'
with:
script: |
const { labelsToAdd, labelsToRemove } = process.env
if(labelsToAdd.length > 0) {
await github.rest.issues.addLabels({
issue_number: context.payload.pull_request.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: labelsToAdd.split(',')
});
}
if(labelsToRemove.length > 0) {
const requests = labelsToRemove.split(',').map(label => {
return github.rest.issues.removeLabel({
issue_number: context.payload.pull_request.number,
name: label,
owner: context.repo.owner,
repo: context.repo.repo
});
});
await Promise.all(requests);
}
146 changes: 146 additions & 0 deletions scripts/compute-labels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// This is a github action script and can be run only from github actions. It is not possible to run this script locally.
module.exports = async ({ github, context, core }) => {
const authorLabels = await computeAuthorLabels(github, context, core)
const { add, remove } = await computeFileBasedLabels(github, context, core)
core.setOutput('add', [...authorLabels, ...add].join(','))
core.setOutput('remove', remove.join(','))
return
}

async function computeAuthorLabels(github, context, core) {
const teamSlugs = ['build-experience-team', 'libraries-web-team', 'strategic-connections-team']
const username = context.payload.sender.login
const organization = context.repo.owner
const SEGMENT_CORE_LABEL = 'team:segment-core'
const EXTERNAL_LABEL = 'team:external'
const SEGMENT_LABEL = 'team:segment'

// If team member label already exists, then no need to add any labels
const existingLabels = context.payload.pull_request.labels.map((label) => label.name)
if (existingLabels.some((label) => [SEGMENT_CORE_LABEL, EXTERNAL_LABEL, SEGMENT_LABEL].includes(label))) {
return []
}

// check against all internal teams
const teamMembers = await Promise.all(
teamSlugs.map(async (teamSlug) => {
const team = await github.rest.teams.listMembersInOrg({
team_slug: teamSlug,
org: organization
})
return team.data.some((member) => member.login === username)
})
)

// Add labels based on the team membership
const labels = []
if (teamMembers.some((member) => member === true)) {
labels.push(SEGMENT_CORE_LABEL)
} else {
// check if the user is a member of the organization - eg; Engage and other internal integration devs
await github.rest.orgs
.checkMembershipForUser({
org: organization,
username: username
})
// if the user is not a member of the organization, then add the external label
.catch((e) => {
if (e.status === 404) {
labels.push(EXTERNAL_LABEL)
}
})
// if the user is a member of the organization, then add the segment label
.then((data) => {
if (data && data.status === 204) {
labels.push(SEGMENT_LABEL)
}
})
}
core.debug(`Added ${labels.join(',')} labels to PR based on the author's team membership.`)
return labels
}

async function computeFileBasedLabels(github, context, core) {
const org = context.repo.owner
const repo = context.repo.repo
const pull_number = context.payload.pull_request.number
const labels = context.payload.pull_request.labels.map((label) => label.name)
const DEPLOY_REGISTRATION_LABEL = 'deploy:registration'
const DEPLOY_PUSH_LABEL = 'deploy:push'
const MODE_CLOUD_LABEL = 'mode:cloud'
const MODE_DEVICE_LABEL = 'mode:device'
const ACTIONS_CORE_LABEL = 'actions:core'

const allLabels = [
DEPLOY_REGISTRATION_LABEL,
DEPLOY_PUSH_LABEL,
MODE_CLOUD_LABEL,
MODE_DEVICE_LABEL,
ACTIONS_CORE_LABEL
]

const newLabels = []

// Get the list of files in the PR
const opts = github.rest.pulls.listFiles.endpoint.merge({
owner: org,
repo: repo,
pull_number: pull_number
})

// Paginate the list of files in the PR
const files = await github.paginate(opts)

// The following regexes are used to match the new destinations
const newCloudDestinationRegex = /packages\/destination\-actions\/src\/destinations\/[^\/]+\/index\.ts/i
const newBrowserDestinationRegex = /packages\/browser\-destinations\/destinations\/[^\/]+\/src\/index\.ts/i
const isNew = (filename) => newCloudDestinationRegex.test(filename) || newBrowserDestinationRegex.test(filename)

// Check if the PR contains new destinations
const isNewDestination = files.some((file) => isNew(file.filename) && file.status === 'added')
if (isNewDestination) {
newLabels.push(DEPLOY_REGISTRATION_LABEL)
}

// The following regexes are used to match the updated destinations
const updatedCloudDestinationRegex = /packages\/destination\-actions\/src\/destinations\/.*/i
const updatedBrowserDestinationRegex = /packages\/browser\-destinations\/destinations\/.*/i
const updateCorePackageRegex = /packages\/core\/.*/i
const updatedDestinationSubscription = /packages\/destination\-subscriptions\/.*/i

// Check if the PR contains updates to browser destinations
if (files.some((file) => updatedBrowserDestinationRegex.test(file.filename))) {
newLabels.push(MODE_DEVICE_LABEL)
}

// Check if the PR contains updates to cloud destinations
if (files.some((file) => updatedCloudDestinationRegex.test(file.filename))) {
newLabels.push(MODE_CLOUD_LABEL)
}

// Check if the PR contains updates to core packages
if (
files.some(
(file) => updateCorePackageRegex.test(file.filename) || updatedDestinationSubscription.test(file.filename)
)
) {
newLabels.push(ACTIONS_CORE_LABEL)
}

// Check if the PR contains changes that requires a push.
const generatedTypesRegex = /packages\/.*\/generated\-types.ts/i
if (files.some((file) => generatedTypesRegex.test(file.filename))) {
newLabels.push(DEPLOY_PUSH_LABEL)
}

// Remove the existing custom labels if they are not required anymore
const labelsToRemove = labels.filter((label) => allLabels.includes(label) && !newLabels.includes(label))

core.debug(`Labels to remove: ${labelsToRemove.join(',')}`)
core.debug(`Labels to add: ${newLabels.join(',')}`)

return {
add: newLabels,
remove: labelsToRemove
}
}

0 comments on commit 53b4441

Please sign in to comment.