Skip to content

Commit 0ea20b5

Browse files
authored
Merge branch 'main' into perf/skip-structural-sharing
2 parents 7f62ff0 + 9c0cc59 commit 0ea20b5

205 files changed

Lines changed: 8922 additions & 4808 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
const RELEASE_PLEASE_BRANCH_PREFIX = 'release-please--';
2+
3+
export function releaseNoticeMarker(tag) {
4+
return `<!-- gitify-release-notice:${tag} -->`;
5+
}
6+
7+
export function releaseNoticeBody(tag, releaseUrl) {
8+
return [
9+
`🎉 This pull request is included in **[Gitify ${tag}](${releaseUrl})**! 🎉`,
10+
'',
11+
'Downloads for macOS, Windows, and Linux are available on the release page.',
12+
'',
13+
'Thanks for helping make Gitify better!',
14+
'',
15+
releaseNoticeMarker(tag),
16+
].join('\n');
17+
}
18+
19+
export async function findVersionMilestone(github, repository, tag) {
20+
const milestoneTitle = `Release ${tag.replace(/^v/, '')}`;
21+
const milestones = await github.paginate(github.rest.issues.listMilestones, {
22+
...repository,
23+
state: 'all',
24+
per_page: 100,
25+
});
26+
const matches = milestones.filter((milestone) => milestone.title === milestoneTitle);
27+
28+
if (matches.length !== 1) {
29+
throw new Error(
30+
`Expected exactly one milestone titled ${milestoneTitle}; found ${matches.length}`,
31+
);
32+
}
33+
34+
return matches[0];
35+
}
36+
37+
export async function listMilestoneItems(github, repository, milestoneNumber) {
38+
return github.paginate(github.rest.issues.listForRepo, {
39+
...repository,
40+
milestone: String(milestoneNumber),
41+
state: 'all',
42+
per_page: 100,
43+
});
44+
}
45+
46+
export function isEligiblePullRequest(pullRequest) {
47+
return Boolean(
48+
pullRequest.merged_at && !pullRequest.head.ref.startsWith(RELEASE_PLEASE_BRANCH_PREFIX),
49+
);
50+
}
51+
52+
export async function hasReleaseNotice(github, repository, pullRequestNumber, marker) {
53+
const comments = await github.paginate(github.rest.issues.listComments, {
54+
...repository,
55+
issue_number: pullRequestNumber,
56+
per_page: 100,
57+
});
58+
return comments.some((comment) => comment.body?.includes(marker));
59+
}
60+
61+
export async function notifyReleasedPullRequests(github, { owner, repo, tag }) {
62+
const repository = { owner, repo };
63+
const { data: release } = await github.rest.repos.getReleaseByTag({
64+
...repository,
65+
tag,
66+
});
67+
if (release.draft) {
68+
throw new Error(`Release ${tag} is still a draft`);
69+
}
70+
71+
const milestone = await findVersionMilestone(github, repository, tag);
72+
const items = await listMilestoneItems(github, repository, milestone.number);
73+
const marker = releaseNoticeMarker(tag);
74+
const failures = [];
75+
let notified = 0;
76+
let skipped = 0;
77+
78+
for (const item of items) {
79+
if (!item.pull_request) {
80+
skipped += 1;
81+
continue;
82+
}
83+
84+
try {
85+
const { data: pullRequest } = await github.rest.pulls.get({
86+
...repository,
87+
pull_number: item.number,
88+
});
89+
if (!isEligiblePullRequest(pullRequest)) {
90+
skipped += 1;
91+
continue;
92+
}
93+
94+
if (await hasReleaseNotice(github, repository, item.number, marker)) {
95+
skipped += 1;
96+
continue;
97+
}
98+
99+
await github.rest.issues.createComment({
100+
...repository,
101+
issue_number: item.number,
102+
body: releaseNoticeBody(tag, release.html_url),
103+
});
104+
notified += 1;
105+
} catch (error) {
106+
const message = error instanceof Error ? error.message : String(error);
107+
failures.push(new Error(`Pull request #${item.number}: ${message}`, { cause: error }));
108+
}
109+
}
110+
111+
if (failures.length > 0) {
112+
throw new AggregateError(
113+
failures,
114+
`Failed to notify ${failures.length} pull request(s); ${notified} succeeded`,
115+
);
116+
}
117+
118+
return { notified, skipped };
119+
}

