Skip to content

Commit 59a647e

Browse files
authored
Merge branch 'main' into samejr/login-magic-link-redirect
2 parents 0d6f166 + 105f489 commit 59a647e

5 files changed

Lines changed: 139 additions & 24 deletions

File tree

.changeset/config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"updateInternalDependencies": "patch",
1515
"ignore": [
1616
"webapp",
17-
"supervisor"
17+
"supervisor",
18+
"@trigger.dev/plugins"
1819
],
1920
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
2021
"onlyUpdatePeerDependentsWhenOutOfRange": true

.github/workflows/release.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,21 @@ jobs:
143143
STEPS_GET_VERSION_OUTPUTS_IS_PRERELEASE: ${{ steps.get_version.outputs.is_prerelease }}
144144
run: |
145145
VERSION="${STEPS_GET_VERSION_OUTPUTS_PACKAGE_VERSION}"
146+
147+
# On the pull_request merge event RELEASE_PR_BODY is the merged "Version Packages"
148+
# PR body, which holds the changelog. On a workflow_dispatch re-run (the recovery
149+
# path after a failed merge-triggered run) there is no pull_request context, so it's
150+
# empty. Fall back to the body of the most recently merged changeset-release/main PR
151+
# so the "What's changed" section is still populated.
152+
if [ -z "${RELEASE_PR_BODY}" ]; then
153+
echo "RELEASE_PR_BODY is empty (workflow_dispatch re-run); fetching merged changeset-release/main PR body"
154+
RELEASE_PR_BODY="$(gh pr list --repo triggerdotdev/trigger.dev \
155+
--head changeset-release/main --state merged \
156+
--json body,mergedAt --limit 10 \
157+
-q 'sort_by(.mergedAt) | reverse | .[0].body')"
158+
fi
159+
export RELEASE_PR_BODY
160+
146161
node scripts/generate-github-release.mjs "$VERSION" > /tmp/release-body.md
147162
PRERELEASE_FLAG=""
148163
if [ "${STEPS_GET_VERSION_OUTPUTS_IS_PRERELEASE}" = "true" ]; then

apps/webapp/app/services/directorySyncEffects.server.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ import { createPlatformNotification } from "~/services/platformNotifications.ser
1111

1212
const LAST_OWNER_NOTIFICATION_TITLE = "Directory Sync: last Owner protected";
1313

14+
// Directory-sync effects are idempotent and the accounts-webhook worker retries
15+
// the whole event (maxAttempts: 5). A single failed attempt is therefore an
16+
// expected, self-healing condition rather than an alert-worthy error — the most
17+
// common case is a role assignment losing a serializable race during a backfill
18+
// burst (the plugin already retries that internally; the worker retry mops up
19+
// the rest). Tag the thrown error with `logLevel: "warn"` so the worker logs it
20+
// at warn instead of error (see redis-worker `processItem`), keeping it visible
21+
// for triage without paging as an error on every transient retry.
22+
function retryableEffectError(message: string): Error {
23+
return Object.assign(new Error(message), { logLevel: "warn" as const });
24+
}
25+
1426
// Raise a user-scoped, deduped notification when the directory tried to
1527
// remove the org's last Owner. We keep the member and tell the Owner what to
1628
// do; a single undismissed notification is enough (don't spam on every retry).
@@ -92,7 +104,7 @@ async function applyEffect(effect: DirectorySyncEffect): Promise<void> {
92104
roleId: effect.roleId,
93105
});
94106
if (!result.ok) {
95-
throw new Error(`directorySync provision setUserRole failed: ${result.error}`);
107+
throw retryableEffectError(`directorySync provision setUserRole failed: ${result.error}`);
96108
}
97109
}
98110
return;
@@ -104,7 +116,7 @@ async function applyEffect(effect: DirectorySyncEffect): Promise<void> {
104116
roleId: effect.roleId,
105117
});
106118
if (!result.ok) {
107-
throw new Error(`directorySync set_role failed: ${result.error}`);
119+
throw retryableEffectError(`directorySync set_role failed: ${result.error}`);
108120
}
109121
return;
110122
}

scripts/enhance-release-pr.mjs

