@wyxos/zephyr 0.9.9 → 0.9.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyxos/zephyr",
3
- "version": "0.9.9",
3
+ "version": "0.9.10",
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",
@@ -12,6 +12,7 @@ import {
12
12
  runReleaseCommand,
13
13
  validateReleaseDependencies
14
14
  } from '../../release/shared.mjs'
15
+ import {waitForGitHubReleaseWorkflows} from '../../release/github-actions.mjs'
15
16
  import {resolveReleaseType} from '../../release/release-type.mjs'
16
17
  import {gitCommitArgs, gitPushArgs, npmVersionArgs} from '../../utils/git-hooks.mjs'
17
18
 
@@ -469,10 +470,18 @@ export async function releaseNodePackage({
469
470
  if (skipVersioning) {
470
471
  await createReleaseTag(updatedPkg.version, rootDir, {logStep, runCommand})
471
472
  }
473
+ const releasePushStartedAt = new Date()
472
474
  await pushChanges(rootDir, {logStep, logSuccess, runCommand, skipGitHooks})
475
+ logStep?.('Publishing will be handled by GitHub Actions via trusted publishing.')
476
+ await waitForGitHubReleaseWorkflows(rootDir, {
477
+ runCommand,
478
+ logStep,
479
+ logSuccess,
480
+ logWarning,
481
+ pushStartedAt: releasePushStartedAt
482
+ })
473
483
  await deployGHPages(skipDeploy, updatedPkg, rootDir, {logStep, logSuccess, logWarning, runCommand, skipGitHooks})
474
484
 
475
- logStep?.('Publishing will be handled by GitHub Actions via trusted publishing.')
476
485
  logSuccess?.(`Release workflow completed for ${updatedPkg.name}@${updatedPkg.version}.`)
477
486
  return updatedPkg
478
487
  }
@@ -11,6 +11,7 @@ import {
11
11
  runReleaseCommand,
12
12
  validateReleaseDependencies
13
13
  } from '../../release/shared.mjs'
14
+ import {waitForGitHubReleaseWorkflows} from '../../release/github-actions.mjs'
14
15
  import {resolveReleaseType} from '../../release/release-type.mjs'
15
16
  import {gitCommitArgs, gitPushArgs} from '../../utils/git-hooks.mjs'
16
17
 
@@ -322,7 +323,15 @@ export async function releasePackagistPackage({
322
323
  if (skipVersioning) {
323
324
  await createReleaseTag(updatedComposer.version, rootDir, {logStep, runCommand})
324
325
  }
326
+ const releasePushStartedAt = new Date()
325
327
  await pushChanges(rootDir, {logStep, logSuccess, runCommand, skipGitHooks})
328
+ await waitForGitHubReleaseWorkflows(rootDir, {
329
+ runCommand,
330
+ logStep,
331
+ logSuccess,
332
+ logWarning,
333
+ pushStartedAt: releasePushStartedAt
334
+ })
326
335
 
327
336
  logSuccess?.(`Release workflow completed for ${composer.name}@${updatedComposer.version}.`)
328
337
  logStep?.('Note: Packagist will automatically detect the new git tag and update the package.')
@@ -0,0 +1,197 @@
1
+ import {readdir} from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import process from 'node:process'
4
+
5
+ import {runReleaseCommand} from './shared.mjs'
6
+
7
+ const DEFAULT_WORKFLOW_WAIT_TIMEOUT_MS = 60_000
8
+ const DEFAULT_WORKFLOW_POLL_INTERVAL_MS = 3_000
9
+ const RUN_CREATED_AT_TOLERANCE_MS = 5_000
10
+
11
+ function sleep(ms) {
12
+ return new Promise((resolve) => {
13
+ setTimeout(resolve, ms)
14
+ })
15
+ }
16
+
17
+ function describeError(error) {
18
+ const stderr = String(error?.stderr ?? '').trim()
19
+ if (stderr) {
20
+ return stderr
21
+ }
22
+
23
+ const message = String(error?.message ?? '').trim()
24
+ if (message) {
25
+ return message
26
+ }
27
+
28
+ return String(error ?? 'unknown error')
29
+ }
30
+
31
+ async function hasGitHubWorkflowFiles(rootDir) {
32
+ const workflowDir = path.join(rootDir, '.github', 'workflows')
33
+
34
+ try {
35
+ const entries = await readdir(workflowDir, {withFileTypes: true})
36
+
37
+ return entries.some((entry) => entry.isFile() && /\.ya?ml$/i.test(entry.name))
38
+ } catch {
39
+ return false
40
+ }
41
+ }
42
+
43
+ function parseWorkflowRuns(stdout) {
44
+ try {
45
+ const parsed = JSON.parse(stdout || '[]')
46
+
47
+ return Array.isArray(parsed) ? parsed : []
48
+ } catch {
49
+ return []
50
+ }
51
+ }
52
+
53
+ function isRunForCurrentPush(run, pushStartedAt) {
54
+ if (!pushStartedAt || !run?.createdAt) {
55
+ return true
56
+ }
57
+
58
+ const runCreatedAt = Date.parse(run.createdAt)
59
+ const startedAt = pushStartedAt instanceof Date
60
+ ? pushStartedAt.getTime()
61
+ : Date.parse(pushStartedAt)
62
+
63
+ if (!Number.isFinite(runCreatedAt) || !Number.isFinite(startedAt)) {
64
+ return true
65
+ }
66
+
67
+ return runCreatedAt >= startedAt - RUN_CREATED_AT_TOLERANCE_MS
68
+ }
69
+
70
+ function workflowLabel(run) {
71
+ const workflowName = run?.workflowName || run?.name || 'GitHub Actions workflow'
72
+ const databaseId = run?.databaseId ? `#${run.databaseId}` : ''
73
+
74
+ return `${workflowName}${databaseId ? ` ${databaseId}` : ''}`
75
+ }
76
+
77
+ async function findCurrentPushRuns(rootDir, {
78
+ runCommand,
79
+ headSha,
80
+ pushStartedAt,
81
+ timeoutMs,
82
+ pollIntervalMs,
83
+ sleepImpl
84
+ }) {
85
+ const deadline = Date.now() + timeoutMs
86
+ let shouldPoll = true
87
+
88
+ while (shouldPoll) {
89
+ const {stdout} = await runCommand('gh', [
90
+ 'run',
91
+ 'list',
92
+ '--commit',
93
+ headSha,
94
+ '--event',
95
+ 'push',
96
+ '--json',
97
+ 'databaseId,status,conclusion,workflowName,createdAt,url',
98
+ '--limit',
99
+ '20'
100
+ ], {
101
+ capture: true,
102
+ cwd: rootDir
103
+ })
104
+
105
+ const runs = parseWorkflowRuns(stdout)
106
+ .filter((run) => run?.databaseId)
107
+ .filter((run) => isRunForCurrentPush(run, pushStartedAt))
108
+
109
+ if (runs.length > 0) {
110
+ return runs
111
+ }
112
+
113
+ shouldPoll = Date.now() < deadline
114
+
115
+ if (shouldPoll) {
116
+ await sleepImpl(pollIntervalMs)
117
+ }
118
+ }
119
+
120
+ return []
121
+ }
122
+
123
+ export async function waitForGitHubReleaseWorkflows(rootDir = process.cwd(), {
124
+ runCommand = runReleaseCommand,
125
+ logStep,
126
+ logSuccess,
127
+ logWarning,
128
+ pushStartedAt = new Date(),
129
+ timeoutMs = DEFAULT_WORKFLOW_WAIT_TIMEOUT_MS,
130
+ pollIntervalMs = DEFAULT_WORKFLOW_POLL_INTERVAL_MS,
131
+ sleepImpl = sleep
132
+ } = {}) {
133
+ if (!await hasGitHubWorkflowFiles(rootDir)) {
134
+ return {status: 'skipped', reason: 'no-workflows', runs: []}
135
+ }
136
+
137
+ try {
138
+ await runCommand('gh', ['--version'], {capture: true, cwd: rootDir})
139
+ } catch (error) {
140
+ logWarning?.(`GitHub Actions workflow monitoring skipped because GitHub CLI is unavailable: ${describeError(error)}`)
141
+ return {status: 'skipped', reason: 'gh-unavailable', runs: []}
142
+ }
143
+
144
+ let headSha
145
+ try {
146
+ const result = await runCommand('git', ['rev-parse', 'HEAD'], {capture: true, cwd: rootDir})
147
+ headSha = result.stdout.trim()
148
+ } catch (error) {
149
+ logWarning?.(`GitHub Actions workflow monitoring skipped because the release commit could not be resolved: ${describeError(error)}`)
150
+ return {status: 'skipped', reason: 'head-unavailable', runs: []}
151
+ }
152
+
153
+ if (!headSha) {
154
+ logWarning?.('GitHub Actions workflow monitoring skipped because the release commit could not be resolved.')
155
+ return {status: 'skipped', reason: 'head-unavailable', runs: []}
156
+ }
157
+
158
+ logStep?.('Checking for GitHub Actions workflows triggered by the release push...')
159
+
160
+ let runs = []
161
+ try {
162
+ runs = await findCurrentPushRuns(rootDir, {
163
+ runCommand,
164
+ headSha,
165
+ pushStartedAt,
166
+ timeoutMs,
167
+ pollIntervalMs,
168
+ sleepImpl
169
+ })
170
+ } catch (error) {
171
+ logWarning?.(`GitHub Actions workflow monitoring skipped because workflow runs could not be listed: ${describeError(error)}`)
172
+ return {status: 'skipped', reason: 'list-failed', runs: []}
173
+ }
174
+
175
+ if (runs.length === 0) {
176
+ logWarning?.('GitHub Actions workflow monitoring skipped because no workflow run appeared for the release push.')
177
+ return {status: 'skipped', reason: 'no-runs', runs: []}
178
+ }
179
+
180
+ for (const run of runs) {
181
+ const label = workflowLabel(run)
182
+ logStep?.(`Watching GitHub Actions workflow ${label}...`)
183
+
184
+ try {
185
+ await runCommand('gh', ['run', 'watch', String(run.databaseId), '--exit-status', '--compact'], {
186
+ cwd: rootDir
187
+ })
188
+ } catch (_error) {
189
+ const suffix = run.url ? ` ${run.url}` : ''
190
+ throw new Error(`GitHub Actions workflow ${label} failed or could not be watched.${suffix}`)
191
+ }
192
+
193
+ logSuccess?.(`GitHub Actions workflow ${label} completed successfully.`)
194
+ }
195
+
196
+ return {status: 'watched', reason: null, runs}
197
+ }