@palettelab/cli 0.3.46 → 0.3.48
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 +3 -2
- package/lib/commands/build.js +59 -18
- package/lib/commands/dev.js +23 -0
- package/lib/commands/package.js +19 -0
- package/lib/commands/publish.js +28 -21
- package/lib/commands/test.js +63 -11
- package/package.json +1 -1
- package/template-fallback/package.json +1 -1
- package/template-fallback/templates/dashboard/package.json +1 -1
- package/template-fallback/templates/database/package.json +1 -1
- package/template-fallback/templates/external-service/package.json +1 -1
- package/template-fallback/templates/frontend-only/package.json +1 -1
- package/template-fallback/templates/next/package.json +1 -1
- package/template-fallback/templates/palette-app/package.json +1 -1
package/README.md
CHANGED
|
@@ -678,9 +678,10 @@ Run local contract checks before publishing.
|
|
|
678
678
|
```bash
|
|
679
679
|
pltt test
|
|
680
680
|
pltt test --json
|
|
681
|
+
pltt test --publish-type preview
|
|
681
682
|
```
|
|
682
683
|
|
|
683
|
-
Checks include manifest validity, SDK/platform compatibility, semver bump detection, forbidden platform imports, frontend bundling and size limits, sandbox bridge smoke, backend dependency installation/import, route permission gates, route permission declarations, migration linting, frontend sandbox policy, and dependency policy for `package.json` / `pyproject.toml`. The default frontend and backend bundle limits are 15 MiB; set `PALETTE_MAX_FRONTEND_BUNDLE_BYTES` or `PALETTE_MAX_BACKEND_BUNDLE_BYTES` for reviewed exceptions.
|
|
684
|
+
Checks include manifest validity, SDK/platform compatibility, release semver bump detection, forbidden platform imports, frontend bundling and size limits, sandbox bridge smoke, backend dependency installation/import, route permission gates, route permission declarations, migration linting, frontend sandbox policy, and dependency policy for `package.json` / `pyproject.toml`. The semver bump check is skipped for `--publish-type preview` because hosted previews may reuse the current app version. The default frontend and backend bundle limits are 15 MiB; set `PALETTE_MAX_FRONTEND_BUNDLE_BYTES` or `PALETTE_MAX_BACKEND_BUNDLE_BYTES` for reviewed exceptions.
|
|
684
685
|
|
|
685
686
|
### `pltt package`
|
|
686
687
|
|
|
@@ -706,7 +707,7 @@ pltt publish --env staging --json
|
|
|
706
707
|
pltt publish --env staging --ttl-hours 24
|
|
707
708
|
```
|
|
708
709
|
|
|
709
|
-
Publishing first runs the same contract checks as `pltt test`. If preflight passes, it bundles frontend/backend artifacts, uploads them, creates a
|
|
710
|
+
Publishing first runs the same contract checks as `pltt test`. Preview publishes skip the release semver bump check, while release publishes still require a version newer than the last release. If preflight passes, it bundles frontend/backend artifacts, uploads them, creates a review record, and prints review/preview URLs when the platform returns them.
|
|
710
711
|
|
|
711
712
|
`--ttl-hours` sets an expiration on the preview URL. It is mainly used by `pltt dev --sandbox`, which defaults previews to 24 hours.
|
|
712
713
|
|
package/lib/commands/build.js
CHANGED
|
@@ -31,6 +31,22 @@ function pluginTablePrefix(pluginId) {
|
|
|
31
31
|
return `${pluginSafeId(pluginId)}__`
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function makeIssue(message, fix = null, meta = {}) {
|
|
35
|
+
return { message, fix, ...meta }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatIssue(issue) {
|
|
39
|
+
if (typeof issue === "string") return issue
|
|
40
|
+
const lines = [issue.message || String(issue)]
|
|
41
|
+
if (issue.details) lines.push(`Why: ${issue.details}`)
|
|
42
|
+
if (issue.fix) lines.push(`Fix: ${issue.fix}`)
|
|
43
|
+
if (issue.example) {
|
|
44
|
+
lines.push("Example:")
|
|
45
|
+
for (const line of String(issue.example).split("\n")) lines.push(` ${line}`)
|
|
46
|
+
}
|
|
47
|
+
return lines.join("\n ")
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
function crossPluginSchemaRefs(src, allowedSchema) {
|
|
35
51
|
if (!allowedSchema) return []
|
|
36
52
|
const refs = new Set()
|
|
@@ -43,28 +59,27 @@ function crossPluginSchemaRefs(src, allowedSchema) {
|
|
|
43
59
|
return [...refs].sort()
|
|
44
60
|
}
|
|
45
61
|
|
|
46
|
-
function lintMigrationFile(absPath, pluginId) {
|
|
62
|
+
function lintMigrationFile(absPath, pluginId, sharedTables = []) {
|
|
47
63
|
const issues = []
|
|
48
64
|
const src = fs.readFileSync(absPath, "utf8")
|
|
49
65
|
const requiredPrefix = pluginId ? pluginTablePrefix(pluginId) : null
|
|
50
66
|
const allowedSchema = pluginId ? pluginSchema(pluginId) : null
|
|
67
|
+
const sharedTableNames = new Set(sharedTables || [])
|
|
51
68
|
|
|
52
69
|
for (const { re, reason } of BANNED_PATTERNS) {
|
|
53
70
|
if (re.test(src)) {
|
|
54
|
-
issues.push(`${path.basename(absPath)}: ${reason}`)
|
|
71
|
+
issues.push(makeIssue(`${path.basename(absPath)}: ${reason}`))
|
|
55
72
|
}
|
|
56
73
|
}
|
|
57
74
|
|
|
58
75
|
for (const schema of crossPluginSchemaRefs(src, allowedSchema)) {
|
|
59
|
-
issues.push(`${path.basename(absPath)}: plugin migrations must not reference another app schema (${schema})`)
|
|
76
|
+
issues.push(makeIssue(`${path.basename(absPath)}: plugin migrations must not reference another app schema (${schema})`))
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
// Every op.create_table("foo", ...) in the file must have a matching
|
|
63
|
-
// ensure_org_rls(op, "foo") somewhere in the same file
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// magic comment `# palette:rls-ok` on the same logical migration.
|
|
67
|
-
const skipRlsCheck = /#\s*palette:rls-ok\b/.test(src)
|
|
80
|
+
// ensure_org_rls(op, "foo") somewhere in the same file unless the table is
|
|
81
|
+
// declared in manifest.shared_tables. Caveat: this is a cheap syntactic
|
|
82
|
+
// check, not a full AST walk.
|
|
68
83
|
|
|
69
84
|
const tableNames = new Set()
|
|
70
85
|
const createTableRe = /op\.create_table\(\s*['"]([a-zA-Z_][a-zA-Z0-9_]*)['"]/g
|
|
@@ -75,23 +90,48 @@ function lintMigrationFile(absPath, pluginId) {
|
|
|
75
90
|
|
|
76
91
|
for (const name of tableNames) {
|
|
77
92
|
if (requiredPrefix && !name.startsWith(requiredPrefix)) {
|
|
78
|
-
issues.push(
|
|
93
|
+
issues.push(makeIssue(
|
|
79
94
|
`${path.basename(absPath)}: create_table("${name}") must use the app table prefix "${requiredPrefix}"`,
|
|
80
|
-
|
|
95
|
+
`Rename the table to "${requiredPrefix}${name}" or another name that starts with "${requiredPrefix}", then update model and query references.`,
|
|
96
|
+
{
|
|
97
|
+
code: "database_table_prefix",
|
|
98
|
+
file: path.basename(absPath),
|
|
99
|
+
table: name,
|
|
100
|
+
required_prefix: requiredPrefix,
|
|
101
|
+
},
|
|
102
|
+
))
|
|
81
103
|
}
|
|
82
104
|
const rlsRe = new RegExp(`ensure_org_rls\\(\\s*op\\s*,\\s*['"]${name}['"]`)
|
|
83
|
-
if (!
|
|
84
|
-
|
|
105
|
+
if (!sharedTableNames.has(name) && !rlsRe.test(src)) {
|
|
106
|
+
const schemaHint = pluginId ? `${pluginSchema(pluginId)}.${name}` : name
|
|
107
|
+
issues.push(makeIssue(
|
|
85
108
|
`${path.basename(absPath)}: create_table("${name}") is missing ensure_org_rls(op, "${name}"). ` +
|
|
86
|
-
`
|
|
87
|
-
|
|
109
|
+
`Preview/install will fail RLS compliance for ${schemaHint}: missing ENABLE + FORCE row level security.`,
|
|
110
|
+
`Import ensure_org_rls from palette_sdk.db and call ensure_org_rls(op, "${name}") immediately after op.create_table("${name}", ...). ` +
|
|
111
|
+
`Keep an organization_id column on tenant data tables. If this table is intentionally shared across every organization, add "${name}" to manifest.shared_tables instead and document why it is global.`,
|
|
112
|
+
{
|
|
113
|
+
code: "database_missing_org_rls",
|
|
114
|
+
file: path.basename(absPath),
|
|
115
|
+
table: name,
|
|
116
|
+
schema: pluginId ? pluginSchema(pluginId) : null,
|
|
117
|
+
details:
|
|
118
|
+
"Palette verifies every plugin table after migrations run. Tables that hold tenant data must have organization_id, at least one RLS policy, and ENABLE + FORCE row level security.",
|
|
119
|
+
example: [
|
|
120
|
+
"from palette_sdk.db import ensure_org_rls",
|
|
121
|
+
"",
|
|
122
|
+
"def upgrade():",
|
|
123
|
+
` op.create_table("${name}", ...)`,
|
|
124
|
+
` ensure_org_rls(op, "${name}")`,
|
|
125
|
+
].join("\n"),
|
|
126
|
+
},
|
|
127
|
+
))
|
|
88
128
|
}
|
|
89
129
|
}
|
|
90
130
|
|
|
91
131
|
return issues
|
|
92
132
|
}
|
|
93
133
|
|
|
94
|
-
function lintMigrationsDir(migrationsDir, pluginId) {
|
|
134
|
+
function lintMigrationsDir(migrationsDir, pluginId, sharedTables = []) {
|
|
95
135
|
const errors = []
|
|
96
136
|
const versionsDir = path.join(migrationsDir, "versions")
|
|
97
137
|
if (!fs.existsSync(versionsDir)) {
|
|
@@ -101,7 +141,7 @@ function lintMigrationsDir(migrationsDir, pluginId) {
|
|
|
101
141
|
for (const entry of fs.readdirSync(versionsDir)) {
|
|
102
142
|
if (!entry.endsWith(".py")) continue
|
|
103
143
|
const abs = path.join(versionsDir, entry)
|
|
104
|
-
errors.push(...lintMigrationFile(abs, pluginId))
|
|
144
|
+
errors.push(...lintMigrationFile(abs, pluginId, sharedTables))
|
|
105
145
|
}
|
|
106
146
|
return errors
|
|
107
147
|
}
|
|
@@ -127,13 +167,13 @@ async function run(args, { cwd }) {
|
|
|
127
167
|
} else if (!fs.existsSync(path.join(migrationsAbs, "env.py"))) {
|
|
128
168
|
errors.push(`database.migrations directory is missing env.py: ${migrationsRel}`)
|
|
129
169
|
} else {
|
|
130
|
-
errors.push(...lintMigrationsDir(migrationsAbs, manifest.id))
|
|
170
|
+
errors.push(...lintMigrationsDir(migrationsAbs, manifest.id, manifest.shared_tables || []))
|
|
131
171
|
}
|
|
132
172
|
}
|
|
133
173
|
|
|
134
174
|
if (errors.length) {
|
|
135
175
|
console.error("[pltt] validation failed:")
|
|
136
|
-
for (const e of errors) console.error(` - ${e}`)
|
|
176
|
+
for (const e of errors) console.error(` - ${formatIssue(e)}`)
|
|
137
177
|
process.exit(1)
|
|
138
178
|
}
|
|
139
179
|
console.log(`[pltt] ok — ${manifest.id} v${manifest.version}`)
|
|
@@ -142,4 +182,5 @@ async function run(args, { cwd }) {
|
|
|
142
182
|
module.exports = run
|
|
143
183
|
module.exports.lintMigrationsDir = lintMigrationsDir
|
|
144
184
|
module.exports.lintMigrationFile = lintMigrationFile
|
|
185
|
+
module.exports.formatIssue = formatIssue
|
|
145
186
|
module.exports.pluginTablePrefix = pluginTablePrefix
|
package/lib/commands/dev.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
|
|
3
|
+
const fs = require("fs")
|
|
3
4
|
const path = require("path")
|
|
4
5
|
const { spawn, spawnSync } = require("child_process")
|
|
5
6
|
const { loadManifest } = require("../manifest")
|
|
@@ -8,6 +9,7 @@ const { parseFlags, resolveEnvironment } = require("../environments")
|
|
|
8
9
|
const { resolveDevPorts } = require("../ports")
|
|
9
10
|
const { startSimulator } = require("../dev-simulator")
|
|
10
11
|
const { loadLocalEnv } = require("../secrets")
|
|
12
|
+
const buildCommand = require("./build")
|
|
11
13
|
const publish = require("./publish")
|
|
12
14
|
const logs = require("./logs")
|
|
13
15
|
|
|
@@ -76,6 +78,26 @@ function imagePullHelp(image, output) {
|
|
|
76
78
|
)
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
function lintMigrationsForDev(cwd, manifest) {
|
|
82
|
+
if (!manifest.database) return
|
|
83
|
+
const migrationsRel = manifest.database.migrations || "./backend/migrations"
|
|
84
|
+
const migrationsAbs = path.resolve(cwd, migrationsRel)
|
|
85
|
+
const errors = []
|
|
86
|
+
if (!fs.existsSync(migrationsAbs)) {
|
|
87
|
+
errors.push(`database.migrations directory not found: ${migrationsRel}`)
|
|
88
|
+
} else if (!fs.existsSync(path.join(migrationsAbs, "env.py"))) {
|
|
89
|
+
errors.push(`database.migrations directory is missing env.py: ${migrationsRel}`)
|
|
90
|
+
} else {
|
|
91
|
+
errors.push(...buildCommand.lintMigrationsDir(migrationsAbs, manifest.id, manifest.shared_tables || []))
|
|
92
|
+
}
|
|
93
|
+
if (!errors.length) return
|
|
94
|
+
|
|
95
|
+
console.error("[pltt] dev blocked by database migration validation:")
|
|
96
|
+
for (const err of errors) console.error(` - ${buildCommand.formatIssue(err)}`)
|
|
97
|
+
console.error("[pltt] Fix these issues or run `pltt test --json` for machine-readable details.")
|
|
98
|
+
process.exit(1)
|
|
99
|
+
}
|
|
100
|
+
|
|
79
101
|
async function run(args, { cwd }) {
|
|
80
102
|
const { flags, rest } = parseFlags(args)
|
|
81
103
|
const cloud = rest.includes("--cloud") || rest.includes("--sandbox")
|
|
@@ -124,6 +146,7 @@ async function run(args, { cwd }) {
|
|
|
124
146
|
}
|
|
125
147
|
|
|
126
148
|
const manifest = loadManifest(cwd)
|
|
149
|
+
lintMigrationsForDev(cwd, manifest)
|
|
127
150
|
const pluginId = manifest.id
|
|
128
151
|
const ports = await resolveDevPorts({ host: platform ? "0.0.0.0" : "127.0.0.1" })
|
|
129
152
|
|
package/lib/commands/package.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require("path")
|
|
|
5
5
|
const crypto = require("crypto")
|
|
6
6
|
const { loadManifest, validateManifest } = require("../manifest")
|
|
7
7
|
const { bundleFrontend, bundleBackend } = require("../bundler")
|
|
8
|
+
const buildCommand = require("./build")
|
|
8
9
|
|
|
9
10
|
function sha256(buf) {
|
|
10
11
|
return crypto.createHash("sha256").update(buf).digest("hex")
|
|
@@ -20,6 +21,24 @@ async function run(argv, { cwd }) {
|
|
|
20
21
|
process.exit(1)
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
if (manifest.database) {
|
|
25
|
+
const migrationsRel = manifest.database.migrations || "./backend/migrations"
|
|
26
|
+
const migrationsAbs = path.resolve(cwd, migrationsRel)
|
|
27
|
+
const migrationErrors = []
|
|
28
|
+
if (!fs.existsSync(migrationsAbs)) {
|
|
29
|
+
migrationErrors.push(`database.migrations directory not found: ${migrationsRel}`)
|
|
30
|
+
} else if (!fs.existsSync(path.join(migrationsAbs, "env.py"))) {
|
|
31
|
+
migrationErrors.push(`database.migrations directory is missing env.py: ${migrationsRel}`)
|
|
32
|
+
} else {
|
|
33
|
+
migrationErrors.push(...buildCommand.lintMigrationsDir(migrationsAbs, manifest.id, manifest.shared_tables || []))
|
|
34
|
+
}
|
|
35
|
+
if (migrationErrors.length) {
|
|
36
|
+
console.error("[pltt] package blocked by database migration validation:")
|
|
37
|
+
for (const e of migrationErrors) console.error(` - ${buildCommand.formatIssue(e)}`)
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
const distDir = path.join(cwd, "dist")
|
|
24
43
|
fs.mkdirSync(distDir, { recursive: true })
|
|
25
44
|
|
package/lib/commands/publish.js
CHANGED
|
@@ -80,14 +80,21 @@ function explainPreflightFailure(result) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
const migration =
|
|
83
|
+
const migration =
|
|
84
|
+
result?.code === "database_missing_org_rls" && result?.table
|
|
85
|
+
? { file: result.file || "migration", table: result.table }
|
|
86
|
+
: extractMigrationHint(message)
|
|
84
87
|
if (migration) {
|
|
85
88
|
return {
|
|
86
89
|
title: "Database migration is missing organization RLS",
|
|
87
|
-
details: [
|
|
90
|
+
details: [
|
|
91
|
+
`File: ${migration.file}`,
|
|
92
|
+
`Table: ${migration.table}`,
|
|
93
|
+
"Preview/install will reject this table unless RLS is enabled and forced.",
|
|
94
|
+
],
|
|
88
95
|
fix:
|
|
89
96
|
fix ||
|
|
90
|
-
`Call ensure_org_rls(op, "${migration.table}") after create_table, or add
|
|
97
|
+
`Call ensure_org_rls(op, "${migration.table}") after create_table, or add "${migration.table}" to manifest.shared_tables only if the table is intentionally global.`,
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
|
|
@@ -131,9 +138,9 @@ function printPreflightFailure(payload, fallbackOutput) {
|
|
|
131
138
|
console.error("[pltt] Need machine-readable details? Run `pltt test --json`.")
|
|
132
139
|
}
|
|
133
140
|
|
|
134
|
-
function runPreflight(cwd, json) {
|
|
141
|
+
function runPreflight(cwd, json, publishType = "release") {
|
|
135
142
|
const cliBin = path.resolve(__dirname, "..", "..", "bin", "pltt.js")
|
|
136
|
-
const res = spawnSync(process.execPath, [cliBin, "test", "--json"], {
|
|
143
|
+
const res = spawnSync(process.execPath, [cliBin, "test", "--json", "--publish-type", publishType], {
|
|
137
144
|
cwd,
|
|
138
145
|
encoding: "utf8",
|
|
139
146
|
env: process.env,
|
|
@@ -289,7 +296,7 @@ async function run(argv, { cwd }) {
|
|
|
289
296
|
process.exit(1)
|
|
290
297
|
}
|
|
291
298
|
|
|
292
|
-
runPreflight(cwd, flags.json)
|
|
299
|
+
runPreflight(cwd, flags.json, publishType)
|
|
293
300
|
|
|
294
301
|
log(
|
|
295
302
|
`[pltt] publishing ${manifest.id}@${manifest.version} → ${env.name} (${env.url})`,
|
|
@@ -361,28 +368,28 @@ async function run(argv, { cwd }) {
|
|
|
361
368
|
method: "POST",
|
|
362
369
|
body: publishBody,
|
|
363
370
|
})
|
|
371
|
+
const lastPublish = {
|
|
372
|
+
id: record.id,
|
|
373
|
+
plugin_id: record.plugin_id,
|
|
374
|
+
version: record.version,
|
|
375
|
+
env: env.name,
|
|
376
|
+
url: env.url,
|
|
377
|
+
publish_type: record.publish_type || publishType,
|
|
378
|
+
preview_url: record.preview_url,
|
|
379
|
+
preview_expires_at: record.preview_expires_at,
|
|
380
|
+
published_at: new Date().toISOString(),
|
|
381
|
+
}
|
|
364
382
|
|
|
365
383
|
try {
|
|
366
384
|
const dir = path.join(cwd, ".palette")
|
|
367
385
|
fs.mkdirSync(dir, { recursive: true })
|
|
368
386
|
fs.writeFileSync(
|
|
369
387
|
path.join(dir, "last-publish.json"),
|
|
370
|
-
JSON.stringify(
|
|
371
|
-
{
|
|
372
|
-
id: record.id,
|
|
373
|
-
plugin_id: record.plugin_id,
|
|
374
|
-
version: record.version,
|
|
375
|
-
env: env.name,
|
|
376
|
-
url: env.url,
|
|
377
|
-
publish_type: record.publish_type || publishType,
|
|
378
|
-
preview_url: record.preview_url,
|
|
379
|
-
preview_expires_at: record.preview_expires_at,
|
|
380
|
-
published_at: new Date().toISOString(),
|
|
381
|
-
},
|
|
382
|
-
null,
|
|
383
|
-
2,
|
|
384
|
-
),
|
|
388
|
+
JSON.stringify(lastPublish, null, 2),
|
|
385
389
|
)
|
|
390
|
+
if ((record.publish_type || publishType) === "release") {
|
|
391
|
+
fs.writeFileSync(path.join(dir, "last-release.json"), JSON.stringify(lastPublish, null, 2))
|
|
392
|
+
}
|
|
386
393
|
} catch (err) {
|
|
387
394
|
// best-effort
|
|
388
395
|
}
|
package/lib/commands/test.js
CHANGED
|
@@ -12,6 +12,14 @@ const DEFAULT_FRONTEND_BUNDLE_LIMIT = 15 * 1024 * 1024
|
|
|
12
12
|
const DEFAULT_BACKEND_BUNDLE_LIMIT = 15 * 1024 * 1024
|
|
13
13
|
|
|
14
14
|
function reporter(json, results) {
|
|
15
|
+
function printExtra(meta) {
|
|
16
|
+
if (meta.details) console.log(`WHY ${meta.details}`)
|
|
17
|
+
if (meta.example) {
|
|
18
|
+
console.log("EXAMPLE")
|
|
19
|
+
for (const line of String(meta.example).split("\n")) console.log(` ${line}`)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
return {
|
|
16
24
|
ok(message, meta = {}) {
|
|
17
25
|
results.push({ status: "ok", message, ...meta })
|
|
@@ -22,6 +30,7 @@ function reporter(json, results) {
|
|
|
22
30
|
if (!json) {
|
|
23
31
|
console.log(`WARN ${message}`)
|
|
24
32
|
if (fix) console.log(`FIX ${fix}`)
|
|
33
|
+
printExtra(meta)
|
|
25
34
|
}
|
|
26
35
|
return 0
|
|
27
36
|
},
|
|
@@ -30,6 +39,7 @@ function reporter(json, results) {
|
|
|
30
39
|
if (!json) {
|
|
31
40
|
console.log(`FAIL ${message}`)
|
|
32
41
|
if (fix) console.log(`FIX ${fix}`)
|
|
42
|
+
printExtra(meta)
|
|
33
43
|
}
|
|
34
44
|
return 1
|
|
35
45
|
},
|
|
@@ -253,8 +263,15 @@ function lintMigrations(cwd, manifest, out) {
|
|
|
253
263
|
if (!fs.existsSync(path.join(migrationsAbs, "env.py"))) {
|
|
254
264
|
return out.fail(`database.migrations directory is missing env.py: ${migrationsRel}`)
|
|
255
265
|
}
|
|
256
|
-
const errors = buildCommand.lintMigrationsDir(migrationsAbs, manifest.id)
|
|
257
|
-
for (const err of errors)
|
|
266
|
+
const errors = buildCommand.lintMigrationsDir(migrationsAbs, manifest.id, manifest.shared_tables || [])
|
|
267
|
+
for (const err of errors) {
|
|
268
|
+
if (typeof err === "string") {
|
|
269
|
+
out.fail(err)
|
|
270
|
+
} else {
|
|
271
|
+
const { message, fix, ...meta } = err
|
|
272
|
+
out.fail(message, fix, meta)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
258
275
|
if (errors.length === 0) out.ok("migration lint passed")
|
|
259
276
|
return errors.length
|
|
260
277
|
}
|
|
@@ -537,17 +554,51 @@ function scanForbiddenImports(cwd, manifest, out) {
|
|
|
537
554
|
return issues.length
|
|
538
555
|
}
|
|
539
556
|
|
|
540
|
-
function
|
|
541
|
-
|
|
557
|
+
function parsePublishType(argv) {
|
|
558
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
559
|
+
const arg = argv[i]
|
|
560
|
+
if (arg === "--preview") return "preview"
|
|
561
|
+
if (arg === "--release") return "release"
|
|
562
|
+
if (arg === "--publish-type") return argv[i + 1] === "preview" ? "preview" : "release"
|
|
563
|
+
if (arg.startsWith("--publish-type=")) {
|
|
564
|
+
return arg.slice("--publish-type=".length) === "preview" ? "preview" : "release"
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return "release"
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function readPublishState(statePath) {
|
|
542
571
|
if (!fs.existsSync(statePath)) {
|
|
543
|
-
|
|
544
|
-
return 0
|
|
572
|
+
return { state: null, invalid: false }
|
|
545
573
|
}
|
|
546
|
-
let previous
|
|
547
574
|
try {
|
|
548
|
-
|
|
575
|
+
return { state: JSON.parse(fs.readFileSync(statePath, "utf8")), invalid: false }
|
|
549
576
|
} catch (_err) {
|
|
550
|
-
|
|
577
|
+
return { state: null, invalid: true }
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function checkSemverBump(cwd, manifest, out, publishType = "release") {
|
|
582
|
+
if (publishType === "preview") {
|
|
583
|
+
out.ok("semver bump check skipped for preview publish")
|
|
584
|
+
return 0
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const releaseStatePath = path.join(cwd, ".palette", "last-release.json")
|
|
588
|
+
const publishStatePath = path.join(cwd, ".palette", "last-publish.json")
|
|
589
|
+
let { state: previous, invalid } = readPublishState(releaseStatePath)
|
|
590
|
+
if (!previous && !invalid) {
|
|
591
|
+
const lastPublish = readPublishState(publishStatePath)
|
|
592
|
+
invalid = lastPublish.invalid
|
|
593
|
+
if (lastPublish.state?.publish_type !== "preview") previous = lastPublish.state
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (!previous && !invalid) {
|
|
597
|
+
out.ok("semver bump check skipped; no previous release state")
|
|
598
|
+
return 0
|
|
599
|
+
}
|
|
600
|
+
if (invalid) {
|
|
601
|
+
out.warn("semver bump check skipped; previous publish state is invalid JSON")
|
|
551
602
|
return 0
|
|
552
603
|
}
|
|
553
604
|
const previousVersion = previous.version || previous.manifest?.version
|
|
@@ -557,7 +608,7 @@ function checkSemverBump(cwd, manifest, out) {
|
|
|
557
608
|
}
|
|
558
609
|
if (compareVersions(manifest.version, previousVersion) <= 0) {
|
|
559
610
|
return out.fail(
|
|
560
|
-
`plugin version ${manifest.version} is not newer than last
|
|
611
|
+
`plugin version ${manifest.version} is not newer than last release ${previousVersion}`,
|
|
561
612
|
"Bump manifest.version before publishing another build.",
|
|
562
613
|
{ current: manifest.version, previous: previousVersion },
|
|
563
614
|
)
|
|
@@ -666,6 +717,7 @@ function checkFrontendSecretLeaks(cwd, frontendBuffer, manifest, out) {
|
|
|
666
717
|
|
|
667
718
|
async function run(args, { cwd }) {
|
|
668
719
|
const json = args.includes("--json")
|
|
720
|
+
const publishType = parsePublishType(args)
|
|
669
721
|
let failures = 0
|
|
670
722
|
const results = []
|
|
671
723
|
const out = reporter(json, results)
|
|
@@ -692,7 +744,7 @@ async function run(args, { cwd }) {
|
|
|
692
744
|
|
|
693
745
|
failures += checkSdkCompatibility(cwd, manifest, out)
|
|
694
746
|
failures += scanForbiddenImports(cwd, manifest, out)
|
|
695
|
-
failures += checkSemverBump(cwd, manifest, out)
|
|
747
|
+
failures += checkSemverBump(cwd, manifest, out, publishType)
|
|
696
748
|
failures += checkDeclaredSecrets(cwd, manifest, out)
|
|
697
749
|
|
|
698
750
|
for (const permission of manifest.permissions || []) {
|
package/package.json
CHANGED