@dbx-tools/projen 0.9.152 → 0.9.154

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/src/release.ts CHANGED
@@ -16,12 +16,17 @@ import { GithubWorkflow } from "projen/lib/github";
16
16
  import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
17
17
  import { BUN_VERSION } from "./bun-workflow.ts";
18
18
  import { projectReleaseBranch, taskCommand, type DBXToolsJavaScriptProject } from "./project-js.ts";
19
- import { RELEASE_VERSION, RELEASE_WORKFLOW_FILE } from "./release-context.ts";
19
+ import {
20
+ PUBLIC_NPM_REGISTRY_URL,
21
+ RELEASE_VERSION,
22
+ RELEASE_WORKFLOW_FILE,
23
+ } from "./release-context.ts";
20
24
  import type { ReleaseStepSelection } from "./release-options.ts";
21
25
 
22
26
  const NODE_VERSION = "24";
23
27
  const NPM_VERSION = "11.4.2";
24
- const NPM_REGISTRY_URL = "https://registry.npmjs.org";
28
+ const PUBLICATION_VERIFIER_ARTIFACT = "release-publication-verifier";
29
+ const PUBLICATION_VERIFIER_JOB = "verify-publications";
25
30
  const releaseTagPrefixes = new WeakMap<DBXToolsJavaScriptProject, string>();
26
31
  const releaseWorkflows = new WeakMap<DBXToolsJavaScriptProject, GithubWorkflow>();
27
32
 
@@ -95,7 +100,7 @@ export function nodeReleaseSetupSteps(): readonly JobStep[] {
95
100
  uses: github.ActionRefs.ACTIONS_SETUP_NODE,
96
101
  with: {
97
102
  "node-version": NODE_VERSION,
98
- "registry-url": NPM_REGISTRY_URL,
103
+ "registry-url": PUBLIC_NPM_REGISTRY_URL,
99
104
  "package-manager-cache": false,
100
105
  },
101
106
  },
