@fuzdev/fuz_gitops 0.76.0 → 0.78.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.
- package/README.md +5 -5
- package/dist/ModulesDetail.svelte +1 -1
- package/dist/PageFooter.svelte +2 -2
- package/dist/PageHeader.svelte +1 -1
- package/dist/PageHeader.svelte.d.ts +1 -1
- package/dist/ReposTable.svelte +1 -1
- package/dist/ReposTree.svelte +1 -1
- package/dist/cargo_toml.d.ts +33 -0
- package/dist/cargo_toml.d.ts.map +1 -0
- package/dist/cargo_toml.js +74 -0
- package/dist/changeset_generator.d.ts +1 -1
- package/dist/changeset_reader.d.ts +1 -1
- package/dist/ci_reconcile.d.ts +50 -0
- package/dist/ci_reconcile.d.ts.map +1 -0
- package/dist/ci_reconcile.js +46 -0
- package/dist/config_reconcile.d.ts +46 -0
- package/dist/config_reconcile.d.ts.map +1 -0
- package/dist/config_reconcile.js +60 -0
- package/dist/dependency_graph.d.ts +2 -2
- package/dist/dependency_graph.js +4 -4
- package/dist/dependency_updater.d.ts +1 -1
- package/dist/fetch_repo_data.d.ts +2 -2
- package/dist/fetch_repo_data.js +1 -1
- package/dist/fs_fetch_value_cache.d.ts +1 -1
- package/dist/fs_fetch_value_cache.js +3 -3
- package/dist/git_operations.d.ts +1 -1
- package/dist/git_operations.js +2 -2
- package/dist/github.d.ts +2 -2
- package/dist/github.js +1 -1
- package/dist/github_helpers.js +1 -1
- package/dist/gitops_analyze.task.d.ts.map +1 -1
- package/dist/gitops_analyze.task.js +15 -8
- package/dist/gitops_config.d.ts +27 -2
- package/dist/gitops_config.d.ts.map +1 -1
- package/dist/gitops_config.js +13 -2
- package/dist/gitops_run.task.js +2 -2
- package/dist/gitops_sync.task.js +4 -4
- package/dist/gitops_task_helpers.d.ts +1 -1
- package/dist/gitops_task_helpers.js +1 -1
- package/dist/gitops_validate.task.d.ts.map +1 -1
- package/dist/gitops_validate.task.js +54 -6
- package/dist/graph_validation.d.ts +2 -2
- package/dist/graph_validation.d.ts.map +1 -1
- package/dist/graph_validation.js +3 -1
- package/dist/local_repo.d.ts +17 -3
- package/dist/local_repo.d.ts.map +1 -1
- package/dist/local_repo.js +54 -6
- package/dist/log_helpers.d.ts +1 -1
- package/dist/multi_repo_publisher.d.ts +3 -3
- package/dist/multi_repo_publisher.d.ts.map +1 -1
- package/dist/multi_repo_publisher.js +5 -1
- package/dist/npm_registry.d.ts +1 -1
- package/dist/npm_registry.js +2 -2
- package/dist/operations.d.ts +4 -4
- package/dist/operations_defaults.js +4 -4
- package/dist/output_helpers.d.ts +1 -1
- package/dist/preflight_checks.d.ts +1 -1
- package/dist/preflight_checks.js +1 -1
- package/dist/publish_steps.js +1 -1
- package/dist/publishing_plan.d.ts +3 -3
- package/dist/publishing_plan.d.ts.map +1 -1
- package/dist/publishing_plan.js +11 -1
- package/dist/publishing_plan_logging.d.ts +1 -1
- package/dist/repo.svelte.d.ts +5 -5
- package/dist/repo.svelte.js +2 -2
- package/package.json +10 -10
- package/src/lib/cargo_toml.ts +82 -0
- package/src/lib/changeset_generator.ts +6 -6
- package/src/lib/changeset_reader.ts +3 -3
- package/src/lib/ci_reconcile.ts +73 -0
- package/src/lib/config_reconcile.ts +94 -0
- package/src/lib/dependency_graph.ts +5 -5
- package/src/lib/dependency_updater.ts +7 -7
- package/src/lib/fetch_repo_data.ts +6 -6
- package/src/lib/fs_fetch_value_cache.ts +3 -3
- package/src/lib/git_operations.ts +2 -2
- package/src/lib/github.ts +2 -2
- package/src/lib/github_helpers.ts +3 -3
- package/src/lib/gitops_analyze.task.ts +26 -18
- package/src/lib/gitops_config.ts +45 -5
- package/src/lib/gitops_plan.task.ts +4 -4
- package/src/lib/gitops_publish.task.ts +9 -9
- package/src/lib/gitops_run.task.ts +4 -4
- package/src/lib/gitops_sync.task.ts +8 -8
- package/src/lib/gitops_task_helpers.ts +7 -7
- package/src/lib/gitops_validate.task.ts +63 -11
- package/src/lib/graph_validation.ts +5 -4
- package/src/lib/local_repo.ts +81 -15
- package/src/lib/log_helpers.ts +2 -2
- package/src/lib/multi_repo_publisher.ts +15 -11
- package/src/lib/npm_registry.ts +3 -3
- package/src/lib/operations.ts +9 -9
- package/src/lib/operations_defaults.ts +9 -9
- package/src/lib/output_helpers.ts +1 -1
- package/src/lib/preflight_checks.ts +6 -6
- package/src/lib/publish_gate.ts +2 -2
- package/src/lib/publish_steps.ts +3 -3
- package/src/lib/publishing_event_handler.ts +1 -1
- package/src/lib/publishing_plan.ts +22 -10
- package/src/lib/publishing_plan_helpers.ts +3 -3
- package/src/lib/publishing_plan_logging.ts +2 -2
- package/src/lib/repo.svelte.ts +7 -7
- package/src/lib/repo_ops.ts +3 -3
- package/src/lib/resolved_gitops_config.ts +2 -2
package/dist/publishing_plan.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { styleText as st } from 'node:util';
|
|
2
|
+
import { repo_is_npm } from './local_repo.js';
|
|
2
3
|
import { validate_dependency_graph } from './graph_validation.js';
|
|
3
4
|
import { is_breaking_change, compare_bump_types, calculate_next_version, } from './version_utils.js';
|
|
4
5
|
import { default_changeset_operations } from './operations_defaults.js';
|
|
@@ -11,12 +12,21 @@ export { log_publishing_plan } from './publishing_plan_logging.js';
|
|
|
11
12
|
* Shows version changes, dependency updates, and breaking change cascades.
|
|
12
13
|
* Uses fixed-point iteration to resolve transitive cascades.
|
|
13
14
|
*/
|
|
14
|
-
export const generate_publishing_plan = async (
|
|
15
|
+
export const generate_publishing_plan = async (all_repos, options = {}) => {
|
|
15
16
|
const { log, ops = default_changeset_operations, verbose } = options;
|
|
16
17
|
log?.info(st('cyan', 'Generating publishing plan...'));
|
|
17
18
|
const warnings = [];
|
|
18
19
|
const info = []; // Informational status (not warnings)
|
|
19
20
|
const errors = [];
|
|
21
|
+
// Publishing concerns only npm packages. Non-npm repos (e.g. Rust cargo repos) are
|
|
22
|
+
// loaded for the dashboard but have no npm identity in the changeset cascade — drop them
|
|
23
|
+
// before building the graph so they never appear in the plan or publishing order.
|
|
24
|
+
const non_npm_repos = all_repos.filter((r) => !repo_is_npm(r));
|
|
25
|
+
if (non_npm_repos.length > 0) {
|
|
26
|
+
info.push(`Excluded ${non_npm_repos.length} non-npm repo(s) from publishing: ` +
|
|
27
|
+
non_npm_repos.map((r) => r.library.name).join(', '));
|
|
28
|
+
}
|
|
29
|
+
const repos = all_repos.filter(repo_is_npm);
|
|
20
30
|
// Verbose data collection
|
|
21
31
|
const verbose_changeset_details = [];
|
|
22
32
|
const verbose_iterations = [];
|
package/dist/repo.svelte.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { LibraryJson, SourceJson } from '@fuzdev/fuz_util/library_json.
|
|
2
|
-
import type { PkgJson } from '@fuzdev/fuz_util/pkg_json.
|
|
3
|
-
import type { PackageJson } from '@fuzdev/fuz_util/package_json.
|
|
4
|
-
import type { Url } from '@fuzdev/fuz_util/url.
|
|
5
|
-
import { Library } from '@fuzdev/fuz_ui/library.svelte.
|
|
1
|
+
import type { LibraryJson, SourceJson } from '@fuzdev/fuz_util/library_json.ts';
|
|
2
|
+
import type { PkgJson } from '@fuzdev/fuz_util/pkg_json.ts';
|
|
3
|
+
import type { PackageJson } from '@fuzdev/fuz_util/package_json.ts';
|
|
4
|
+
import type { Url } from '@fuzdev/fuz_util/url.ts';
|
|
5
|
+
import { Library } from '@fuzdev/fuz_ui/library.svelte.ts';
|
|
6
6
|
import { GithubCheckRunsItem, type GithubPullRequest } from './github.js';
|
|
7
7
|
/**
|
|
8
8
|
* Serialized repo data as stored in `repos.ts` (JSON).
|
package/dist/repo.svelte.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { create_context } from '@fuzdev/fuz_ui/context_helpers.
|
|
2
|
-
import { Library } from '@fuzdev/fuz_ui/library.svelte.
|
|
1
|
+
import { create_context } from '@fuzdev/fuz_ui/context_helpers.ts';
|
|
2
|
+
import { Library } from '@fuzdev/fuz_ui/library.svelte.ts';
|
|
3
3
|
import { GithubCheckRunsItem } from './github.js';
|
|
4
4
|
/**
|
|
5
5
|
* Runtime repo with `Library` composition for package metadata.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/fuz_gitops",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.78.0",
|
|
4
4
|
"description": "a tool for managing many repos",
|
|
5
5
|
"glyph": "🪄",
|
|
6
6
|
"logo": "logo.svg",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@fuzdev/fuz_css": ">=0.62.0",
|
|
33
|
-
"@fuzdev/fuz_ui": ">=0.205.
|
|
34
|
-
"@fuzdev/fuz_util": ">=0.65.
|
|
33
|
+
"@fuzdev/fuz_ui": ">=0.205.1",
|
|
34
|
+
"@fuzdev/fuz_util": ">=0.65.2",
|
|
35
35
|
"@fuzdev/gro": ">=0.204.0",
|
|
36
36
|
"@sveltejs/kit": "^2",
|
|
37
37
|
"svelte": "^5",
|
|
@@ -40,18 +40,18 @@
|
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@changesets/changelog-git": "^0.2.1",
|
|
42
42
|
"@fuzdev/blake3_wasm": "^0.1.1",
|
|
43
|
-
"@fuzdev/fuz_code": "^0.46.
|
|
44
|
-
"@fuzdev/fuz_css": "^0.63.
|
|
45
|
-
"@fuzdev/fuz_ui": "^0.205.
|
|
46
|
-
"@fuzdev/fuz_util": "^0.65.
|
|
47
|
-
"@fuzdev/gro": "^0.
|
|
43
|
+
"@fuzdev/fuz_code": "^0.46.1",
|
|
44
|
+
"@fuzdev/fuz_css": "^0.63.2",
|
|
45
|
+
"@fuzdev/fuz_ui": "^0.205.1",
|
|
46
|
+
"@fuzdev/fuz_util": "^0.65.2",
|
|
47
|
+
"@fuzdev/gro": "^0.205.1",
|
|
48
48
|
"@fuzdev/mdz": "^0.1.0",
|
|
49
49
|
"@ryanatkn/eslint-config": "^0.12.1",
|
|
50
50
|
"@sveltejs/acorn-typescript": "^1.0.9",
|
|
51
51
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
52
52
|
"@sveltejs/kit": "^2.63.0",
|
|
53
53
|
"@sveltejs/package": "^2.5.7",
|
|
54
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
54
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
55
55
|
"@types/estree": "^1.0.8",
|
|
56
56
|
"@types/node": "^24.10.1",
|
|
57
57
|
"@webref/css": "^8.2.0",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"tslib": "^2.8.1",
|
|
69
69
|
"typescript": "^5.9.3",
|
|
70
70
|
"typescript-eslint": "^8.48.1",
|
|
71
|
-
"vite": "^
|
|
71
|
+
"vite": "^8.0.16",
|
|
72
72
|
"vitest": "^4.0.15",
|
|
73
73
|
"zimmerframe": "^1.1.4",
|
|
74
74
|
"zod": "^4.3.6"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {existsSync} from 'node:fs';
|
|
2
|
+
import {readFile} from 'node:fs/promises';
|
|
3
|
+
import {join} from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The handful of identity fields gitops reads from a Rust repo's `Cargo.toml`
|
|
7
|
+
* to render it on the dashboard. Everything is optional — a workspace root has
|
|
8
|
+
* no `name`, and any field may be absent or inherited.
|
|
9
|
+
*/
|
|
10
|
+
export interface CargoMetadata {
|
|
11
|
+
name?: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
repository?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The `Cargo.toml` tables that carry package identity (single-crate vs workspace root). */
|
|
18
|
+
const CARGO_METADATA_SECTIONS: ReadonlySet<string> = new Set(['package', 'workspace.package']);
|
|
19
|
+
const CARGO_METADATA_KEYS = ['name', 'version', 'description', 'repository'] as const;
|
|
20
|
+
type CargoMetadataKey = (typeof CARGO_METADATA_KEYS)[number];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Best-effort read of a repo's root `Cargo.toml` for the identity fields the
|
|
24
|
+
* gitops dashboard renders. Returns `null` when there's no `Cargo.toml`.
|
|
25
|
+
*
|
|
26
|
+
* @param repo_dir - absolute path to the repo
|
|
27
|
+
*/
|
|
28
|
+
export const cargo_toml_load = async (repo_dir: string): Promise<CargoMetadata | null> => {
|
|
29
|
+
const cargo_toml_path = join(repo_dir, 'Cargo.toml');
|
|
30
|
+
if (!existsSync(cargo_toml_path)) return null;
|
|
31
|
+
return cargo_toml_parse(await readFile(cargo_toml_path, 'utf8'));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extracts `name`/`version`/`description`/`repository` from the `[package]` and
|
|
36
|
+
* `[workspace.package]` tables of a `Cargo.toml`.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately not a full TOML parser: it scans for simple `key = "value"`
|
|
39
|
+
* string entries in those two tables, which covers both a single-crate manifest
|
|
40
|
+
* and a workspace root. Inline-table values like `version = { workspace = true }`
|
|
41
|
+
* (and the equivalent `version.workspace = true`) are ignored — a member crate
|
|
42
|
+
* inheriting from the workspace has no literal here, and gitops only ever reads a
|
|
43
|
+
* repo's root manifest, where these are concrete. The first non-empty value for a
|
|
44
|
+
* key wins, so a top-level `[package]` takes precedence over `[workspace.package]`
|
|
45
|
+
* when both appear.
|
|
46
|
+
*/
|
|
47
|
+
export const cargo_toml_parse = (contents: string): CargoMetadata => {
|
|
48
|
+
const metadata: CargoMetadata = {};
|
|
49
|
+
let in_metadata_section = false;
|
|
50
|
+
for (const raw_line of contents.split('\n')) {
|
|
51
|
+
const line = raw_line.trim();
|
|
52
|
+
if (line === '' || line.startsWith('#')) continue;
|
|
53
|
+
// Section header — only `[package]`/`[workspace.package]` arm value capture.
|
|
54
|
+
if (line.startsWith('[')) {
|
|
55
|
+
const end = line.indexOf(']');
|
|
56
|
+
in_metadata_section = end !== -1 && CARGO_METADATA_SECTIONS.has(line.slice(1, end).trim());
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!in_metadata_section) continue;
|
|
60
|
+
const eq = line.indexOf('=');
|
|
61
|
+
if (eq === -1) continue;
|
|
62
|
+
const key = line.slice(0, eq).trim();
|
|
63
|
+
if (!CARGO_METADATA_KEYS.includes(key as CargoMetadataKey)) continue;
|
|
64
|
+
if (metadata[key as CargoMetadataKey] !== undefined) continue; // first value wins
|
|
65
|
+
const value = cargo_toml_string_value(line.slice(eq + 1).trim());
|
|
66
|
+
if (value !== null) metadata[key as CargoMetadataKey] = value;
|
|
67
|
+
}
|
|
68
|
+
return metadata;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Reads a simple single- or double-quoted TOML string, ignoring any trailing
|
|
73
|
+
* inline comment. Returns `null` for non-string values (inline tables, arrays,
|
|
74
|
+
* booleans, numbers), which the caller skips.
|
|
75
|
+
*/
|
|
76
|
+
const cargo_toml_string_value = (raw: string): string | null => {
|
|
77
|
+
const quote = raw[0];
|
|
78
|
+
if (quote !== '"' && quote !== "'") return null;
|
|
79
|
+
const end = raw.indexOf(quote, 1);
|
|
80
|
+
if (end === -1) return null;
|
|
81
|
+
return raw.slice(1, end);
|
|
82
|
+
};
|
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import {join} from 'node:path';
|
|
11
|
-
import type {Logger} from '@fuzdev/fuz_util/log.
|
|
12
|
-
import type {LocalRepo} from './local_repo.
|
|
13
|
-
import type {PublishedVersion} from './multi_repo_publisher.
|
|
11
|
+
import type {Logger} from '@fuzdev/fuz_util/log.ts';
|
|
12
|
+
import type {LocalRepo} from './local_repo.ts';
|
|
13
|
+
import type {PublishedVersion} from './multi_repo_publisher.ts';
|
|
14
14
|
import {
|
|
15
15
|
strip_version_prefix,
|
|
16
16
|
required_bump_for_dependency_update,
|
|
17
17
|
type BumpType,
|
|
18
|
-
} from './version_utils.
|
|
19
|
-
import type {FsOperations} from './operations.
|
|
20
|
-
import {default_fs_operations} from './operations_defaults.
|
|
18
|
+
} from './version_utils.ts';
|
|
19
|
+
import type {FsOperations} from './operations.ts';
|
|
20
|
+
import {default_fs_operations} from './operations_defaults.ts';
|
|
21
21
|
|
|
22
22
|
export interface DependencyVersionChange {
|
|
23
23
|
package_name: string;
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type {Logger} from '@fuzdev/fuz_util/log.
|
|
10
|
+
import type {Logger} from '@fuzdev/fuz_util/log.ts';
|
|
11
11
|
import {existsSync} from 'node:fs';
|
|
12
12
|
import {readdir, readFile} from 'node:fs/promises';
|
|
13
13
|
import {join} from 'node:path';
|
|
14
14
|
|
|
15
|
-
import type {LocalRepo} from './local_repo.
|
|
16
|
-
import {compare_bump_types, calculate_next_version, type BumpType} from './version_utils.
|
|
15
|
+
import type {LocalRepo} from './local_repo.ts';
|
|
16
|
+
import {compare_bump_types, calculate_next_version, type BumpType} from './version_utils.ts';
|
|
17
17
|
|
|
18
18
|
export interface ChangesetInfo {
|
|
19
19
|
filename: string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconciles each repo's declared `ci` flag against whether it actually has
|
|
3
|
+
* GitHub Actions workflow files on disk.
|
|
4
|
+
*
|
|
5
|
+
* The gitops config derives `ci` from visibility (on for public repos, off for
|
|
6
|
+
* private) with per-repo overrides; this check catches drift between that
|
|
7
|
+
* declaration and reality —
|
|
8
|
+
* a repo that claims CI but ships no workflow, or one that disclaims CI yet
|
|
9
|
+
* still carries one. Repos that aren't checked out locally can't be judged, so
|
|
10
|
+
* the caller marks them uncheckable and they're skipped. Archived repos are
|
|
11
|
+
* frozen on their host, so their CI state is intentionally left alone and they're
|
|
12
|
+
* skipped too.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {existsSync, readdirSync} from 'node:fs';
|
|
18
|
+
import {join} from 'node:path';
|
|
19
|
+
|
|
20
|
+
/** How a repo's declared `ci` diverges from its workflow files on disk. */
|
|
21
|
+
export type CiDriftKind =
|
|
22
|
+
/** `ci` is `true` but the repo has no workflow files. */
|
|
23
|
+
| 'missing_ci'
|
|
24
|
+
/** `ci` is `false` but the repo has workflow files. */
|
|
25
|
+
| 'stray_ci';
|
|
26
|
+
|
|
27
|
+
export interface CiDrift {
|
|
28
|
+
repo_url: string;
|
|
29
|
+
/** The declared/derived `ci` value. */
|
|
30
|
+
ci: boolean;
|
|
31
|
+
has_workflows: boolean;
|
|
32
|
+
kind: CiDriftKind;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CiReconcileInput {
|
|
36
|
+
repo_url: string;
|
|
37
|
+
/** The declared/derived `ci` value from the gitops config. */
|
|
38
|
+
ci: boolean;
|
|
39
|
+
/** Whether the repo has at least one workflow file on disk. */
|
|
40
|
+
has_workflows: boolean;
|
|
41
|
+
/** Whether the repo is checked out locally; uncheckable repos are skipped. */
|
|
42
|
+
checkable: boolean;
|
|
43
|
+
/** Whether the repo is archived (frozen) on its host; archived repos are skipped. */
|
|
44
|
+
archived: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compares each repo's declared `ci` against its actual workflow presence.
|
|
49
|
+
* @returns one `CiDrift` per repo whose declaration and reality disagree
|
|
50
|
+
*/
|
|
51
|
+
export const reconcile_ci = (repos: Array<CiReconcileInput>): Array<CiDrift> => {
|
|
52
|
+
const drift: Array<CiDrift> = [];
|
|
53
|
+
for (const repo of repos) {
|
|
54
|
+
if (!repo.checkable || repo.archived) continue;
|
|
55
|
+
const {repo_url, ci, has_workflows} = repo;
|
|
56
|
+
if (ci && !has_workflows) {
|
|
57
|
+
drift.push({repo_url, ci, has_workflows, kind: 'missing_ci'});
|
|
58
|
+
} else if (!ci && has_workflows) {
|
|
59
|
+
drift.push({repo_url, ci, has_workflows, kind: 'stray_ci'});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return drift;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Whether a local repo directory contains at least one GitHub Actions workflow.
|
|
67
|
+
* @param repo_dir - absolute or cwd-relative path to the repo's local directory
|
|
68
|
+
*/
|
|
69
|
+
export const repo_has_workflows = (repo_dir: string): boolean => {
|
|
70
|
+
const workflows_dir = join(repo_dir, '.github', 'workflows');
|
|
71
|
+
if (!existsSync(workflows_dir)) return false;
|
|
72
|
+
return readdirSync(workflows_dir).some((file) => file.endsWith('.yml') || file.endsWith('.yaml'));
|
|
73
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconciles repo metadata across multiple gitops configs.
|
|
3
|
+
*
|
|
4
|
+
* The ecosystem keeps several gitops configs — a canonical superset plus
|
|
5
|
+
* narrower per-project subsets. They can't share one source of truth (the
|
|
6
|
+
* superset includes private repos that public configs may not reference), so the
|
|
7
|
+
* same repo is declared independently in each. This check flags when a subset's
|
|
8
|
+
* intrinsic fields drift from the canonical config, or when a subset lists a
|
|
9
|
+
* repo the canonical config doesn't. `repo_dir` is per-config and ignored.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {GitopsRepoConfig} from './gitops_config.ts';
|
|
15
|
+
|
|
16
|
+
/** Repo fields that must agree across every config that lists the repo. */
|
|
17
|
+
const INTRINSIC_FIELDS = ['visibility', 'ci', 'archived', 'branch'] as const;
|
|
18
|
+
|
|
19
|
+
export type IntrinsicField = (typeof INTRINSIC_FIELDS)[number];
|
|
20
|
+
|
|
21
|
+
export type ConfigDriftKind =
|
|
22
|
+
/** The repo is in a subset config but absent from the canonical config. */
|
|
23
|
+
| 'missing_from_canonical'
|
|
24
|
+
/** An intrinsic field disagrees between the subset and the canonical config. */
|
|
25
|
+
| 'field_mismatch';
|
|
26
|
+
|
|
27
|
+
export interface ConfigDrift {
|
|
28
|
+
repo_url: string;
|
|
29
|
+
/** Name of the subset config whose entry diverges. */
|
|
30
|
+
config: string;
|
|
31
|
+
kind: ConfigDriftKind;
|
|
32
|
+
/** The field that disagrees, set when `kind` is `'field_mismatch'`. */
|
|
33
|
+
field?: IntrinsicField;
|
|
34
|
+
/** The canonical config's value, set when `kind` is `'field_mismatch'`. */
|
|
35
|
+
canonical_value?: string;
|
|
36
|
+
/** The subset config's value, set when `kind` is `'field_mismatch'`. */
|
|
37
|
+
config_value?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface NamedRepos {
|
|
41
|
+
name: string;
|
|
42
|
+
repos: Array<GitopsRepoConfig>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const field_value = (repo: GitopsRepoConfig, field: IntrinsicField): string => {
|
|
46
|
+
switch (field) {
|
|
47
|
+
case 'visibility':
|
|
48
|
+
return repo.visibility;
|
|
49
|
+
case 'ci':
|
|
50
|
+
return String(repo.ci);
|
|
51
|
+
case 'archived':
|
|
52
|
+
return String(repo.archived);
|
|
53
|
+
case 'branch':
|
|
54
|
+
return repo.branch;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Compares each subset config against the canonical config. Repos that live only
|
|
60
|
+
* in the canonical config are fine — the subsets are intentional partial views.
|
|
61
|
+
* @returns one `ConfigDrift` per subset entry that is missing from the canonical
|
|
62
|
+
* config or whose intrinsic fields disagree with it
|
|
63
|
+
*/
|
|
64
|
+
export const reconcile_configs = (
|
|
65
|
+
canonical: NamedRepos,
|
|
66
|
+
subsets: Array<NamedRepos>,
|
|
67
|
+
): Array<ConfigDrift> => {
|
|
68
|
+
const drift: Array<ConfigDrift> = [];
|
|
69
|
+
const canonical_by_url = new Map(canonical.repos.map((r) => [r.repo_url, r] as const));
|
|
70
|
+
for (const subset of subsets) {
|
|
71
|
+
for (const repo of subset.repos) {
|
|
72
|
+
const canon = canonical_by_url.get(repo.repo_url);
|
|
73
|
+
if (!canon) {
|
|
74
|
+
drift.push({repo_url: repo.repo_url, config: subset.name, kind: 'missing_from_canonical'});
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
for (const field of INTRINSIC_FIELDS) {
|
|
78
|
+
const canonical_value = field_value(canon, field);
|
|
79
|
+
const config_value = field_value(repo, field);
|
|
80
|
+
if (canonical_value !== config_value) {
|
|
81
|
+
drift.push({
|
|
82
|
+
repo_url: repo.repo_url,
|
|
83
|
+
config: subset.name,
|
|
84
|
+
kind: 'field_mismatch',
|
|
85
|
+
field,
|
|
86
|
+
canonical_value,
|
|
87
|
+
config_value,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return drift;
|
|
94
|
+
};
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dependency graph data structure and algorithms for multi-repo publishing.
|
|
3
3
|
*
|
|
4
|
-
* Provides `DependencyGraph` class with topological sort (via `@fuzdev/fuz_util/sort.
|
|
4
|
+
* Provides `DependencyGraph` class with topological sort (via `@fuzdev/fuz_util/sort.ts`)
|
|
5
5
|
* and cycle detection by dependency type.
|
|
6
6
|
* For validation workflow and publishing order computation, see `graph_validation.ts`.
|
|
7
7
|
*
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {EMPTY_OBJECT} from '@fuzdev/fuz_util/object.
|
|
12
|
-
import {topological_sort as topological_sort_generic} from '@fuzdev/fuz_util/sort.
|
|
11
|
+
import {EMPTY_OBJECT} from '@fuzdev/fuz_util/object.ts';
|
|
12
|
+
import {topological_sort as topological_sort_generic} from '@fuzdev/fuz_util/sort.ts';
|
|
13
13
|
|
|
14
|
-
import type {LocalRepo} from './local_repo.
|
|
14
|
+
import type {LocalRepo} from './local_repo.ts';
|
|
15
15
|
|
|
16
16
|
export const DEPENDENCY_TYPE = {
|
|
17
17
|
PROD: 'prod',
|
|
@@ -122,7 +122,7 @@ export class DependencyGraph {
|
|
|
122
122
|
/**
|
|
123
123
|
* Computes topological sort order for dependency graph.
|
|
124
124
|
*
|
|
125
|
-
* Delegates to `@fuzdev/fuz_util/sort.
|
|
125
|
+
* Delegates to `@fuzdev/fuz_util/sort.ts` for the sorting algorithm.
|
|
126
126
|
* Throws if cycles detected.
|
|
127
127
|
*
|
|
128
128
|
* @param exclude_dev - if true, excludes dev dependencies to break cycles
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import type {Logger} from '@fuzdev/fuz_util/log.
|
|
1
|
+
import type {Logger} from '@fuzdev/fuz_util/log.ts';
|
|
2
2
|
import {join} from 'node:path';
|
|
3
3
|
|
|
4
|
-
import type {LocalRepo} from './local_repo.
|
|
5
|
-
import type {PublishedVersion} from './multi_repo_publisher.
|
|
4
|
+
import type {LocalRepo} from './local_repo.ts';
|
|
5
|
+
import type {PublishedVersion} from './multi_repo_publisher.ts';
|
|
6
6
|
import {
|
|
7
7
|
create_changeset_for_dependency_updates,
|
|
8
8
|
create_dependency_updates,
|
|
9
|
-
} from './changeset_generator.
|
|
10
|
-
import {needs_update, get_update_prefix} from './version_utils.
|
|
11
|
-
import type {GitOperations, FsOperations} from './operations.
|
|
12
|
-
import {default_git_operations, default_fs_operations} from './operations_defaults.
|
|
9
|
+
} from './changeset_generator.ts';
|
|
10
|
+
import {needs_update, get_update_prefix} from './version_utils.ts';
|
|
11
|
+
import type {GitOperations, FsOperations} from './operations.ts';
|
|
12
|
+
import {default_git_operations, default_fs_operations} from './operations_defaults.ts';
|
|
13
13
|
|
|
14
14
|
export type VersionStrategy = 'exact' | 'caret' | 'tilde' | 'gte';
|
|
15
15
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {Logger} from '@fuzdev/fuz_util/log.
|
|
2
|
-
import {wait} from '@fuzdev/fuz_util/async.
|
|
3
|
-
import type {FetchValueCache} from '@fuzdev/fuz_util/fetch.
|
|
1
|
+
import type {Logger} from '@fuzdev/fuz_util/log.ts';
|
|
2
|
+
import {wait} from '@fuzdev/fuz_util/async.ts';
|
|
3
|
+
import type {FetchValueCache} from '@fuzdev/fuz_util/fetch.ts';
|
|
4
4
|
|
|
5
|
-
import {fetch_github_check_runs, fetch_github_pull_requests} from './github.
|
|
6
|
-
import type {RepoJson} from './repo.svelte.
|
|
7
|
-
import type {LocalRepo} from './local_repo.
|
|
5
|
+
import {fetch_github_check_runs, fetch_github_pull_requests} from './github.ts';
|
|
6
|
+
import type {RepoJson} from './repo.svelte.ts';
|
|
7
|
+
import type {LocalRepo} from './local_repo.ts';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Fetches GitHub metadata (CI status, PRs) for all repos.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {mkdir, readFile, rm, writeFile} from 'node:fs/promises';
|
|
2
2
|
import {dirname, join} from 'node:path';
|
|
3
|
-
import {paths} from '@fuzdev/gro/paths.
|
|
4
|
-
import {format_file} from '@fuzdev/gro/format_file.
|
|
5
|
-
import {deserialize_cache, serialize_cache, type FetchValueCache} from '@fuzdev/fuz_util/fetch.
|
|
3
|
+
import {paths} from '@fuzdev/gro/paths.ts';
|
|
4
|
+
import {format_file} from '@fuzdev/gro/format_file.ts';
|
|
5
|
+
import {deserialize_cache, serialize_cache, type FetchValueCache} from '@fuzdev/fuz_util/fetch.ts';
|
|
6
6
|
import {existsSync} from 'node:fs';
|
|
7
7
|
|
|
8
8
|
// TODO upstream to Gro probably, and rename/redesign?
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {spawn_out, spawn_result_to_message} from '@fuzdev/fuz_util/process.
|
|
1
|
+
import {spawn_out, spawn_result_to_message} from '@fuzdev/fuz_util/process.ts';
|
|
2
2
|
import type {SpawnOptions} from 'node:child_process';
|
|
3
3
|
import {
|
|
4
4
|
git_check_clean_workspace as gro_git_check_clean_workspace,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
git_current_commit_hash as gro_git_current_commit_hash,
|
|
9
9
|
type GitBranch,
|
|
10
10
|
type GitOrigin,
|
|
11
|
-
} from '@fuzdev/fuz_util/git.
|
|
11
|
+
} from '@fuzdev/fuz_util/git.ts';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Adds files to git staging area and throws if anything goes wrong.
|
package/src/lib/github.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {Logger} from '@fuzdev/fuz_util/log.
|
|
1
|
+
import type {Logger} from '@fuzdev/fuz_util/log.ts';
|
|
2
2
|
import {z} from 'zod';
|
|
3
|
-
import {fetch_value, type FetchValueCache} from '@fuzdev/fuz_util/fetch.
|
|
3
|
+
import {fetch_value, type FetchValueCache} from '@fuzdev/fuz_util/fetch.ts';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Minimal interface for GitHub API calls - works with both `Pkg` and `Repo`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {ensure_end} from '@fuzdev/fuz_util/string.
|
|
1
|
+
import {ensure_end} from '@fuzdev/fuz_util/string.ts';
|
|
2
2
|
|
|
3
|
-
import type {GithubPullRequest} from './github.
|
|
4
|
-
import type {Repo} from './repo.svelte.
|
|
3
|
+
import type {GithubPullRequest} from './github.ts';
|
|
4
|
+
import type {Repo} from './repo.svelte.ts';
|
|
5
5
|
|
|
6
6
|
export type FilterPullRequest = (pull_request: GithubPullRequest, repo: Repo) => boolean;
|
|
7
7
|
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import type {Task} from '@fuzdev/gro';
|
|
2
2
|
import {z} from 'zod';
|
|
3
3
|
import {styleText as st} from 'node:util';
|
|
4
|
-
import type {Logger} from '@fuzdev/fuz_util/log.
|
|
4
|
+
import type {Logger} from '@fuzdev/fuz_util/log.ts';
|
|
5
5
|
|
|
6
|
-
import {get_gitops_ready} from './gitops_task_helpers.
|
|
7
|
-
import type {DependencyGraph} from './dependency_graph.
|
|
8
|
-
import
|
|
9
|
-
import {analyze_repos, type DependencyAnalysis} from './graph_validation.
|
|
6
|
+
import {get_gitops_ready} from './gitops_task_helpers.ts';
|
|
7
|
+
import type {DependencyGraph} from './dependency_graph.ts';
|
|
8
|
+
import {repo_is_npm} from './local_repo.ts';
|
|
9
|
+
import {analyze_repos, type DependencyAnalysis} from './graph_validation.ts';
|
|
10
10
|
import {
|
|
11
11
|
format_wildcard_dependencies,
|
|
12
12
|
format_dev_cycles,
|
|
13
13
|
format_production_cycles,
|
|
14
|
-
} from './log_helpers.
|
|
15
|
-
import {format_and_output, type OutputFormatters} from './output_helpers.
|
|
16
|
-
import {GITOPS_CONFIG_PATH_DEFAULT} from './gitops_constants.
|
|
14
|
+
} from './log_helpers.ts';
|
|
15
|
+
import {format_and_output, type OutputFormatters} from './output_helpers.ts';
|
|
16
|
+
import {GITOPS_CONFIG_PATH_DEFAULT} from './gitops_constants.ts';
|
|
17
17
|
|
|
18
18
|
/** @nodocs */
|
|
19
19
|
export const Args = z.strictObject({
|
|
@@ -50,12 +50,24 @@ export const task: Task<Args> = {
|
|
|
50
50
|
// Get repos ready (without downloading); read the working tree as-is unless `--sync`
|
|
51
51
|
const {local_repos} = await get_gitops_ready({config, dir, download: false, sync, log});
|
|
52
52
|
|
|
53
|
+
// Only npm packages form the dependency graph; note any non-npm repos (e.g. cargo)
|
|
54
|
+
// that are excluded so the omission isn't silent.
|
|
55
|
+
const non_npm_repos = local_repos.filter((r) => !repo_is_npm(r));
|
|
56
|
+
if (non_npm_repos.length > 0) {
|
|
57
|
+
log.info(
|
|
58
|
+
st(
|
|
59
|
+
'dim',
|
|
60
|
+
`excluding ${non_npm_repos.length} non-npm repo(s) from analysis (dashboard-only): ` +
|
|
61
|
+
non_npm_repos.map((r) => r.library.name).join(', '),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
// Build the dependency graph and analyze cycles/wildcards (tolerating cycles)
|
|
54
67
|
const {graph, analysis, publishing_order} = analyze_repos(local_repos);
|
|
55
68
|
|
|
56
69
|
// Format and output using output_helpers
|
|
57
70
|
const data = {
|
|
58
|
-
repos: local_repos,
|
|
59
71
|
graph,
|
|
60
72
|
analysis,
|
|
61
73
|
publishing_order,
|
|
@@ -67,7 +79,6 @@ export const task: Task<Args> = {
|
|
|
67
79
|
|
|
68
80
|
// Data type for analysis output
|
|
69
81
|
interface AnalysisData {
|
|
70
|
-
repos: Array<LocalRepo>;
|
|
71
82
|
graph: DependencyGraph;
|
|
72
83
|
analysis: DependencyAnalysis;
|
|
73
84
|
publishing_order: Array<string> | null;
|
|
@@ -76,9 +87,8 @@ interface AnalysisData {
|
|
|
76
87
|
// Create formatters for output_helpers
|
|
77
88
|
const create_formatters = (): OutputFormatters<AnalysisData> => ({
|
|
78
89
|
json: (data) => format_json(data.graph, data.analysis, data.publishing_order),
|
|
79
|
-
markdown: (data) => format_markdown(data.
|
|
80
|
-
stdout: (data, log) =>
|
|
81
|
-
format_stdout(data.repos, data.graph, data.analysis, data.publishing_order, log),
|
|
90
|
+
markdown: (data) => format_markdown(data.graph, data.analysis, data.publishing_order),
|
|
91
|
+
stdout: (data, log) => format_stdout(data.graph, data.analysis, data.publishing_order, log),
|
|
82
92
|
});
|
|
83
93
|
|
|
84
94
|
// Helper to calculate common statistics
|
|
@@ -109,7 +119,6 @@ const format_json = (
|
|
|
109
119
|
};
|
|
110
120
|
|
|
111
121
|
const format_markdown = (
|
|
112
|
-
repos: Array<LocalRepo>,
|
|
113
122
|
graph: DependencyGraph,
|
|
114
123
|
analysis: DependencyAnalysis,
|
|
115
124
|
publishing_order: Array<string> | null,
|
|
@@ -120,7 +129,7 @@ const format_markdown = (
|
|
|
120
129
|
const {total_deps, internal_deps} = calculate_stats(graph);
|
|
121
130
|
|
|
122
131
|
lines.push('', '## Summary', '');
|
|
123
|
-
lines.push(`- **Total packages**: ${
|
|
132
|
+
lines.push(`- **Total packages**: ${graph.nodes.size}`);
|
|
124
133
|
lines.push(`- **Total dependencies**: ${total_deps}`);
|
|
125
134
|
lines.push(`- **Internal dependencies**: ${internal_deps}`);
|
|
126
135
|
lines.push(`- **Wildcard dependencies**: ${analysis.wildcard_deps.length}`);
|
|
@@ -185,13 +194,12 @@ const format_markdown = (
|
|
|
185
194
|
};
|
|
186
195
|
|
|
187
196
|
const format_stdout = (
|
|
188
|
-
repos: Array<LocalRepo>,
|
|
189
197
|
graph: DependencyGraph,
|
|
190
198
|
analysis: DependencyAnalysis,
|
|
191
199
|
publishing_order: Array<string> | null,
|
|
192
200
|
log: Logger,
|
|
193
201
|
): void => {
|
|
194
|
-
log.info(st('cyan', `📊 Analyzing ${
|
|
202
|
+
log.info(st('cyan', `📊 Analyzing ${graph.nodes.size} repositories...`));
|
|
195
203
|
|
|
196
204
|
// Publishing order
|
|
197
205
|
if (publishing_order) {
|
|
@@ -248,7 +256,7 @@ const format_stdout = (
|
|
|
248
256
|
|
|
249
257
|
log.info('');
|
|
250
258
|
log.info(st('cyan', 'Summary:'));
|
|
251
|
-
log.info(` Total packages: ${
|
|
259
|
+
log.info(` Total packages: ${graph.nodes.size}`);
|
|
252
260
|
log.info(` Total dependencies: ${total_deps}`);
|
|
253
261
|
log.info(` Internal dependencies: ${internal_deps}`);
|
|
254
262
|
log.info(` Wildcard dependencies: ${analysis.wildcard_deps.length}`);
|