@wyxos/zephyr 0.4.1 → 0.4.3

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 CHANGED
@@ -87,6 +87,8 @@ If Zephyr would normally prompt to:
87
87
 
88
88
  then non-interactive mode stops immediately with a clear error instead.
89
89
 
90
+ For Laravel app deployments, `--maintenance on|off` overrides the maintenance prompt when you want an explicit choice instead of an interactive confirm.
91
+
90
92
  ## AI Agents and Automation
91
93
 
92
94
  Zephyr can be used safely by Codex, CI jobs, or other automation once configuration is already in place.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyxos/zephyr",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "A streamlined deployment tool for web applications with intelligent Laravel project detection",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -253,17 +253,17 @@ async function resolveMaintenanceMode({
253
253
  return snapshot.maintenanceModeEnabled
254
254
  }
255
255
 
256
- if (executionMode?.interactive === false) {
257
- if (typeof executionMode.maintenanceMode !== 'boolean') {
258
- throw new ZephyrError(
259
- 'Zephyr cannot run this Laravel deployment non-interactively without an explicit maintenance-mode decision. Pass --maintenance on or --maintenance off.',
260
- {code: 'ZEPHYR_MAINTENANCE_FLAG_REQUIRED'}
261
- )
262
- }
263
-
256
+ if (typeof executionMode.maintenanceMode === 'boolean') {
264
257
  return executionMode.maintenanceMode
265
258
  }
266
259
 
260
+ if (executionMode?.interactive === false) {
261
+ throw new ZephyrError(
262
+ 'Zephyr cannot run this Laravel deployment non-interactively without an explicit maintenance-mode decision. Pass --maintenance on or --maintenance off.',
263
+ {code: 'ZEPHYR_MAINTENANCE_FLAG_REQUIRED'}
264
+ )
265
+ }
266
+
267
267
  if (typeof runPrompt !== 'function') {
268
268
  return false
269
269
  }
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
 
4
4
  import {commandExists} from '../../utils/command.mjs'
5
+ import {gitCommitArgs} from '../../utils/git-hooks.mjs'
5
6
 
6
7
  async function readPackageJson(rootDir) {
7
8
  const packageJsonPath = path.join(rootDir, 'package.json')
@@ -20,6 +21,7 @@ async function isGitIgnored(rootDir, filePath, {runCommand} = {}) {
20
21
 
21
22
  export async function bumpLocalPackageVersion(rootDir, {
22
23
  versionArg = null,
24
+ skipGitHooks = false,
23
25
  runCommand,
24
26
  logProcessing,
25
27
  logSuccess,
@@ -72,7 +74,9 @@ export async function bumpLocalPackageVersion(rootDir, {
72
74
  return updatedPkg
73
75
  }
74
76
 
75
- await runCommand('git', ['commit', '-m', `chore: bump version to ${nextVersion}`, '--', ...filesToStage], {
77
+ await runCommand('git', gitCommitArgs(['-m', `chore: bump version to ${nextVersion}`, '--', ...filesToStage], {
78
+ skipGitHooks
79
+ }), {
76
80
  cwd: rootDir
77
81
  })
78
82
  logSuccess?.(`Version updated to ${nextVersion}.`)
@@ -9,6 +9,7 @@ export async function prepareLocalDeployment(config, {
9
9
  snapshot = null,
10
10
  rootDir = process.cwd(),
11
11
  versionArg = null,
12
+ skipGitHooks = false,
12
13
  runPrompt,
13
14
  runCommand,
14
15
  runCommandCapture,
@@ -26,6 +27,7 @@ export async function prepareLocalDeployment(config, {
26
27
  if (!snapshot && context.isLaravel) {
27
28
  await bumpLocalPackageVersion(rootDir, {
28
29
  versionArg,
30
+ skipGitHooks,
29
31
  runCommand,
30
32
  logProcessing,
31
33
  logSuccess,
@@ -39,13 +41,15 @@ export async function prepareLocalDeployment(config, {
39
41
  runCommandCapture,
40
42
  logProcessing,
41
43
  logSuccess,
42
- logWarning
44
+ logWarning,
45
+ skipGitHooks
43
46
  })
44
47
 
45
48
  await runLocalDeploymentChecks({
46
49
  rootDir,
47
50
  isLaravel: context.isLaravel,
48
51
  hasHook: context.hasHook,
52
+ skipGitHooks,
49
53
  runCommand,
50
54
  runCommandCapture,
51
55
  logProcessing,
@@ -179,6 +179,7 @@ export async function runDeployment(config, options = {}) {
179
179
  snapshot,
180
180
  rootDir,
181
181
  versionArg,
182
+ skipGitHooks: executionMode?.skipGitHooks === true,
182
183
  runPrompt,
183
184
  runCommand,
184
185
  runCommandCapture: context.runCommandCapture,
@@ -85,6 +85,7 @@ export async function runLocalDeploymentChecks({
85
85
  rootDir,
86
86
  isLaravel,
87
87
  hasHook,
88
+ skipGitHooks = false,
88
89
  runCommand,
89
90
  runCommandCapture,
90
91
  logProcessing,
@@ -102,9 +103,15 @@ export async function runLocalDeploymentChecks({
102
103
  })
103
104
 
104
105
  if (hasHook) {
105
- logProcessing?.(
106
- 'Pre-push git hook detected. Built-in release checks are supported, but Zephyr will skip executing them here. If Zephyr pushes local commits during this release, the hook will run during git push.'
107
- )
106
+ if (skipGitHooks) {
107
+ logWarning?.(
108
+ 'Pre-push git hook detected. Built-in release checks are supported, and Zephyr will skip executing them here. WARNING: --skip-git-hooks is enabled, so Zephyr will also bypass that hook if it needs to push local commits during this release.'
109
+ )
110
+ } else {
111
+ logProcessing?.(
112
+ 'Pre-push git hook detected. Built-in release checks are supported, but Zephyr will skip executing them here. If Zephyr pushes local commits during this release, the hook will run during git push.'
113
+ )
114
+ }
108
115
  return
109
116
  }
110
117
 
@@ -124,7 +131,8 @@ export async function runLocalDeploymentChecks({
124
131
  getGitStatus: (dir) => getGitStatus(dir, {runCommandCapture}),
125
132
  runCommand,
126
133
  logProcessing,
127
- logSuccess
134
+ logSuccess,
135
+ skipGitHooks
128
136
  })
129
137
  }
130
138
  }
@@ -11,6 +11,7 @@ import {
11
11
  runReleaseCommand,
12
12
  validateReleaseDependencies
13
13
  } from '../../release/shared.mjs'
14
+ import {gitCommitArgs, gitPushArgs, npmVersionArgs} from '../../utils/git-hooks.mjs'
14
15
 
15
16
  async function readPackage(rootDir = process.cwd()) {
16
17
  const packagePath = join(rootDir, 'package.json')
@@ -137,7 +138,8 @@ async function runLibBuild(skipBuild, pkg, rootDir = process.cwd(), {
137
138
  logStep,
138
139
  logSuccess,
139
140
  logWarning,
140
- runCommand = runReleaseCommand
141
+ runCommand = runReleaseCommand,
142
+ skipGitHooks = false
141
143
  } = {}) {
142
144
  if (skipBuild) {
143
145
  logWarning?.('Skipping library build because --skip-build flag was provided.')
@@ -173,7 +175,7 @@ async function runLibBuild(skipBuild, pkg, rootDir = process.cwd(), {
173
175
  if (hasLibChanges) {
174
176
  logStep?.('Committing lib build artifacts...')
175
177
  await runCommand('git', ['add', 'lib/'], {capture: true, cwd: rootDir})
176
- await runCommand('git', ['commit', '-m', 'chore: build lib artifacts'], {capture: true, cwd: rootDir})
178
+ await runCommand('git', gitCommitArgs(['-m', 'chore: build lib artifacts'], {skipGitHooks}), {capture: true, cwd: rootDir})
177
179
  logSuccess?.('Lib build artifacts committed.')
178
180
  }
179
181
 
@@ -183,7 +185,8 @@ async function runLibBuild(skipBuild, pkg, rootDir = process.cwd(), {
183
185
  async function bumpVersion(releaseType, rootDir = process.cwd(), {
184
186
  logStep,
185
187
  logSuccess,
186
- runCommand = runReleaseCommand
188
+ runCommand = runReleaseCommand,
189
+ skipGitHooks = false
187
190
  } = {}) {
188
191
  logStep?.('Bumping package version...')
189
192
 
@@ -199,7 +202,7 @@ async function bumpVersion(releaseType, rootDir = process.cwd(), {
199
202
  }
200
203
 
201
204
  try {
202
- await runCommand('npm', ['version', releaseType], {capture: true, cwd: rootDir})
205
+ await runCommand('npm', npmVersionArgs(releaseType, {skipGitHooks}), {capture: true, cwd: rootDir})
203
206
  } finally {
204
207
  if (hasLibChanges) {
205
208
  logStep?.('Restoring lib build artifacts...')
@@ -207,14 +210,14 @@ async function bumpVersion(releaseType, rootDir = process.cwd(), {
207
210
  await runCommand('git', ['add', 'lib/'], {capture: true, cwd: rootDir})
208
211
  const {stdout: statusAfter} = await runCommand('git', ['status', '--porcelain'], {capture: true, cwd: rootDir})
209
212
  if (statusAfter.includes('lib/')) {
210
- await runCommand('git', ['commit', '--amend', '--no-edit'], {capture: true, cwd: rootDir})
213
+ await runCommand('git', gitCommitArgs(['--amend', '--no-edit'], {skipGitHooks}), {capture: true, cwd: rootDir})
211
214
  }
212
215
  }
213
216
  }
214
217
 
215
218
  const pkg = await readPackage(rootDir)
216
219
  const commitMessage = `chore: release ${pkg.version}`
217
- await runCommand('git', ['commit', '--amend', '-m', commitMessage], {capture: true, cwd: rootDir})
220
+ await runCommand('git', gitCommitArgs(['--amend', '-m', commitMessage], {skipGitHooks}), {capture: true, cwd: rootDir})
218
221
  await runCommand('git', ['tag', '-fa', `v${pkg.version}`, '-m', `v${pkg.version}`], {capture: true, cwd: rootDir})
219
222
 
220
223
  logSuccess?.(`Version updated to ${pkg.version}.`)
@@ -224,11 +227,12 @@ async function bumpVersion(releaseType, rootDir = process.cwd(), {
224
227
  async function pushChanges(rootDir = process.cwd(), {
225
228
  logStep,
226
229
  logSuccess,
227
- runCommand = runReleaseCommand
230
+ runCommand = runReleaseCommand,
231
+ skipGitHooks = false
228
232
  } = {}) {
229
233
  logStep?.('Pushing commits and tags to origin...')
230
234
  try {
231
- await runCommand('git', ['push', '--follow-tags'], {capture: true, cwd: rootDir})
235
+ await runCommand('git', gitPushArgs(['--follow-tags'], {skipGitHooks}), {capture: true, cwd: rootDir})
232
236
  logSuccess?.('Git push completed.')
233
237
  } catch (error) {
234
238
  if (error.stdout) {
@@ -256,7 +260,8 @@ async function deployGHPages(skipDeploy, pkg, rootDir = process.cwd(), {
256
260
  logStep,
257
261
  logSuccess,
258
262
  logWarning,
259
- runCommand = runReleaseCommand
263
+ runCommand = runReleaseCommand,
264
+ skipGitHooks = false
260
265
  } = {}) {
261
266
  if (skipDeploy) {
262
267
  logWarning?.('Skipping GitHub Pages deployment because --skip-deploy flag was provided.')
@@ -324,8 +329,12 @@ async function deployGHPages(skipDeploy, pkg, rootDir = process.cwd(), {
324
329
  fs.cpSync(distPath, worktreeDir, {recursive: true})
325
330
 
326
331
  await runCommand('git', ['-C', worktreeDir, 'add', '-A'], {capture: true})
327
- await runCommand('git', ['-C', worktreeDir, 'commit', '-m', `deploy: demo ${new Date().toISOString()}`, '--allow-empty'], {capture: true})
328
- await runCommand('git', ['-C', worktreeDir, 'push', '-f', 'origin', 'gh-pages'], {capture: true})
332
+ await runCommand('git', [
333
+ '-C',
334
+ worktreeDir,
335
+ ...gitCommitArgs(['-m', `deploy: demo ${new Date().toISOString()}`, '--allow-empty'], {skipGitHooks})
336
+ ], {capture: true})
337
+ await runCommand('git', ['-C', worktreeDir, ...gitPushArgs(['-f', 'origin', 'gh-pages'], {skipGitHooks})], {capture: true})
329
338
 
330
339
  logSuccess?.('GitHub Pages deployment completed.')
331
340
  } catch (error) {
@@ -341,6 +350,7 @@ async function deployGHPages(skipDeploy, pkg, rootDir = process.cwd(), {
341
350
 
342
351
  export async function releaseNodePackage({
343
352
  releaseType,
353
+ skipGitHooks = false,
344
354
  skipTests = false,
345
355
  skipLint = false,
346
356
  skipBuild = false,
@@ -367,7 +377,8 @@ export async function releaseNodePackage({
367
377
  await validateReleaseDependencies(rootDir, {
368
378
  prompt: runPrompt,
369
379
  logSuccess,
370
- interactive
380
+ interactive,
381
+ skipGitHooks
371
382
  })
372
383
 
373
384
  logStep?.('Checking working tree status...')
@@ -376,12 +387,12 @@ export async function releaseNodePackage({
376
387
 
377
388
  await runLint(skipLint, pkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
378
389
  await runTests(skipTests, pkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
379
- await runLibBuild(skipBuild, pkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
390
+ await runLibBuild(skipBuild, pkg, rootDir, {logStep, logSuccess, logWarning, runCommand, skipGitHooks})
380
391
 
381
- const updatedPkg = await bumpVersion(releaseType, rootDir, {logStep, logSuccess, runCommand})
392
+ const updatedPkg = await bumpVersion(releaseType, rootDir, {logStep, logSuccess, runCommand, skipGitHooks})
382
393
  await runBuild(skipBuild, updatedPkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
383
- await pushChanges(rootDir, {logStep, logSuccess, runCommand})
384
- await deployGHPages(skipDeploy, updatedPkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
394
+ await pushChanges(rootDir, {logStep, logSuccess, runCommand, skipGitHooks})
395
+ await deployGHPages(skipDeploy, updatedPkg, rootDir, {logStep, logSuccess, logWarning, runCommand, skipGitHooks})
385
396
 
386
397
  logStep?.('Publishing will be handled by GitHub Actions via trusted publishing.')
387
398
  logSuccess?.(`Release workflow completed for ${updatedPkg.name}@${updatedPkg.version}.`)
@@ -11,6 +11,7 @@ import {
11
11
  runReleaseCommand,
12
12
  validateReleaseDependencies
13
13
  } from '../../release/shared.mjs'
14
+ import {gitCommitArgs, gitPushArgs} from '../../utils/git-hooks.mjs'
14
15
 
15
16
  async function readComposer(rootDir = process.cwd()) {
16
17
  const composerPath = join(rootDir, 'composer.json')
@@ -162,7 +163,8 @@ async function runTests(skipTests, composer, rootDir = process.cwd(), {
162
163
  async function bumpVersion(releaseType, rootDir = process.cwd(), {
163
164
  logStep,
164
165
  logSuccess,
165
- runCommand = runReleaseCommand
166
+ runCommand = runReleaseCommand,
167
+ skipGitHooks = false
166
168
  } = {}) {
167
169
  logStep?.('Bumping composer version...')
168
170
 
@@ -186,7 +188,7 @@ async function bumpVersion(releaseType, rootDir = process.cwd(), {
186
188
 
187
189
  const commitMessage = `chore: release ${newVersion}`
188
190
  logStep?.('Committing version bump...')
189
- await runCommand('git', ['commit', '-m', commitMessage], {cwd: rootDir})
191
+ await runCommand('git', gitCommitArgs(['-m', commitMessage], {skipGitHooks}), {cwd: rootDir})
190
192
 
191
193
  logStep?.('Creating git tag...')
192
194
  await runCommand('git', ['tag', `v${newVersion}`], {cwd: rootDir})
@@ -198,19 +200,21 @@ async function bumpVersion(releaseType, rootDir = process.cwd(), {
198
200
  async function pushChanges(rootDir = process.cwd(), {
199
201
  logStep,
200
202
  logSuccess,
201
- runCommand = runReleaseCommand
203
+ runCommand = runReleaseCommand,
204
+ skipGitHooks = false
202
205
  } = {}) {
203
206
  logStep?.('Pushing commits to origin...')
204
- await runCommand('git', ['push'], {cwd: rootDir})
207
+ await runCommand('git', gitPushArgs([], {skipGitHooks}), {cwd: rootDir})
205
208
 
206
209
  logStep?.('Pushing tags to origin...')
207
- await runCommand('git', ['push', 'origin', '--tags'], {cwd: rootDir})
210
+ await runCommand('git', gitPushArgs(['origin', '--tags'], {skipGitHooks}), {cwd: rootDir})
208
211
 
209
212
  logSuccess?.('Git push completed.')
210
213
  }
211
214
 
212
215
  export async function releasePackagistPackage({
213
216
  releaseType,
217
+ skipGitHooks = false,
214
218
  skipTests = false,
215
219
  skipLint = false,
216
220
  rootDir = process.cwd(),
@@ -240,7 +244,8 @@ export async function releasePackagistPackage({
240
244
  await validateReleaseDependencies(rootDir, {
241
245
  prompt: runPrompt,
242
246
  logSuccess,
243
- interactive
247
+ interactive,
248
+ skipGitHooks
244
249
  })
245
250
 
246
251
  logStep?.('Checking working tree status...')
@@ -250,8 +255,8 @@ export async function releasePackagistPackage({
250
255
  await runLint(skipLint, rootDir, {logStep, logSuccess, logWarning, runCommand, progressWriter})
251
256
  await runTests(skipTests, composer, rootDir, {logStep, logSuccess, logWarning, runCommand, progressWriter})
252
257
 
253
- const updatedComposer = await bumpVersion(releaseType, rootDir, {logStep, logSuccess, runCommand})
254
- await pushChanges(rootDir, {logStep, logSuccess, runCommand})
258
+ const updatedComposer = await bumpVersion(releaseType, rootDir, {logStep, logSuccess, runCommand, skipGitHooks})
259
+ await pushChanges(rootDir, {logStep, logSuccess, runCommand, skipGitHooks})
255
260
 
256
261
  logSuccess?.(`Release workflow completed for ${composer.name}@${updatedComposer.version}.`)
257
262
  logStep?.('Note: Packagist will automatically detect the new git tag and update the package.')
@@ -34,7 +34,8 @@ export function parseCliOptions(args = process.argv.slice(2)) {
34
34
  .option('--preset <name>', 'Preset name to use for non-interactive app deployments.')
35
35
  .option('--resume-pending', 'Resume a saved pending deployment snapshot without prompting.')
36
36
  .option('--discard-pending', 'Discard a saved pending deployment snapshot without prompting.')
37
- .option('--maintenance <mode>', 'Laravel maintenance mode policy for non-interactive app deploys (on|off).')
37
+ .option('--maintenance <mode>', 'Laravel maintenance mode policy for app deployments (on|off).')
38
+ .option('--skip-git-hooks', 'Bypass local git hooks for any commits and pushes Zephyr performs.')
38
39
  .option('--skip-tests', 'Skip test execution in package release workflows.')
39
40
  .option('--skip-lint', 'Skip lint execution in package release workflows.')
40
41
  .option('--skip-build', 'Skip build execution in node/vue release workflows.')
@@ -66,6 +67,7 @@ export function parseCliOptions(args = process.argv.slice(2)) {
66
67
  resumePending: Boolean(options.resumePending),
67
68
  discardPending: Boolean(options.discardPending),
68
69
  maintenanceMode: normalizeMaintenanceMode(options.maintenance),
70
+ skipGitHooks: Boolean(options.skipGitHooks),
69
71
  skipTests: Boolean(options.skipTests),
70
72
  skipLint: Boolean(options.skipLint),
71
73
  skipBuild: Boolean(options.skipBuild),
@@ -4,6 +4,7 @@ import process from 'node:process'
4
4
  import chalk from 'chalk'
5
5
  import {ZephyrError} from './runtime/errors.mjs'
6
6
  import { runCommand as runCommandBase, runCommandCapture as runCommandCaptureBase } from './utils/command.mjs'
7
+ import {gitCommitArgs} from './utils/git-hooks.mjs'
7
8
 
8
9
  function isLocalPathOutsideRepo(depPath, rootDir) {
9
10
  if (!depPath || typeof depPath !== 'string') {
@@ -227,7 +228,9 @@ async function runCommand(command, args, { cwd = process.cwd(), capture = false
227
228
  }
228
229
 
229
230
 
230
- async function commitDependencyUpdates(rootDir, updatedFiles, promptFn, logFn) {
231
+ async function commitDependencyUpdates(rootDir, updatedFiles, promptFn, logFn, {
232
+ skipGitHooks = false
233
+ } = {}) {
231
234
  try {
232
235
  // Check if we're in a git repository
233
236
  await runCommand('git', ['rev-parse', '--is-inside-work-tree'], { capture: true, cwd: rootDir })
@@ -254,7 +257,7 @@ async function commitDependencyUpdates(rootDir, updatedFiles, promptFn, logFn) {
254
257
  logFn('Committing dependency updates...')
255
258
  }
256
259
 
257
- await runCommand('git', ['commit', '-m', commitMessage, '--', ...updatedFiles], { cwd: rootDir })
260
+ await runCommand('git', gitCommitArgs(['-m', commitMessage, '--', ...updatedFiles], {skipGitHooks}), { cwd: rootDir })
258
261
 
259
262
  if (logFn) {
260
263
  logFn('Dependency updates committed.')
@@ -264,7 +267,8 @@ async function commitDependencyUpdates(rootDir, updatedFiles, promptFn, logFn) {
264
267
  }
265
268
 
266
269
  async function validateLocalDependencies(rootDir, promptFn, logFn = null, {
267
- interactive = true
270
+ interactive = true,
271
+ skipGitHooks = false
268
272
  } = {}) {
269
273
  const packageDeps = await scanPackageJsonDependencies(rootDir)
270
274
  const composerDeps = await scanComposerJsonDependencies(rootDir)
@@ -401,7 +405,9 @@ async function validateLocalDependencies(rootDir, promptFn, logFn = null, {
401
405
 
402
406
  // Commit the changes if any files were updated
403
407
  if (updatedFiles.size > 0) {
404
- const committed = await commitDependencyUpdates(rootDir, Array.from(updatedFiles), promptFn, logFn)
408
+ const committed = await commitDependencyUpdates(rootDir, Array.from(updatedFiles), promptFn, logFn, {
409
+ skipGitHooks
410
+ })
405
411
  if (!committed) {
406
412
  throw new Error(
407
413
  'Release cancelled: dependency updates were applied but were not committed. Commit/stash your changes and rerun.'
@@ -1,5 +1,6 @@
1
1
  import { getCurrentBranch as getCurrentBranchImpl, getUpstreamRef as getUpstreamRefImpl } from '../utils/git.mjs'
2
2
  import {hasPrePushHook} from './preflight.mjs'
3
+ import {gitCommitArgs, gitPushArgs} from '../utils/git-hooks.mjs'
3
4
 
4
5
  export async function getCurrentBranch(rootDir) {
5
6
  const branch = await getCurrentBranchImpl(rootDir)
@@ -165,7 +166,9 @@ async function commitAndPushStagedChanges(targetBranch, rootDir, {
165
166
  runCommand,
166
167
  getGitStatus,
167
168
  logProcessing,
168
- logSuccess
169
+ logSuccess,
170
+ logWarning,
171
+ skipGitHooks = false
169
172
  } = {}) {
170
173
  const { commitMessage } = await runPrompt([
171
174
  {
@@ -179,15 +182,19 @@ async function commitAndPushStagedChanges(targetBranch, rootDir, {
179
182
  const message = commitMessage.trim()
180
183
 
181
184
  logProcessing?.('Committing staged changes before deployment...')
182
- await runCommand('git', ['commit', '-m', message], { cwd: rootDir })
185
+ await runCommand('git', gitCommitArgs(['-m', message], {skipGitHooks}), { cwd: rootDir })
183
186
 
184
187
  const prePushHookPresent = await hasPrePushHook(rootDir)
185
188
  if (prePushHookPresent) {
186
- logProcessing?.('Pre-push git hook detected. Running hook during git push...')
189
+ if (skipGitHooks) {
190
+ logWarning?.('Pre-push git hook detected, but Zephyr will bypass it because --skip-git-hooks was provided.')
191
+ } else {
192
+ logProcessing?.('Pre-push git hook detected. Running hook during git push...')
193
+ }
187
194
  }
188
195
 
189
196
  try {
190
- await runCommand('git', ['push', 'origin', targetBranch], { cwd: rootDir })
197
+ await runCommand('git', gitPushArgs(['origin', targetBranch], {skipGitHooks}), { cwd: rootDir })
191
198
  } catch (error) {
192
199
  if (prePushHookPresent) {
193
200
  throw new Error(`Git push failed while the pre-push hook was running. See hook output above.\n${error.message}`)
@@ -211,6 +218,7 @@ export async function ensureCommittedChangesPushed(targetBranch, rootDir, {
211
218
  logProcessing,
212
219
  logSuccess,
213
220
  logWarning,
221
+ skipGitHooks = false,
214
222
  getUpstreamRef: getUpstreamRefFn = getUpstreamRef,
215
223
  readUpstreamSyncState: readUpstreamSyncStateFn = (branch, dir) =>
216
224
  readUpstreamSyncState(branch, dir, {
@@ -256,11 +264,15 @@ export async function ensureCommittedChangesPushed(targetBranch, rootDir, {
256
264
  const prePushHookPresent = await hasPrePushHookFn(rootDir)
257
265
 
258
266
  if (prePushHookPresent) {
259
- logProcessing?.('Pre-push git hook detected. Running hook during git push...')
267
+ if (skipGitHooks) {
268
+ logWarning?.('Pre-push git hook detected, but Zephyr will bypass it because --skip-git-hooks was provided.')
269
+ } else {
270
+ logProcessing?.('Pre-push git hook detected. Running hook during git push...')
271
+ }
260
272
  }
261
273
 
262
274
  try {
263
- await runCommandCapture('git', ['push', remoteName, `${targetBranch}:${upstreamBranch}`], { cwd: rootDir })
275
+ await runCommandCapture('git', gitPushArgs([remoteName, `${targetBranch}:${upstreamBranch}`], {skipGitHooks}), { cwd: rootDir })
264
276
  } catch (error) {
265
277
  if (prePushHookPresent) {
266
278
  const hookOutput = [error.stdout, error.stderr]
@@ -290,6 +302,7 @@ export async function ensureLocalRepositoryState(targetBranch, rootDir = process
290
302
  logProcessing,
291
303
  logSuccess,
292
304
  logWarning,
305
+ skipGitHooks = false,
293
306
  getCurrentBranch: getCurrentBranchFn = getCurrentBranch,
294
307
  getGitStatus: getGitStatusFn = (dir) => getGitStatus(dir, { runCommandCapture }),
295
308
  readUpstreamSyncState: readUpstreamSyncStateFn = (branch, dir) =>
@@ -304,7 +317,8 @@ export async function ensureLocalRepositoryState(targetBranch, rootDir = process
304
317
  runCommandCapture,
305
318
  logProcessing,
306
319
  logSuccess,
307
- logWarning
320
+ logWarning,
321
+ skipGitHooks
308
322
  })
309
323
  } = {}) {
310
324
  if (!targetBranch) {
@@ -369,7 +383,9 @@ export async function ensureLocalRepositoryState(targetBranch, rootDir = process
369
383
  runCommand,
370
384
  getGitStatus: getGitStatusFn,
371
385
  logProcessing,
372
- logSuccess
386
+ logSuccess,
387
+ logWarning,
388
+ skipGitHooks
373
389
  })
374
390
 
375
391
  await ensureCommittedChangesPushedFn(targetBranch, rootDir)
@@ -1,6 +1,8 @@
1
1
  import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
 
4
+ import {gitCommitArgs} from '../utils/git-hooks.mjs'
5
+
4
6
  export async function hasPrePushHook(rootDir) {
5
7
  const hookPaths = [
6
8
  path.join(rootDir, '.git', 'hooks', 'pre-push'),
@@ -126,7 +128,13 @@ export function hasStagedChanges(statusOutput) {
126
128
  })
127
129
  }
128
130
 
129
- export async function commitLintingChanges(rootDir, { getGitStatus, runCommand, logProcessing, logSuccess } = {}) {
131
+ export async function commitLintingChanges(rootDir, {
132
+ getGitStatus,
133
+ runCommand,
134
+ logProcessing,
135
+ logSuccess,
136
+ skipGitHooks = false
137
+ } = {}) {
130
138
  const status = await getGitStatus(rootDir)
131
139
 
132
140
  if (!hasStagedChanges(status)) {
@@ -138,7 +146,7 @@ export async function commitLintingChanges(rootDir, { getGitStatus, runCommand,
138
146
  }
139
147
 
140
148
  logProcessing?.('Committing linting changes...')
141
- await runCommand('git', ['commit', '-m', 'style: apply linting fixes'], { cwd: rootDir })
149
+ await runCommand('git', gitCommitArgs(['-m', 'style: apply linting fixes'], {skipGitHooks}), { cwd: rootDir })
142
150
  logSuccess?.('Linting changes committed.')
143
151
  return true
144
152
  }
package/src/main.mjs CHANGED
@@ -16,6 +16,7 @@ import {createConfigurationService} from './application/configuration/service.mj
16
16
  import {selectDeploymentTarget} from './application/configuration/select-deployment-target.mjs'
17
17
  import {resolvePendingSnapshot} from './application/deploy/resolve-pending-snapshot.mjs'
18
18
  import {runDeployment} from './application/deploy/run-deployment.mjs'
19
+ import {SKIP_GIT_HOOKS_WARNING} from './utils/git-hooks.mjs'
19
20
 
20
21
  const RELEASE_SCRIPT_NAME = 'release'
21
22
  const RELEASE_SCRIPT_COMMAND = 'npx @wyxos/zephyr@latest'
@@ -33,6 +34,7 @@ function normalizeMainOptions(firstArg = null, secondArg = null) {
33
34
  resumePending: firstArg.resumePending === true,
34
35
  discardPending: firstArg.discardPending === true,
35
36
  maintenanceMode: firstArg.maintenanceMode ?? null,
37
+ skipGitHooks: firstArg.skipGitHooks === true,
36
38
  skipTests: firstArg.skipTests === true,
37
39
  skipLint: firstArg.skipLint === true,
38
40
  skipBuild: firstArg.skipBuild === true,
@@ -50,6 +52,7 @@ function normalizeMainOptions(firstArg = null, secondArg = null) {
50
52
  resumePending: false,
51
53
  discardPending: false,
52
54
  maintenanceMode: null,
55
+ skipGitHooks: false,
53
56
  skipTests: false,
54
57
  skipLint: false,
55
58
  skipBuild: false,
@@ -86,6 +89,7 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
86
89
  workflow: resolveWorkflowName(options.workflowType),
87
90
  presetName: options.presetName,
88
91
  maintenanceMode: options.maintenanceMode,
92
+ skipGitHooks: options.skipGitHooks === true,
89
93
  resumePending: options.resumePending,
90
94
  discardPending: options.discardPending
91
95
  }
@@ -114,6 +118,7 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
114
118
  nonInteractive: currentExecutionMode.interactive === false,
115
119
  presetName: currentExecutionMode.presetName,
116
120
  maintenanceMode: currentExecutionMode.maintenanceMode,
121
+ skipGitHooks: currentExecutionMode.skipGitHooks === true,
117
122
  resumePending: currentExecutionMode.resumePending,
118
123
  discardPending: currentExecutionMode.discardPending
119
124
  }
@@ -122,9 +127,14 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
122
127
  logProcessing(`Zephyr v${ZEPHYR_VERSION}`)
123
128
  }
124
129
 
130
+ if (currentExecutionMode.skipGitHooks) {
131
+ logWarning(SKIP_GIT_HOOKS_WARNING)
132
+ }
133
+
125
134
  if (options.workflowType === 'node' || options.workflowType === 'vue') {
126
135
  await releaseNode({
127
136
  releaseType: options.versionArg,
137
+ skipGitHooks: options.skipGitHooks,
128
138
  skipTests: options.skipTests,
129
139
  skipLint: options.skipLint,
130
140
  skipBuild: options.skipBuild,
@@ -144,6 +154,7 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
144
154
  if (options.workflowType === 'packagist') {
145
155
  await releasePackagist({
146
156
  releaseType: options.versionArg,
157
+ skipGitHooks: options.skipGitHooks,
147
158
  skipTests: options.skipTests,
148
159
  skipLint: options.skipLint,
149
160
  context: appContext
@@ -164,13 +175,15 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
164
175
  projectConfigDir: PROJECT_CONFIG_DIR,
165
176
  runCommand,
166
177
  logSuccess,
167
- logWarning
178
+ logWarning,
179
+ skipGitHooks: currentExecutionMode.skipGitHooks
168
180
  })
169
181
  await bootstrap.ensureProjectReleaseScript(rootDir, {
170
182
  runPrompt,
171
183
  runCommand,
172
184
  logSuccess,
173
185
  logWarning,
186
+ skipGitHooks: currentExecutionMode.skipGitHooks,
174
187
  interactive: currentExecutionMode.interactive,
175
188
  releaseScriptName: RELEASE_SCRIPT_NAME,
176
189
  releaseScriptCommand: RELEASE_SCRIPT_COMMAND
@@ -184,7 +197,8 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
184
197
  if (hasPackageJson || hasComposerJson) {
185
198
  logProcessing('Validating dependencies...')
186
199
  await validateLocalDependencies(rootDir, runPrompt, logSuccess, {
187
- interactive: currentExecutionMode.interactive
200
+ interactive: currentExecutionMode.interactive,
201
+ skipGitHooks: currentExecutionMode.skipGitHooks
188
202
  })
189
203
  }
190
204
 
@@ -2,12 +2,14 @@ import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
 
4
4
  import {ZephyrError} from '../runtime/errors.mjs'
5
+ import {gitCommitArgs} from '../utils/git-hooks.mjs'
5
6
 
6
7
  export async function ensureGitignoreEntry(rootDir, {
7
8
  projectConfigDir = '.zephyr',
8
9
  runCommand,
9
10
  logSuccess,
10
- logWarning
11
+ logWarning,
12
+ skipGitHooks = false
11
13
  } = {}) {
12
14
  const gitignorePath = path.join(rootDir, '.gitignore')
13
15
  const targetEntry = `${projectConfigDir}/`
@@ -53,7 +55,7 @@ export async function ensureGitignoreEntry(rootDir, {
53
55
 
54
56
  try {
55
57
  await runCommand('git', ['add', '.gitignore'], { cwd: rootDir })
56
- await runCommand('git', ['commit', '-m', 'chore: ignore zephyr config'], { cwd: rootDir })
58
+ await runCommand('git', gitCommitArgs(['-m', 'chore: ignore zephyr config'], {skipGitHooks}), { cwd: rootDir })
57
59
  } catch (error) {
58
60
  if (error.exitCode === 1) {
59
61
  logWarning?.('Git commit skipped: nothing to commit or pre-commit hook prevented commit.')
@@ -68,6 +70,7 @@ export async function ensureProjectReleaseScript(rootDir, {
68
70
  runCommand,
69
71
  logSuccess,
70
72
  logWarning,
73
+ skipGitHooks = false,
71
74
  interactive = true,
72
75
  releaseScriptName = 'release',
73
76
  releaseScriptCommand = 'npx @wyxos/zephyr@latest'
@@ -141,7 +144,7 @@ export async function ensureProjectReleaseScript(rootDir, {
141
144
  if (isGitRepo) {
142
145
  try {
143
146
  await runCommand('git', ['add', 'package.json'], { cwd: rootDir, silent: true })
144
- await runCommand('git', ['commit', '-m', 'chore: add zephyr release script'], { cwd: rootDir, silent: true })
147
+ await runCommand('git', gitCommitArgs(['-m', 'chore: add zephyr release script'], {skipGitHooks}), { cwd: rootDir, silent: true })
145
148
  logSuccess?.('Committed package.json release script addition.')
146
149
  } catch (error) {
147
150
  if (error.exitCode === 1) {
@@ -101,9 +101,13 @@ export async function ensureCleanWorkingTree(rootDir = process.cwd(), {
101
101
  export async function validateReleaseDependencies(rootDir = process.cwd(), {
102
102
  prompt = (questions) => inquirer.prompt(questions),
103
103
  logSuccess,
104
- interactive = true
104
+ interactive = true,
105
+ skipGitHooks = false
105
106
  } = {}) {
106
- await validateLocalDependencies(rootDir, prompt, logSuccess, { interactive })
107
+ await validateLocalDependencies(rootDir, prompt, logSuccess, {
108
+ interactive,
109
+ skipGitHooks
110
+ })
107
111
  }
108
112
 
109
113
  export async function ensureReleaseBranchReady({
@@ -3,11 +3,29 @@ import {createAppContext} from './runtime/app-context.mjs'
3
3
  import {parseReleaseArgs} from './release/shared.mjs'
4
4
  import {releaseNodePackage} from './application/release/release-node-package.mjs'
5
5
 
6
+ function hasExplicitReleaseOptions(options = {}) {
7
+ return [
8
+ 'releaseType',
9
+ 'skipGitHooks',
10
+ 'skipTests',
11
+ 'skipLint',
12
+ 'skipBuild',
13
+ 'skipDeploy'
14
+ ].some((key) => key in options)
15
+ }
16
+
6
17
  export async function releaseNode(options = {}) {
7
- const parsed = options.releaseType
8
- ? options
18
+ const parsed = hasExplicitReleaseOptions(options)
19
+ ? {
20
+ releaseType: options.releaseType ?? 'patch',
21
+ skipGitHooks: options.skipGitHooks === true,
22
+ skipTests: options.skipTests === true,
23
+ skipLint: options.skipLint === true,
24
+ skipBuild: options.skipBuild === true,
25
+ skipDeploy: options.skipDeploy === true
26
+ }
9
27
  : parseReleaseArgs({
10
- booleanFlags: ['--skip-tests', '--skip-lint', '--skip-build', '--skip-deploy']
28
+ booleanFlags: ['--skip-git-hooks', '--skip-tests', '--skip-lint', '--skip-build', '--skip-deploy']
11
29
  })
12
30
  const rootDir = options.rootDir ?? process.cwd()
13
31
  const context = options.context ?? createAppContext({
@@ -21,6 +39,7 @@ export async function releaseNode(options = {}) {
21
39
 
22
40
  await releaseNodePackage({
23
41
  releaseType: parsed.releaseType,
42
+ skipGitHooks: parsed.skipGitHooks === true || executionMode?.skipGitHooks === true,
24
43
  skipTests: parsed.skipTests === true,
25
44
  skipLint: parsed.skipLint === true,
26
45
  skipBuild: parsed.skipBuild === true,
@@ -3,11 +3,25 @@ import {createAppContext} from './runtime/app-context.mjs'
3
3
  import {parseReleaseArgs} from './release/shared.mjs'
4
4
  import {releasePackagistPackage} from './application/release/release-packagist-package.mjs'
5
5
 
6
+ function hasExplicitReleaseOptions(options = {}) {
7
+ return [
8
+ 'releaseType',
9
+ 'skipGitHooks',
10
+ 'skipTests',
11
+ 'skipLint'
12
+ ].some((key) => key in options)
13
+ }
14
+
6
15
  export async function releasePackagist(options = {}) {
7
- const parsed = options.releaseType
8
- ? options
16
+ const parsed = hasExplicitReleaseOptions(options)
17
+ ? {
18
+ releaseType: options.releaseType ?? 'patch',
19
+ skipGitHooks: options.skipGitHooks === true,
20
+ skipTests: options.skipTests === true,
21
+ skipLint: options.skipLint === true
22
+ }
9
23
  : parseReleaseArgs({
10
- booleanFlags: ['--skip-tests', '--skip-lint']
24
+ booleanFlags: ['--skip-git-hooks', '--skip-tests', '--skip-lint']
11
25
  })
12
26
  const rootDir = options.rootDir ?? process.cwd()
13
27
  const context = options.context ?? createAppContext({
@@ -21,6 +35,7 @@ export async function releasePackagist(options = {}) {
21
35
 
22
36
  await releasePackagistPackage({
23
37
  releaseType: parsed.releaseType,
38
+ skipGitHooks: parsed.skipGitHooks === true || executionMode?.skipGitHooks === true,
24
39
  skipTests: parsed.skipTests === true,
25
40
  skipLint: parsed.skipLint === true,
26
41
  rootDir,
@@ -22,6 +22,7 @@ export function createAppContext({
22
22
  workflow: executionMode.workflow ?? 'deploy',
23
23
  presetName: executionMode.presetName ?? null,
24
24
  maintenanceMode: executionMode.maintenanceMode ?? null,
25
+ skipGitHooks: executionMode.skipGitHooks === true,
25
26
  resumePending: executionMode.resumePending === true,
26
27
  discardPending: executionMode.discardPending === true
27
28
  }
@@ -0,0 +1,26 @@
1
+ export const SKIP_GIT_HOOKS_WARNING =
2
+ 'WARNING: --skip-git-hooks is enabled. Zephyr will bypass local git hooks for any commits and pushes it performs during this run.'
3
+
4
+ export function gitCommitArgs(args = [], {skipGitHooks = false} = {}) {
5
+ return skipGitHooks
6
+ ? ['commit', '--no-verify', ...args]
7
+ : ['commit', ...args]
8
+ }
9
+
10
+ export function gitPushArgs(args = [], {skipGitHooks = false} = {}) {
11
+ return skipGitHooks
12
+ ? ['push', '--no-verify', ...args]
13
+ : ['push', ...args]
14
+ }
15
+
16
+ export function npmVersionArgs(releaseType, {
17
+ skipGitHooks = false,
18
+ extraArgs = []
19
+ } = {}) {
20
+ return [
21
+ 'version',
22
+ releaseType,
23
+ ...(skipGitHooks ? ['--no-commit-hooks'] : []),
24
+ ...extraArgs
25
+ ]
26
+ }