@@ -114,19 +119,14 @@ export function uvSetupStep(): JobStep {
114
119
  function refreshDocsRegistryDependencies(workflow: GithubWorkflow): void {
115
120
  const docs = workflow.getJob("deploy-docs") as Job | undefined;
116
121
  if (!docs) return;
117
- const registryJobs = Object.keys(workflow.jobs).filter(
118
- (name) => name === "publish-node" || name.startsWith("publish-pypi-"),
119
- );
120
- if (registryJobs.length > 0) {
122
+ if (workflow.getJob(PUBLICATION_VERIFIER_JOB)) {
121
123
  workflow.updateJob("deploy-docs", {
122
124
  ...docs,
123
- needs: ["build-release", ...registryJobs],
125
+ needs: ["build-release", PUBLICATION_VERIFIER_JOB],
124
126
  if: releaseCondition([
125
127
  "needs['build-release'].result == 'success'",
126
128
  "needs['build-release'].outputs.docs == 'true'",
127
- ...registryJobs.map(
128
- (name) => `needs['${name}'].result == 'success' || needs['${name}'].result == 'skipped'`,
129
- ),
129
+ `needs['${PUBLICATION_VERIFIER_JOB}'].result == 'success' || needs['${PUBLICATION_VERIFIER_JOB}'].result == 'skipped'`,
130
130
  ]),
131
131
  });
132
132
  }
@@ -140,6 +140,107 @@ export function refreshReleaseDocsDependencies(project: DBXToolsJavaScriptProjec
140
140
  }
141
141
  }
142
142
 
143
+ function publicationRegistryJobs(workflow: GithubWorkflow): string[] {
144
+ return Object.keys(workflow.jobs).filter(
145
+ (name) => name === "publish-node" || name.startsWith("publish-pypi-"),
146
+ );
147
+ }
148
+
149
+ /** Keep one public availability barrier behind every configured registry publisher. */
150
+ export function refreshPublicationVerification(project: DBXToolsJavaScriptProject): void {
151
+ const workflow = releaseWorkflows.get(project);
152
+ if (!workflow) return;
153
+ const registryJobs = publicationRegistryJobs(workflow);
154
+ if (registryJobs.length === 0) return;
155
+ const hasNpm = registryJobs.includes("publish-node");
156
+ const hasPypi = registryJobs.some((name) => name.startsWith("publish-pypi-"));
157
+ const selected = [
158
+ ...(hasNpm ? ["needs['build-release'].outputs.npm == 'true'"] : []),
159
+ ...(hasPypi ? ["needs['build-release'].outputs.pypi == 'true'"] : []),
160
+ ].join(" || ");
161
+ const prerequisites = registryJobs.map((name) => {
162
+ const output = name === "publish-node" ? "npm" : "pypi";
163
+ return `needs['build-release'].outputs.${output} != 'true' || needs['${name}'].result == 'success'`;
164
+ });
165
+ const job: Job = {
166
+ if: releaseCondition([
167
+ "needs['build-release'].result == 'success'",
168
+ selected,
169
+ ...prerequisites,
170
+ ]),
171
+ needs: ["build-release", ...registryJobs],
172
+ runsOn: ["ubuntu-latest"],
173
+ permissions: { contents: JobPermission.READ },
174
+ timeoutMinutes: 15,
175
+ steps: [
176
+ {
177
+ name: "Download publication verifier",
178
+ uses: github.ActionRefs.ACTIONS_DOWNLOAD_ARTIFACT,
179
+ with: { name: PUBLICATION_VERIFIER_ARTIFACT, path: ".release/verifier" },
180
+ },
181
+ ...(hasNpm
182
+ ? [
183
+ {
184
+ name: "Download npm archives",
185
+ if: "${{ needs.build-release.outputs.npm == 'true' }}",
186
+ uses: github.ActionRefs.ACTIONS_DOWNLOAD_ARTIFACT,
187
+ with: { name: "release-npm", path: ".release/npm" },
188
+ },
189
+ ]
190
+ : []),
191
+ ...(hasPypi
192
+ ? [
193
+ {
194
+ name: "Download Python distributions",
195
+ if: "${{ needs.build-release.outputs.pypi == 'true' }}",
196
+ uses: github.ActionRefs.ACTIONS_DOWNLOAD_ARTIFACT,
197
+ with: {
198
+ pattern: "release-python-*",
199
+ path: ".release/python",
200
+ "merge-multiple": true,
201
+ },
202
+ },
203
+ ]
204
+ : []),
205
+ {
206
+ name: "Setup Bun",
207
+ uses: github.ActionRefs.OVEN_SH_SETUP_BUN,
208
+ with: { "bun-version": BUN_VERSION },
209
+ },
210
+ {
211
+ name: "Wait for public package availability",
212
+ shell: "bash",
213
+ env: {
214
+ RELEASE_VERSION,
215
+ VERIFY_NPM: hasNpm ? "${{ needs.build-release.outputs.npm }}" : "false",
216
+ VERIFY_PYPI: hasPypi ? "${{ needs.build-release.outputs.pypi }}" : "false",
217
+ },
218
+ // prettier-ignore
219
+ run: stringUtils.dedent(
220
+ // ============================================================================
221
+ /*bash*/`
222
+ args=(--version "$RELEASE_VERSION")
223
+ if [[ "$VERIFY_NPM" == "true" ]]; then
224
+ args+=(--npm-directory .release/npm)
225
+ fi
226
+ if [[ "$VERIFY_PYPI" == "true" ]]; then
227
+ args+=(--pypi-directory .release/python)
228
+ fi
229
+ bun .release/verifier/verify-publications.mjs "\${args[@]}"
230
+ `
231
+ // ============================================================================
232
+ ),
233
+ },
234
+ ],
235
+ };
236
+ if (workflow.getJob(PUBLICATION_VERIFIER_JOB)) {
237
+ workflow.updateJob(PUBLICATION_VERIFIER_JOB, job);
238
+ } else {
239
+ workflow.addJob(PUBLICATION_VERIFIER_JOB, job);
240
+ }
241
+ refreshDocsRegistryDependencies(workflow);
242
+ }
243
+
143
244
  function releaseBuildJob(
144
245
  project: DBXToolsJavaScriptProject,
145
246
  tagPrefix: string,
@@ -229,6 +330,24 @@ function releaseBuildJob(
229
330
  with: { name: "release-npm", path: ".release/npm", "if-no-files-found": "error" },
230
331
  },
231
332
  ])),
