Assign Milestone workflow (typeorm/typeorm)
The Assign Milestone workflow from typeorm/typeorm, explained and optimized by Latchkey.
C
CI health: C - fair
Point runs-on at Latchkey and get run de-duplication, job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Assign Milestone workflow from the typeorm/typeorm repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
workflow (.yml)
name: Assign Milestone
on:
push:
branches:
- master
- "v[0-9]*"
jobs:
assign-milestone:
if: github.repository == 'typeorm/typeorm'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const branch = context.ref.replace('refs/heads/', '')
// Extract PR number from merge commit message (e.g. "feat: something (#1234)")
const match = context.payload.head_commit?.message?.match(/\(#(\d+)\)/)
if (!match) {
console.log(`No PR number found in commit message: "${context.payload.head_commit?.message}", skipping`)
return
}
const prNumber = parseInt(match[1])
// Fetch all milestones (open and closed)
const semverRegex = /^(\d+)\.(\d+)(?:\.(\d+|next))?$/
const parseMilestones = (data) => data
.map(m => {
const match = m.title.match(semverRegex)
if (!match) return null
return {
...m,
major: parseInt(match[1]),
minor: parseInt(match[2]),
patch: match[3],
}
})
.filter(Boolean)
const [openMilestones, closedMilestones] = await Promise.all([
github.rest.issues.listMilestones({ owner: context.repo.owner, repo: context.repo.repo, state: 'open', per_page: 100 }),
github.rest.issues.listMilestones({ owner: context.repo.owner, repo: context.repo.repo, state: 'closed', per_page: 100 }),
])
const openSemverMilestones = parseMilestones(openMilestones.data)
const allSemverMilestones = parseMilestones([...openMilestones.data, ...closedMilestones.data])
let milestone
const sortDesc = (a, b) => {
if (a.major !== b.major) return b.major - a.major
if (a.minor !== b.minor) return b.minor - a.minor
const aPatch = a.patch === 'next' ? -1 : parseInt(a.patch || '0')
const bPatch = b.patch === 'next' ? -1 : parseInt(b.patch || '0')
return bPatch - aPatch
}
if (branch === 'master') {
// Find the biggest open semver milestone
milestone = openSemverMilestones.sort(sortDesc)[0]
// If no open milestone, derive from the biggest across all milestones
if (!milestone) {
const biggest = allSemverMilestones.sort(sortDesc)[0]
const nextTitle = biggest
? `${biggest.major}.${biggest.minor}.next`
: '0.0.next'
console.log(`No open semver milestone found for master, creating ${nextTitle}`)
const created = await github.rest.issues.createMilestone({
owner: context.repo.owner,
repo: context.repo.repo,
title: nextTitle,
})
milestone = created.data
}
} else {
// Branch is vX.Y or vX.Y.Z - extract major.minor
const branchMatch = branch.match(/^v(\d+)\.(\d+)/)
if (!branchMatch) {
core.setFailed(`Branch "${branch}" does not match vX.Y pattern`)
return
}
const branchMajor = parseInt(branchMatch[1])
const branchMinor = parseInt(branchMatch[2])
// Find matching open milestone for this major.minor
milestone = openSemverMilestones
.filter(m => m.major === branchMajor && m.minor === branchMinor)
.sort(sortDesc)[0]
// If no matching milestone, create X.Y.next
if (!milestone) {
console.log(`No milestone found for ${branchMajor}.${branchMinor}, creating ${branchMajor}.${branchMinor}.next`)
const created = await github.rest.issues.createMilestone({
owner: context.repo.owner,
repo: context.repo.repo,
title: `${branchMajor}.${branchMinor}.next`,
})
milestone = created.data
}
}
console.log(`Assigning milestone "${milestone.title}" to PR #${prNumber}`)
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
milestone: milestone.number,
})
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
name: Assign Milestone on: push: branches: - master - "v[0-9]*" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: assign-milestone: timeout-minutes: 30 if: github.repository == 'typeorm/typeorm' runs-on: latchkey-small permissions: issues: write pull-requests: write steps: - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const branch = context.ref.replace('refs/heads/', '') // Extract PR number from merge commit message (e.g. "feat: something (#1234)") const match = context.payload.head_commit?.message?.match(/\(#(\d+)\)/) if (!match) { console.log(`No PR number found in commit message: "${context.payload.head_commit?.message}", skipping`) return } const prNumber = parseInt(match[1]) // Fetch all milestones (open and closed) const semverRegex = /^(\d+)\.(\d+)(?:\.(\d+|next))?$/ const parseMilestones = (data) => data .map(m => { const match = m.title.match(semverRegex) if (!match) return null return { ...m, major: parseInt(match[1]), minor: parseInt(match[2]), patch: match[3], } }) .filter(Boolean) const [openMilestones, closedMilestones] = await Promise.all([ github.rest.issues.listMilestones({ owner: context.repo.owner, repo: context.repo.repo, state: 'open', per_page: 100 }), github.rest.issues.listMilestones({ owner: context.repo.owner, repo: context.repo.repo, state: 'closed', per_page: 100 }), ]) const openSemverMilestones = parseMilestones(openMilestones.data) const allSemverMilestones = parseMilestones([...openMilestones.data, ...closedMilestones.data]) let milestone const sortDesc = (a, b) => { if (a.major !== b.major) return b.major - a.major if (a.minor !== b.minor) return b.minor - a.minor const aPatch = a.patch === 'next' ? -1 : parseInt(a.patch || '0') const bPatch = b.patch === 'next' ? -1 : parseInt(b.patch || '0') return bPatch - aPatch } if (branch === 'master') { // Find the biggest open semver milestone milestone = openSemverMilestones.sort(sortDesc)[0] // If no open milestone, derive from the biggest across all milestones if (!milestone) { const biggest = allSemverMilestones.sort(sortDesc)[0] const nextTitle = biggest ? `${biggest.major}.${biggest.minor}.next` : '0.0.next' console.log(`No open semver milestone found for master, creating ${nextTitle}`) const created = await github.rest.issues.createMilestone({ owner: context.repo.owner, repo: context.repo.repo, title: nextTitle, }) milestone = created.data } } else { // Branch is vX.Y or vX.Y.Z - extract major.minor const branchMatch = branch.match(/^v(\d+)\.(\d+)/) if (!branchMatch) { core.setFailed(`Branch "${branch}" does not match vX.Y pattern`) return } const branchMajor = parseInt(branchMatch[1]) const branchMinor = parseInt(branchMatch[2]) // Find matching open milestone for this major.minor milestone = openSemverMilestones .filter(m => m.major === branchMajor && m.minor === branchMinor) .sort(sortDesc)[0] // If no matching milestone, create X.Y.next if (!milestone) { console.log(`No milestone found for ${branchMajor}.${branchMinor}, creating ${branchMajor}.${branchMinor}.next`) const created = await github.rest.issues.createMilestone({ owner: context.repo.owner, repo: context.repo.repo, title: `${branchMajor}.${branchMinor}.next`, }) milestone = created.data } } console.log(`Assigning milestone "${milestone.title}" to PR #${prNumber}`) await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, milestone: milestone.number, })
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cancel superseded runs when a branch or PR gets a newer push.
- Add a job timeout so a hung step cannot burn hours of runner time.
This workflow runs 1 job per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.