ci: automate release milestones and notify included pull requests (#3… · gitify-app/gitify@d3dde75 · GitHub
Skip to content

Commit d3dde75

Browse files
authored
ci: automate release milestones and notify included pull requests (#3357)
1 parent 3c46eff commit d3dde75

4 files changed

Lines changed: 280 additions & 4 deletions

File tree

Lines changed: 119 additions & 0 deletions

‎.github/workflows/publish.yml‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,65 @@ jobs:
163163
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
164164
TAG_NAME: ${{ inputs.tag_name }}
165165
REPO: ${{ github.repository }}
166+
167+
notify-pull-requests:
168+
name: Notify included pull requests
169+
needs: publish-release
170+
runs-on: ubuntu-latest
171+
permissions:
172+
contents: read
173+
issues: write
174+
pull-requests: write
175+
steps:
176+
- name: Close milestone for released version
177+
env:
178+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
179+
REPO: ${{ github.repository }}
180+
TAG_NAME: ${{ inputs.tag_name }}
181+
run: |
182+
# The release is published, so the tag URL is definitively live. Finalize
183+
# the milestone before notifying: description becomes the release URL, the
184+
# due date becomes today, and the milestone is closed. The notification
185+
# step queries milestones across all states, so a closed milestone still
186+
# finds the pull requests it contains on re-runs.
187+
version="${TAG_NAME#v}"
188+
title="Release ${version}"
189+
url="https://github.com/${REPO}/releases/tag/${TAG_NAME}"
190+
191+
number=$(gh api --method GET "repos/${REPO}/milestones?state=open&per_page=100" \
192+
--jq ".[] | select(.title == \"${title}\") | .number" | head -1)
193+
if [ -z "${number}" ]; then
194+
echo "::warning::No open milestone titled '${title}' found; skipping finalize"
195+
exit 0
196+
fi
197+
198+
now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
199+
gh api --method PATCH "repos/${REPO}/milestones/${number}" \
200+
-f "description=${url}" -f "due_on=${now}" -f state=closed
201+
echo "Finalized milestone '${title}': ${url}"
202+
203+
- name: Checkout trusted release automation
204+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
205+
with:
206+
persist-credentials: false
207+
ref: ${{ github.sha }}
208+
209+
- name: Notify pull requests included in the release
210+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
211+
with:
212+
github-token: ${{ secrets.GITHUB_TOKEN }}
213+
script: |
214+
const { pathToFileURL } = await import('node:url');
215+
const scriptUrl = pathToFileURL(
216+
`${process.env.GITHUB_WORKSPACE}/.github/scripts/notify-release-pull-requests.mjs`,
217+
);
218+
const { notifyReleasedPullRequests } = await import(scriptUrl);
219+
const result = await notifyReleasedPullRequests(github, {
220+
...context.repo,
221+
tag: process.env.TAG_NAME,
222+
});
223+
core.info(
224+
`Release ${process.env.TAG_NAME}: notified ${result.notified} pull request(s), skipped ${result.skipped}`,
225+
);
226+
env:
227+
TAG_NAME: ${{ inputs.tag_name }}

‎.github/workflows/release.yml‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jobs:
2121
outputs:
2222
release_created: ${{ steps.release.outputs.release_created }}
2323
tag_name: ${{ steps.release.outputs.tag_name }}
24+
release_pr_title: ${{ steps.pr.outputs.prs_created == 'true' && fromJSON(steps.pr.outputs.pr).title || '' }}
2425
steps:
2526
- name: Run release-please (releases)
2627
id: release
@@ -38,11 +39,94 @@ jobs:
3839
echo "count=${count}" >> "$GITHUB_OUTPUT"
3940
4041
- name: Run release-please (release PR)
42+
id: pr
4143
if: ${{ steps.release.outputs.release_created != 'true' && steps.drafts.outputs.count == '0' }}
4244
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
4345
with:
4446
skip-github-release: true
4547

48+
milestones:
49+
name: Milestones
50+
needs: release-please
51+
if: ${{ needs.release-please.outputs.release_pr_title != '' }}
52+
runs-on: ubuntu-latest
53+
permissions:
54+
issues: write
55+
pull-requests: write
56+
steps:
57+
- name: Ensure open milestone for upcoming release
58+
id: ensure
59+
env:
60+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
61+
REPO: ${{ github.repository }}
62+
TITLE: ${{ needs.release-please.outputs.release_pr_title }}
63+
run: |
64+
# The release-please release PR title is `chore(main): release X.Y.Z`.
65+
# Parse the version so the open milestone always tracks the next release.
66+
version=$(printf '%s' "${TITLE}" | sed -n -E 's/.*release[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+).*/\1/p')
67+
if [ -z "${version}" ]; then
68+
echo "::warning::Could not parse a version from the release-please PR title '${TITLE}'"
69+
exit 0
70+
fi
71+
72+
target="Release ${version}"
73+
open=$(gh api --method GET "repos/${REPO}/milestones?state=open&per_page=100" \
74+
--jq '[.[] | {number, title}]')
75+
count=$(printf '%s' "${open}" | jq 'length')
76+
77+
if [ "${count}" -eq 0 ]; then
78+
gh api --method POST "repos/${REPO}/milestones" \
79+
-f "title=${target}" \
80+
-f "description=Upcoming release; merged pull requests ship in v${version}."
81+
echo "Created open milestone '${target}'"
82+
elif [ "${count}" -eq 1 ]; then
83+
current=$(printf '%s' "${open}" | jq -r '.[0].title')
84+
if [ "${current}" != "${target}" ]; then
85+
number=$(printf '%s' "${open}" | jq -r '.[0].number')
86+
# Rename (not recreate): pull requests already attached stay attached
87+
# and still ship in the bumped version.
88+
gh api --method PATCH "repos/${REPO}/milestones/${number}" -f "title=${target}"
89+
echo "Renamed open milestone '${current}' to '${target}'"
90+
else
91+
echo "Open milestone '${target}' already exists"
92+
fi
93+
else
94+
echo "::warning::Expected at most one open milestone, found ${count}; leaving them untouched"
95+
exit 0
96+
fi
97+
98+
echo "target=${target}" >> "$GITHUB_OUTPUT"
99+
100+
- name: Attach triggering pull request(s)
101+
if: steps.ensure.outputs.target != ''
102+
env:
103+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
104+
REPO: ${{ github.repository }}
105+
SHA: ${{ github.sha }}
106+
TARGET: ${{ steps.ensure.outputs.target }}
107+
run: |
108+
# milestone.yml can lose a race with this job: it evaluates the open
109+
# milestone on `pull_request: closed`, before release-please has opened
110+
# the next release PR and this job has created the milestone. Resolve the
111+
# pull request(s) for the push tip and attach any that shipped without one,
112+
# so they are neither missing from the cycle's milestone nor the notify job.
113+
prs=$(gh api "repos/${REPO}/commits/${SHA}/pulls" \
114+
--jq '.[] | select(.merged_at != null) | [.number, (.milestone.title // "")] | @tsv')
115+
if [ -z "${prs}" ]; then
116+
echo "No merged pull request associated with ${SHA}; nothing to attach"
117+
exit 0
118+
fi
119+
120+
while IFS=$'\t' read -r number current; do
121+
if [ -z "${number}" ]; then continue; fi
122+
if [ "${current}" = "${TARGET}" ]; then
123+
echo "Pull request #${number} already on '${TARGET}'; skipping"
124+
continue
125+
fi
126+
gh pr edit "${number}" --repo "${REPO}" --milestone "${TARGET}"
127+
echo "Attached pull request #${number} to '${TARGET}'"
128+
done <<< "${prs}"
129+
46130
lint:
47131
name: Lint App
48132
uses: ./.github/workflows/lint.yml
@@ -66,6 +150,8 @@ jobs:
66150
needs: [release-please, tests]
67151
permissions:
68152
contents: write
153+
issues: write
154+
pull-requests: write
69155
with:
70156
tag_name: ${{ needs.release-please.outputs.tag_name }}
71157
secrets:

‎CONTRIBUTING.md‎

Lines changed: 13 additions & 4 deletions

0 commit comments

Comments
 (0)