@dbx-tools/projen 0.9.167 → 0.9.173
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/index.ts +1 -1
- package/package.json +6 -6
- package/tasks/publish.ts +42 -47
- package/tasks/release-notes.ts +29 -15
- package/tasks/release.ts +96 -4
package/README.md
CHANGED
|
@@ -626,9 +626,14 @@ and profile are not recorded in the annotated tag.
|
|
|
626
626
|
|
|
627
627
|
After bump and configured validation, release writes
|
|
628
628
|
`docs/releases/vX.Y.Z.md` with `dbx genie exec -C "$PWD" --sandbox read-only
|
|
629
|
-
--ephemeral -o`.
|
|
630
|
-
|
|
631
|
-
|
|
629
|
+
--ephemeral -o`. It first finds the newest tag whose GitHub Actions release
|
|
630
|
+
workflow succeeded, then asks Genie for cumulative notes from that deployed
|
|
631
|
+
baseline through HEAD. Newer failed-release tags and their notes remain source
|
|
632
|
+
material instead of shortening the delta to a retry-only workflow fix. The
|
|
633
|
+
standard prompt cannot run validation commands. Failures fall back to a short
|
|
634
|
+
git-log summary from the same successful baseline. Notes are committed with the
|
|
635
|
+
version bump, not stored in the annotated tag, and are omitted for an existing
|
|
636
|
+
committed bump.
|
|
632
637
|
|
|
633
638
|
Automatic installation retains Projen's manifest-based install trigger; the
|
|
634
639
|
other install modes force installation or reuse the current dependency tree.
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
5
|
export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
|
|
6
|
-
export const PACKAGE_VERSION = "0.9.
|
|
6
|
+
export const PACKAGE_VERSION = "0.9.173";
|
|
7
7
|
export * as appkitWorkspace from "./src/appkit-workspace.ts";
|
|
8
8
|
export * as barrels from "./src/barrels.ts";
|
|
9
9
|
export * as bunApp from "./src/bun-app.ts";
|
package/package.json
CHANGED
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@clack/prompts": "^1.7.0",
|
|
33
|
-
"@dbx-tools/auth": "^0.9.
|
|
34
|
-
"@dbx-tools/cli-args": "^0.9.
|
|
35
|
-
"@dbx-tools/core": "^0.9.
|
|
36
|
-
"@dbx-tools/path": "^0.9.
|
|
37
|
-
"@dbx-tools/shared-core": "^0.9.
|
|
33
|
+
"@dbx-tools/auth": "^0.9.173",
|
|
34
|
+
"@dbx-tools/cli-args": "^0.9.173",
|
|
35
|
+
"@dbx-tools/core": "^0.9.173",
|
|
36
|
+
"@dbx-tools/path": "^0.9.173",
|
|
37
|
+
"@dbx-tools/shared-core": "^0.9.173",
|
|
38
38
|
"commander": "^15.0.0",
|
|
39
39
|
"concurrently": "^10.0.3",
|
|
40
40
|
"constructs": "^10.6.0",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"main": "index.ts",
|
|
58
58
|
"license": "Apache-2.0",
|
|
59
|
-
"version": "0.9.
|
|
59
|
+
"version": "0.9.173",
|
|
60
60
|
"types": "index.ts",
|
|
61
61
|
"type": "module",
|
|
62
62
|
"exports": {
|
package/tasks/publish.ts
CHANGED
|
@@ -50,6 +50,7 @@ import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:pa
|
|
|
50
50
|
import { log, mapConcurrent, sequence } from "@dbx-tools/shared-core";
|
|
51
51
|
import { z } from "zod";
|
|
52
52
|
import { runTaskMain, taskCommand, taskOptions, taskPositionals, taskRoot } from "./cli.ts";
|
|
53
|
+
import { publishNpmArchives } from "./publish-npm.ts";
|
|
53
54
|
import {
|
|
54
55
|
TaskConcurrencyOptionSchema,
|
|
55
56
|
TaskDirectoriesOptionSchema,
|
|
@@ -59,7 +60,7 @@ import {
|
|
|
59
60
|
} from "./options.ts";
|
|
60
61
|
import { runTaskCommand } from "../src/_task-command.ts";
|
|
61
62
|
import { applyPublishConfig, packNpmPackage } from "../src/npm-packaging.ts";
|
|
62
|
-
import {
|
|
63
|
+
import { readNpmArchiveIdentity } from "../src/npm-release.ts";
|
|
63
64
|
import { recordedPackages } from "../src/packages.ts";
|
|
64
65
|
|
|
65
66
|
const logger = log.logger("projen:publish");
|
|
@@ -212,58 +213,52 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
|
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
logger.info(
|
|
216
|
-
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
if (output) {
|
|
240
|
-
const destination = join(output, archive.split(/[\\/]/).at(-1)!);
|
|
216
|
+
logger.info(`packing ${publishable.length} packages with concurrency ${concurrency}`);
|
|
217
|
+
const archiveDirectory = output ?? mkdtempSync(join(tmpdir(), "projen-npm-publish-"));
|
|
218
|
+
if (output) rmSync(archiveDirectory, { recursive: true, force: true });
|
|
219
|
+
mkdirSync(archiveDirectory, { recursive: true });
|
|
220
|
+
try {
|
|
221
|
+
await mapConcurrent(
|
|
222
|
+
publishable,
|
|
223
|
+
async ({ dir, name, version: packageVersion, access }) => {
|
|
224
|
+
const packed = mkdtempSync(join(tmpdir(), "projen-npm-release-"));
|
|
225
|
+
try {
|
|
226
|
+
const archive = packNpmPackage(dir, packed, path, applyPublishConfig);
|
|
227
|
+
const local = readNpmArchiveIdentity(archive);
|
|
228
|
+
if (local.name !== name || local.version !== packageVersion) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Packed npm identity ${local.name}@${local.version} does not match ${name}@${packageVersion}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (local.access !== access) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`Packed npm access ${String(local.access)} does not match ${String(access)} for ${name}`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const destination = join(archiveDirectory, archive.split(/[\\/]/).at(-1)!);
|
|
241
239
|
if (existsSync(destination)) {
|
|
242
240
|
throw new Error(`Duplicate npm archive name: ${destination}`);
|
|
243
241
|
}
|
|
244
242
|
copyFileSync(archive, destination);
|
|
245
243
|
logger.info(`packed ${name} @ ${packageVersion}`);
|
|
246
|
-
|
|
244
|
+
} finally {
|
|
245
|
+
rmSync(packed, { recursive: true, force: true });
|
|
247
246
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
},
|
|
265
|
-
{ concurrency, errorMode: "settle" },
|
|
266
|
-
);
|
|
247
|
+
},
|
|
248
|
+
{ concurrency, errorMode: "settle" },
|
|
249
|
+
);
|
|
250
|
+
if (!output) {
|
|
251
|
+
await publishNpmArchives({
|
|
252
|
+
concurrency,
|
|
253
|
+
directory: archiveDirectory,
|
|
254
|
+
dryRun,
|
|
255
|
+
registry,
|
|
256
|
+
version,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
} finally {
|
|
260
|
+
if (!output) rmSync(archiveDirectory, { recursive: true, force: true });
|
|
261
|
+
}
|
|
267
262
|
logger.success(
|
|
268
263
|
`${output || dryRun ? "packed" : "published"} ${publishable.length} packages${output ? ` to ${output}` : ""}`,
|
|
269
264
|
);
|
package/tasks/release-notes.ts
CHANGED
|
@@ -16,7 +16,17 @@ import { captureTaskCommand, tryTaskCommand } from "../src/_task-command.ts";
|
|
|
16
16
|
const logger = log.logger("projen:release-notes");
|
|
17
17
|
|
|
18
18
|
const GENIE_PROMPT =
|
|
19
|
-
"
|
|
19
|
+
"Produce concise user-facing release notes. Ignore generated-file churn and test-only changes. Include breaking changes, fixes, and validation results. The configured release validation phase has already completed, so do not run tests or other validation commands and do not invent validation counts. Do not modify files.";
|
|
20
|
+
|
|
21
|
+
/** Inputs that define the release-note comparison and optional caller guidance. */
|
|
22
|
+
export interface ReleaseNotesPromptOptions {
|
|
23
|
+
/** Release tag whose GitHub Actions release workflow most recently succeeded. */
|
|
24
|
+
readonly successfulTag?: string;
|
|
25
|
+
/** Extra agent instructions appended after the standard release-note requirements. */
|
|
26
|
+
readonly instructions?: string;
|
|
27
|
+
/** Release tag prefix used when no successful GitHub baseline exists. */
|
|
28
|
+
readonly prefix: string;
|
|
29
|
+
}
|
|
20
30
|
|
|
21
31
|
/** Inputs for one notes write. */
|
|
22
32
|
export interface WriteReleaseNotesOptions {
|
|
@@ -28,15 +38,21 @@ export interface WriteReleaseNotesOptions {
|
|
|
28
38
|
readonly instructions?: string;
|
|
29
39
|
readonly prefix: string;
|
|
30
40
|
readonly root: string;
|
|
41
|
+
/** Release tag whose GitHub Actions release workflow most recently succeeded. */
|
|
42
|
+
readonly successfulTag?: string;
|
|
31
43
|
readonly version: string;
|
|
32
44
|
/** Override Genie invocation; return false to force the git-log fallback. */
|
|
33
45
|
readonly runGenie?: (args: readonly string[]) => boolean;
|
|
34
46
|
}
|
|
35
47
|
|
|
36
|
-
/** Compose the stable release-note prompt with
|
|
37
|
-
export function releaseNotesPrompt(
|
|
38
|
-
const
|
|
39
|
-
|
|
48
|
+
/** Compose the stable release-note prompt with the deployed baseline and caller guidance. */
|
|
49
|
+
export function releaseNotesPrompt(options: ReleaseNotesPromptOptions): string {
|
|
50
|
+
const baseline = options.successfulTag
|
|
51
|
+
? `The latest GitHub release workflow that completed successfully is ${options.successfulTag}. Compare ${options.successfulTag}..HEAD plus the working tree. Treat any newer ${options.prefix}* tags and their release-note files as failed release attempts: combine their user-facing changes into this release, verify them against the cumulative code delta, and do not shorten the comparison to those failed tags. Do not reduce the notes to retry-only deployment or workflow fixes such as \"changed workflow X\" when unreleased product changes are part of the same cumulative delta.`
|
|
52
|
+
: `No successful GitHub release workflow baseline was found. Do not assume the latest ${options.prefix}* tag deployed successfully. Inspect the available release-note files, tags, git history, HEAD, and the working tree, then produce cumulative notes for the user-facing changes not known to have shipped successfully. Do not reduce the notes to retry-only deployment or workflow fixes when unreleased product changes are present.`;
|
|
53
|
+
const prompt = `${baseline}\n\n${GENIE_PROMPT}`;
|
|
54
|
+
const additional = options.instructions?.trim();
|
|
55
|
+
return additional ? `${prompt}\n\nAdditional instructions:\n${additional}` : prompt;
|
|
40
56
|
}
|
|
41
57
|
|
|
42
58
|
/** Path of the notes file for one workspace version. */
|
|
@@ -92,7 +108,11 @@ export function writeReleaseNotes(options: WriteReleaseNotesOptions): string {
|
|
|
92
108
|
"--ephemeral",
|
|
93
109
|
"-o",
|
|
94
110
|
relative,
|
|
95
|
-
releaseNotesPrompt(
|
|
111
|
+
releaseNotesPrompt({
|
|
112
|
+
instructions: options.instructions,
|
|
113
|
+
prefix,
|
|
114
|
+
successfulTag: options.successfulTag,
|
|
115
|
+
}),
|
|
96
116
|
] as const;
|
|
97
117
|
const ran =
|
|
98
118
|
options.runGenie?.([...genieArgs]) ??
|
|
@@ -101,20 +121,14 @@ export function writeReleaseNotes(options: WriteReleaseNotesOptions): string {
|
|
|
101
121
|
logger.info("wrote Genie release notes", { path: relative });
|
|
102
122
|
return destination;
|
|
103
123
|
}
|
|
104
|
-
const
|
|
105
|
-
"
|
|
106
|
-
"--tags",
|
|
107
|
-
"--abbrev=0",
|
|
108
|
-
`--match=${prefix}*`,
|
|
109
|
-
]);
|
|
110
|
-
const logArgs = previousTag
|
|
111
|
-
? ["log", "--no-merges", "--pretty=format:%s", `${previousTag}..HEAD`]
|
|
124
|
+
const logArgs = options.successfulTag
|
|
125
|
+
? ["log", "--no-merges", "--pretty=format:%s", `${options.successfulTag}..HEAD`]
|
|
112
126
|
: ["log", "--no-merges", "--pretty=format:%s", "-15"];
|
|
113
127
|
const commits = captureTaskCommand(root, "git", logArgs)
|
|
114
128
|
.split("\n")
|
|
115
129
|
.map((line) => line.trim())
|
|
116
130
|
.filter(Boolean);
|
|
117
|
-
writeFileSync(destination, fallbackReleaseNotes(version, commits,
|
|
131
|
+
writeFileSync(destination, fallbackReleaseNotes(version, commits, options.successfulTag));
|
|
118
132
|
logger.warn("Genie release notes unavailable; wrote git-log summary", { path: relative });
|
|
119
133
|
return destination;
|
|
120
134
|
}
|
package/tasks/release.ts
CHANGED
|
@@ -81,6 +81,18 @@ export type ReleaseGithubActionsWatcher = (
|
|
|
81
81
|
options: WatchReleaseGithubActionsOptions,
|
|
82
82
|
) => Promise<void>;
|
|
83
83
|
|
|
84
|
+
/** Inputs required to find the newest successfully deployed release tag. */
|
|
85
|
+
export interface ResolveSuccessfulReleaseTagOptions {
|
|
86
|
+
readonly account: ProjectGhAccount;
|
|
87
|
+
readonly prefix: string;
|
|
88
|
+
readonly root: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Successful release lookup boundary replaceable by release tests. */
|
|
92
|
+
export type SuccessfulReleaseTagResolver = (
|
|
93
|
+
options: ResolveSuccessfulReleaseTagOptions,
|
|
94
|
+
) => string | undefined;
|
|
95
|
+
|
|
84
96
|
function githubRepository(account: ProjectGhAccount): string {
|
|
85
97
|
return account.host === "github.com"
|
|
86
98
|
? account.remote.repository
|
|
@@ -146,6 +158,61 @@ function reportGithubProgress(
|
|
|
146
158
|
}
|
|
147
159
|
}
|
|
148
160
|
|
|
161
|
+
/** Find the newest release tag whose GitHub Actions release workflow succeeded. */
|
|
162
|
+
export function resolveSuccessfulReleaseTag(
|
|
163
|
+
options: ResolveSuccessfulReleaseTagOptions,
|
|
164
|
+
): string | undefined {
|
|
165
|
+
const { account, prefix, root } = options;
|
|
166
|
+
const output = captureTaskCommand(
|
|
167
|
+
root,
|
|
168
|
+
"gh",
|
|
169
|
+
[
|
|
170
|
+
"run",
|
|
171
|
+
"list",
|
|
172
|
+
"--repo",
|
|
173
|
+
githubRepository(account),
|
|
174
|
+
"--workflow",
|
|
175
|
+
RELEASE_WORKFLOW_FILE,
|
|
176
|
+
"--event",
|
|
177
|
+
"push",
|
|
178
|
+
"--status",
|
|
179
|
+
"success",
|
|
180
|
+
"--limit",
|
|
181
|
+
"100",
|
|
182
|
+
"--json",
|
|
183
|
+
"databaseId,headBranch,headSha,status,url",
|
|
184
|
+
],
|
|
185
|
+
{ env: account.env, stderr: "inherit" },
|
|
186
|
+
);
|
|
187
|
+
const parsed = z.array(GithubReleaseRunSchema).safeParse(parse(output, []));
|
|
188
|
+
if (!parsed.success) {
|
|
189
|
+
logger.warn("GitHub returned invalid successful release workflow metadata", {
|
|
190
|
+
error: parsed.error,
|
|
191
|
+
repository: account.remote.repository,
|
|
192
|
+
});
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
for (const run of parsed.data) {
|
|
196
|
+
if (!run.headBranch.startsWith(prefix)) continue;
|
|
197
|
+
const tagHead = gitHead(root, `refs/tags/${run.headBranch}^{}`);
|
|
198
|
+
if (tagHead !== run.headSha) continue;
|
|
199
|
+
if (!taskCommandSucceeds(root, "git", ["merge-base", "--is-ancestor", tagHead, "HEAD"])) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
logger.info("using successful GitHub release as the release-note baseline", {
|
|
203
|
+
repository: account.remote.repository,
|
|
204
|
+
runId: run.databaseId,
|
|
205
|
+
tag: run.headBranch,
|
|
206
|
+
url: run.url,
|
|
207
|
+
});
|
|
208
|
+
return run.headBranch;
|
|
209
|
+
}
|
|
210
|
+
logger.info("no successful GitHub release baseline was found", {
|
|
211
|
+
repository: account.remote.repository,
|
|
212
|
+
});
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
149
216
|
/** Whether this release selection has remote work worth watching. */
|
|
150
217
|
export function shouldWatchReleaseGithubActions(
|
|
151
218
|
options: Pick<ReleaseSelectionOptions, "publish"> & { readonly watchGithubActions?: boolean },
|
|
@@ -247,7 +314,8 @@ export async function watchReleaseGithubActions(
|
|
|
247
314
|
root,
|
|
248
315
|
"gh",
|
|
249
316
|
["run", "view", String(run.databaseId), "--repo", repository, "--log-failed"],
|
|
250
|
-
|
|
317
|
+
// gh pipes TTY log output through its pager; release logs must stream inline.
|
|
318
|
+
{ env: { ...account.env, GH_PAGER: "cat" } },
|
|
251
319
|
);
|
|
252
320
|
}
|
|
253
321
|
} catch (error) {
|
|
@@ -482,11 +550,21 @@ export async function runRelease(
|
|
|
482
550
|
readonly watchGithubActions?: boolean;
|
|
483
551
|
readonly githubActionsWatcher?: ReleaseGithubActionsWatcher;
|
|
484
552
|
readonly resolveGithubAccount?: typeof resolveProjectGhAccount;
|
|
553
|
+
readonly resolveSuccessfulReleaseTag?: SuccessfulReleaseTagResolver;
|
|
485
554
|
},
|
|
486
555
|
): Promise<string> {
|
|
487
556
|
const { branch, prefix, remote, root } = options;
|
|
488
557
|
const selection = releaseStepSelection(options);
|
|
489
558
|
const install = options.install ?? "auto";
|
|
559
|
+
let githubAccount: ProjectGhAccount | undefined;
|
|
560
|
+
let githubAccountResolved = false;
|
|
561
|
+
const getGithubAccount = (): ProjectGhAccount | undefined => {
|
|
562
|
+
if (!githubAccountResolved) {
|
|
563
|
+
githubAccount = (options.resolveGithubAccount ?? resolveProjectGhAccount)(root, { remote });
|
|
564
|
+
githubAccountResolved = true;
|
|
565
|
+
}
|
|
566
|
+
return githubAccount;
|
|
567
|
+
};
|
|
490
568
|
|
|
491
569
|
if (options.releaseNotesText !== undefined && options.releaseNotesFile !== undefined) {
|
|
492
570
|
throw new Error("--release-notes-text and --release-notes-file are mutually exclusive");
|
|
@@ -539,9 +617,25 @@ export async function runRelease(
|
|
|
539
617
|
runTaskCommand(root, "bun", ["run", task]);
|
|
540
618
|
}
|
|
541
619
|
if ((options.releaseNotes ?? true) && (options.bump ?? true)) {
|
|
620
|
+
let successfulTag: string | undefined;
|
|
621
|
+
if (options.releaseNotesText === undefined && options.releaseNotesFile === undefined) {
|
|
622
|
+
const account = getGithubAccount();
|
|
623
|
+
if (account) {
|
|
624
|
+
successfulTag = (options.resolveSuccessfulReleaseTag ?? resolveSuccessfulReleaseTag)({
|
|
625
|
+
account,
|
|
626
|
+
prefix,
|
|
627
|
+
root,
|
|
628
|
+
});
|
|
629
|
+
} else {
|
|
630
|
+
logger.info("successful GitHub release baseline is unavailable: no authenticated account", {
|
|
631
|
+
remote,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
542
635
|
(options.writeReleaseNotes ?? writeReleaseNotes)({
|
|
543
636
|
prefix,
|
|
544
637
|
root,
|
|
638
|
+
successfulTag,
|
|
545
639
|
version,
|
|
546
640
|
content: options.releaseNotesText,
|
|
547
641
|
file: options.releaseNotesFile,
|
|
@@ -619,9 +713,7 @@ export async function runRelease(
|
|
|
619
713
|
}
|
|
620
714
|
|
|
621
715
|
if (shouldWatchReleaseGithubActions(options, selection)) {
|
|
622
|
-
const account = (
|
|
623
|
-
remote,
|
|
624
|
-
});
|
|
716
|
+
const account = getGithubAccount();
|
|
625
717
|
if (account) {
|
|
626
718
|
await (options.githubActionsWatcher ?? watchReleaseGithubActions)({
|
|
627
719
|
account,
|