Skip to content

Instantly share code, notes, and snippets.

@brandtkeller
Created May 8, 2025 21:44
Show Gist options
  • Select an option

  • Save brandtkeller/ef2dbc44acf9263a0f8c71f1a395ada0 to your computer and use it in GitHub Desktop.

Select an option

Save brandtkeller/ef2dbc44acf9263a0f8c71f1a395ada0 to your computer and use it in GitHub Desktop.
Automate milestone assignment to merged pr's
# File: .github/workflows/auto-milestone.yml
name: Auto-Assign Next-Minor Milestone
on:
pull_request:
types: [closed]
jobs:
add-milestone:
if: ${{ github.event.pull_request.merged == true }}
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm install semver
- name: Determine latest release tag
id: get_latest
run: |
# Fetch all tags
git fetch --tags
# List tags sorted by semver, take the latest
latest_tag=$(git tag -l 'v*.*.*' --sort=-v:refname | head -n1)
echo "latest=$latest_tag" >> "$GITHUB_OUTPUT"
- name: Compute next minor version
id: bump_version
run: |
node <<'EOF'
const semver = require('semver');
const latest = process.env.INPUT_LATEST;
if (!latest) {
console.error('No valid semver tag found.');
process.exit(1);
}
const next = semver.inc(latest, 'minor');
console.log(`next=${next}`);
EOF
env:
INPUT_LATEST: ${{ steps.get_latest.outputs.latest }}
shell: bash
continue-on-error: false
run: |
# This block prints "next=x.y.z" for GITHUB_OUTPUT
node <<'EOF'
const semver = require('semver');
const { latest } = process.env;
const next = semver.inc(latest, 'minor');
console.log(`::set-output name=next::${next}`);
EOF
- name: Check for existing milestone
id: find_milestone
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { data: milestones } = await github.issues.listMilestones({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open'
});
const title = `v${process.env.NEXT}`;
const ms = milestones.find(m => m.title === title);
return ms ? ms.number : null;
env:
NEXT: ${{ steps.bump_version.outputs.next }}
- name: Create milestone if missing
if: ${{ steps.find_milestone.outputs.result == 'null' }}
id: create_milestone
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const title = `v${process.env.NEXT}`;
const { data: milestone } = await github.issues.createMilestone({
owner: context.repo.owner,
repo: context.repo.repo,
title,
state: 'open'
});
return milestone.number;
env:
NEXT: ${{ steps.bump_version.outputs.next }}
- name: Determine milestone number
run: |
echo "milestone_number=${{ steps.find_milestone.outputs.result || steps.create_milestone.outputs.result }}" >> $GITHUB_OUTPUT
- name: Assign milestone to PR
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const milestone_number = parseInt(process.env.MILESTONE_NUMBER, 10);
const pr_number = context.payload.pull_request.number;
await github.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr_number,
milestone: milestone_number
});
env:
MILESTONE_NUMBER: ${{ steps.determine_milestone_number.outputs.milestone_number }}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment