@dbx-tools/projen 0.9.152 → 0.9.153
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 +23 -7
- package/index.ts +2 -1
- package/package.json +7 -6
- package/src/appkit-workspace.ts +41 -6
- package/src/bun-app.ts +7 -1
- package/src/generated.ts +16 -7
- package/src/packages.ts +6 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/project-js.ts +20 -4
- package/src/project-py.ts +50 -19
- package/src/tsconfig.ts +9 -2
- package/src/workspace-manifest.ts +10 -1
- package/tasks/appkit-deploy.ts +192 -17
- package/tasks/pypi-trusted-publisher-instructions.ts +54 -0
- package/tasks/python-node-bindings.ts +11 -7
package/README.md
CHANGED
|
@@ -24,10 +24,13 @@ packages. It can also generate Zod schemas from declarations and coordinate
|
|
|
24
24
|
generated-file cleanup and watch tasks.
|
|
25
25
|
|
|
26
26
|
Every `src`-bearing folder under the configured roots becomes a
|
|
27
|
-
`DBXToolsTypeScriptProject`.
|
|
27
|
+
`DBXToolsTypeScriptProject`. `packages/js` is selected automatically when it
|
|
28
|
+
exists; otherwise discovery uses `packages`. Folder path drives package name and
|
|
29
|
+
runtime tags.
|
|
28
30
|
Use `packageDefaults` for native options shared by every discovered package,
|
|
29
31
|
such as `{ publishable: false }`; reserve `resolvePackageOptions` for genuine
|
|
30
|
-
per-package differences.
|
|
32
|
+
per-package differences. Use `rootTsconfig` only for repository-wide compiler
|
|
33
|
+
differences; root tests, Bun/Node types, and React JSX are already included.
|
|
31
34
|
|
|
32
35
|
Use `DBXToolsTypeScriptProject` itself as the root when one package needs the
|
|
33
36
|
same Bun workspace, repository, task, and release defaults without a separate
|
|
@@ -107,7 +110,11 @@ new projectPy.DBXToolsPythonWorkspace(root, {
|
|
|
107
110
|
```
|
|
108
111
|
|
|
109
112
|
Distribution names and modules come from the parent scope plus each package
|
|
110
|
-
directory.
|
|
113
|
+
directory. Direct children of the Python root that contain `src/` are discovered
|
|
114
|
+
automatically; `packages` only needs entries whose metadata differs from the
|
|
115
|
+
generated defaults. Set `discoverPackages: false` for a fully explicit list.
|
|
116
|
+
The repository comes from the parent project metadata, existing generated root
|
|
117
|
+
manifest, or Git remote.
|
|
111
118
|
`internalDependencies` renders standalone Git `#subdirectory=` requirements
|
|
112
119
|
without repeating repository coordinates. Pass `repository`, `name`, or
|
|
113
120
|
`module` only to override those conventions. `root.vscode` is Projen's existing
|
|
@@ -315,13 +322,18 @@ parameter to `Project`). Drop to `mixin.create(predicate, fn)` +
|
|
|
315
322
|
Built-in tag mixins set runtime defaults for `shared`, `node`, `cli`, `server`,
|
|
316
323
|
`ui`, and `app`. Repo-specific mixins layer package-specific dependencies,
|
|
317
324
|
scripts, and generated files on top. `ui` is for reusable React component
|
|
318
|
-
libraries. `app` owns a Bun-built browser application.
|
|
325
|
+
libraries. `app` owns a Bun-built browser application. Terminal
|
|
326
|
+
`packages/**/app` and `packages/**/server` directories select those specific
|
|
327
|
+
roles instead of inheriting a generic `ui` or `node` ancestor.
|
|
319
328
|
|
|
320
329
|
AppKit mode defaults to `auto`: when the workspace has both `app` and `server`
|
|
321
330
|
packages, the engine adds the AppKit, Vite, React plugin, and Tailwind
|
|
322
331
|
dependencies to the package that uses each one. It also disables AppKit's
|
|
323
332
|
package migration, includes the Vite and generated-type inputs in the browser
|
|
324
|
-
tsconfig,
|
|
333
|
+
tsconfig, generates a read-only monorepo-safe `vite.config.ts`, adds
|
|
334
|
+
`appkit:sync`, and ignores local Databricks state. Put repository-specific Vite
|
|
335
|
+
settings in the optional unmanaged `vite.config.override.ts`; the generated
|
|
336
|
+
config merges it over the engine defaults. Set
|
|
325
337
|
`appkit: false` to disable this ownership. Set `appkit: true` to require AppKit
|
|
326
338
|
and fail synthesis when either package role or its required entry files are
|
|
327
339
|
missing. Keep application-specific UI and server dependencies in the consuming
|
|
@@ -338,8 +350,12 @@ with concrete versions and keeps every manifest stable through upload. The
|
|
|
338
350
|
generated working tree is never modified. The task passes an explicit profile,
|
|
339
351
|
using the supplied flag or resolving one through `@dbx-tools/auth`, and fails
|
|
340
352
|
before staging when neither profile nor complete environment credentials are
|
|
341
|
-
available.
|
|
342
|
-
|
|
353
|
+
available. It validates the bundle strictly, removes only stale direct-engine
|
|
354
|
+
state from the temporary snapshot, and defaults to active-run protection while
|
|
355
|
+
skipping duplicate npm validation and tests. If deployment or app startup
|
|
356
|
+
fails, the task prints the current Databricks App logs before returning the
|
|
357
|
+
original failure. A repository-owned deployment flow can run its own Bun checks
|
|
358
|
+
and spawn `appkit:deploy` for the upload step instead of maintaining staging or
|
|
343
359
|
materialization code.
|
|
344
360
|
|
|
345
361
|
Every workspace root exposes a generic development watcher:
|
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.153";
|
|
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";
|
|
@@ -77,6 +77,7 @@ export type { SharedCoreUsageKind, SharedCoreExportUsage } from "./src/shared-co
|
|
|
77
77
|
export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
|
|
78
78
|
export type { PackageTag } from "./src/tags.ts";
|
|
79
79
|
export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
|
|
80
|
+
export type { DBXToolsRootTsconfigOptions } from "./src/tsconfig.ts";
|
|
80
81
|
export type { IgnoreGroupOptions, WatchLoopOptions } from "./src/watch.ts";
|
|
81
82
|
export type { WorkspaceMutationLockOptions } from "./src/workspace-lock.ts";
|
|
82
83
|
export { VERSION_FILE, DEFAULT_VERSION } from "./src/workspace-version.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.153",
|
|
34
|
+
"@dbx-tools/cli-args": "^0.9.153",
|
|
35
|
+
"@dbx-tools/core": "^0.9.153",
|
|
36
|
+
"@dbx-tools/path": "^0.9.153",
|
|
37
|
+
"@dbx-tools/shared-core": "^0.9.153",
|
|
38
38
|
"commander": "^15.0.0",
|
|
39
39
|
"concurrently": "^10.0.3",
|
|
40
40
|
"constructs": "^10.6.0",
|
|
@@ -56,11 +56,12 @@
|
|
|
56
56
|
},
|
|
57
57
|
"main": "index.ts",
|
|
58
58
|
"license": "Apache-2.0",
|
|
59
|
-
"version": "0.9.
|
|
59
|
+
"version": "0.9.153",
|
|
60
60
|
"types": "index.ts",
|
|
61
61
|
"type": "module",
|
|
62
62
|
"exports": {
|
|
63
63
|
".": "./index.ts",
|
|
64
|
+
"./pypi-trusted-publisher-instructions": "./tasks/pypi-trusted-publisher-instructions.ts",
|
|
64
65
|
"./python-release-packaging": "./tasks/publish-python.ts",
|
|
65
66
|
"./release-packaging": "./tasks/publish-npm.ts",
|
|
66
67
|
"./package.json": "./package.json"
|
package/src/appkit-workspace.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
|
-
import { join, relative, sep } from "node:path";
|
|
8
|
-
import { Component, type Task, javascript, typescript } from "projen";
|
|
7
|
+
import { basename, join, relative, sep } from "node:path";
|
|
8
|
+
import { Component, type Task, TextFile, javascript, typescript } from "projen";
|
|
9
|
+
import { headerLines } from "./generated.ts";
|
|
9
10
|
|
|
10
11
|
/** AppKit workspace discovery and validation mode. */
|
|
11
12
|
export type AppKitWorkspaceMode = "auto" | boolean;
|
|
@@ -94,9 +95,7 @@ function validateWorkspace(options: AppKitWorkspaceOptions): void {
|
|
|
94
95
|
if (options.apps.length === 0) missing.push("an `app` package");
|
|
95
96
|
if (options.servers.length === 0) missing.push("a `server` package");
|
|
96
97
|
for (const app of options.apps) {
|
|
97
|
-
|
|
98
|
-
if (!existsSync(join(app.outdir, file))) missing.push(`${app.name}/${file}`);
|
|
99
|
-
}
|
|
98
|
+
if (!existsSync(join(app.outdir, "index.html"))) missing.push(`${app.name}/index.html`);
|
|
100
99
|
}
|
|
101
100
|
for (const server of options.servers) {
|
|
102
101
|
const entrypoint = serverEntrypoint(server);
|
|
@@ -117,11 +116,47 @@ function configureApp(project: javascript.NodeProject): void {
|
|
|
117
116
|
"@tailwindcss/vite@catalog:",
|
|
118
117
|
"tailwindcss@catalog:",
|
|
119
118
|
);
|
|
120
|
-
project.addDevDeps("vite@catalog:");
|
|
119
|
+
project.addDevDeps("@databricks/appkit@catalog:", "vite@catalog:");
|
|
120
|
+
const typesDirectory = `${basename(project.outdir)}/shared/appkit-types`;
|
|
121
|
+
new TextFile(project, "vite.config.ts", {
|
|
122
|
+
marker: true,
|
|
123
|
+
readonly: true,
|
|
124
|
+
lines: [
|
|
125
|
+
...headerLines({ tool: "projen", source: ".projenrc.ts" }),
|
|
126
|
+
'import { existsSync } from "node:fs";',
|
|
127
|
+
'import { appKitServingTypesPlugin, appKitTypesPlugin } from "@databricks/appkit";',
|
|
128
|
+
'import tailwindcss from "@tailwindcss/vite";',
|
|
129
|
+
'import { defineConfig, mergeConfig, type UserConfig } from "vite";',
|
|
130
|
+
"",
|
|
131
|
+
`const appKitTypesDirectory = ${JSON.stringify(typesDirectory)};`,
|
|
132
|
+
'const overrideUrl = new URL("./vite.config.override.ts", import.meta.url);',
|
|
133
|
+
"const override = existsSync(overrideUrl)",
|
|
134
|
+
" ? ((await import(overrideUrl.href)).default as UserConfig)",
|
|
135
|
+
" : {};",
|
|
136
|
+
"",
|
|
137
|
+
"export default defineConfig(",
|
|
138
|
+
" mergeConfig(",
|
|
139
|
+
" {",
|
|
140
|
+
" plugins: [",
|
|
141
|
+
" tailwindcss(),",
|
|
142
|
+
" appKitTypesPlugin({",
|
|
143
|
+
" outFile: `${appKitTypesDirectory}/analytics.d.ts`,",
|
|
144
|
+
" }),",
|
|
145
|
+
" appKitServingTypesPlugin({",
|
|
146
|
+
" outFile: `${appKitTypesDirectory}/serving.d.ts`,",
|
|
147
|
+
" }),",
|
|
148
|
+
" ],",
|
|
149
|
+
" },",
|
|
150
|
+
" override,",
|
|
151
|
+
" ),",
|
|
152
|
+
");",
|
|
153
|
+
],
|
|
154
|
+
});
|
|
121
155
|
|
|
122
156
|
if (!(project instanceof typescript.TypeScriptProject)) return;
|
|
123
157
|
project.tsconfig?.file.addOverride("compilerOptions.rootDir", ".");
|
|
124
158
|
project.tsconfig?.addInclude("vite.config.ts");
|
|
159
|
+
project.tsconfig?.addInclude("vite.config.override.ts");
|
|
125
160
|
project.tsconfig?.addInclude("shared/appkit-types");
|
|
126
161
|
}
|
|
127
162
|
|
package/src/bun-app.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { stringUtils } from "@dbx-tools/shared-core";
|
|
26
26
|
import { type Project, TextFile } from "projen";
|
|
27
|
+
import { header, headerLines } from "./generated.ts";
|
|
27
28
|
|
|
28
29
|
/** Unmanaged dev-server override module, merged over the generated options if present. */
|
|
29
30
|
export const BUN_DEV_OVERRIDE = "bun-dev.override.ts";
|
|
@@ -31,6 +32,8 @@ export const BUN_DEV_OVERRIDE = "bun-dev.override.ts";
|
|
|
31
32
|
export const BUN_BUILD_OVERRIDE = "bun-build.override.ts";
|
|
32
33
|
/** Unmanaged generated-script overrides excluded from formatting and linting. */
|
|
33
34
|
export const BUN_APP_OVERRIDES = [BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE];
|
|
35
|
+
const PROJEN_HEADER = { tool: "projen", source: ".projenrc.ts" } as const;
|
|
36
|
+
const PROJEN_TS_HEADER = header(PROJEN_HEADER).trimEnd().replaceAll("\n", "\n ");
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
39
|
* Root `bunfig.toml`: pin bun's HOISTED linker workspace-wide.
|
|
@@ -49,7 +52,7 @@ export class RootBunfigFile extends TextFile {
|
|
|
49
52
|
marker: false,
|
|
50
53
|
readonly: true,
|
|
51
54
|
lines: [
|
|
52
|
-
|
|
55
|
+
...headerLines({ ...PROJEN_HEADER, comment: "#" }),
|
|
53
56
|
"# Hoisted (flat) node_modules so a peer dep like @mastra/core resolves to ONE",
|
|
54
57
|
"# copy - the isolated default splits it into peer-hash variants and breaks",
|
|
55
58
|
"# identity-based singletons + cross-package types.",
|
|
@@ -76,6 +79,7 @@ export class BunfigFile extends TextFile {
|
|
|
76
79
|
marker: true,
|
|
77
80
|
readonly: true,
|
|
78
81
|
lines: [
|
|
82
|
+
...headerLines({ ...PROJEN_HEADER, comment: "#" }),
|
|
79
83
|
"# Tailwind v4 for the dev server (Bun.serve reads [serve.static] plugins).",
|
|
80
84
|
"# Production build applies the plugin via Bun.build's API in build.ts - the",
|
|
81
85
|
"# `bun build` CLI does not load bunfig plugins, so there is no [build] here.",
|
|
@@ -94,6 +98,7 @@ export class BunDevServerFile extends TextFile {
|
|
|
94
98
|
const source = (
|
|
95
99
|
// ============================================================================
|
|
96
100
|
/*ts*/`
|
|
101
|
+
${PROJEN_TS_HEADER}
|
|
97
102
|
import { existsSync } from "node:fs";
|
|
98
103
|
import index from "./index.html";
|
|
99
104
|
|
|
@@ -135,6 +140,7 @@ export class BunBuildFile extends TextFile {
|
|
|
135
140
|
String.raw
|
|
136
141
|
// ============================================================================
|
|
137
142
|
/*ts*/`
|
|
143
|
+
${PROJEN_TS_HEADER}
|
|
138
144
|
import { existsSync } from "node:fs";
|
|
139
145
|
import { cp, rm } from "node:fs/promises";
|
|
140
146
|
import { basename } from "node:path";
|
package/src/generated.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
const MARKER = "
|
|
13
|
+
/** Tool-owned marker text shared by every supported comment style. */
|
|
14
|
+
const MARKER = "GENERATED by";
|
|
15
15
|
|
|
16
16
|
/** Python subpackage, under a package's module directory, that holds all toolchain output. */
|
|
17
17
|
export const PYTHON_GENERATED_PACKAGE = "_generated";
|
|
@@ -34,6 +34,8 @@ export function makeReadonly(file: string): void {
|
|
|
34
34
|
|
|
35
35
|
/** Inputs for the durable generated-file banner. */
|
|
36
36
|
export interface HeaderOpts {
|
|
37
|
+
/** Line-comment prefix. Defaults to `//`. */
|
|
38
|
+
readonly comment?: "//" | "#";
|
|
37
39
|
/** What produced the file, e.g. `"projen watch"`. */
|
|
38
40
|
readonly tool: string;
|
|
39
41
|
/** What the file is derived from, e.g. `"the exporting modules beside it"`. */
|
|
@@ -42,12 +44,18 @@ export interface HeaderOpts {
|
|
|
42
44
|
|
|
43
45
|
/** Build the do-not-edit banner (line comments, for TS/JS). */
|
|
44
46
|
export function header(opts: HeaderOpts): string {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
lines.push(
|
|
47
|
+
const comment = opts.comment ?? "//";
|
|
48
|
+
const lines = [`${comment} ${MARKER} ${opts.tool} - DO NOT EDIT.`];
|
|
49
|
+
if (opts.source) lines.push(`${comment} Regenerated from ${opts.source}.`);
|
|
50
|
+
lines.push(`${comment} Hand edits are overwritten on the next watch; this file is read-only.`);
|
|
48
51
|
return `${lines.join("\n")}\n`;
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
/** Build a generated banner as lines for Projen file components. */
|
|
55
|
+
export function headerLines(opts: HeaderOpts): string[] {
|
|
56
|
+
return header(opts).trimEnd().split("\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
/**
|
|
52
60
|
* True if the file exists and carries the do-not-edit banner {@link header}
|
|
53
61
|
* writes - i.e. a toolchain wrote it and a hand edit would be overwritten.
|
|
@@ -61,7 +69,8 @@ export function isGenerated(file: string): boolean {
|
|
|
61
69
|
try {
|
|
62
70
|
const source = readFileSync(file, "utf8");
|
|
63
71
|
return (
|
|
64
|
-
source.startsWith(MARKER) ||
|
|
72
|
+
source.startsWith(`// ${MARKER}`) ||
|
|
73
|
+
source.startsWith(`# ${MARKER}`) ||
|
|
65
74
|
source.startsWith("// ~~ Generated by projen") ||
|
|
66
75
|
source.startsWith("# ~~ Generated by projen") ||
|
|
67
76
|
/"\/\/"\s*:\s*"~~ Generated by projen/.test(source)
|
|
@@ -78,7 +87,7 @@ export function isGenerated(file: string): boolean {
|
|
|
78
87
|
export function stampGenerated(file: string, opts: HeaderOpts): void {
|
|
79
88
|
makeWritable(file);
|
|
80
89
|
const body = readFileSync(file, "utf8");
|
|
81
|
-
const next =
|
|
90
|
+
const next = isGenerated(file) ? body : `${header(opts)}\n${body}`;
|
|
82
91
|
writeFileSync(file, next);
|
|
83
92
|
makeReadonly(file);
|
|
84
93
|
}
|
package/src/packages.ts
CHANGED
|
@@ -146,11 +146,16 @@ export class DiscoveredPackage {
|
|
|
146
146
|
|
|
147
147
|
/**
|
|
148
148
|
* Tag candidates from nesting under the discovery root: cumulative kebab-case join
|
|
149
|
-
* of every ancestor folder, excluding the leaf package folder when depth >= 2
|
|
149
|
+
* of every ancestor folder, excluding the leaf package folder when depth >= 2.
|
|
150
|
+
* Terminal `app` and `server` directories are runtime roles and replace their
|
|
151
|
+
* generic ancestor (`ui/app` -> `app`, `node/server` -> `server`).
|
|
152
|
+
* Other paths keep cumulative tags
|
|
150
153
|
* (`shared/path/coolDude/another` -> `[shared, shared-path, shared-path-cool-dude]`).
|
|
151
154
|
* Matched against `packageTagPaths` to resolve applied mixin tags.
|
|
152
155
|
*/
|
|
153
156
|
get tagCandidates(): string[] {
|
|
157
|
+
const leaf = stringUtils.toSlug(this.name);
|
|
158
|
+
if (leaf === "app" || leaf === "server") return [leaf];
|
|
154
159
|
return nestingTagsFromSegments(this.relSegments);
|
|
155
160
|
}
|
|
156
161
|
}
|
package/src/pnpm-workspace.ts
CHANGED
|
@@ -64,7 +64,7 @@ const DEFAULT_CATALOG: Catalog = {
|
|
|
64
64
|
"@types/react-dom": "^19.2.2",
|
|
65
65
|
tailwindcss: "^4.3.2",
|
|
66
66
|
"@tailwindcss/vite": "^4.3.2",
|
|
67
|
-
vite: "
|
|
67
|
+
vite: "npm:rolldown-vite@7.1.14",
|
|
68
68
|
"@vitejs/plugin-react": "^5.1.1",
|
|
69
69
|
// Tailwind v4 compiler for bun's dev server + `Bun.build` (the `app` tag).
|
|
70
70
|
"bun-plugin-tailwind": "^0.1.2",
|
package/src/project-js.ts
CHANGED
|
@@ -40,7 +40,7 @@ import { applyCompiledPublish } from "./publish.ts";
|
|
|
40
40
|
import { generatePythonNodeBindings } from "./python-node-bindings.ts";
|
|
41
41
|
import { DBXToolsRelease, type ReleaseDocsOptions } from "./release.ts";
|
|
42
42
|
import { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS, type PackageTag } from "./tags.ts";
|
|
43
|
-
import { DBXToolsRootTsconfig } from "./tsconfig.ts";
|
|
43
|
+
import { DBXToolsRootTsconfig, type DBXToolsRootTsconfigOptions } from "./tsconfig.ts";
|
|
44
44
|
import { configureVsCode } from "./vscode.ts";
|
|
45
45
|
import { readWorkspaceVersion, syncWorkspaceManifestVersion } from "./workspace-version.ts";
|
|
46
46
|
|
|
@@ -140,7 +140,15 @@ function repositoryOptions(
|
|
|
140
140
|
options: DBXToolsJavaScriptProjectOptions,
|
|
141
141
|
): Pick<javascript.NodeProjectOptions, "repository" | "repositoryDirectory"> {
|
|
142
142
|
const workspaceRoot = resolve(options.parent?.root.outdir ?? options.outdir ?? process.cwd());
|
|
143
|
-
const
|
|
143
|
+
const existingRepository = readPackageManifest(workspaceRoot)?.repository;
|
|
144
|
+
const existingRepositoryUrl =
|
|
145
|
+
typeof existingRepository === "string"
|
|
146
|
+
? existingRepository
|
|
147
|
+
: object.isRecord(existingRepository)
|
|
148
|
+
? stringUtils.trimToNull(existingRepository.url)
|
|
149
|
+
: undefined;
|
|
150
|
+
const repository =
|
|
151
|
+
options.repository ?? projectUtils.repositoryUrl(workspaceRoot, "npm") ?? existingRepositoryUrl;
|
|
144
152
|
const projectRoot = options.parent
|
|
145
153
|
? resolve(options.parent.outdir, options.outdir ?? ".")
|
|
146
154
|
: resolve(options.outdir ?? process.cwd());
|
|
@@ -623,6 +631,8 @@ export type DBXToolsJavaScriptProjectOptions = CommonProjectOptions &
|
|
|
623
631
|
* a list = only those tags.
|
|
624
632
|
*/
|
|
625
633
|
readonly defaultTagMixins?: false | PackageTag[];
|
|
634
|
+
/** Overrides for the generated repository-root TypeScript program. */
|
|
635
|
+
readonly rootTsconfig?: DBXToolsRootTsconfigOptions;
|
|
626
636
|
/**
|
|
627
637
|
* Apply AppKit conventions to `app` and `server` packages. Defaults to
|
|
628
638
|
* `auto`; `true` also validates the required package roles and entry files.
|
|
@@ -1065,6 +1075,7 @@ class PrettierIgnoreGenerated extends Component {
|
|
|
1065
1075
|
if (!(sub instanceof DBXToolsTypeScriptProject)) continue;
|
|
1066
1076
|
const rel = toPosix(relative(rootAbs, sub.outdir));
|
|
1067
1077
|
prettier.addIgnorePattern(`${rel}/index.ts`);
|
|
1078
|
+
prettier.addIgnorePattern(`${rel}/src/**/index.ts`);
|
|
1068
1079
|
for (const module of codegenModulePaths(sub.dbxToolsConfig.codegenInputs)) {
|
|
1069
1080
|
prettier.addIgnorePattern(`${rel}/${module}`);
|
|
1070
1081
|
}
|
|
@@ -1236,7 +1247,7 @@ function initProject(
|
|
|
1236
1247
|
}
|
|
1237
1248
|
|
|
1238
1249
|
if (project instanceof DBXToolsNodeProject) {
|
|
1239
|
-
project.rootTsconfig = new DBXToolsRootTsconfig(project);
|
|
1250
|
+
project.rootTsconfig = new DBXToolsRootTsconfig(project, options.rootTsconfig);
|
|
1240
1251
|
}
|
|
1241
1252
|
const projenrc = new typescript.ProjenrcTs(project, {
|
|
1242
1253
|
runner: typescript.TypeScriptRunner.nodejs(),
|
|
@@ -1264,7 +1275,9 @@ function initProject(
|
|
|
1264
1275
|
// lockfiles, so the CI-named install task must resolve from manifests as well.
|
|
1265
1276
|
project.package.installCiTask.reset("bun install");
|
|
1266
1277
|
configureRootBunRuntime(project);
|
|
1267
|
-
const roots =
|
|
1278
|
+
const roots =
|
|
1279
|
+
options.packageRoots ??
|
|
1280
|
+
(existsSync(resolve(project.outdir, "packages/js")) ? ["packages/js"] : DEFAULT_PACKAGE_ROOTS);
|
|
1268
1281
|
project.dbxToolsConfig.packageRoots = [...roots];
|
|
1269
1282
|
if (options.syncResynthPaths?.length) {
|
|
1270
1283
|
project.dbxToolsConfig.syncResynthPaths = [...options.syncResynthPaths];
|
|
@@ -1289,6 +1302,9 @@ function initProject(
|
|
|
1289
1302
|
project.gitignore.addPatterns(
|
|
1290
1303
|
...[...ignore.ignorePatterns({ test: false, dot: false, lock: true })],
|
|
1291
1304
|
);
|
|
1305
|
+
// The generic compiled-output pattern `**/lib/**` must not hide authored
|
|
1306
|
+
// modules beneath a package's `src/lib/`.
|
|
1307
|
+
project.gitignore.include("**/src/lib/**");
|
|
1292
1308
|
// What the dot group was actually earning here, named explicitly: secrets and
|
|
1293
1309
|
// local editor state. Both ignore CONTENTS (`.idea/*`) rather than the
|
|
1294
1310
|
// directory, so a later `!` negation can still reach a file inside.
|
package/src/project-py.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/** Reusable uv workspace generation for Python packages hosted in a projen tree. */
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
2
4
|
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
3
5
|
import { stringUtils, type OneOrMany } from "@dbx-tools/shared-core";
|
|
4
6
|
import { Component, License, TextFile, type Project, github, javascript, python } from "projen";
|
|
5
7
|
import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
|
|
6
|
-
import { PYTHON_GENERATED_PACKAGE } from "./generated.ts";
|
|
8
|
+
import { header, PYTHON_GENERATED_PACKAGE } from "./generated.ts";
|
|
7
9
|
import { LICENSE, projectRepositoryUrl, taskCommand } from "./project-js.ts";
|
|
8
10
|
import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
|
|
9
11
|
import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
|
|
@@ -107,7 +109,10 @@ interface PythonPublication {
|
|
|
107
109
|
|
|
108
110
|
/** Options for {@link DBXToolsPythonWorkspace}. */
|
|
109
111
|
export interface DBXToolsPythonWorkspaceOptions {
|
|
110
|
-
|
|
112
|
+
/** Package-specific options merged over packages discovered under the Python root. */
|
|
113
|
+
readonly packages?: readonly PythonPackageOptions[];
|
|
114
|
+
/** Discover direct child directories containing `src/`. Defaults to true. */
|
|
115
|
+
readonly discoverPackages?: boolean;
|
|
111
116
|
readonly repository?: PythonRepositoryOptions;
|
|
112
117
|
/** Repository-relative Python package root. Defaults to `packages/py`. */
|
|
113
118
|
readonly root?: string;
|
|
@@ -214,6 +219,15 @@ export function pythonModuleName(scope: string, directory: string): string {
|
|
|
214
219
|
return [scope, ...directory.split("/")].map((part) => part.replaceAll("-", "_")).join(".");
|
|
215
220
|
}
|
|
216
221
|
|
|
222
|
+
/** Discover direct Python workspace members by their conventional source directory. */
|
|
223
|
+
function discoverPythonPackages(root: string): string[] {
|
|
224
|
+
if (!existsSync(root)) return [];
|
|
225
|
+
return readdirSync(root, { withFileTypes: true })
|
|
226
|
+
.filter((entry) => entry.isDirectory() && existsSync(resolve(root, entry.name, "src")))
|
|
227
|
+
.map((entry) => entry.name)
|
|
228
|
+
.sort();
|
|
229
|
+
}
|
|
230
|
+
|
|
217
231
|
/** A Python package implemented with projen's `PythonProject` and uv backend. */
|
|
218
232
|
export class DBXToolsPythonProject extends python.PythonProject implements DBXToolsProject {
|
|
219
233
|
readonly language = "python" as const;
|
|
@@ -349,11 +363,30 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
349
363
|
ref: options.repository?.ref ?? "main",
|
|
350
364
|
root: options.root ?? options.repository?.root ?? "packages/py",
|
|
351
365
|
};
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
366
|
+
const configuredPackages = new Map((options.packages ?? []).map((pkg) => [pkg.directory, pkg]));
|
|
367
|
+
const discoveredDirectories =
|
|
368
|
+
options.discoverPackages === false
|
|
369
|
+
? []
|
|
370
|
+
: discoverPythonPackages(resolve(project.outdir, this.repository.root));
|
|
371
|
+
const directories = [
|
|
372
|
+
...configuredPackages.keys(),
|
|
373
|
+
...discoveredDirectories.filter((directory) => !configuredPackages.has(directory)),
|
|
374
|
+
];
|
|
375
|
+
const packageIdentities: ResolvedPythonPackageOptions[] = directories.map((directory) => {
|
|
376
|
+
const pkg = configuredPackages.get(directory);
|
|
377
|
+
return {
|
|
378
|
+
directory,
|
|
379
|
+
description: `${scope} ${directory} Python package.`,
|
|
380
|
+
...pkg,
|
|
381
|
+
name: pkg?.name ?? `${scope}-${stringUtils.toSlug(directory)}`,
|
|
382
|
+
module: pkg?.module ?? pythonModuleName(scope, directory),
|
|
383
|
+
};
|
|
384
|
+
});
|
|
385
|
+
if (packageIdentities.length === 0) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`No Python packages were configured or discovered under ${this.repository.root}`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
357
390
|
const configuredNodeRuntimes = packageIdentities.filter(({ nodeRuntime }) => nodeRuntime);
|
|
358
391
|
if (configuredNodeRuntimes.length > 1) {
|
|
359
392
|
throw new Error("A Python workspace can configure only one shared Node runtime package");
|
|
@@ -760,26 +793,24 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
760
793
|
}),
|
|
761
794
|
];
|
|
762
795
|
const helper = ".projen/pypi-trusted-publisher-instructions.mjs";
|
|
796
|
+
const helperHeader = header({ tool: "projen", source: ".projenrc.ts" })
|
|
797
|
+
.trimEnd()
|
|
798
|
+
.replaceAll("\n", "\n ");
|
|
763
799
|
new TextFile(project.root, helper, {
|
|
800
|
+
marker: true,
|
|
801
|
+
readonly: true,
|
|
764
802
|
// prettier-ignore
|
|
765
803
|
lines: stringUtils.dedent(
|
|
766
804
|
// ============================================================================
|
|
767
805
|
/*js*/`
|
|
768
806
|
#!/usr/bin/env bun
|
|
769
|
-
|
|
807
|
+
${helperHeader}
|
|
808
|
+
import { printPypiTrustedPublisherInstructions } from "@dbx-tools/projen/pypi-trusted-publisher-instructions";
|
|
770
809
|
|
|
771
|
-
|
|
772
|
-
|
|
810
|
+
await printPypiTrustedPublisherInstructions({
|
|
811
|
+
beforeAuthentication: ${JSON.stringify(linesBeforeAuthentication)},
|
|
812
|
+
afterAuthentication: ${JSON.stringify(linesAfterAuthentication)},
|
|
773
813
|
});
|
|
774
|
-
const beforeAuthentication = ${JSON.stringify(linesBeforeAuthentication)};
|
|
775
|
-
const afterAuthentication = ${JSON.stringify(linesAfterAuthentication)};
|
|
776
|
-
const authentication = values.secretFile
|
|
777
|
-
? [
|
|
778
|
-
\`- If browser authentication is required, read credentials from \${values.secretFile} directly into the browser without printing, logging, or exposing secret values.\`,
|
|
779
|
-
\`- If \${values.secretFile} is absent, invalid, or insufficient for authentication, pause and ask the user to complete authentication.\`,
|
|
780
|
-
]
|
|
781
|
-
: ["- If browser authentication is required, pause and ask the user to complete authentication."];
|
|
782
|
-
process.stdout.write(\`\${[...beforeAuthentication, ...authentication, ...afterAuthentication].join("\\n").trimEnd()}\\n\`);
|
|
783
814
|
`
|
|
784
815
|
// ============================================================================
|
|
785
816
|
).split("\n"),
|
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) {
|
package/tasks/appkit-deploy.ts
CHANGED
|
@@ -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 {
|
|
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<
|
|
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
|
|
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
|
|
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
|
-
):
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
228
|
+
parsed.profile ??
|
|
229
|
+
configuredProfile ??
|
|
230
|
+
(hasEnvironmentAuth
|
|
56
231
|
? undefined
|
|
57
|
-
: (dependencies.resolveProfile ?? (() => profile.resolveProfile()?.name))();
|
|
58
|
-
if (!
|
|
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
|
-
...
|
|
65
|
-
|
|
66
|
-
|
|
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)));
|
|
@@ -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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
"",
|