333
+ {
334
+ name: "Build publication verifier",
335
+ if: "${{ success() && (steps.release.outputs.npm == 'true' || steps.release.outputs.pypi == 'true') }}",
336
+ run: [
337
+ "mkdir -p .release/verifier",
338
+ "bun build node_modules/@dbx-tools/projen/tasks/verify-publications.ts --target=bun --outfile=.release/verifier/verify-publications.mjs",
339
+ ].join("\n"),
340
+ },
341
+ {
342
+ name: "Upload publication verifier",
343
+ if: "${{ success() && (steps.release.outputs.npm == 'true' || steps.release.outputs.pypi == 'true') }}",
344
+ uses: github.ActionRefs.ACTIONS_UPLOAD_ARTIFACT,
345
+ with: {
346
+ name: PUBLICATION_VERIFIER_ARTIFACT,
347
+ path: ".release/verifier/verify-publications.mjs",
348
+ "if-no-files-found": "error",
349
+ },
350
+ },
232
351
  ...(options.docs
233
352
  ? releaseBuildSteps("docs", [
234
353
  ...options.docs.prepareSteps,
@@ -356,6 +475,7 @@ export class DBXToolsRelease extends Component {
356
475
  if (options.nodeRelease !== false) {
357
476
  workflow.addJob("publish-node", nodePublishJob());
358
477
  }
478
+ refreshPublicationVerification(project);
359
479
  if (options.docs) {
360
480
  addDocsJobs(workflow);
361
481
  }
package/src/tsconfig.ts CHANGED
@@ -3,6 +3,12 @@
3
3
  */
4
4
  import { Component, type Project, javascript, typescript } from "projen";
5
5
 
6
+ /** Consumer overrides for the repository-root TypeScript program. */
7
+ export interface DBXToolsRootTsconfigOptions {
8
+ readonly compilerOptions?: javascript.TypeScriptCompilerOptions;
9
+ readonly include?: readonly string[];
10
+ }
11
+
6
12
  /**
7
13
  * Compiler options for the ROOT program only (`.projenrc.ts` + the engine as
8
14
  * seen from the root). Each package owns its own projen-generated
@@ -40,7 +46,7 @@ export class DBXToolsRootTsconfig extends Component {
40
46
  /** Root program config inherited by the generated Projen config. */
41
47
  readonly config?: javascript.TypescriptConfig;
42
48
 
43
- constructor(scope: Project) {
49
+ constructor(scope: Project, options: DBXToolsRootTsconfigOptions = {}) {
44
50
  super(scope);
45
51
 
46
52
  if (scope instanceof typescript.TypeScriptProject) {
@@ -57,8 +63,9 @@ export class DBXToolsRootTsconfig extends Component {
57
63
  lib: ["ESNext"],
58
64
  types: ["node", "bun"],
59
65
  jsx: javascript.TypeScriptJsxMode.REACT_JSX,
66
+ ...options.compilerOptions,
60
67
  },
61
- include: [".projenrc.ts", "test/**/*.ts", "test/**/*.tsx"],
68
+ include: [".projenrc.ts", "test/**/*.ts", "test/**/*.tsx", ...(options.include ?? [])],
62
69
  exclude: ["node_modules", "**/dist", "**/node_modules"],
63
70
  });
64
71
  }
@@ -43,6 +43,10 @@ export function materializeWorkspaceManifest(
43
43
  workspace.catalog && typeof workspace.catalog === "object"
44
44
  ? (workspace.catalog as Readonly<Record<string, unknown>>)
45
45
  : {};
46
+ const overrides =
47
+ workspace.overrides && typeof workspace.overrides === "object"
48
+ ? (workspace.overrides as Readonly<Record<string, unknown>>)
49
+ : {};
46
50
  if (typeof version !== "string" || !version) {
47
51
  throw new Error("Workspace manifest has no version");
48
52
  }
@@ -52,7 +56,7 @@ export function materializeWorkspaceManifest(
52
56
  manifest[field] = Object.fromEntries(
53
57
  Object.entries(dependencies).map(([name, value]) => [
54
58
  name,
55
- materializeDependency(name, value, version, catalog),
59
+ materializeDependency(name, value, version, catalog, overrides),
56
60
  ]),
57
61
  );
58
62
  }
@@ -134,7 +138,12 @@ function materializeDependency(
134
138
  value: unknown,
135
139
  version: string,
136
140
  catalog: Readonly<Record<string, unknown>>,
141
+ overrides: Readonly<Record<string, unknown>>,
137
142
  ): unknown {
143
+ const override = overrides[name];
144
+ if (typeof override === "string" && override && !override.startsWith("$")) {
145
+ return override;
146
+ }
138
147
  if (value === "catalog:") {
139
148
  const resolved = catalog[name];
140
149
  if (typeof resolved !== "string" || !resolved) {
@@ -1,16 +1,77 @@
1
1
  #!/usr/bin/env -S bun
2
2
  /** Deploy an AppKit workspace through npm-compatible temporary manifests. */
3
+ import { existsSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
3
5
  import { profile } from "@dbx-tools/auth";
6
+ import { serializeArgs } from "@dbx-tools/cli-args";
4
7
  import * as exec from "@dbx-tools/core/exec";
5
- import { runTaskMain } from "./cli.ts";
8
+ import { errorUtils, log } from "@dbx-tools/shared-core";
9
+ import { z } from "zod";
10
+ import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
6
11
  import { withStagedWorkspaceManifests } from "../src/workspace-manifest.ts";
7
12
 
13
+ const logger = log.logger("projen:appkit-deploy");
14
+
15
+ export const AppKitDeployContextOptionsSchema = z.object({
16
+ target: z
17
+ .string()
18
+ .trim()
19
+ .min(1)
20
+ .optional()
21
+ .describe("Databricks bundle target")
22
+ .meta({ env: [], short: "t" }),
23
+ profile: z.string().trim().min(1).optional().describe("Databricks CLI profile").meta({
24
+ env: [],
25
+ helpDefault: false,
26
+ short: "p",
27
+ }),
28
+ var: z
29
+ .array(z.string().trim().min(1))
30
+ .default([])
31
+ .describe("Databricks bundle variable assignment")
32
+ .meta({ env: [] }),
33
+ });
34
+
35
+ export const AppKitDeployOptionsSchema = AppKitDeployContextOptionsSchema.extend({
36
+ autoApprove: z
37
+ .boolean()
38
+ .default(false)
39
+ .describe("Skip interactive deployment approval")
40
+ .meta({ env: [] }),
41
+ failOnActiveRuns: z
42
+ .boolean()
43
+ .default(true)
44
+ .describe("Fail when bundle jobs or pipelines are active")
45
+ .meta({ env: [] }),
46
+ skipTests: z
47
+ .boolean()
48
+ .default(true)
49
+ .describe("Skip Databricks CLI tests after the Bun validation")
50
+ .meta({ env: [] }),
51
+ skipValidation: z
52
+ .boolean()
53
+ .default(true)
54
+ .describe("Skip Databricks CLI npm validation after the Bun validation")
55
+ .meta({ env: [] }),
56
+ });
57
+
58
+ export type AppKitDeployOptions = z.output<typeof AppKitDeployOptionsSchema>;
59
+
60
+ interface DeployCommandResult {
61
+ readonly exitCode: number;
62
+ readonly stderr?: string;
63
+ readonly stdout?: string;
64
+ }
65
+
8
66
  /** Effects accepted by the AppKit deploy task for focused tests. */
9
67
  export interface AppKitDeployDependencies {
10
68
  readonly environment?: NodeJS.ProcessEnv;
69
+ readonly logs?: (args: readonly string[], cwd: string) => Promise<DeployCommandResult>;
70
+ readonly plan?: (args: readonly string[], cwd: string) => Promise<DeployCommandResult>;
11
71
  readonly root?: string;
12
72
  readonly resolveProfile?: () => string | undefined;
13
- readonly run?: (args: readonly string[], cwd: string) => Promise<{ readonly exitCode: number }>;
73
+ readonly run?: (args: readonly string[], cwd: string) => Promise<DeployCommandResult>;
74
+ readonly validate?: (args: readonly string[], cwd: string) => Promise<DeployCommandResult>;
14
75
  }
15
76
 
16
77
  /** Run Databricks Apps deploy while generated workspace manifests are materialized. */
@@ -19,7 +80,42 @@ export async function deployApp(
19
80
  dependencies: AppKitDeployDependencies = {},
20
81
  ): Promise<void> {
21
82
  const root = dependencies.root ?? process.cwd();
22
- const deployArgs = resolveDeployArgs(args, dependencies);
83
+ const options = await resolveDeployOptions(args, dependencies);
84
+ const context = AppKitDeployContextOptionsSchema.parse({
85
+ target: options.target,
86
+ profile: options.profile,
87
+ var: options.var,
88
+ });
89
+ const contextArgs = serializeArgs(context);
90
+ const deployArgs = serializeArgs({
91
+ ...context,
92
+ ...(options.autoApprove ? { autoApprove: true } : {}),
93
+ ...(options.failOnActiveRuns ? { failOnActiveRuns: true } : {}),
94
+ ...(options.skipTests ? { skipTests: true } : {}),
95
+ ...(options.skipValidation ? { skipValidation: true } : {}),
96
+ });
97
+ const validate =
98
+ dependencies.validate ??
99
+ (dependencies.run
100
+ ? undefined
101
+ : (validateArgs: readonly string[], cwd: string) =>
102
+ exec.spawn(
103
+ "databricks",
104
+ ["bundle", "validate", ...serializeArgs({ strict: true }), ...validateArgs],
105
+ {
106
+ cwd,
107
+ env: dependencies.environment ?? process.env,
108
+ stdin: "ignore",
109
+ stdout: "inherit",
110
+ stderr: "inherit",
111
+ },
112
+ ));
113
+ if (validate) {
114
+ const validation = await validate(contextArgs, root);
115
+ if (validation.exitCode !== 0) {
116
+ throw new Error(`databricks bundle validate exited with code ${validation.exitCode}`);
117
+ }
118
+ }
23
119
  const run =
24
120
  dependencies.run ??
25
121
  ((deployArgs: readonly string[], cwd: string) =>
@@ -31,40 +127,119 @@ export async function deployApp(
31
127
  stderr: "inherit",
32
128
  }));
33
129
  await withStagedWorkspaceManifests(root, async (stage) => {
130
+ await reconcileStagedBundleState(stage, contextArgs, options.target, dependencies);
34
131
  const result = await run(deployArgs, stage);
35
132
  if (result.exitCode !== 0) {
133
+ await printAppLogs(contextArgs, stage, dependencies);
36
134
  throw new Error(`databricks apps deploy exited with code ${result.exitCode}`);
37
135
  }
38
136
  });
39
137
  }
40
138
 
41
- function resolveDeployArgs(
139
+ async function reconcileStagedBundleState(
140
+ stage: string,
141
+ contextArgs: readonly string[],
142
+ target: string | undefined,
143
+ dependencies: AppKitDeployDependencies,
144
+ ): Promise<void> {
145
+ const plan =
146
+ dependencies.plan ??
147
+ (dependencies.run
148
+ ? undefined
149
+ : (args: readonly string[], cwd: string) =>
150
+ Promise.resolve(
151
+ exec.spawnSync(
152
+ "databricks",
153
+ ["bundle", "plan", ...serializeArgs({ quiet: true }), ...args],
154
+ {
155
+ cwd,
156
+ env: dependencies.environment ?? process.env,
157
+ stdin: "ignore",
158
+ stdout: "capture",
159
+ stderr: "capture",
160
+ },
161
+ ),
162
+ ));
163
+ if (!plan) return;
164
+ const result = await plan(contextArgs, stage);
165
+ if (result.exitCode === 0) return;
166
+ const output = `${result.stderr ?? ""}\n${result.stdout ?? ""}`.toLowerCase();
167
+ if (
168
+ !output.includes("lineage mismatch in state files") ||
169
+ !output.includes("remote direct state")
170
+ ) {
171
+ return;
172
+ }
173
+ if (!target) return;
174
+ const state = join(stage, ".databricks", "bundle", target, "resources.json");
175
+ if (!existsSync(state)) return;
176
+ rmSync(state);
177
+ logger.warn("Removed stale staged bundle state", { target });
178
+ }
179
+
180
+ async function printAppLogs(
181
+ contextArgs: readonly string[],
182
+ cwd: string,
183
+ dependencies: AppKitDeployDependencies,
184
+ ): Promise<void> {
185
+ logger.error("Databricks Apps deployment failed; recent app logs follow");
186
+ const showLogs =
187
+ dependencies.logs ??
188
+ ((args: readonly string[], logsCwd: string) =>
189
+ exec.spawn("databricks", ["apps", "logs", ...serializeArgs({ tailLines: 200 }), ...args], {
190
+ cwd: logsCwd,
191
+ env: dependencies.environment ?? process.env,
192
+ stdin: "ignore",
193
+ stdout: "inherit",
194
+ stderr: "inherit",
195
+ }));
196
+ try {
197
+ const result = await showLogs(contextArgs, cwd);
198
+ if (result.exitCode !== 0) {
199
+ logger.warn("Could not read Databricks App logs", { exitCode: result.exitCode });
200
+ }
201
+ } catch (error) {
202
+ logger.warn("Could not read Databricks App logs", {
203
+ message: errorUtils.toError(error).message,
204
+ });
205
+ }
206
+ }
207
+
208
+ async function resolveDeployOptions(
42
209
  args: readonly string[],
43
210
  dependencies: AppKitDeployDependencies,
44
- ): readonly string[] {
45
- const environment = dependencies.environment ?? process.env;
46
- const hasProfile = args.some(
47
- (argument) =>
48
- argument === "--profile" || argument === "-p" || argument.startsWith("--profile="),
211
+ ): Promise<AppKitDeployOptions> {
212
+ const parsed = await taskOptions(
213
+ taskCommand(
214
+ import.meta.url,
215
+ "Validate and deploy an AppKit workspace through Databricks Apps",
216
+ AppKitDeployOptionsSchema,
217
+ ),
218
+ AppKitDeployOptionsSchema,
219
+ args,
49
220
  );
221
+ const environment = dependencies.environment ?? process.env;
50
222
  const hasEnvironmentAuth =
51
223
  Boolean(environment.DATABRICKS_HOST) &&
52
224
  (Boolean(environment.DATABRICKS_TOKEN) ||
53
225
  (Boolean(environment.DATABRICKS_CLIENT_ID) && Boolean(environment.DATABRICKS_CLIENT_SECRET)));
226
+ const configuredProfile = environment.DATABRICKS_CONFIG_PROFILE ?? environment.DATABRICKS_PROFILE;
54
227
  const resolvedProfile =
55
- hasProfile || hasEnvironmentAuth
228
+ parsed.profile ??
229
+ configuredProfile ??
230
+ (hasEnvironmentAuth
56
231
  ? undefined
57
- : (dependencies.resolveProfile ?? (() => profile.resolveProfile()?.name))();
58
- if (!hasProfile && !hasEnvironmentAuth && !resolvedProfile) {
232
+ : (dependencies.resolveProfile ?? (() => profile.resolveProfile()?.name))());
233
+ if (!hasEnvironmentAuth && !resolvedProfile) {
59
234
  throw new Error(
60
235
  "Select a Databricks profile with --profile or configure Databricks environment credentials",
61
236
  );
62
237
  }
63
- return [
64
- ...(args.includes("--skip-validation") ? [] : ["--skip-validation"]),
65
- ...args,
66
- ...(resolvedProfile ? ["--profile", resolvedProfile] : []),
67
- ];
238
+ return AppKitDeployOptionsSchema.parse({
239
+ ...parsed,
240
+ target: parsed.target ?? environment.DATABRICKS_BUNDLE_TARGET,
241
+ profile: resolvedProfile,
242
+ });
68
243
  }
69
244
 
70
245
  await runTaskMain(import.meta, () => deployApp(process.argv.slice(2)));
@@ -20,11 +20,11 @@ import { z } from "zod";
20
20
  import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
21
21
  import { TaskConcurrencyOptionSchema, TaskDryRunOptionSchema } from "./options.ts";
22
22
  import { runTaskCommandAsync } from "../src/_task-command.ts";
23
+ import { PUBLIC_NPM_REGISTRY_URL } from "../src/release-context.ts";
23
24
  import { materializeWorkspaceManifest } from "../src/workspace-manifest.ts";
24
25
 
25
26
  export { materializeWorkspaceManifest } from "../src/workspace-manifest.ts";
26
27
 
27
- const DEFAULT_REGISTRY = "https://registry.npmjs.org";
28
28
  const DEFAULT_PUBLISH_CONCURRENCY = 4;
29
29
  const logger = log.logger("projen:publish-npm");
30
30
 
@@ -132,10 +132,12 @@ export function npmReleaseMatches(
132
132
  export async function publishedNpmRelease(
133
133
  name: string,
134
134
  version: string,
135
- registry = process.env.NPM_CONFIG_REGISTRY ?? DEFAULT_REGISTRY,
135
+ registry = process.env.NPM_CONFIG_REGISTRY ?? PUBLIC_NPM_REGISTRY_URL,
136
+ signal?: AbortSignal,
136
137
  ): Promise<NpmReleaseIdentity | undefined> {
137
138
  const response = await fetch(registryUrl(registry, name, version), {
138
139
  headers: { accept: "application/json" },
140
+ signal,
139
141
  });
140
142
  if (response.status === 404) return undefined;
141
143
  if (!response.ok) {
@@ -152,7 +154,7 @@ export async function publishedNpmRelease(
152
154
  }
153
155
  let contentDigest: string | undefined;
154
156
  if (metadata.dist?.tarball) {
155
- const archive = await fetch(metadata.dist.tarball);
157
+ const archive = await fetch(metadata.dist.tarball, { signal });
156
158
  if (!archive.ok) {
157
159
  throw new Error(`npm tarball lookup failed for ${name}@${version}: ${archive.status}`);
158
160
  }
@@ -0,0 +1,54 @@
1
+ /** Parse and print generated PyPI trusted-publisher setup instructions. */
2
+ import { z } from "zod";
3
+ import { taskCommand, taskOptions } from "./cli.ts";
4
+
5
+ export const PypiTrustedPublisherInstructionsOptionsSchema = z
6
+ .object({
7
+ secretFile: z
8
+ .string()
9
+ .trim()
10
+ .min(1)
11
+ .optional()
12
+ .describe("Credential file used only when browser authentication is required")
13
+ .meta({ env: [], helpDefault: false, short: false }),
14
+ })
15
+ .describe("Options for rendering PyPI trusted-publisher setup instructions");
16
+
17
+ /** Project-specific instruction sections wrapped around browser authentication guidance. */
18
+ export interface PypiTrustedPublisherInstructions {
19
+ readonly afterAuthentication: readonly string[];
20
+ readonly beforeAuthentication: readonly string[];
21
+ }
22
+
23
+ /** Parse task options and write the complete trusted-publisher instructions. */
24
+ export async function printPypiTrustedPublisherInstructions(
25
+ instructions: PypiTrustedPublisherInstructions,
26
+ argv: readonly string[] = process.argv.slice(2),
27
+ ): Promise<void> {
28
+ const { secretFile } = await taskOptions(
29
+ taskCommand(
30
+ import.meta.url,
31
+ "Print system-browser instructions for PyPI trusted publishers",
32
+ PypiTrustedPublisherInstructionsOptionsSchema,
33
+ ),
34
+ PypiTrustedPublisherInstructionsOptionsSchema,
35
+ argv,
36
+ );
37
+ const authentication = secretFile
38
+ ? [
39
+ `- If browser authentication is required, read credentials from ${secretFile} directly into the browser without printing, logging, or exposing secret values.`,
40
+ `- If ${secretFile} is absent, invalid, or insufficient for authentication, pause and ask the user to complete authentication.`,
41
+ ]
42
+ : [
43
+ "- If browser authentication is required, pause and ask the user to complete authentication.",
44
+ ];
45
+ process.stdout.write(
46
+ `${[
47
+ ...instructions.beforeAuthentication,
48
+ ...authentication,
49
+ ...instructions.afterAuthentication,
50
+ ]
51
+ .join("\n")
52
+ .trimEnd()}\n`,
53
+ );
54
+ }
@@ -13,7 +13,7 @@ import ts from "typescript";
13
13
  import { z } from "zod";
14
14
  import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
15
15
  import { TaskCheckOptionSchema, TaskForceOptionSchema, TaskRootOptionSchema } from "./options.ts";
16
- import { header, makeReadonly, makeWritable } from "../src/generated.ts";
16
+ import { header, headerLines, makeReadonly, makeWritable } from "../src/generated.ts";
17
17
  import { publicFunctionExports } from "../src/module-exports.ts";
18
18
  import { resolveRepoRoot } from "../src/packages.ts";
19
19
  import {
@@ -434,9 +434,11 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
434
434
  section.includes("Callable["),
435
435
  );
436
436
  return [
437
- "# GENERATED by projen/tasks/python-node-bindings.ts - DO NOT EDIT.",
438
- `# Regenerated from ${source}.`,
439
- "# Hand edits are overwritten; this file is read-only.",
437
+ ...headerLines({
438
+ comment: "#",
439
+ tool: "projen/tasks/python-node-bindings.ts",
440
+ source,
441
+ }),
440
442
  "",
441
443
  "from __future__ import annotations",
442
444
  "",
@@ -1162,9 +1164,11 @@ function removeEmptyDirectories(directory: string): void {
1162
1164
 
1163
1165
  function pythonRuntimeLoader(source: string): string {
1164
1166
  return [
1165
- "# GENERATED by projen/tasks/python-node-bindings.ts - DO NOT EDIT.",
1166
- `# Regenerated from ${source}.`,
1167
- "# Hand edits are overwritten; this file is read-only.",
1167
+ ...headerLines({
1168
+ comment: "#",
1169
+ tool: "projen/tasks/python-node-bindings.ts",
1170
+ source,
1171
+ }),
1168
1172
  "",
1169
1173
  "from __future__ import annotations",
1170
1174
  "",