@wyxos/zephyr 0.8.0 → 0.8.1
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 +14 -1
- package/package.json +1 -1
- package/src/application/consumer/npm-publish-wait.mjs +72 -0
- package/src/application/consumer/release-package-then-deploy-consumer.mjs +245 -0
- package/src/application/consumer/update-consumer-dependency.mjs +159 -0
- package/src/application/release/release-node-package.mjs +3 -0
- package/src/application/release/release-packagist-package.mjs +3 -0
- package/src/cli/options.mjs +66 -7
- package/src/main.mjs +65 -1
- package/src/release/shared.mjs +27 -16
- package/src/release-node.mjs +5 -2
- package/src/release-packagist.mjs +5 -2
package/README.md
CHANGED
|
@@ -70,6 +70,9 @@ zephyr --type node
|
|
|
70
70
|
# Release a Node/Vue package with an explicit bump
|
|
71
71
|
zephyr --type node minor
|
|
72
72
|
|
|
73
|
+
# Release a Node/Vue package, update a local consumer app, then deploy that app
|
|
74
|
+
zephyr --type node --then-deploy ../php/atlas --consumer-preset wyxos-release --consumer-package @wyxos/vibe
|
|
75
|
+
|
|
73
76
|
# Release a Packagist package
|
|
74
77
|
zephyr --type packagist patch
|
|
75
78
|
|
|
@@ -111,10 +114,12 @@ For Laravel app deployments, `--maintenance on|off` overrides both the saved pre
|
|
|
111
114
|
|
|
112
115
|
`--setup` is Laravel-only. It first verifies that the current project is a local Laravel app, then runs the normal local configuration prompts, tests SSH authentication to the selected server, and exits before local deploy preparation, pending snapshot handling, maintenance-mode decisions, locks, or remote deployment commands. On non-Laravel projects it fails before local setup changes are written.
|
|
113
116
|
|
|
114
|
-
`--auto-commit` is available for app deployments and tells Zephyr to let local Codex inspect the repo and generate the dirty-tree commit message instead of prompting for one.
|
|
117
|
+
`--auto-commit` is available for app deployments and package releases. It tells Zephyr to let local Codex inspect the repo and generate the dirty-tree commit message instead of prompting for one.
|
|
115
118
|
|
|
116
119
|
`--skip-versioning` keeps Zephyr from mutating `package.json` or `composer.json`. On app deploys it skips the local npm version bump step. On package release workflows it releases the version already present in the manifest and creates the release tag from the current `HEAD`.
|
|
117
120
|
|
|
121
|
+
`--then-deploy <path>` is available for `--type node` and `--type vue` package releases. After the package release succeeds and the published version is visible on npm, Zephyr updates the local consumer repository, commits the consumer dependency change, and deploys that consumer through its normal Zephyr app-deploy preset. Use `--consumer-preset <name>` to choose the app preset and `--consumer-package <name>` when the consumer dependency name differs from the released package name. Consumer lockfiles are refreshed only when the consumer repository already tracks an npm lockfile; Zephyr does not create or commit server-side lockfiles.
|
|
122
|
+
|
|
118
123
|
## AI Agents and Automation
|
|
119
124
|
|
|
120
125
|
Zephyr can be used safely by Codex, CI jobs, or other automation once configuration is already in place.
|
|
@@ -132,6 +137,14 @@ zephyr --type node --non-interactive --json minor
|
|
|
132
137
|
zephyr --type packagist --non-interactive --json patch
|
|
133
138
|
```
|
|
134
139
|
|
|
140
|
+
Recommended pattern for releasing a package and immediately deploying a consumer app:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
zephyr --type node --non-interactive --json --then-deploy ../php/atlas --consumer-preset wyxos-release --consumer-package @wyxos/vibe minor
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The consumer update happens in the local consumer repository before deployment. Production receives the committed consumer state through the normal deployment path.
|
|
147
|
+
|
|
135
148
|
In `--json` mode Zephyr emits NDJSON events on `stdout` with a stable shape:
|
|
136
149
|
|
|
137
150
|
- `run_started`
|
package/package.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000
|
|
2
|
+
const DEFAULT_INTERVAL_MS = 10 * 1000
|
|
3
|
+
|
|
4
|
+
function sleep(ms) {
|
|
5
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function npmPackageMetadataUrl(packageName) {
|
|
9
|
+
return `https://registry.npmjs.org/${encodeURIComponent(packageName)}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function waitForNpmPackageVersion({
|
|
13
|
+
packageName,
|
|
14
|
+
version,
|
|
15
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
16
|
+
intervalMs = DEFAULT_INTERVAL_MS,
|
|
17
|
+
fetchImpl = globalThis.fetch,
|
|
18
|
+
delayImpl = sleep,
|
|
19
|
+
nowImpl = Date.now,
|
|
20
|
+
logProcessing,
|
|
21
|
+
logSuccess,
|
|
22
|
+
logWarning
|
|
23
|
+
} = {}) {
|
|
24
|
+
if (!packageName) {
|
|
25
|
+
throw new Error('Package name is required before waiting for npm publication.')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!version) {
|
|
29
|
+
throw new Error('Package version is required before waiting for npm publication.')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof fetchImpl !== 'function') {
|
|
33
|
+
throw new Error('fetch is not available, so Zephyr cannot verify npm publication.')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const deadline = nowImpl() + timeoutMs
|
|
37
|
+
const url = npmPackageMetadataUrl(packageName)
|
|
38
|
+
let attempts = 0
|
|
39
|
+
let lastError = null
|
|
40
|
+
|
|
41
|
+
logProcessing?.(`Waiting for ${packageName}@${version} to be visible on npm...`)
|
|
42
|
+
|
|
43
|
+
while (nowImpl() <= deadline) {
|
|
44
|
+
attempts += 1
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetchImpl(url)
|
|
48
|
+
|
|
49
|
+
if (response.ok) {
|
|
50
|
+
const metadata = await response.json()
|
|
51
|
+
if (metadata?.versions?.[version]) {
|
|
52
|
+
logSuccess?.(`${packageName}@${version} is visible on npm.`)
|
|
53
|
+
return {packageName, version, attempts}
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
lastError = new Error(`npm registry responded with ${response.status}`)
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
lastError = error
|
|
60
|
+
logWarning?.(`npm visibility check failed: ${error.message}`)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (nowImpl() + intervalMs > deadline) {
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await delayImpl(intervalMs)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const suffix = lastError ? ` Last error: ${lastError.message}` : ''
|
|
71
|
+
throw new Error(`Timed out waiting for ${packageName}@${version} to be visible on npm.${suffix}`)
|
|
72
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
|
|
5
|
+
import {validateLocalDependencies} from '../../dependency-scanner.mjs'
|
|
6
|
+
import * as bootstrap from '../../project/bootstrap.mjs'
|
|
7
|
+
import {createAppContext} from '../../runtime/app-context.mjs'
|
|
8
|
+
import {mergeDeployOptions} from '../../config/preset-options.mjs'
|
|
9
|
+
import {createConfigurationService} from '../configuration/service.mjs'
|
|
10
|
+
import {selectDeploymentTarget} from '../configuration/select-deployment-target.mjs'
|
|
11
|
+
import {resolvePendingSnapshot} from '../deploy/resolve-pending-snapshot.mjs'
|
|
12
|
+
import {runDeployment} from '../deploy/run-deployment.mjs'
|
|
13
|
+
import {waitForNpmPackageVersion} from './npm-publish-wait.mjs'
|
|
14
|
+
import {
|
|
15
|
+
assertCleanConsumerRepo,
|
|
16
|
+
updateConsumerDependency
|
|
17
|
+
} from './update-consumer-dependency.mjs'
|
|
18
|
+
|
|
19
|
+
function resolveConsumerRootDir(producerRootDir, consumerRootDir) {
|
|
20
|
+
if (!consumerRootDir) {
|
|
21
|
+
throw new Error('--then-deploy requires a consumer repository path.')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return path.isAbsolute(consumerRootDir)
|
|
25
|
+
? path.resolve(consumerRootDir)
|
|
26
|
+
: path.resolve(producerRootDir, consumerRootDir)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function fileExists(filePath) {
|
|
30
|
+
return fs.access(filePath).then(() => true).catch(() => false)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createConsumerExecutionMode({
|
|
34
|
+
presetName,
|
|
35
|
+
maintenanceMode,
|
|
36
|
+
skipChecks = false,
|
|
37
|
+
skipTests = false,
|
|
38
|
+
skipLint = false,
|
|
39
|
+
skipVersioning = false,
|
|
40
|
+
skipGitHooks = false,
|
|
41
|
+
autoCommit = false,
|
|
42
|
+
json = false,
|
|
43
|
+
explicitMaintenanceMode = false,
|
|
44
|
+
explicitSkipChecks = false,
|
|
45
|
+
explicitSkipTests = false,
|
|
46
|
+
explicitSkipLint = false,
|
|
47
|
+
explicitSkipVersioning = false,
|
|
48
|
+
explicitSkipGitHooks = false,
|
|
49
|
+
explicitAutoCommit = false
|
|
50
|
+
} = {}) {
|
|
51
|
+
return {
|
|
52
|
+
interactive: false,
|
|
53
|
+
json: json === true,
|
|
54
|
+
workflow: 'deploy',
|
|
55
|
+
setup: false,
|
|
56
|
+
presetName,
|
|
57
|
+
maintenanceMode,
|
|
58
|
+
skipChecks: skipChecks === true,
|
|
59
|
+
skipTests: skipTests === true || skipChecks === true,
|
|
60
|
+
skipLint: skipLint === true || skipChecks === true,
|
|
61
|
+
skipVersioning: skipVersioning === true,
|
|
62
|
+
skipGitHooks: skipGitHooks === true,
|
|
63
|
+
autoCommit: autoCommit === true,
|
|
64
|
+
resumePending: false,
|
|
65
|
+
discardPending: false,
|
|
66
|
+
explicitMaintenanceMode: explicitMaintenanceMode === true,
|
|
67
|
+
explicitSkipChecks: explicitSkipChecks === true,
|
|
68
|
+
explicitSkipTests: explicitSkipTests === true || explicitSkipChecks === true,
|
|
69
|
+
explicitSkipLint: explicitSkipLint === true || explicitSkipChecks === true,
|
|
70
|
+
explicitSkipVersioning: explicitSkipVersioning === true,
|
|
71
|
+
explicitSkipGitHooks: explicitSkipGitHooks === true,
|
|
72
|
+
explicitAutoCommit: explicitAutoCommit === true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolvePackageDetails({releasedPackage, packageName, version}) {
|
|
77
|
+
const resolvedPackageName = packageName ?? releasedPackage?.name
|
|
78
|
+
const resolvedVersion = version ?? releasedPackage?.version
|
|
79
|
+
|
|
80
|
+
if (!resolvedPackageName) {
|
|
81
|
+
throw new Error('Unable to determine the package name to update in the consumer app. Pass --consumer-package <name>.')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!resolvedVersion) {
|
|
85
|
+
throw new Error('Unable to determine the released package version for the consumer app update.')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {packageName: resolvedPackageName, version: resolvedVersion}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function releasePackageThenDeployConsumer({
|
|
92
|
+
producerRootDir,
|
|
93
|
+
consumerRootDir,
|
|
94
|
+
releasedPackage,
|
|
95
|
+
packageName = null,
|
|
96
|
+
version = null,
|
|
97
|
+
presetName,
|
|
98
|
+
maintenanceMode = null,
|
|
99
|
+
skipChecks = false,
|
|
100
|
+
skipTests = false,
|
|
101
|
+
skipLint = false,
|
|
102
|
+
skipVersioning = false,
|
|
103
|
+
skipGitHooks = false,
|
|
104
|
+
autoCommit = false,
|
|
105
|
+
json = false,
|
|
106
|
+
explicitMaintenanceMode = false,
|
|
107
|
+
explicitSkipChecks = false,
|
|
108
|
+
explicitSkipTests = false,
|
|
109
|
+
explicitSkipLint = false,
|
|
110
|
+
explicitSkipVersioning = false,
|
|
111
|
+
explicitSkipGitHooks = false,
|
|
112
|
+
explicitAutoCommit = false,
|
|
113
|
+
createAppContextImpl = createAppContext,
|
|
114
|
+
waitForNpmPackageVersionImpl = waitForNpmPackageVersion,
|
|
115
|
+
updateConsumerDependencyImpl = updateConsumerDependency,
|
|
116
|
+
assertCleanConsumerRepoImpl = assertCleanConsumerRepo,
|
|
117
|
+
validateLocalDependenciesImpl = validateLocalDependencies,
|
|
118
|
+
selectDeploymentTargetImpl = selectDeploymentTarget,
|
|
119
|
+
resolvePendingSnapshotImpl = resolvePendingSnapshot,
|
|
120
|
+
runDeploymentImpl = runDeployment,
|
|
121
|
+
bootstrapImpl = bootstrap
|
|
122
|
+
} = {}) {
|
|
123
|
+
if (!presetName) {
|
|
124
|
+
throw new Error('--then-deploy requires --consumer-preset <name>.')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const resolvedProducerRootDir = producerRootDir ?? process.cwd()
|
|
128
|
+
const resolvedConsumerRootDir = resolveConsumerRootDir(resolvedProducerRootDir, consumerRootDir)
|
|
129
|
+
const details = resolvePackageDetails({releasedPackage, packageName, version})
|
|
130
|
+
const executionMode = createConsumerExecutionMode({
|
|
131
|
+
presetName,
|
|
132
|
+
maintenanceMode,
|
|
133
|
+
skipChecks,
|
|
134
|
+
skipTests,
|
|
135
|
+
skipLint,
|
|
136
|
+
skipVersioning,
|
|
137
|
+
skipGitHooks,
|
|
138
|
+
autoCommit,
|
|
139
|
+
json,
|
|
140
|
+
explicitMaintenanceMode,
|
|
141
|
+
explicitSkipChecks,
|
|
142
|
+
explicitSkipTests,
|
|
143
|
+
explicitSkipLint,
|
|
144
|
+
explicitSkipVersioning,
|
|
145
|
+
explicitSkipGitHooks,
|
|
146
|
+
explicitAutoCommit
|
|
147
|
+
})
|
|
148
|
+
const context = createAppContextImpl({executionMode})
|
|
149
|
+
const {
|
|
150
|
+
logProcessing,
|
|
151
|
+
logSuccess,
|
|
152
|
+
logWarning,
|
|
153
|
+
runPrompt,
|
|
154
|
+
runCommand,
|
|
155
|
+
runCommandCapture,
|
|
156
|
+
emitEvent
|
|
157
|
+
} = context
|
|
158
|
+
const configurationService = createConfigurationService(context)
|
|
159
|
+
|
|
160
|
+
logProcessing?.(`Preparing consumer app at ${resolvedConsumerRootDir}...`)
|
|
161
|
+
await assertCleanConsumerRepoImpl(resolvedConsumerRootDir, {runCommandCapture})
|
|
162
|
+
|
|
163
|
+
await bootstrapImpl.ensureGitignoreEntry(resolvedConsumerRootDir, {
|
|
164
|
+
runCommand,
|
|
165
|
+
logSuccess,
|
|
166
|
+
logWarning,
|
|
167
|
+
skipGitHooks: executionMode.skipGitHooks
|
|
168
|
+
})
|
|
169
|
+
await bootstrapImpl.ensureProjectReleaseScript(resolvedConsumerRootDir, {
|
|
170
|
+
runPrompt,
|
|
171
|
+
runCommand,
|
|
172
|
+
logSuccess,
|
|
173
|
+
logWarning,
|
|
174
|
+
skipGitHooks: executionMode.skipGitHooks,
|
|
175
|
+
interactive: executionMode.interactive
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const hasPackageJson = await fileExists(path.join(resolvedConsumerRootDir, 'package.json'))
|
|
179
|
+
const hasComposerJson = await fileExists(path.join(resolvedConsumerRootDir, 'composer.json'))
|
|
180
|
+
|
|
181
|
+
if (hasPackageJson || hasComposerJson) {
|
|
182
|
+
logProcessing?.('Validating consumer dependencies...')
|
|
183
|
+
await validateLocalDependenciesImpl(resolvedConsumerRootDir, runPrompt, logSuccess, {
|
|
184
|
+
interactive: executionMode.interactive,
|
|
185
|
+
skipGitHooks: executionMode.skipGitHooks
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const {deploymentConfig, presetState} = await selectDeploymentTargetImpl(resolvedConsumerRootDir, {
|
|
190
|
+
configurationService,
|
|
191
|
+
runPrompt,
|
|
192
|
+
logProcessing,
|
|
193
|
+
logSuccess,
|
|
194
|
+
logWarning,
|
|
195
|
+
emitEvent,
|
|
196
|
+
executionMode,
|
|
197
|
+
promptPresetOptions: true
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
if (presetState) {
|
|
201
|
+
const effectiveDeployOptions = mergeDeployOptions(executionMode, presetState.options)
|
|
202
|
+
Object.assign(executionMode, {
|
|
203
|
+
presetName: presetState.name,
|
|
204
|
+
...effectiveDeployOptions,
|
|
205
|
+
skipChecks: executionMode.skipChecks === true ||
|
|
206
|
+
(effectiveDeployOptions.skipTests === true && effectiveDeployOptions.skipLint === true)
|
|
207
|
+
})
|
|
208
|
+
context.executionMode = executionMode
|
|
209
|
+
await presetState.applyExecutionMode(executionMode)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await waitForNpmPackageVersionImpl({
|
|
213
|
+
packageName: details.packageName,
|
|
214
|
+
version: details.version,
|
|
215
|
+
logProcessing,
|
|
216
|
+
logSuccess,
|
|
217
|
+
logWarning
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
await updateConsumerDependencyImpl({
|
|
221
|
+
rootDir: resolvedConsumerRootDir,
|
|
222
|
+
packageName: details.packageName,
|
|
223
|
+
version: details.version,
|
|
224
|
+
runCommand,
|
|
225
|
+
runCommandCapture,
|
|
226
|
+
logProcessing,
|
|
227
|
+
logSuccess,
|
|
228
|
+
logWarning,
|
|
229
|
+
skipGitHooks: executionMode.skipGitHooks
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const snapshotToUse = await resolvePendingSnapshotImpl(resolvedConsumerRootDir, deploymentConfig, {
|
|
233
|
+
runPrompt,
|
|
234
|
+
logProcessing,
|
|
235
|
+
logWarning,
|
|
236
|
+
executionMode
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
await runDeploymentImpl(deploymentConfig, {
|
|
240
|
+
rootDir: resolvedConsumerRootDir,
|
|
241
|
+
snapshot: snapshotToUse,
|
|
242
|
+
context,
|
|
243
|
+
presetState
|
|
244
|
+
})
|
|
245
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import {gitCommitArgs} from '../../utils/git-hooks.mjs'
|
|
5
|
+
|
|
6
|
+
const DEPENDENCY_FIELDS = [
|
|
7
|
+
'dependencies',
|
|
8
|
+
'devDependencies',
|
|
9
|
+
'peerDependencies',
|
|
10
|
+
'optionalDependencies'
|
|
11
|
+
]
|
|
12
|
+
const TRACKED_NPM_LOCK_FILES = ['package-lock.json', 'npm-shrinkwrap.json']
|
|
13
|
+
|
|
14
|
+
async function readPackageJson(rootDir) {
|
|
15
|
+
const packageJsonPath = path.join(rootDir, 'package.json')
|
|
16
|
+
const raw = await fs.readFile(packageJsonPath, 'utf8')
|
|
17
|
+
return JSON.parse(raw)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function writePackageJson(rootDir, pkg) {
|
|
21
|
+
const packageJsonPath = path.join(rootDir, 'package.json')
|
|
22
|
+
await fs.writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findDependencyField(pkg, packageName) {
|
|
26
|
+
return DEPENDENCY_FIELDS.find((field) => Object.hasOwn(pkg?.[field] ?? {}, packageName)) ?? null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatDependencyVersion(currentValue, version) {
|
|
30
|
+
if (typeof currentValue !== 'string' || currentValue.length === 0) {
|
|
31
|
+
return `^${version}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (currentValue.startsWith('~')) {
|
|
35
|
+
return `~${version}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (currentValue.startsWith('^')) {
|
|
39
|
+
return `^${version}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (/^\d/.test(currentValue)) {
|
|
43
|
+
return version
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return `^${version}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function assertCleanConsumerRepo(rootDir, {runCommandCapture} = {}) {
|
|
50
|
+
const status = await runCommandCapture('git', ['status', '--porcelain'], {cwd: rootDir})
|
|
51
|
+
const normalizedStatus = typeof status === 'string' ? status : status?.stdout ?? ''
|
|
52
|
+
|
|
53
|
+
if (normalizedStatus.trim().length > 0) {
|
|
54
|
+
throw new Error('Consumer repository has uncommitted changes. Commit, stash, or clean them before running a package-to-consumer release.')
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function isTracked(rootDir, filePath, {runCommandCapture} = {}) {
|
|
59
|
+
try {
|
|
60
|
+
await runCommandCapture('git', ['ls-files', '--error-unmatch', filePath], {cwd: rootDir})
|
|
61
|
+
return true
|
|
62
|
+
} catch {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function getChangedFiles(rootDir, files, {runCommandCapture} = {}) {
|
|
68
|
+
const status = await runCommandCapture('git', ['status', '--porcelain', '--', ...files], {cwd: rootDir})
|
|
69
|
+
const normalizedStatus = typeof status === 'string' ? status : status?.stdout ?? ''
|
|
70
|
+
return normalizedStatus
|
|
71
|
+
.split('\n')
|
|
72
|
+
.map((line) => line.trim())
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function trackedNpmLockFiles(rootDir, {runCommandCapture} = {}) {
|
|
77
|
+
const tracked = []
|
|
78
|
+
|
|
79
|
+
for (const filePath of TRACKED_NPM_LOCK_FILES) {
|
|
80
|
+
if (await isTracked(rootDir, filePath, {runCommandCapture})) {
|
|
81
|
+
tracked.push(filePath)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return tracked
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function updateConsumerDependency({
|
|
89
|
+
rootDir,
|
|
90
|
+
packageName,
|
|
91
|
+
version,
|
|
92
|
+
runCommand,
|
|
93
|
+
runCommandCapture,
|
|
94
|
+
logProcessing,
|
|
95
|
+
logSuccess,
|
|
96
|
+
logWarning,
|
|
97
|
+
skipGitHooks = false
|
|
98
|
+
} = {}) {
|
|
99
|
+
if (!rootDir) {
|
|
100
|
+
throw new Error('Consumer root directory is required.')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!packageName) {
|
|
104
|
+
throw new Error('Consumer package name is required.')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!version) {
|
|
108
|
+
throw new Error('Consumer package version is required.')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (typeof runCommand !== 'function' || typeof runCommandCapture !== 'function') {
|
|
112
|
+
throw new Error('Consumer dependency updates require command runners.')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await assertCleanConsumerRepo(rootDir, {runCommandCapture})
|
|
116
|
+
|
|
117
|
+
const pkg = await readPackageJson(rootDir)
|
|
118
|
+
const dependencyField = findDependencyField(pkg, packageName)
|
|
119
|
+
|
|
120
|
+
if (!dependencyField) {
|
|
121
|
+
throw new Error(`Consumer package.json does not depend on ${packageName}. Add it before running --then-deploy.`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const currentValue = pkg[dependencyField][packageName]
|
|
125
|
+
const nextValue = formatDependencyVersion(currentValue, version)
|
|
126
|
+
const lockFiles = await trackedNpmLockFiles(rootDir, {runCommandCapture})
|
|
127
|
+
|
|
128
|
+
if (currentValue !== nextValue) {
|
|
129
|
+
logProcessing?.(`Updating ${packageName} in consumer package.json from ${currentValue} to ${nextValue}...`)
|
|
130
|
+
pkg[dependencyField][packageName] = nextValue
|
|
131
|
+
await writePackageJson(rootDir, pkg)
|
|
132
|
+
} else {
|
|
133
|
+
logProcessing?.(`Consumer package.json already references ${packageName}@${nextValue}.`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (lockFiles.length > 0) {
|
|
137
|
+
logProcessing?.(`Refreshing tracked npm lock file${lockFiles.length === 1 ? '' : 's'}: ${lockFiles.join(', ')}...`)
|
|
138
|
+
await runCommand('npm', ['install', '--package-lock-only', '--ignore-scripts'], {cwd: rootDir})
|
|
139
|
+
} else {
|
|
140
|
+
logWarning?.('No tracked npm lock file found in consumer repo; committing manifest update only.')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const filesToCommit = ['package.json', ...lockFiles]
|
|
144
|
+
const changedFiles = await getChangedFiles(rootDir, filesToCommit, {runCommandCapture})
|
|
145
|
+
|
|
146
|
+
if (changedFiles.length === 0) {
|
|
147
|
+
logSuccess?.(`Consumer already uses ${packageName}@${version}.`)
|
|
148
|
+
return {committed: false, files: []}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const commitMessage = `chore: update ${packageName} to ${version}`
|
|
152
|
+
|
|
153
|
+
await runCommand('git', ['add', '--', ...filesToCommit], {cwd: rootDir})
|
|
154
|
+
await runCommand('git', gitCommitArgs(['-m', commitMessage, '--', ...filesToCommit], {skipGitHooks}), {cwd: rootDir})
|
|
155
|
+
|
|
156
|
+
logSuccess?.(`Committed consumer dependency update with "${commitMessage}".`)
|
|
157
|
+
|
|
158
|
+
return {committed: true, files: filesToCommit, message: commitMessage}
|
|
159
|
+
}
|
|
@@ -391,6 +391,7 @@ async function deployGHPages(skipDeploy, pkg, rootDir = process.cwd(), {
|
|
|
391
391
|
export async function releaseNodePackage({
|
|
392
392
|
releaseType,
|
|
393
393
|
skipGitHooks = false,
|
|
394
|
+
autoCommit = false,
|
|
394
395
|
skipTests = false,
|
|
395
396
|
skipLint = false,
|
|
396
397
|
skipVersioning = false,
|
|
@@ -431,6 +432,7 @@ export async function releaseNodePackage({
|
|
|
431
432
|
logSuccess,
|
|
432
433
|
logWarning,
|
|
433
434
|
interactive,
|
|
435
|
+
autoCommit,
|
|
434
436
|
skipGitHooks
|
|
435
437
|
})
|
|
436
438
|
await ensureReleaseBranchReady({rootDir, branchMethod: 'show-current', logStep, logWarning})
|
|
@@ -472,4 +474,5 @@ export async function releaseNodePackage({
|
|
|
472
474
|
|
|
473
475
|
logStep?.('Publishing will be handled by GitHub Actions via trusted publishing.')
|
|
474
476
|
logSuccess?.(`Release workflow completed for ${updatedPkg.name}@${updatedPkg.version}.`)
|
|
477
|
+
return updatedPkg
|
|
475
478
|
}
|
|
@@ -240,6 +240,7 @@ async function pushChanges(rootDir = process.cwd(), {
|
|
|
240
240
|
export async function releasePackagistPackage({
|
|
241
241
|
releaseType,
|
|
242
242
|
skipGitHooks = false,
|
|
243
|
+
autoCommit = false,
|
|
243
244
|
skipTests = false,
|
|
244
245
|
skipLint = false,
|
|
245
246
|
skipVersioning = false,
|
|
@@ -286,6 +287,7 @@ export async function releasePackagistPackage({
|
|
|
286
287
|
logSuccess,
|
|
287
288
|
logWarning,
|
|
288
289
|
interactive,
|
|
290
|
+
autoCommit,
|
|
289
291
|
skipGitHooks
|
|
290
292
|
})
|
|
291
293
|
await ensureReleaseBranchReady({rootDir, branchMethod: 'show-current', logStep, logWarning})
|
|
@@ -324,4 +326,5 @@ export async function releasePackagistPackage({
|
|
|
324
326
|
|
|
325
327
|
logSuccess?.(`Release workflow completed for ${composer.name}@${updatedComposer.version}.`)
|
|
326
328
|
logStep?.('Note: Packagist will automatically detect the new git tag and update the package.')
|
|
329
|
+
return updatedComposer
|
|
327
330
|
}
|
package/src/cli/options.mjs
CHANGED
|
@@ -35,13 +35,17 @@ export function parseCliOptions(args = process.argv.slice(2)) {
|
|
|
35
35
|
.exitOverride()
|
|
36
36
|
.option('--type <type>', 'Workflow type (node|vue|packagist). Omit for normal app deployments.')
|
|
37
37
|
.option('--non-interactive', 'Fail instead of prompting when Zephyr needs user input.')
|
|
38
|
+
.option('--then-deploy <path>', 'After a node/vue package release, update and deploy a local consumer app repo.')
|
|
39
|
+
.option('--consumer-package <name>', 'Package name to update in the --then-deploy consumer. Defaults to the released package name.')
|
|
40
|
+
.option('--consumer-preset <name>', 'Preset name to use for the --then-deploy consumer app deployment.')
|
|
41
|
+
.option('--consumer-maintenance <mode>', 'Laravel maintenance mode policy for the --then-deploy consumer app deployment (on|off).')
|
|
38
42
|
.option('--json', 'Emit NDJSON events to stdout. Requires --non-interactive.')
|
|
39
43
|
.option('--setup', 'Configure an app deployment target and verify SSH connectivity without deploying.')
|
|
40
44
|
.option('--preset <name>', 'Preset name to use for non-interactive app deployments.')
|
|
41
45
|
.option('--resume-pending', 'Resume a saved pending deployment snapshot without prompting.')
|
|
42
46
|
.option('--discard-pending', 'Discard a saved pending deployment snapshot without prompting.')
|
|
43
47
|
.option('--maintenance <mode>', 'Laravel maintenance mode policy for app deployments (on|off).')
|
|
44
|
-
.option('--auto-commit', 'Automatically commit dirty
|
|
48
|
+
.option('--auto-commit', 'Automatically commit dirty changes with a Codex-generated message.')
|
|
45
49
|
.option('--skip-versioning', 'Skip updating package/composer version files before continuing.')
|
|
46
50
|
.option('--skip-git-hooks', 'Bypass local git hooks for any commits and pushes Zephyr performs.')
|
|
47
51
|
.option('--skip-checks', 'Skip Zephyr local lint and test execution.')
|
|
@@ -49,6 +53,12 @@ export function parseCliOptions(args = process.argv.slice(2)) {
|
|
|
49
53
|
.option('--skip-lint', 'Skip Zephyr local lint execution in package release and app deployment workflows.')
|
|
50
54
|
.option('--skip-build', 'Skip build execution in node/vue release workflows.')
|
|
51
55
|
.option('--skip-deploy', 'Skip GitHub Pages deployment in node/vue release workflows.')
|
|
56
|
+
.option('--consumer-skip-checks', 'Skip Zephyr local lint and test execution in the --then-deploy consumer deployment.')
|
|
57
|
+
.option('--consumer-skip-tests', 'Skip Zephyr local test execution in the --then-deploy consumer deployment.')
|
|
58
|
+
.option('--consumer-skip-lint', 'Skip Zephyr local lint execution in the --then-deploy consumer deployment.')
|
|
59
|
+
.option('--consumer-skip-versioning', 'Skip local version bumping in the --then-deploy consumer deployment.')
|
|
60
|
+
.option('--consumer-skip-git-hooks', 'Bypass local git hooks for commits and pushes in the --then-deploy consumer repo.')
|
|
61
|
+
.option('--consumer-auto-commit', 'Automatically commit dirty deploy changes in the --then-deploy consumer repo.')
|
|
52
62
|
.argument(
|
|
53
63
|
'[version]',
|
|
54
64
|
'Version or npm bump type for deployments (e.g. 1.2.3, patch, minor, major).'
|
|
@@ -63,6 +73,7 @@ export function parseCliOptions(args = process.argv.slice(2)) {
|
|
|
63
73
|
const options = program.opts()
|
|
64
74
|
const workflowType = options.type ?? null
|
|
65
75
|
const explicitSkipChecks = hasFlag(args, '--skip-checks')
|
|
76
|
+
const explicitConsumerSkipChecks = hasFlag(args, '--consumer-skip-checks')
|
|
66
77
|
|
|
67
78
|
if (workflowType && !WORKFLOW_TYPES.has(workflowType)) {
|
|
68
79
|
throw new InvalidCliOptionsError('Invalid value for --type. Use one of: node, vue, packagist.')
|
|
@@ -72,6 +83,7 @@ export function parseCliOptions(args = process.argv.slice(2)) {
|
|
|
72
83
|
workflowType,
|
|
73
84
|
versionArg: program.args[0] ?? null,
|
|
74
85
|
nonInteractive: Boolean(options.nonInteractive),
|
|
86
|
+
thenDeploy: options.thenDeploy ?? null,
|
|
75
87
|
json: Boolean(options.json),
|
|
76
88
|
setup: Boolean(options.setup),
|
|
77
89
|
presetName: options.preset ?? null,
|
|
@@ -86,13 +98,29 @@ export function parseCliOptions(args = process.argv.slice(2)) {
|
|
|
86
98
|
skipLint: Boolean(options.skipLint || options.skipChecks),
|
|
87
99
|
skipBuild: Boolean(options.skipBuild),
|
|
88
100
|
skipDeploy: Boolean(options.skipDeploy),
|
|
101
|
+
consumerPackage: options.consumerPackage ?? null,
|
|
102
|
+
consumerPresetName: options.consumerPreset ?? null,
|
|
103
|
+
consumerMaintenanceMode: normalizeMaintenanceMode(options.consumerMaintenance),
|
|
104
|
+
consumerSkipChecks: Boolean(options.consumerSkipChecks),
|
|
105
|
+
consumerSkipTests: Boolean(options.consumerSkipTests || options.consumerSkipChecks),
|
|
106
|
+
consumerSkipLint: Boolean(options.consumerSkipLint || options.consumerSkipChecks),
|
|
107
|
+
consumerSkipVersioning: Boolean(options.consumerSkipVersioning),
|
|
108
|
+
consumerSkipGitHooks: Boolean(options.consumerSkipGitHooks),
|
|
109
|
+
consumerAutoCommit: Boolean(options.consumerAutoCommit),
|
|
89
110
|
explicitMaintenanceMode: hasFlag(args, '--maintenance'),
|
|
90
111
|
explicitAutoCommit: hasFlag(args, '--auto-commit'),
|
|
91
112
|
explicitSkipVersioning: hasFlag(args, '--skip-versioning'),
|
|
92
113
|
explicitSkipGitHooks: hasFlag(args, '--skip-git-hooks'),
|
|
93
114
|
explicitSkipChecks,
|
|
94
115
|
explicitSkipTests: hasFlag(args, '--skip-tests') || explicitSkipChecks,
|
|
95
|
-
explicitSkipLint: hasFlag(args, '--skip-lint') || explicitSkipChecks
|
|
116
|
+
explicitSkipLint: hasFlag(args, '--skip-lint') || explicitSkipChecks,
|
|
117
|
+
explicitConsumerMaintenanceMode: hasFlag(args, '--consumer-maintenance'),
|
|
118
|
+
explicitConsumerSkipChecks,
|
|
119
|
+
explicitConsumerSkipTests: hasFlag(args, '--consumer-skip-tests') || explicitConsumerSkipChecks,
|
|
120
|
+
explicitConsumerSkipLint: hasFlag(args, '--consumer-skip-lint') || explicitConsumerSkipChecks,
|
|
121
|
+
explicitConsumerSkipVersioning: hasFlag(args, '--consumer-skip-versioning'),
|
|
122
|
+
explicitConsumerSkipGitHooks: hasFlag(args, '--consumer-skip-git-hooks'),
|
|
123
|
+
explicitConsumerAutoCommit: hasFlag(args, '--consumer-auto-commit')
|
|
96
124
|
}
|
|
97
125
|
}
|
|
98
126
|
|
|
@@ -102,6 +130,7 @@ export function validateCliOptions(options = {}) {
|
|
|
102
130
|
nonInteractive = false,
|
|
103
131
|
json = false,
|
|
104
132
|
setup = false,
|
|
133
|
+
thenDeploy = null,
|
|
105
134
|
presetName = null,
|
|
106
135
|
resumePending = false,
|
|
107
136
|
discardPending = false,
|
|
@@ -113,7 +142,16 @@ export function validateCliOptions(options = {}) {
|
|
|
113
142
|
skipLint = false,
|
|
114
143
|
skipBuild = false,
|
|
115
144
|
skipDeploy = false,
|
|
116
|
-
versionArg = null
|
|
145
|
+
versionArg = null,
|
|
146
|
+
consumerPackage = null,
|
|
147
|
+
consumerPresetName = null,
|
|
148
|
+
consumerMaintenanceMode = null,
|
|
149
|
+
consumerSkipChecks = false,
|
|
150
|
+
consumerSkipTests = false,
|
|
151
|
+
consumerSkipLint = false,
|
|
152
|
+
consumerSkipVersioning = false,
|
|
153
|
+
consumerSkipGitHooks = false,
|
|
154
|
+
consumerAutoCommit = false
|
|
117
155
|
} = options
|
|
118
156
|
|
|
119
157
|
if (json && !nonInteractive) {
|
|
@@ -125,6 +163,31 @@ export function validateCliOptions(options = {}) {
|
|
|
125
163
|
}
|
|
126
164
|
|
|
127
165
|
const isPackageRelease = workflowType === 'node' || workflowType === 'vue' || workflowType === 'packagist'
|
|
166
|
+
const isNodePackageRelease = workflowType === 'node' || workflowType === 'vue'
|
|
167
|
+
const hasConsumerOptions = Boolean(
|
|
168
|
+
thenDeploy ||
|
|
169
|
+
consumerPackage ||
|
|
170
|
+
consumerPresetName ||
|
|
171
|
+
consumerMaintenanceMode !== null ||
|
|
172
|
+
consumerSkipChecks ||
|
|
173
|
+
consumerSkipTests ||
|
|
174
|
+
consumerSkipLint ||
|
|
175
|
+
consumerSkipVersioning ||
|
|
176
|
+
consumerSkipGitHooks ||
|
|
177
|
+
consumerAutoCommit
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if (hasConsumerOptions && !thenDeploy) {
|
|
181
|
+
throw new InvalidCliOptionsError('--consumer-* options require --then-deploy <path>.')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (thenDeploy && !isNodePackageRelease) {
|
|
185
|
+
throw new InvalidCliOptionsError('--then-deploy is only valid for node/vue package release workflows.')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (thenDeploy && !consumerPresetName) {
|
|
189
|
+
throw new InvalidCliOptionsError('--then-deploy requires --consumer-preset <name>.')
|
|
190
|
+
}
|
|
128
191
|
|
|
129
192
|
if (isPackageRelease) {
|
|
130
193
|
if (setup) {
|
|
@@ -142,10 +205,6 @@ export function validateCliOptions(options = {}) {
|
|
|
142
205
|
if (maintenanceMode !== null) {
|
|
143
206
|
throw new InvalidCliOptionsError('--maintenance is only valid for app deployments.')
|
|
144
207
|
}
|
|
145
|
-
|
|
146
|
-
if (autoCommit) {
|
|
147
|
-
throw new InvalidCliOptionsError('--auto-commit is only valid for app deployments.')
|
|
148
|
-
}
|
|
149
208
|
} else {
|
|
150
209
|
if (skipBuild || skipDeploy) {
|
|
151
210
|
throw new InvalidCliOptionsError('--skip-build and --skip-deploy are only valid for node/vue release workflows.')
|
package/src/main.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import {selectDeploymentTarget} from './application/configuration/select-deploym
|
|
|
18
18
|
import {resolvePendingSnapshot} from './application/deploy/resolve-pending-snapshot.mjs'
|
|
19
19
|
import {runDeployment} from './application/deploy/run-deployment.mjs'
|
|
20
20
|
import {assertLaravelSetupProject} from './application/deploy/verify-laravel-setup.mjs'
|
|
21
|
+
import {releasePackageThenDeployConsumer} from './application/consumer/release-package-then-deploy-consumer.mjs'
|
|
21
22
|
import {SKIP_GIT_HOOKS_WARNING} from './utils/git-hooks.mjs'
|
|
22
23
|
import {notifyWorkflowResult} from './utils/notifications.mjs'
|
|
23
24
|
|
|
@@ -32,6 +33,7 @@ function normalizeMainOptions(firstArg = null, secondArg = null) {
|
|
|
32
33
|
workflowType: firstArg.workflowType ?? firstArg.type ?? null,
|
|
33
34
|
versionArg: firstArg.versionArg ?? null,
|
|
34
35
|
nonInteractive: firstArg.nonInteractive === true,
|
|
36
|
+
thenDeploy: firstArg.thenDeploy ?? null,
|
|
35
37
|
json: firstArg.json === true,
|
|
36
38
|
setup: firstArg.setup === true,
|
|
37
39
|
presetName: firstArg.presetName ?? null,
|
|
@@ -46,6 +48,22 @@ function normalizeMainOptions(firstArg = null, secondArg = null) {
|
|
|
46
48
|
skipLint: firstArg.skipLint === true || firstArg.skipChecks === true,
|
|
47
49
|
skipBuild: firstArg.skipBuild === true,
|
|
48
50
|
skipDeploy: firstArg.skipDeploy === true,
|
|
51
|
+
consumerPackage: firstArg.consumerPackage ?? null,
|
|
52
|
+
consumerPresetName: firstArg.consumerPresetName ?? null,
|
|
53
|
+
consumerMaintenanceMode: firstArg.consumerMaintenanceMode ?? null,
|
|
54
|
+
consumerSkipChecks: firstArg.consumerSkipChecks === true,
|
|
55
|
+
consumerSkipTests: firstArg.consumerSkipTests === true || firstArg.consumerSkipChecks === true,
|
|
56
|
+
consumerSkipLint: firstArg.consumerSkipLint === true || firstArg.consumerSkipChecks === true,
|
|
57
|
+
consumerSkipVersioning: firstArg.consumerSkipVersioning === true,
|
|
58
|
+
consumerSkipGitHooks: firstArg.consumerSkipGitHooks === true,
|
|
59
|
+
consumerAutoCommit: firstArg.consumerAutoCommit === true,
|
|
60
|
+
explicitConsumerMaintenanceMode: firstArg.explicitConsumerMaintenanceMode === true || 'consumerMaintenanceMode' in firstArg,
|
|
61
|
+
explicitConsumerSkipChecks: firstArg.explicitConsumerSkipChecks === true || 'consumerSkipChecks' in firstArg,
|
|
62
|
+
explicitConsumerSkipTests: firstArg.explicitConsumerSkipTests === true || 'consumerSkipTests' in firstArg || 'consumerSkipChecks' in firstArg,
|
|
63
|
+
explicitConsumerSkipLint: firstArg.explicitConsumerSkipLint === true || 'consumerSkipLint' in firstArg || 'consumerSkipChecks' in firstArg,
|
|
64
|
+
explicitConsumerSkipVersioning: firstArg.explicitConsumerSkipVersioning === true || 'consumerSkipVersioning' in firstArg,
|
|
65
|
+
explicitConsumerSkipGitHooks: firstArg.explicitConsumerSkipGitHooks === true || 'consumerSkipGitHooks' in firstArg,
|
|
66
|
+
explicitConsumerAutoCommit: firstArg.explicitConsumerAutoCommit === true || 'consumerAutoCommit' in firstArg,
|
|
49
67
|
explicitMaintenanceMode: firstArg.explicitMaintenanceMode === true || 'maintenanceMode' in firstArg,
|
|
50
68
|
explicitAutoCommit: firstArg.explicitAutoCommit === true || 'autoCommit' in firstArg,
|
|
51
69
|
explicitSkipVersioning: firstArg.explicitSkipVersioning === true || 'skipVersioning' in firstArg,
|
|
@@ -61,6 +79,7 @@ function normalizeMainOptions(firstArg = null, secondArg = null) {
|
|
|
61
79
|
workflowType: firstArg ?? null,
|
|
62
80
|
versionArg: secondArg ?? null,
|
|
63
81
|
nonInteractive: false,
|
|
82
|
+
thenDeploy: null,
|
|
64
83
|
json: false,
|
|
65
84
|
setup: false,
|
|
66
85
|
presetName: null,
|
|
@@ -75,6 +94,22 @@ function normalizeMainOptions(firstArg = null, secondArg = null) {
|
|
|
75
94
|
skipLint: false,
|
|
76
95
|
skipBuild: false,
|
|
77
96
|
skipDeploy: false,
|
|
97
|
+
consumerPackage: null,
|
|
98
|
+
consumerPresetName: null,
|
|
99
|
+
consumerMaintenanceMode: null,
|
|
100
|
+
consumerSkipChecks: false,
|
|
101
|
+
consumerSkipTests: false,
|
|
102
|
+
consumerSkipLint: false,
|
|
103
|
+
consumerSkipVersioning: false,
|
|
104
|
+
consumerSkipGitHooks: false,
|
|
105
|
+
consumerAutoCommit: false,
|
|
106
|
+
explicitConsumerMaintenanceMode: false,
|
|
107
|
+
explicitConsumerSkipChecks: false,
|
|
108
|
+
explicitConsumerSkipTests: false,
|
|
109
|
+
explicitConsumerSkipLint: false,
|
|
110
|
+
explicitConsumerSkipVersioning: false,
|
|
111
|
+
explicitConsumerSkipGitHooks: false,
|
|
112
|
+
explicitConsumerAutoCommit: false,
|
|
78
113
|
explicitMaintenanceMode: false,
|
|
79
114
|
explicitAutoCommit: false,
|
|
80
115
|
explicitSkipVersioning: false,
|
|
@@ -209,8 +244,9 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
209
244
|
}
|
|
210
245
|
|
|
211
246
|
if (options.workflowType === 'node' || options.workflowType === 'vue') {
|
|
212
|
-
await releaseNode({
|
|
247
|
+
const releasedPackage = await releaseNode({
|
|
213
248
|
releaseType: options.versionArg,
|
|
249
|
+
autoCommit: options.autoCommit,
|
|
214
250
|
skipGitHooks: options.skipGitHooks,
|
|
215
251
|
skipTests: options.skipTests,
|
|
216
252
|
skipLint: options.skipLint,
|
|
@@ -219,6 +255,32 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
219
255
|
skipDeploy: options.skipDeploy,
|
|
220
256
|
context: appContext
|
|
221
257
|
})
|
|
258
|
+
|
|
259
|
+
if (options.thenDeploy) {
|
|
260
|
+
await releasePackageThenDeployConsumer({
|
|
261
|
+
producerRootDir: rootDir,
|
|
262
|
+
consumerRootDir: options.thenDeploy,
|
|
263
|
+
releasedPackage,
|
|
264
|
+
packageName: options.consumerPackage,
|
|
265
|
+
presetName: options.consumerPresetName,
|
|
266
|
+
maintenanceMode: options.consumerMaintenanceMode,
|
|
267
|
+
skipChecks: options.consumerSkipChecks,
|
|
268
|
+
skipTests: options.consumerSkipTests,
|
|
269
|
+
skipLint: options.consumerSkipLint,
|
|
270
|
+
skipVersioning: options.consumerSkipVersioning,
|
|
271
|
+
skipGitHooks: options.consumerSkipGitHooks,
|
|
272
|
+
autoCommit: options.consumerAutoCommit,
|
|
273
|
+
explicitMaintenanceMode: options.explicitConsumerMaintenanceMode,
|
|
274
|
+
explicitSkipChecks: options.explicitConsumerSkipChecks,
|
|
275
|
+
explicitSkipTests: options.explicitConsumerSkipTests,
|
|
276
|
+
explicitSkipLint: options.explicitConsumerSkipLint,
|
|
277
|
+
explicitSkipVersioning: options.explicitConsumerSkipVersioning,
|
|
278
|
+
explicitSkipGitHooks: options.explicitConsumerSkipGitHooks,
|
|
279
|
+
explicitAutoCommit: options.explicitConsumerAutoCommit,
|
|
280
|
+
json: currentExecutionMode.json
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
222
284
|
emitEvent?.('run_completed', {
|
|
223
285
|
message: 'Zephyr workflow completed successfully.',
|
|
224
286
|
data: {
|
|
@@ -240,12 +302,14 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
240
302
|
if (options.workflowType === 'packagist') {
|
|
241
303
|
await releasePackagist({
|
|
242
304
|
releaseType: options.versionArg,
|
|
305
|
+
autoCommit: options.autoCommit,
|
|
243
306
|
skipGitHooks: options.skipGitHooks,
|
|
244
307
|
skipTests: options.skipTests,
|
|
245
308
|
skipLint: options.skipLint,
|
|
246
309
|
skipVersioning: options.skipVersioning,
|
|
247
310
|
context: appContext
|
|
248
311
|
})
|
|
312
|
+
|
|
249
313
|
emitEvent?.('run_completed', {
|
|
250
314
|
message: 'Zephyr workflow completed successfully.',
|
|
251
315
|
data: {
|
package/src/release/shared.mjs
CHANGED
|
@@ -94,6 +94,7 @@ export async function ensureCleanWorkingTree(rootDir = process.cwd(), {
|
|
|
94
94
|
logSuccess,
|
|
95
95
|
logWarning,
|
|
96
96
|
interactive = true,
|
|
97
|
+
autoCommit = false,
|
|
97
98
|
skipGitHooks = false,
|
|
98
99
|
suggestCommitMessage = suggestReleaseCommitMessage
|
|
99
100
|
} = {}) {
|
|
@@ -107,7 +108,7 @@ export async function ensureCleanWorkingTree(rootDir = process.cwd(), {
|
|
|
107
108
|
return
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
if (!interactive || typeof runPrompt !== 'function') {
|
|
111
|
+
if (!autoCommit && (!interactive || typeof runPrompt !== 'function')) {
|
|
111
112
|
throw new Error(DIRTY_WORKING_TREE_MESSAGE)
|
|
112
113
|
}
|
|
113
114
|
|
|
@@ -117,24 +118,34 @@ export async function ensureCleanWorkingTree(rootDir = process.cwd(), {
|
|
|
117
118
|
logWarning,
|
|
118
119
|
statusEntries
|
|
119
120
|
})
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
message
|
|
125
|
-
'Pending changes detected before release:\n\n' +
|
|
126
|
-
`${formatWorkingTreePreview(statusEntries)}\n\n` +
|
|
127
|
-
'Enter a commit message to stage and commit all current changes before continuing.\n' +
|
|
128
|
-
'Leave blank to cancel.',
|
|
129
|
-
default: suggestedCommitMessage ?? ''
|
|
121
|
+
let message = suggestedCommitMessage?.trim() ?? ''
|
|
122
|
+
|
|
123
|
+
if (autoCommit) {
|
|
124
|
+
if (!message) {
|
|
125
|
+
throw new Error('Release auto-commit failed because Codex could not determine a usable commit message.')
|
|
130
126
|
}
|
|
131
|
-
])
|
|
132
127
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
logStep?.(`Auto-commit enabled. Using Codex-generated commit message "${message}".`)
|
|
129
|
+
} else {
|
|
130
|
+
const {commitMessage} = await runPrompt([
|
|
131
|
+
{
|
|
132
|
+
type: 'input',
|
|
133
|
+
name: 'commitMessage',
|
|
134
|
+
message:
|
|
135
|
+
'Pending changes detected before release:\n\n' +
|
|
136
|
+
`${formatWorkingTreePreview(statusEntries)}\n\n` +
|
|
137
|
+
'Enter a commit message to stage and commit all current changes before continuing.\n' +
|
|
138
|
+
'Leave blank to cancel.',
|
|
139
|
+
default: message
|
|
140
|
+
}
|
|
141
|
+
])
|
|
142
|
+
|
|
143
|
+
if (!commitMessage || commitMessage.trim().length === 0) {
|
|
144
|
+
throw new Error(DIRTY_WORKING_TREE_CANCELLED_MESSAGE)
|
|
145
|
+
}
|
|
136
146
|
|
|
137
|
-
|
|
147
|
+
message = commitMessage.trim()
|
|
148
|
+
}
|
|
138
149
|
|
|
139
150
|
logStep?.('Staging all pending changes before release...')
|
|
140
151
|
await runCommand('git', ['add', '-A'], {
|
package/src/release-node.mjs
CHANGED
|
@@ -10,6 +10,7 @@ function hasExplicitReleaseOptions(options = {}) {
|
|
|
10
10
|
'skipTests',
|
|
11
11
|
'skipLint',
|
|
12
12
|
'skipVersioning',
|
|
13
|
+
'autoCommit',
|
|
13
14
|
'skipBuild',
|
|
14
15
|
'skipDeploy'
|
|
15
16
|
].some((key) => key in options)
|
|
@@ -23,11 +24,12 @@ export async function releaseNode(options = {}) {
|
|
|
23
24
|
skipTests: options.skipTests === true,
|
|
24
25
|
skipLint: options.skipLint === true,
|
|
25
26
|
skipVersioning: options.skipVersioning === true,
|
|
27
|
+
autoCommit: options.autoCommit === true,
|
|
26
28
|
skipBuild: options.skipBuild === true,
|
|
27
29
|
skipDeploy: options.skipDeploy === true
|
|
28
30
|
}
|
|
29
31
|
: parseReleaseArgs({
|
|
30
|
-
booleanFlags: ['--skip-git-hooks', '--skip-tests', '--skip-lint', '--skip-versioning', '--skip-build', '--skip-deploy']
|
|
32
|
+
booleanFlags: ['--skip-git-hooks', '--auto-commit', '--skip-tests', '--skip-lint', '--skip-versioning', '--skip-build', '--skip-deploy']
|
|
31
33
|
})
|
|
32
34
|
|
|
33
35
|
if (parsed.skipVersioning && parsed.releaseType) {
|
|
@@ -43,10 +45,11 @@ export async function releaseNode(options = {}) {
|
|
|
43
45
|
})
|
|
44
46
|
const {logProcessing: logStep, logSuccess, logWarning, runPrompt, runCommand, runCommandCapture, executionMode} = context
|
|
45
47
|
|
|
46
|
-
await releaseNodePackage({
|
|
48
|
+
return await releaseNodePackage({
|
|
47
49
|
releaseType: parsed.releaseType,
|
|
48
50
|
skipGitHooks: parsed.skipGitHooks === true || executionMode?.skipGitHooks === true,
|
|
49
51
|
skipTests: parsed.skipTests === true,
|
|
52
|
+
autoCommit: parsed.autoCommit === true,
|
|
50
53
|
skipLint: parsed.skipLint === true,
|
|
51
54
|
skipVersioning: parsed.skipVersioning === true,
|
|
52
55
|
skipBuild: parsed.skipBuild === true,
|
|
@@ -9,6 +9,7 @@ function hasExplicitReleaseOptions(options = {}) {
|
|
|
9
9
|
'skipGitHooks',
|
|
10
10
|
'skipTests',
|
|
11
11
|
'skipLint',
|
|
12
|
+
'autoCommit',
|
|
12
13
|
'skipVersioning'
|
|
13
14
|
].some((key) => key in options)
|
|
14
15
|
}
|
|
@@ -20,10 +21,11 @@ export async function releasePackagist(options = {}) {
|
|
|
20
21
|
skipGitHooks: options.skipGitHooks === true,
|
|
21
22
|
skipTests: options.skipTests === true,
|
|
22
23
|
skipLint: options.skipLint === true,
|
|
24
|
+
autoCommit: options.autoCommit === true,
|
|
23
25
|
skipVersioning: options.skipVersioning === true
|
|
24
26
|
}
|
|
25
27
|
: parseReleaseArgs({
|
|
26
|
-
booleanFlags: ['--skip-git-hooks', '--skip-tests', '--skip-lint', '--skip-versioning']
|
|
28
|
+
booleanFlags: ['--skip-git-hooks', '--auto-commit', '--skip-tests', '--skip-lint', '--skip-versioning']
|
|
27
29
|
})
|
|
28
30
|
|
|
29
31
|
if (parsed.skipVersioning && parsed.releaseType) {
|
|
@@ -39,9 +41,10 @@ export async function releasePackagist(options = {}) {
|
|
|
39
41
|
})
|
|
40
42
|
const {logProcessing: logStep, logSuccess, logWarning, runPrompt, runCommand, runCommandCapture, executionMode} = context
|
|
41
43
|
|
|
42
|
-
await releasePackagistPackage({
|
|
44
|
+
return await releasePackagistPackage({
|
|
43
45
|
releaseType: parsed.releaseType,
|
|
44
46
|
skipGitHooks: parsed.skipGitHooks === true || executionMode?.skipGitHooks === true,
|
|
47
|
+
autoCommit: parsed.autoCommit === true,
|
|
45
48
|
skipTests: parsed.skipTests === true,
|
|
46
49
|
skipLint: parsed.skipLint === true,
|
|
47
50
|
skipVersioning: parsed.skipVersioning === true,
|