Lines changed: 93 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -183,28 +183,8 @@ async function getPrForCommit(commitSha) {
183183
// --- Parse .server-changes/ files ---
184184

185185
async function parseServerChanges() {
186-
const dir = join(ROOT_DIR, ".server-changes");
187186
const entries = [];
188-
189-
let files;
190-
try {
191-
files = await fs.readdir(dir);
192-
} catch {
193-
return entries;
194-
}
195-
196-
// Collect file info and look up commits in parallel
197-
const fileData = [];
198-
for (const file of files) {
199-
if (!file.endsWith(".md") || file === "README.md") continue;
200-
201-
const filePath = join(".server-changes", file);
202-
const content = await fs.readFile(join(dir, file), "utf-8");
203-
const parsed = parseFrontmatter(content);
204-
if (!parsed.body.trim()) continue;
205-
206-
fileData.push({ filePath, parsed });
207-
}
187+
const fileData = await getServerChangeFileData();
208188

209189
// Look up commits for all files in parallel
210190
const commits = await Promise.all(fileData.map((f) => getCommitForFile(f.filePath)));
@@ -232,6 +212,98 @@ async function parseServerChanges() {
232212
return entries;
233213
}
234214

215+
async function getServerChangeFileData() {
216+
// The changesets version command deletes .server-changes before this script
217+
// enhances the release PR body. We combine files still live on disk with the
218+
// ones recovered from the release branch diff, deduped by filename, rather
219+
// than picking one source or the other. This is additive so a partial cleanup
220+
// (some files deleted, some still live) can't silently drop entries. Live
221+
// files win on collision since they are the current on-disk truth.
222+
const [live, deleted] = await Promise.all([
223+
getLiveServerChangeFileData(),
224+
getDeletedServerChangeFileDataFromReleaseBranch(),
225+
]);
226+
227+
const byName = new Map();
228+
for (const fileData of deleted) {
229+
byName.set(fileData.filePath.split("/").pop(), fileData);
230+
}
231+
for (const fileData of live) {
232+
byName.set(fileData.filePath.split("/").pop(), fileData);
233+
}
234+
235+
return [...byName.values()].sort((a, b) => a.filePath.localeCompare(b.filePath));
236+
}
237+
238+
async function getLiveServerChangeFileData() {
239+
const dir = join(ROOT_DIR, ".server-changes");
240+
241+
let files;
242+
try {
243+
files = await fs.readdir(dir);
244+
} catch {
245+
return [];
246+
}
247+
248+
const fileData = [];
249+
for (const file of files.sort()) {
250+
if (!file.endsWith(".md") || file === "README.md") continue;
251+
252+
const filePath = join(".server-changes", file);
253+
const content = await fs.readFile(join(dir, file), "utf-8");
254+
const parsed = parseFrontmatter(content);
255+
if (!parsed.body.trim()) continue;
256+
257+
fileData.push({ filePath, parsed });
258+
}
259+
260+
return fileData;
261+
}
262+
263+
async function getDeletedServerChangeFileDataFromReleaseBranch() {
264+
const baseRef = process.env.SERVER_CHANGES_BASE_REF || "origin/main";
265+
const releaseRef = process.env.SERVER_CHANGES_RELEASE_REF || "origin/changeset-release/main";
266+
267+
let mergeBase;
268+
let deletedFiles;
269+
try {
270+
mergeBase = await gitExec(["merge-base", baseRef, releaseRef]);
271+
deletedFiles = await gitExec([
272+
"diff",
273+
"--name-only",
274+
"--diff-filter=D",
275+
`${mergeBase}..${releaseRef}`,
276+
"--",
277+
".server-changes",
278+
]);
279+
} catch (err) {
280+
console.error(
281+
"[enhance-release-pr] failed to recover deleted server-changes from release branch:",
282+
err
283+
);
284+
return [];
285+
}
286+
287+
const fileData = [];
288+
for (const filePath of deletedFiles.split("\n").filter(Boolean).sort()) {
289+
const file = filePath.split("/").pop();
290+
if (!file?.endsWith(".md") || file === "README.md") continue;
291+
292+
try {
293+
const content = await gitExec(["show", `${mergeBase}:${filePath}`]);
294+
const parsed = parseFrontmatter(content);
295+
if (!parsed.body.trim()) continue;
296+
fileData.push({ filePath, parsed });
297+
} catch (err) {
298+
// If an individual file cannot be recovered, skip it rather than hiding
299+
// all other server changes.
300+
console.error(`[enhance-release-pr] failed to read deleted server-change ${filePath}:`, err);
301+
}
302+
}
303+
304+
return fileData;
305+
}
306+
235307
function parseFrontmatter(content) {
236308
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
237309
if (!match) return { frontmatter: {}, body: content };

scripts/generate-github-release.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,21 @@ async function main() {
239239
}
240240

241241
const changesContent = extractChangesFromPrBody(prBody);
242+
243+
// Fail loudly when a stable (non-prerelease) release resolved no changelog. This
244+
// guards against publishing an empty "What's changed" section, which previously
245+
// happened silently on workflow_dispatch re-runs where RELEASE_PR_BODY was empty.
246+
// Prereleases (any version with a hyphen, e.g. 4.5.0-rc.0) are allowed to be empty.
247+
const isPrerelease = version.includes("-");
248+
if (!changesContent && !isPrerelease) {
249+
console.error(
250+
`Error: no "What's changed" content could be resolved for v${version}. ` +
251+
`RELEASE_PR_BODY was empty or contained no changelog entries. ` +
252+
`Refusing to publish a release with an empty changelog.`
253+
);
254+
process.exit(1);
255+
}
256+
242257
const contributors = getContributors(getPreviousVersion(version));
243258
const packages = getPublishedPackages();
244259

0 commit comments

Comments
 (0)