|
1 | 1 | import { execSync } from "child_process"; |
2 | | -import type { ExecSyncOptionsWithStringEncoding } from "child_process"; |
3 | 2 | import { join, dirname } from "path"; |
4 | 3 | import { fileURLToPath } from "url"; |
5 | 4 |
|
6 | 5 | // ───────────────────────────────────────────────────────────────────────────── |
7 | | -// Bidirectional Sync: pull platform changes, merge with local changes, push |
| 6 | +// Apply: Pull → Merge → Push (safe bidirectional sync) |
8 | 7 | // |
9 | | -// Flow: |
10 | | -// 1. Check for local uncommitted changes (git status) |
11 | | -// 2. If local changes exist, stash them |
12 | | -// 3. Pull platform state (npm run pull:<env>) |
13 | | -// 4. Commit the pulled state as a "platform sync" commit |
14 | | -// 5. Pop the stash (reapply local changes) |
15 | | -// 6. If merge conflicts, warn and exit for manual resolution |
16 | | -// 7. Push merged state to platform (npm run push:<env>) |
| 8 | +// 1. Pull latest platform state, merge with local changes (git stash/pop) |
| 9 | +// 2. If merge is clean, push the result to the platform |
| 10 | +// 3. If conflicts, stop — user resolves, then runs push manually |
17 | 11 | // ───────────────────────────────────────────────────────────────────────────── |
18 | 12 |
|
19 | 13 | const __dirname = dirname(fileURLToPath(import.meta.url)); |
20 | 14 | const BASE_DIR = join(__dirname, ".."); |
21 | 15 |
|
22 | 16 | const VALID_ENVIRONMENTS = ["dev", "staging", "prod"] as const; |
23 | 17 |
|
24 | | -// ───────────────────────────────────────────────────────────────────────────── |
25 | | -// Helpers |
26 | | -// ───────────────────────────────────────────────────────────────────────────── |
27 | | - |
28 | | -const execOpts: ExecSyncOptionsWithStringEncoding = { |
29 | | - cwd: BASE_DIR, |
30 | | - encoding: "utf-8", |
31 | | - stdio: ["pipe", "pipe", "pipe"], |
32 | | -}; |
33 | | - |
34 | | -function run(cmd: string, opts?: { silent?: boolean }): string { |
35 | | - try { |
36 | | - const output = execSync(cmd, execOpts).trim(); |
37 | | - if (!opts?.silent && output) { |
38 | | - console.log(output); |
39 | | - } |
40 | | - return output; |
41 | | - } catch (error: unknown) { |
42 | | - const execError = error as { stderr?: string; stdout?: string; status?: number }; |
43 | | - const stderr = execError.stderr?.trim() || ""; |
44 | | - const stdout = execError.stdout?.trim() || ""; |
45 | | - throw new Error(`Command failed: ${cmd}\n${stderr}\n${stdout}`); |
46 | | - } |
47 | | -} |
48 | | - |
49 | 18 | function runPassthrough(cmd: string): number { |
50 | 19 | try { |
51 | 20 | execSync(cmd, { cwd: BASE_DIR, stdio: "inherit" }); |
52 | 21 | return 0; |
53 | 22 | } catch (error: unknown) { |
54 | | - const execError = error as { status?: number }; |
55 | | - return execError.status ?? 1; |
| 23 | + return (error as { status?: number }).status ?? 1; |
56 | 24 | } |
57 | 25 | } |
58 | 26 |
|
59 | | -function hasLocalChanges(): boolean { |
60 | | - const status = run("git status --porcelain", { silent: true }); |
61 | | - return status.length > 0; |
62 | | -} |
63 | | - |
64 | | -function isGitRepo(): boolean { |
65 | | - try { |
66 | | - run("git rev-parse --is-inside-work-tree", { silent: true }); |
67 | | - return true; |
68 | | - } catch { |
69 | | - return false; |
70 | | - } |
71 | | -} |
72 | | - |
73 | | -function hasStash(): boolean { |
74 | | - const stashList = run("git stash list", { silent: true }); |
75 | | - return stashList.length > 0; |
76 | | -} |
77 | | - |
78 | | -// ───────────────────────────────────────────────────────────────────────────── |
79 | | -// Main Sync Flow |
80 | | -// ───────────────────────────────────────────────────────────────────────────── |
81 | | - |
82 | 27 | async function main(): Promise<void> { |
83 | 28 | const env = process.argv[2]; |
84 | 29 | const extraArgs = process.argv.slice(3).join(" "); |
85 | 30 |
|
86 | 31 | if (!env || !VALID_ENVIRONMENTS.includes(env as typeof VALID_ENVIRONMENTS[number])) { |
87 | | - console.error("❌ Environment argument is required"); |
88 | | - console.error(" Usage: npm run apply:dev | apply:prod"); |
| 32 | + console.error("Usage: npm run apply:dev | apply:prod"); |
89 | 33 | console.error(""); |
90 | | - console.error(" This command performs a bidirectional sync:"); |
91 | | - console.error(" 1. Stashes your local changes"); |
92 | | - console.error(" 2. Pulls latest platform state"); |
93 | | - console.error(" 3. Reapplies your local changes on top"); |
94 | | - console.error(" 4. Pushes the merged result to the platform"); |
| 34 | + console.error(" Pull → Merge → Push (safe bidirectional sync)"); |
95 | 35 | console.error(""); |
96 | | - console.error(" For one-way push only, use: npm run push:dev | push:prod"); |
| 36 | + console.error(" Pulls latest platform state, merges with your local"); |
| 37 | + console.error(" changes, and pushes the result back to the platform."); |
| 38 | + console.error(" Stops on merge conflicts for manual resolution."); |
97 | 39 | process.exit(1); |
98 | 40 | } |
99 | 41 |
|
100 | 42 | console.log("═══════════════════════════════════════════════════════════════"); |
101 | | - console.log(`🔄 Vapi GitOps Sync - Environment: ${env}`); |
| 43 | + console.log(`🔄 Vapi GitOps Apply - Environment: ${env}`); |
| 44 | + console.log(" Pull → Merge → Push"); |
102 | 45 | console.log("═══════════════════════════════════════════════════════════════\n"); |
103 | 46 |
|
104 | | - // Step 0: Ensure we're in a git repo |
105 | | - if (!isGitRepo()) { |
106 | | - console.error("❌ Not a git repository. Bidirectional sync requires git."); |
107 | | - console.error(" Initialize with: git init && git add . && git commit -m 'initial'"); |
108 | | - console.error(" Or use 'npm run push:<env>' for direct push without git.\n"); |
| 47 | + // Step 1: Pull with merge |
| 48 | + const pullExit = runPassthrough(`npx tsx src/pull.ts ${env}`); |
| 49 | + if (pullExit !== 0) { |
| 50 | + console.error("\n❌ Pull had issues. Resolve conflicts before pushing."); |
109 | 51 | process.exit(1); |
110 | 52 | } |
111 | 53 |
|
112 | | - // Step 1: Check for local changes |
113 | | - const hadLocalChanges = hasLocalChanges(); |
114 | | - |
115 | | - if (hadLocalChanges) { |
116 | | - console.log("📦 Local changes detected, stashing...\n"); |
117 | | - run("git stash push -m \"gitops-sync: local changes before pull\""); |
118 | | - console.log(" ✅ Local changes stashed\n"); |
119 | | - } else { |
120 | | - console.log("📦 No local changes to stash\n"); |
121 | | - } |
122 | | - |
123 | | - // Step 2: Pull platform state |
124 | | - console.log("📥 Pulling platform state...\n"); |
125 | | - const pullExitCode = runPassthrough(`npx tsx src/pull.ts ${env}`); |
126 | | - |
127 | | - if (pullExitCode !== 0) { |
128 | | - console.error("\n❌ Pull failed!"); |
129 | | - if (hadLocalChanges) { |
130 | | - console.log(" Restoring your local changes from stash..."); |
131 | | - run("git stash pop"); |
132 | | - } |
133 | | - process.exit(1); |
134 | | - } |
135 | | - |
136 | | - // Step 3: Commit pulled state (if there are changes from the pull) |
137 | | - if (hasLocalChanges()) { |
138 | | - console.log("\n📝 Committing platform state...\n"); |
139 | | - run("git add -A"); |
140 | | - run(`git commit -m "sync: pull platform state (${env})"`); |
141 | | - console.log(" ✅ Platform state committed\n"); |
142 | | - } else { |
143 | | - console.log("\n📝 No platform changes to commit\n"); |
144 | | - } |
145 | | - |
146 | | - // Step 4: Pop stash (reapply local changes) |
147 | | - if (hadLocalChanges) { |
148 | | - console.log("📦 Reapplying local changes...\n"); |
149 | | - try { |
150 | | - run("git stash pop"); |
151 | | - console.log(" ✅ Local changes reapplied\n"); |
152 | | - } catch (error) { |
153 | | - // Merge conflict during stash pop |
154 | | - console.error("\n⚠️ Merge conflicts detected!\n"); |
155 | | - console.error(" Your local changes conflict with platform changes."); |
156 | | - console.error(" Please resolve the conflicts manually, then run:"); |
157 | | - console.error(` git add . && npm run push:${env}\n`); |
158 | | - console.error(" To see conflicted files:"); |
159 | | - console.error(" git diff --name-only --diff-filter=U\n"); |
160 | | - console.error(" To abort and restore your local changes:"); |
161 | | - console.error(" git checkout --theirs . && git stash pop\n"); |
162 | | - process.exit(1); |
163 | | - } |
164 | | - } |
165 | | - |
166 | | - // Step 5: Push merged state to platform |
167 | | - console.log("🚀 Pushing merged state to platform...\n"); |
168 | | - const pushExitCode = runPassthrough(`npx tsx src/push.ts ${env} ${extraArgs}`.trim()); |
169 | | - |
170 | | - if (pushExitCode !== 0) { |
| 54 | + // Step 2: Push merged state |
| 55 | + console.log("\n🚀 Pushing merged state to platform...\n"); |
| 56 | + const pushCmd = `npx tsx src/push.ts ${env} ${extraArgs}`.trim(); |
| 57 | + const pushExit = runPassthrough(pushCmd); |
| 58 | + if (pushExit !== 0) { |
171 | 59 | console.error("\n❌ Push failed!"); |
172 | 60 | process.exit(1); |
173 | 61 | } |
174 | 62 |
|
175 | | - // Step 6: Commit the final state after push (state file may have changed) |
176 | | - if (hasLocalChanges()) { |
177 | | - console.log("\n📝 Committing final state...\n"); |
178 | | - run("git add -A"); |
179 | | - run(`git commit -m "sync: apply local changes to platform (${env})"`); |
180 | | - } |
181 | | - |
182 | 63 | console.log("\n═══════════════════════════════════════════════════════════════"); |
183 | | - console.log("✅ Bidirectional sync complete!"); |
| 64 | + console.log("✅ Apply complete! (Pull → Merge → Push)"); |
184 | 65 | console.log("═══════════════════════════════════════════════════════════════\n"); |
185 | 66 | } |
186 | 67 |
|
187 | | -// Run the sync engine |
188 | 68 | main().catch((error) => { |
189 | | - console.error("\n❌ Sync failed:", error); |
| 69 | + console.error("\n❌ Apply failed:", error); |
190 | 70 | process.exit(1); |
191 | 71 | }); |
0 commit comments