Skip to content
Latchkey

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.

Grade your own workflow free or run it on Latchkey →
Source: typeorm/typeorm.github/workflows/assign-milestone.ymlLicense MITView source

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

This workflow runs 1 job per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow