@fuzdev/fuz_gitops 0.57.0

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.
Files changed (190) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/dist/ModulesDetail.svelte +180 -0
  4. package/dist/ModulesDetail.svelte.d.ts +10 -0
  5. package/dist/ModulesDetail.svelte.d.ts.map +1 -0
  6. package/dist/ModulesNav.svelte +43 -0
  7. package/dist/ModulesNav.svelte.d.ts +11 -0
  8. package/dist/ModulesNav.svelte.d.ts.map +1 -0
  9. package/dist/ModulesPage.svelte +50 -0
  10. package/dist/ModulesPage.svelte.d.ts +9 -0
  11. package/dist/ModulesPage.svelte.d.ts.map +1 -0
  12. package/dist/PageFooter.svelte +15 -0
  13. package/dist/PageFooter.svelte.d.ts +19 -0
  14. package/dist/PageFooter.svelte.d.ts.map +1 -0
  15. package/dist/PageHeader.svelte +35 -0
  16. package/dist/PageHeader.svelte.d.ts +19 -0
  17. package/dist/PageHeader.svelte.d.ts.map +1 -0
  18. package/dist/PullRequestsDetail.svelte +53 -0
  19. package/dist/PullRequestsDetail.svelte.d.ts +10 -0
  20. package/dist/PullRequestsDetail.svelte.d.ts.map +1 -0
  21. package/dist/PullRequestsPage.svelte +47 -0
  22. package/dist/PullRequestsPage.svelte.d.ts +11 -0
  23. package/dist/PullRequestsPage.svelte.d.ts.map +1 -0
  24. package/dist/ReposTable.svelte +189 -0
  25. package/dist/ReposTable.svelte.d.ts +9 -0
  26. package/dist/ReposTable.svelte.d.ts.map +1 -0
  27. package/dist/ReposTree.svelte +88 -0
  28. package/dist/ReposTree.svelte.d.ts +11 -0
  29. package/dist/ReposTree.svelte.d.ts.map +1 -0
  30. package/dist/ReposTreeNav.svelte +55 -0
  31. package/dist/ReposTreeNav.svelte.d.ts +11 -0
  32. package/dist/ReposTreeNav.svelte.d.ts.map +1 -0
  33. package/dist/TablePage.svelte +46 -0
  34. package/dist/TablePage.svelte.d.ts +9 -0
  35. package/dist/TablePage.svelte.d.ts.map +1 -0
  36. package/dist/TreeItemPage.svelte +75 -0
  37. package/dist/TreeItemPage.svelte.d.ts +10 -0
  38. package/dist/TreeItemPage.svelte.d.ts.map +1 -0
  39. package/dist/TreePage.svelte +64 -0
  40. package/dist/TreePage.svelte.d.ts +9 -0
  41. package/dist/TreePage.svelte.d.ts.map +1 -0
  42. package/dist/changeset_generator.d.ts +38 -0
  43. package/dist/changeset_generator.d.ts.map +1 -0
  44. package/dist/changeset_generator.js +110 -0
  45. package/dist/changeset_reader.d.ts +75 -0
  46. package/dist/changeset_reader.d.ts.map +1 -0
  47. package/dist/changeset_reader.js +167 -0
  48. package/dist/constants.d.ts +9 -0
  49. package/dist/constants.d.ts.map +1 -0
  50. package/dist/constants.js +8 -0
  51. package/dist/dependency_graph.d.ts +120 -0
  52. package/dist/dependency_graph.d.ts.map +1 -0
  53. package/dist/dependency_graph.js +341 -0
  54. package/dist/dependency_updater.d.ts +46 -0
  55. package/dist/dependency_updater.d.ts.map +1 -0
  56. package/dist/dependency_updater.js +213 -0
  57. package/dist/fetch_repo_data.d.ts +19 -0
  58. package/dist/fetch_repo_data.d.ts.map +1 -0
  59. package/dist/fetch_repo_data.js +49 -0
  60. package/dist/fs_fetch_value_cache.d.ts +24 -0
  61. package/dist/fs_fetch_value_cache.d.ts.map +1 -0
  62. package/dist/fs_fetch_value_cache.js +61 -0
  63. package/dist/git_operations.d.ts +54 -0
  64. package/dist/git_operations.d.ts.map +1 -0
  65. package/dist/git_operations.js +144 -0
  66. package/dist/github.d.ts +91 -0
  67. package/dist/github.d.ts.map +1 -0
  68. package/dist/github.js +94 -0
  69. package/dist/github_helpers.d.ts +10 -0
  70. package/dist/github_helpers.d.ts.map +1 -0
  71. package/dist/github_helpers.js +13 -0
  72. package/dist/gitops_analyze.task.d.ts +17 -0
  73. package/dist/gitops_analyze.task.d.ts.map +1 -0
  74. package/dist/gitops_analyze.task.js +188 -0
  75. package/dist/gitops_config.d.ts +56 -0
  76. package/dist/gitops_config.d.ts.map +1 -0
  77. package/dist/gitops_config.js +63 -0
  78. package/dist/gitops_plan.task.d.ts +28 -0
  79. package/dist/gitops_plan.task.d.ts.map +1 -0
  80. package/dist/gitops_plan.task.js +217 -0
  81. package/dist/gitops_publish.task.d.ts +29 -0
  82. package/dist/gitops_publish.task.d.ts.map +1 -0
  83. package/dist/gitops_publish.task.js +178 -0
  84. package/dist/gitops_sync.task.d.ts +18 -0
  85. package/dist/gitops_sync.task.d.ts.map +1 -0
  86. package/dist/gitops_sync.task.js +95 -0
  87. package/dist/gitops_task_helpers.d.ts +63 -0
  88. package/dist/gitops_task_helpers.d.ts.map +1 -0
  89. package/dist/gitops_task_helpers.js +84 -0
  90. package/dist/gitops_validate.task.d.ts +12 -0
  91. package/dist/gitops_validate.task.d.ts.map +1 -0
  92. package/dist/gitops_validate.task.js +210 -0
  93. package/dist/graph_validation.d.ts +39 -0
  94. package/dist/graph_validation.d.ts.map +1 -0
  95. package/dist/graph_validation.js +79 -0
  96. package/dist/local_repo.d.ts +84 -0
  97. package/dist/local_repo.d.ts.map +1 -0
  98. package/dist/local_repo.js +213 -0
  99. package/dist/log_helpers.d.ts +43 -0
  100. package/dist/log_helpers.d.ts.map +1 -0
  101. package/dist/log_helpers.js +98 -0
  102. package/dist/multi_repo_publisher.d.ts +34 -0
  103. package/dist/multi_repo_publisher.d.ts.map +1 -0
  104. package/dist/multi_repo_publisher.js +364 -0
  105. package/dist/npm_install_helpers.d.ts +23 -0
  106. package/dist/npm_install_helpers.d.ts.map +1 -0
  107. package/dist/npm_install_helpers.js +60 -0
  108. package/dist/npm_registry.d.ts +46 -0
  109. package/dist/npm_registry.d.ts.map +1 -0
  110. package/dist/npm_registry.js +96 -0
  111. package/dist/operations.d.ts +409 -0
  112. package/dist/operations.d.ts.map +1 -0
  113. package/dist/operations.js +34 -0
  114. package/dist/operations_defaults.d.ts +19 -0
  115. package/dist/operations_defaults.d.ts.map +1 -0
  116. package/dist/operations_defaults.js +279 -0
  117. package/dist/output_helpers.d.ts +27 -0
  118. package/dist/output_helpers.d.ts.map +1 -0
  119. package/dist/output_helpers.js +39 -0
  120. package/dist/paths.d.ts +11 -0
  121. package/dist/paths.d.ts.map +1 -0
  122. package/dist/paths.js +10 -0
  123. package/dist/preflight_checks.d.ts +47 -0
  124. package/dist/preflight_checks.d.ts.map +1 -0
  125. package/dist/preflight_checks.js +181 -0
  126. package/dist/publishing_plan.d.ts +100 -0
  127. package/dist/publishing_plan.d.ts.map +1 -0
  128. package/dist/publishing_plan.js +353 -0
  129. package/dist/publishing_plan_helpers.d.ts +30 -0
  130. package/dist/publishing_plan_helpers.d.ts.map +1 -0
  131. package/dist/publishing_plan_helpers.js +112 -0
  132. package/dist/publishing_plan_logging.d.ts +18 -0
  133. package/dist/publishing_plan_logging.d.ts.map +1 -0
  134. package/dist/publishing_plan_logging.js +342 -0
  135. package/dist/repo.svelte.d.ts +52 -0
  136. package/dist/repo.svelte.d.ts.map +1 -0
  137. package/dist/repo.svelte.js +70 -0
  138. package/dist/repo_ops.d.ts +57 -0
  139. package/dist/repo_ops.d.ts.map +1 -0
  140. package/dist/repo_ops.js +167 -0
  141. package/dist/resolved_gitops_config.d.ts +9 -0
  142. package/dist/resolved_gitops_config.d.ts.map +1 -0
  143. package/dist/resolved_gitops_config.js +12 -0
  144. package/dist/semver.d.ts +24 -0
  145. package/dist/semver.d.ts.map +1 -0
  146. package/dist/semver.js +140 -0
  147. package/dist/serialization_types.d.ts +57 -0
  148. package/dist/serialization_types.d.ts.map +1 -0
  149. package/dist/serialization_types.js +40 -0
  150. package/dist/version_utils.d.ts +48 -0
  151. package/dist/version_utils.d.ts.map +1 -0
  152. package/dist/version_utils.js +125 -0
  153. package/package.json +107 -0
  154. package/src/lib/changeset_generator.ts +162 -0
  155. package/src/lib/changeset_reader.ts +218 -0
  156. package/src/lib/constants.ts +8 -0
  157. package/src/lib/dependency_graph.ts +423 -0
  158. package/src/lib/dependency_updater.ts +297 -0
  159. package/src/lib/fetch_repo_data.ts +64 -0
  160. package/src/lib/fs_fetch_value_cache.ts +75 -0
  161. package/src/lib/git_operations.ts +208 -0
  162. package/src/lib/github.ts +128 -0
  163. package/src/lib/github_helpers.ts +31 -0
  164. package/src/lib/gitops_analyze.task.ts +261 -0
  165. package/src/lib/gitops_config.ts +123 -0
  166. package/src/lib/gitops_plan.task.ts +272 -0
  167. package/src/lib/gitops_publish.task.ts +227 -0
  168. package/src/lib/gitops_sync.task.ts +109 -0
  169. package/src/lib/gitops_task_helpers.ts +126 -0
  170. package/src/lib/gitops_validate.task.ts +248 -0
  171. package/src/lib/graph_validation.ts +109 -0
  172. package/src/lib/local_repo.ts +359 -0
  173. package/src/lib/log_helpers.ts +147 -0
  174. package/src/lib/multi_repo_publisher.ts +464 -0
  175. package/src/lib/npm_install_helpers.ts +85 -0
  176. package/src/lib/npm_registry.ts +143 -0
  177. package/src/lib/operations.ts +334 -0
  178. package/src/lib/operations_defaults.ts +335 -0
  179. package/src/lib/output_helpers.ts +64 -0
  180. package/src/lib/paths.ts +11 -0
  181. package/src/lib/preflight_checks.ts +269 -0
  182. package/src/lib/publishing_plan.ts +531 -0
  183. package/src/lib/publishing_plan_helpers.ts +145 -0
  184. package/src/lib/publishing_plan_logging.ts +470 -0
  185. package/src/lib/repo.svelte.ts +95 -0
  186. package/src/lib/repo_ops.ts +213 -0
  187. package/src/lib/resolved_gitops_config.ts +27 -0
  188. package/src/lib/semver.ts +166 -0
  189. package/src/lib/serialization_types.ts +90 -0
  190. package/src/lib/version_utils.ts +150 -0