‎.github/workflows/build.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
persist-credentials: false
5050

5151
- name: Setup Vite+
52-
uses: voidzero-dev/setup-vp@9fd26ff49f9b5e6276c2b493da4454ad4527f7f4 # v1.20.0
52+
uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1
5353
with:
5454
cache: true
5555

‎.github/workflows/lint.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
persist-credentials: false
2020

2121
- name: Setup Vite+
22-
uses: voidzero-dev/setup-vp@9fd26ff49f9b5e6276c2b493da4454ad4527f7f4 # v1.20.0
22+
uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1
2323
with:
2424
cache: true
2525
node-version-file: '.nvmrc'

‎.github/workflows/publish.yml‎

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767
persist-credentials: false
6868

6969
- name: Setup Vite+
70-
uses: voidzero-dev/setup-vp@9fd26ff49f9b5e6276c2b493da4454ad4527f7f4 # v1.20.0
70+
uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1
7171
with:
7272
cache: true
7373
node-version-file: '.nvmrc'
@@ -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:

‎.github/workflows/renovate.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
renovate.json
2626
.nvmrc
2727
28-
- uses: voidzero-dev/setup-vp@9fd26ff49f9b5e6276c2b493da4454ad4527f7f4 # v1.20.0
28+
- uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1
2929
with:
3030
cache: true
3131
run-install: 'false'

‎.github/workflows/test.yml‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
persist-credentials: false
2323

2424
- name: Setup Vite+
25-
uses: voidzero-dev/setup-vp@9fd26ff49f9b5e6276c2b493da4454ad4527f7f4 # v1.20.0
25+
uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1
2626
with:
2727
cache: true
2828
node-version-file: '.nvmrc'
@@ -63,7 +63,7 @@ jobs:
6363
persist-credentials: false
6464

6565
- name: Setup Vite+
66-
uses: voidzero-dev/setup-vp@9fd26ff49f9b5e6276c2b493da4454ad4527f7f4 # v1.20.0
66+
uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1
6767
with:
6868
cache: true
6969
node-version-file: '.nvmrc'
@@ -83,6 +83,7 @@ jobs:
8383
with:
8484
name: visual-regression-diffs
8585
path: .vitest-attachments/
86+
include-hidden-files: true
8687
if-no-files-found: ignore
8788

8889
sonarqube:
@@ -102,7 +103,7 @@ jobs:
102103
persist-credentials: false
103104

104105
- name: Setup Vite+
105-
uses: voidzero-dev/setup-vp@9fd26ff49f9b5e6276c2b493da4454ad4527f7f4 # v1.20.0
106+
uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1
106107
with:
107108
cache: true
108109
node-version-file: '.nvmrc'
@@ -114,7 +115,7 @@ jobs:
114115
path: coverage/
115116

116117
- name: SonarQube Cloud Scan
117-
uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1
118+
uses: SonarSource/sonarqube-scan-action@d209202bc7d53ff1cc128f7f907dac145c9d6ae9 # v8.3.0
118119
env:
119120
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
120121
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

‎.gitignore‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,7 @@ src/renderer/**/__screenshots__/**/*.png
3232
.vitest-attachments/
3333

3434
# Mac Files
35-
.DS_Store
35+
.DS_Store
36+
37+
# OpenSpec
38+
openspec/

‎.release-please-manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "7.8.0"
2+
".": "7.9.0"
33
}

0 commit comments

Comments
 (0)