@@ -0,0 +1,64 @@
1
+ import type {Logger} from '@fuzdev/fuz_util/log.js';
2
+ import {wait} from '@fuzdev/fuz_util/async.js';
3
+ import type {FetchValueCache} from '@fuzdev/fuz_util/fetch.js';
4
+
5
+ import {fetch_github_check_runs, fetch_github_pull_requests} from './github.js';
6
+ import type {RepoJson} from './repo.svelte.js';
7
+ import type {LocalRepo} from './local_repo.js';
8
+
9
+ /* eslint-disable no-await-in-loop */
10
+
11
+ /**
12
+ * Fetches GitHub metadata (CI status, PRs) for all repos.
13
+ *
14
+ * Fetches sequentially with delay between requests to respect GitHub API rate limits.
15
+ * Uses `await_in_loop` intentionally to avoid parallel requests overwhelming the API.
16
+ *
17
+ * Error handling: Logs fetch failures but continues processing remaining repos.
18
+ * Repos with failed fetches will have `null` for check_runs or pull_requests.
19
+ *
20
+ * @param delay milliseconds between API requests (default: 33ms)
21
+ * @param cache optional cache from belt's fetch.js for response memoization
22
+ * @returns array of Repo objects with GitHub metadata attached
23
+ */
24
+ export const fetch_repo_data = async (
25
+ resolved_repos: Array<LocalRepo>,
26
+ token?: string,
27
+ cache?: FetchValueCache,
28
+ log?: Logger,
29
+ delay = 33,
30
+ github_api_version?: string,
31
+ ): Promise<Array<RepoJson>> => {
32
+ const repos: Array<RepoJson> = [];
33
+ for (const {library, library_json, repo_config} of resolved_repos) {
34
+ const repo_url = library.repo_url;
35
+
36
+ // CI status
37
+ await wait(delay);
38
+ const check_runs = await fetch_github_check_runs(library, {
39
+ cache,
40
+ log,
41
+ token,
42
+ api_version: github_api_version,
43
+ ref: repo_config.branch,
44
+ });
45
+ if (!check_runs) log?.error('failed to fetch CI status: ' + repo_url);
46
+
47
+ // pull requests
48
+ await wait(delay);
49
+ const pull_requests = await fetch_github_pull_requests(library, {
50
+ cache,
51
+ log,
52
+ token,
53
+ api_version: github_api_version,
54
+ });
55
+ if (!pull_requests) log?.error('failed to fetch issues: ' + repo_url);
56
+
57
+ repos.push({
58
+ library_json,
59
+ check_runs,
60
+ pull_requests,
61
+ });
62
+ }
63
+ return repos;
64
+ };
@@ -0,0 +1,75 @@
1
+ import {mkdir, readFile, rm, writeFile} from 'node:fs/promises';
2
+ import {dirname, join} from 'node:path';
3
+ import {paths} from '@ryanatkn/gro/paths.js';
4
+ import {format_file} from '@ryanatkn/gro/format_file.js';
5
+ import {deserialize_cache, serialize_cache, type FetchValueCache} from '@fuzdev/fuz_util/fetch.js';
6
+ import {existsSync} from 'node:fs';
7
+
8
+ // TODO upstream to Gro probably, and rename/redesign?
9
+
10
+ export interface FetchCache {
11
+ name: string;
12
+ data: FetchValueCache; // TODO probably expose an API for this instead of passing the map directly
13
+ /**
14
+ * @returns true if anything changed, false if no-op
15
+ */
16
+ save: () => Promise<boolean>;
17
+ }
18
+
19
+ /**
20
+ * Creates file-system backed cache for belt's fetch.js API responses.
21
+ *
22
+ * Cache invalidation strategy: If cache file can't be read or parsed, entire
23
+ * cache is cleared (delete file) and starts fresh. This handles format changes.
24
+ *
25
+ * Uses `structuredClone` to track changes - only writes to disk if data modified.
26
+ * Formatted with Prettier before writing for version control friendliness.
27
+ *
28
+ * @param name cache filename (without .json extension)
29
+ * @param dir cache directory (defaults to `.gro/build/fetch/`)
30
+ * @returns cache object with Map-based data and save() method
31
+ */
32
+ export const create_fs_fetch_value_cache = async (
33
+ name: string,
34
+ dir = join(paths.build, 'fetch'),
35
+ ): Promise<FetchCache> => {
36
+ const data_path = join(dir, name + '.json');
37
+ let data: FetchValueCache;
38
+ if (existsSync(data_path)) {
39
+ try {
40
+ data = deserialize_cache(await readFile(data_path, 'utf8')); // TODO pass schema to parse so failures invalidate the cache
41
+ } catch (_err) {
42
+ // something went wrong, maybe the data format changed, so clear the cache
43
+ data = new Map();
44
+ await rm(data_path);
45
+ }
46
+ } else {
47
+ data = new Map();
48
+ }
49
+ const initial = structuredClone(data);
50
+ return {
51
+ name,
52
+ data,
53
+ save: async () => {
54
+ if (deep_equal_maps(initial, data)) {
55
+ return false; // no changes to save
56
+ }
57
+ await mkdir(dirname(data_path), {recursive: true});
58
+ await writeFile(data_path, await format_file(serialize_cache(data), {filepath: data_path}));
59
+ return true;
60
+ },
61
+ };
62
+ };
63
+
64
+ // TODO this is quick and dirty, but fine because it's only expected to be called during development
65
+ const deep_equal_maps = (a: Map<unknown, unknown>, b: Map<unknown, unknown>): boolean => {
66
+ if (a.size !== b.size) {
67
+ return false;
68
+ }
69
+ for (const [key, value] of a) {
70
+ if (!b.has(key) || JSON.stringify(b.get(key)) !== JSON.stringify(value)) {
71
+ return false;
72
+ }
73
+ }
74
+ return true;
75
+ };
@@ -0,0 +1,208 @@
1
+ import {spawn, spawn_out} from '@fuzdev/fuz_util/process.js';
2
+ import type {SpawnOptions} from 'node:child_process';
3
+ import {
4
+ git_check_clean_workspace as gro_git_check_clean_workspace,
5
+ git_checkout as gro_git_checkout,
6
+ git_pull as gro_git_pull,
7
+ git_current_branch_name as gro_git_current_branch_name,
8
+ git_current_commit_hash as gro_git_current_commit_hash,
9
+ type GitBranch,
10
+ type GitOrigin,
11
+ } from '@fuzdev/fuz_util/git.js';
12
+
13
+ /**
14
+ * Adds files to git staging area and throws if anything goes wrong.
15
+ */
16
+ export const git_add = async (
17
+ files: string | Array<string>,
18
+ options?: SpawnOptions,
19
+ ): Promise<void> => {
20
+ const file_list = Array.isArray(files) ? files : [files];
21
+ const result = await spawn('git', ['add', ...file_list], options);
22
+ if (!result.ok) {
23
+ throw Error(`git_add failed with code ${result.code}`);
24
+ }
25
+ };
26
+
27
+ /**
28
+ * Commits staged changes with a message and throws if anything goes wrong.
29
+ */
30
+ export const git_commit = async (message: string, options?: SpawnOptions): Promise<void> => {
31
+ const result = await spawn('git', ['commit', '-m', message], options);
32
+ if (!result.ok) {
33
+ throw Error(`git_commit failed with code ${result.code}`);
34
+ }
35
+ };
36
+
37
+ /**
38
+ * Adds files and commits in one operation and throws if anything goes wrong.
39
+ */
40
+ export const git_add_and_commit = async (
41
+ files: string | Array<string>,
42
+ message: string,
43
+ options?: SpawnOptions,
44
+ ): Promise<void> => {
45
+ await git_add(files, options);
46
+ await git_commit(message, options);
47
+ };
48
+
49
+ /**
50
+ * Creates a git tag and throws if anything goes wrong.
51
+ */
52
+ export const git_tag = async (
53
+ tag_name: string,
54
+ message?: string,
55
+ options?: SpawnOptions,
56
+ ): Promise<void> => {
57
+ const args = message ? ['tag', '-a', tag_name, '-m', message] : ['tag', tag_name];
58
+
59
+ const result = await spawn('git', args, options);
60
+ if (!result.ok) {
61
+ throw Error(`git_tag failed for tag '${tag_name}' with code ${result.code}`);
62
+ }
63
+ };
64
+
65
+ /**
66
+ * Pushes a tag to origin and throws if anything goes wrong.
67
+ */
68
+ export const git_push_tag = async (
69
+ tag_name: string,
70
+ origin: GitOrigin = 'origin' as GitOrigin,
71
+ options?: SpawnOptions,
72
+ ): Promise<void> => {
73
+ const result = await spawn('git', ['push', origin, tag_name], options);
74
+ if (!result.ok) {
75
+ throw Error(`git_push_tag failed for tag '${tag_name}' with code ${result.code}`);
76
+ }
77
+ };
78
+
79
+ export const git_has_changes = async (options?: SpawnOptions): Promise<boolean> => {
80
+ const {stdout} = await spawn_out('git', ['status', '--porcelain'], options);
81
+ return stdout ? stdout.trim().length > 0 : false;
82
+ };
83
+
84
+ /**
85
+ * Returns list of changed files compared to HEAD.
86
+ */
87
+ export const git_get_changed_files = async (options?: SpawnOptions): Promise<Array<string>> => {
88
+ const {stdout} = await spawn_out('git', ['diff', '--name-only', 'HEAD'], options);
89
+ if (!stdout) return [];
90
+
91
+ return stdout
92
+ .split('\n')
93
+ .map((f) => f.trim())
94
+ .filter(Boolean);
95
+ };
96
+
97
+ export const git_has_file_changed = async (
98
+ from_commit: string,
99
+ to_commit: string,
100
+ file_path: string,
101
+ options?: SpawnOptions,
102
+ ): Promise<boolean> => {
103
+ const {stdout} = await spawn_out(
104
+ 'git',
105
+ ['diff', '--name-only', from_commit, to_commit, '--', file_path],
106
+ options,
107
+ );
108
+ return stdout ? stdout.trim().length > 0 : false;
109
+ };
110
+
111
+ /**
112
+ * Stashes current changes and throws if anything goes wrong.
113
+ */
114
+ export const git_stash = async (message?: string, options?: SpawnOptions): Promise<void> => {
115
+ const args = message ? ['stash', 'push', '-m', message] : ['stash', 'push'];
116
+
117
+ const result = await spawn('git', args, options);
118
+ if (!result.ok) {
119
+ throw Error(`git_stash failed with code ${result.code}`);
120
+ }
121
+ };
122
+
123
+ /**
124
+ * Applies stashed changes and throws if anything goes wrong.
125
+ */
126
+ export const git_stash_pop = async (options?: SpawnOptions): Promise<void> => {
127
+ const result = await spawn('git', ['stash', 'pop'], options);
128
+ if (!result.ok) {
129
+ throw Error(`git_stash_pop failed with code ${result.code}`);
130
+ }
131
+ };
132
+
133
+ /**
134
+ * Switches to a branch with safety checks and throws if workspace is not clean.
135
+ */
136
+ export const git_switch_branch = async (
137
+ branch: GitBranch,
138
+ pull: boolean = true,
139
+ options?: SpawnOptions,
140
+ ): Promise<void> => {
141
+ // Check if workspace is clean first
142
+ const error = await gro_git_check_clean_workspace(options);
143
+ if (error) {
144
+ throw Error(`Cannot switch branch: ${error}`);
145
+ }
146
+
147
+ // Checkout the branch
148
+ await gro_git_checkout(branch, options);
149
+
150
+ // Pull latest changes if requested
151
+ if (pull) {
152
+ await gro_git_pull(undefined, undefined, options);
153
+ }
154
+
155
+ // Verify workspace is still clean
156
+ const error_after = await gro_git_check_clean_workspace(options);
157
+ if (error_after) {
158
+ throw Error(`Workspace unclean after switching to ${branch}: ${error_after}`);
159
+ }
160
+ };
161
+
162
+ /**
163
+ * Wrapper for gro's git_current_branch_name that throws if null.
164
+ */
165
+ export const git_current_branch_name_required = async (options?: SpawnOptions): Promise<string> => {
166
+ const branch = await gro_git_current_branch_name(options);
167
+ if (!branch) {
168
+ throw new Error('Failed to get current branch name');
169
+ }
170
+ return branch;
171
+ };
172
+
173
+ /**
174
+ * Wrapper for gro's git_current_commit_hash that throws if null.
175
+ */
176
+ export const git_current_commit_hash_required = async (
177
+ branch?: string,
178
+ options?: SpawnOptions,
179
+ ): Promise<string> => {
180
+ const hash = await gro_git_current_commit_hash(branch, options);
181
+ if (!hash) {
182
+ throw new Error(`Failed to get commit hash for branch: ${branch || 'current'}`);
183
+ }
184
+ return hash;
185
+ };
186
+
187
+ /**
188
+ * Wrapper for gro's git_check_clean_workspace that returns a boolean.
189
+ */
190
+ export const git_check_clean_workspace_as_boolean = async (
191
+ options?: SpawnOptions,
192
+ ): Promise<boolean> => {
193
+ const error = await gro_git_check_clean_workspace(options);
194
+ return error === null;
195
+ };
196
+
197
+ export const git_has_remote = async (
198
+ remote: string = 'origin',
199
+ options?: SpawnOptions,
200
+ ): Promise<boolean> => {
201
+ const {stdout} = await spawn_out('git', ['remote'], options);
202
+ if (!stdout) return false;
203
+ const remotes = stdout
204
+ .split('\n')
205
+ .map((r) => r.trim())
206
+ .filter(Boolean);
207
+ return remotes.includes(remote);
208
+ };
@@ -0,0 +1,128 @@
1
+ import type {Logger} from '@fuzdev/fuz_util/log.js';
2
+ import {z} from 'zod';
3
+ import {fetch_value, type FetchValueCache} from '@fuzdev/fuz_util/fetch.js';
4
+
5
+ /**
6
+ * Minimal interface for GitHub API calls - works with both Pkg and Repo.
7
+ */
8
+ export interface GithubRepoInfo {
9
+ owner_name: string | null;
10
+ repo_name: string;
11
+ }
12
+
13
+ /**
14
+ * @see https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests
15
+ */
16
+ export const GithubPullRequest = z.object({
17
+ number: z.number(),
18
+ title: z.string(),
19
+ user: z.object({
20
+ login: z.string(),
21
+ }),
22
+ draft: z.boolean(),
23
+ });
24
+ export type GithubPullRequest = z.infer<typeof GithubPullRequest>;
25
+ export const GithubPullRequests = z.array(GithubPullRequest);
26
+ export type GithubPullRequests = z.infer<typeof GithubPullRequests>;
27
+
28
+ /**
29
+ * @see https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests
30
+ */
31
+ export const fetch_github_pull_requests = async (
32
+ repo_info: GithubRepoInfo,
33
+ options: {
34
+ cache?: FetchValueCache;
35
+ log?: Logger;
36
+ token?: string;
37
+ api_version?: string;
38
+ } = {},
39
+ ): Promise<GithubPullRequests | null> => {
40
+ const {cache, log, token, api_version} = options;
41
+ if (!repo_info.owner_name) throw Error('owner_name is required');
42
+ const headers = api_version ? new Headers({'x-github-api-version': api_version}) : undefined;
43
+ const url = `https://api.github.com/repos/${repo_info.owner_name}/${repo_info.repo_name}/pulls`;
44
+ const fetched = await fetch_value(url, {
45
+ request: {headers},
46
+ parse: GithubPullRequests.parse,
47
+ token,
48
+ cache,
49
+ log,
50
+ });
51
+ if (!fetched.ok) {
52
+ // TODO @many this is messy but I think it's the main case we need to worry about?
53
+ if (fetched.status === 401) {
54
+ throw Error(
55
+ '401 response fetching github pull requests - check your SECRET_GITHUB_API_TOKEN',
56
+ );
57
+ }
58
+ return null;
59
+ }
60
+ return fetched.value;
61
+ };
62
+
63
+ /**
64
+ * @see https://docs.github.com/en/rest/checks/runs?apiVersion=2022-11-28#list-check-runs-for-a-git-reference
65
+ */
66
+ export const GithubCheckRunsItem = z.object({
67
+ status: z.enum(['queued', 'in_progress', 'completed']),
68
+ conclusion: z
69
+ .enum(['success', 'failure', 'neutral', 'cancelled', 'skipped', 'timed_out', 'action_required'])
70
+ .nullable(),
71
+ });
72
+ export type GithubCheckRunsItem = z.infer<typeof GithubCheckRunsItem>;
73
+ export const GithubCheckRuns = z.object({
74
+ total_count: z.number(),
75
+ check_runs: z.array(GithubCheckRunsItem),
76
+ });
77
+ export type GithubCheckRuns = z.infer<typeof GithubCheckRuns>;
78
+
79
+ /**
80
+ * @see https://docs.github.com/en/rest/checks/runs?apiVersion=2022-11-28#list-check-runs-for-a-git-reference
81
+ */
82
+ export const fetch_github_check_runs = async (
83
+ repo_info: GithubRepoInfo,
84
+ options: {
85
+ cache?: FetchValueCache;
86
+ log?: Logger;
87
+ token?: string;
88
+ api_version?: string;
89
+ ref?: string;
90
+ } = {},
91
+ ): Promise<GithubCheckRunsItem | null> => {
92
+ const {cache, log, token, api_version, ref = 'main'} = options;
93
+ if (!repo_info.owner_name) throw Error('owner_name is required');
94
+ const headers = api_version ? new Headers({'x-github-api-version': api_version}) : undefined;
95
+ const url = `https://api.github.com/repos/${repo_info.owner_name}/${repo_info.repo_name}/commits/${ref}/check-runs`;
96
+ const fetched = await fetch_value(url, {
97
+ request: {headers},
98
+ parse: (v) => reduce_check_runs(GithubCheckRuns.parse(v).check_runs),
99
+ token,
100
+ cache,
101
+ log,
102
+ });
103
+ if (!fetched.ok) {
104
+ // TODO @many this is messy but I think it's the main case we need to worry about?
105
+ if (fetched.status === 401) {
106
+ throw Error('401 response fetching github CI status - check your SECRET_GITHUB_API_TOKEN');
107
+ }
108
+ return null;
109
+ }
110
+ return fetched.value;
111
+ };
112
+
113
+ const reduce_check_runs = (check_runs: Array<GithubCheckRunsItem>): GithubCheckRunsItem | null => {
114
+ if (!check_runs.length) return null;
115
+ // TODO fix these types and remove the eslint disable, `GithubCheckRunsItem` should maybe have nullable properties?
116
+ let status!: GithubCheckRunsItem['status'];
117
+ let conclusion!: GithubCheckRunsItem['conclusion'];
118
+ for (const check_run of check_runs) {
119
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
120
+ if (!status || status === 'completed') {
121
+ status = check_run.status;
122
+ }
123
+ if (!conclusion || conclusion === 'success') {
124
+ conclusion = check_run.conclusion;
125
+ }
126
+ }
127
+ return {status, conclusion};
128
+ };
@@ -0,0 +1,31 @@
1
+ import {ensure_end} from '@fuzdev/fuz_util/string.js';
2
+
3
+ import type {GithubPullRequest} from './github.js';
4
+ import type {Repo} from './repo.svelte.js';
5
+
6
+ export type FilterPullRequest = (pull_request: GithubPullRequest, repo: Repo) => boolean;
7
+
8
+ export interface PullRequestMeta {
9
+ repo: Repo;
10
+ pull_request: GithubPullRequest;
11
+ }
12
+
13
+ export const to_pull_requests = (
14
+ repos: Array<Repo>,
15
+ filter_pull_request?: FilterPullRequest,
16
+ ): Array<PullRequestMeta> =>
17
+ repos
18
+ .flatMap((repo) => {
19
+ if (!repo.pull_requests) return null;
20
+ // TODO hacky, figure out the data structure
21
+ return repo.pull_requests.map((pull_request) =>
22
+ repo.package_json.homepage &&
23
+ (!filter_pull_request || filter_pull_request(pull_request, repo))
24
+ ? {repo, pull_request}
25
+ : null,
26
+ );
27
+ })
28
+ .filter((v) => v !== null);
29
+
30
+ export const to_pull_url = (repo_url: string, pull: GithubPullRequest): string =>
31
+ ensure_end(repo_url, '/') + 'pull/' + pull.number;