@aspruyt/json-config-sync 2.0.3 → 2.1.1
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 +28 -8
- package/dist/command-executor.d.ts +25 -0
- package/dist/command-executor.js +28 -0
- package/dist/config-formatter.d.ts +9 -0
- package/dist/config-formatter.js +21 -0
- package/dist/config-normalizer.d.ts +6 -0
- package/dist/config-normalizer.js +43 -0
- package/dist/config-validator.d.ts +6 -0
- package/dist/config-validator.js +54 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +13 -88
- package/dist/git-ops.d.ts +29 -7
- package/dist/git-ops.js +123 -40
- package/dist/index.d.ts +19 -1
- package/dist/index.js +34 -64
- package/dist/logger.d.ts +3 -0
- package/dist/merge.d.ts +9 -0
- package/dist/merge.js +14 -9
- package/dist/pr-creator.d.ts +2 -0
- package/dist/pr-creator.js +14 -105
- package/dist/repo-detector.d.ts +15 -5
- package/dist/repo-detector.js +27 -8
- package/dist/repository-processor.d.ts +36 -0
- package/dist/repository-processor.js +106 -0
- package/dist/retry-utils.d.ts +53 -0
- package/dist/retry-utils.js +143 -0
- package/dist/strategies/azure-pr-strategy.d.ts +10 -0
- package/dist/strategies/azure-pr-strategy.js +78 -0
- package/dist/strategies/github-pr-strategy.d.ts +6 -0
- package/dist/strategies/github-pr-strategy.js +65 -0
- package/dist/strategies/index.d.ts +12 -0
- package/dist/strategies/index.js +22 -0
- package/dist/strategies/pr-strategy.d.ts +70 -0
- package/dist/strategies/pr-strategy.js +60 -0
- package/dist/workspace-utils.d.ts +5 -0
- package/dist/workspace-utils.js +10 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# json-config-sync
|
|
2
2
|
|
|
3
3
|
[](https://github.com/anthony-spruyt/json-config-sync/actions/workflows/ci.yml)
|
|
4
|
-
[](https://github.com/anthony-spruyt/json-config-sync/actions/workflows/integration.yml)
|
|
4
|
+
[](https://github.com/anthony-spruyt/json-config-sync/actions/workflows/integration-test.yml)
|
|
5
5
|
[](https://www.npmjs.com/package/@aspruyt/json-config-sync)
|
|
6
6
|
[](https://www.npmjs.com/package/@aspruyt/json-config-sync)
|
|
7
7
|
|
|
@@ -70,6 +70,7 @@ json-config-sync --config ./config.yaml
|
|
|
70
70
|
- **GitHub & Azure DevOps** - Works with both platforms
|
|
71
71
|
- **Dry-Run Mode** - Preview changes without creating PRs
|
|
72
72
|
- **Error Resilience** - Continues processing if individual repos fail
|
|
73
|
+
- **Automatic Retries** - Retries transient network errors with exponential backoff
|
|
73
74
|
|
|
74
75
|
## Installation
|
|
75
76
|
|
|
@@ -122,15 +123,20 @@ json-config-sync --config ./config.yaml --dry-run
|
|
|
122
123
|
|
|
123
124
|
# Custom work directory
|
|
124
125
|
json-config-sync --config ./config.yaml --work-dir ./my-temp
|
|
126
|
+
|
|
127
|
+
# Custom branch name
|
|
128
|
+
json-config-sync --config ./config.yaml --branch feature/update-eslint
|
|
125
129
|
```
|
|
126
130
|
|
|
127
131
|
### Options
|
|
128
132
|
|
|
129
|
-
| Option | Alias | Description
|
|
130
|
-
| ------------ | ----- |
|
|
131
|
-
| `--config` | `-c` | Path to YAML config file
|
|
132
|
-
| `--dry-run` | `-d` | Show what would be done without making changes
|
|
133
|
-
| `--work-dir` | `-w` | Temporary directory for cloning (default: `./tmp`)
|
|
133
|
+
| Option | Alias | Description | Required |
|
|
134
|
+
| ------------ | ----- | ------------------------------------------------------- | -------- |
|
|
135
|
+
| `--config` | `-c` | Path to YAML config file | Yes |
|
|
136
|
+
| `--dry-run` | `-d` | Show what would be done without making changes | No |
|
|
137
|
+
| `--work-dir` | `-w` | Temporary directory for cloning (default: `./tmp`) | No |
|
|
138
|
+
| `--retries` | `-r` | Number of retries for network operations (default: 3) | No |
|
|
139
|
+
| `--branch` | `-b` | Override branch name (default: `chore/sync-{filename}`) | No |
|
|
134
140
|
|
|
135
141
|
## Configuration Format
|
|
136
142
|
|
|
@@ -315,7 +321,7 @@ flowchart TB
|
|
|
315
321
|
end
|
|
316
322
|
|
|
317
323
|
subgraph Processing["For Each Repository"]
|
|
318
|
-
CLONE[Clone Repo] --> BRANCH[Create/Checkout Branch<br
|
|
324
|
+
CLONE[Clone Repo] --> BRANCH[Create/Checkout Branch<br/>--branch or chore/sync-filename]
|
|
319
325
|
BRANCH --> WRITE[Write Config File<br/>JSON or YAML]
|
|
320
326
|
WRITE --> CHECK{Changes?}
|
|
321
327
|
CHECK -->|No| SKIP[Skip - No Changes]
|
|
@@ -345,7 +351,7 @@ For each repository in the config, the tool:
|
|
|
345
351
|
3. Interpolates environment variables
|
|
346
352
|
4. Cleans the temporary workspace
|
|
347
353
|
5. Clones the repository
|
|
348
|
-
6. Creates/checks out branch `chore/sync-{sanitized-filename}`
|
|
354
|
+
6. Creates/checks out branch (custom `--branch` or default `chore/sync-{sanitized-filename}`)
|
|
349
355
|
7. Generates the config file (JSON or YAML based on filename extension)
|
|
350
356
|
8. Checks for changes (skips if no changes)
|
|
351
357
|
9. Commits and pushes changes
|
|
@@ -505,6 +511,20 @@ git config --global http.proxy http://proxy.example.com:8080
|
|
|
505
511
|
git config --global https.proxy http://proxy.example.com:8080
|
|
506
512
|
```
|
|
507
513
|
|
|
514
|
+
### Transient Network Errors
|
|
515
|
+
|
|
516
|
+
The tool automatically retries transient errors (timeouts, connection resets, rate limits) with exponential backoff. By default, it retries 3 times before failing.
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
# Increase retries for unreliable networks
|
|
520
|
+
json-config-sync --config ./config.yaml --retries 5
|
|
521
|
+
|
|
522
|
+
# Disable retries
|
|
523
|
+
json-config-sync --config ./config.yaml --retries 0
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Permanent errors (authentication failures, permission denied, repository not found) are not retried.
|
|
527
|
+
|
|
508
528
|
## IDE Integration
|
|
509
529
|
|
|
510
530
|
### VS Code YAML Schema Support
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for executing shell commands.
|
|
3
|
+
* Enables dependency injection for testing and alternative implementations.
|
|
4
|
+
*/
|
|
5
|
+
export interface CommandExecutor {
|
|
6
|
+
/**
|
|
7
|
+
* Execute a shell command and return the output.
|
|
8
|
+
* @param command The command to execute
|
|
9
|
+
* @param cwd The working directory for the command
|
|
10
|
+
* @returns Promise resolving to the trimmed stdout output
|
|
11
|
+
* @throws Error if the command fails
|
|
12
|
+
*/
|
|
13
|
+
exec(command: string, cwd: string): Promise<string>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Default implementation that uses Node.js child_process.execSync.
|
|
17
|
+
* Note: Commands are escaped using escapeShellArg before being passed here.
|
|
18
|
+
*/
|
|
19
|
+
export declare class ShellCommandExecutor implements CommandExecutor {
|
|
20
|
+
exec(command: string, cwd: string): Promise<string>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Default executor instance for production use.
|
|
24
|
+
*/
|
|
25
|
+
export declare const defaultExecutor: CommandExecutor;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Default implementation that uses Node.js child_process.execSync.
|
|
4
|
+
* Note: Commands are escaped using escapeShellArg before being passed here.
|
|
5
|
+
*/
|
|
6
|
+
export class ShellCommandExecutor {
|
|
7
|
+
async exec(command, cwd) {
|
|
8
|
+
try {
|
|
9
|
+
return execSync(command, {
|
|
10
|
+
cwd,
|
|
11
|
+
encoding: "utf-8",
|
|
12
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
13
|
+
}).trim();
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
// Ensure stderr is always a string for consistent error handling
|
|
17
|
+
const execError = error;
|
|
18
|
+
if (execError.stderr && typeof execError.stderr !== "string") {
|
|
19
|
+
execError.stderr = execError.stderr.toString();
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Default executor instance for production use.
|
|
27
|
+
*/
|
|
28
|
+
export const defaultExecutor = new ShellCommandExecutor();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type OutputFormat = "json" | "yaml";
|
|
2
|
+
/**
|
|
3
|
+
* Detects output format from file extension.
|
|
4
|
+
*/
|
|
5
|
+
export declare function detectOutputFormat(fileName: string): OutputFormat;
|
|
6
|
+
/**
|
|
7
|
+
* Converts content object to string in the appropriate format.
|
|
8
|
+
*/
|
|
9
|
+
export declare function convertContentToString(content: Record<string, unknown>, fileName: string): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { stringify } from "yaml";
|
|
2
|
+
/**
|
|
3
|
+
* Detects output format from file extension.
|
|
4
|
+
*/
|
|
5
|
+
export function detectOutputFormat(fileName) {
|
|
6
|
+
const ext = fileName.toLowerCase().split(".").pop();
|
|
7
|
+
if (ext === "yaml" || ext === "yml") {
|
|
8
|
+
return "yaml";
|
|
9
|
+
}
|
|
10
|
+
return "json";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Converts content object to string in the appropriate format.
|
|
14
|
+
*/
|
|
15
|
+
export function convertContentToString(content, fileName) {
|
|
16
|
+
const format = detectOutputFormat(fileName);
|
|
17
|
+
if (format === "yaml") {
|
|
18
|
+
return stringify(content, { indent: 2 });
|
|
19
|
+
}
|
|
20
|
+
return JSON.stringify(content, null, 2);
|
|
21
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { deepMerge, stripMergeDirectives, createMergeContext, } from "./merge.js";
|
|
2
|
+
import { interpolateEnvVars } from "./env.js";
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes raw config into expanded, merged config.
|
|
5
|
+
* Pipeline: expand git arrays -> merge content -> interpolate env vars
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeConfig(raw) {
|
|
8
|
+
const baseContent = raw.content ?? {};
|
|
9
|
+
const defaultStrategy = raw.mergeStrategy ?? "replace";
|
|
10
|
+
const expandedRepos = [];
|
|
11
|
+
for (const rawRepo of raw.repos) {
|
|
12
|
+
// Step 1: Expand git arrays
|
|
13
|
+
const gitUrls = Array.isArray(rawRepo.git) ? rawRepo.git : [rawRepo.git];
|
|
14
|
+
for (const gitUrl of gitUrls) {
|
|
15
|
+
// Step 2: Compute merged content
|
|
16
|
+
let mergedContent;
|
|
17
|
+
if (rawRepo.override) {
|
|
18
|
+
// Override mode: use only repo content
|
|
19
|
+
mergedContent = stripMergeDirectives(structuredClone(rawRepo.content));
|
|
20
|
+
}
|
|
21
|
+
else if (!rawRepo.content) {
|
|
22
|
+
// No repo content: use root content as-is
|
|
23
|
+
mergedContent = structuredClone(baseContent);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// Merge mode: deep merge base + overlay
|
|
27
|
+
const ctx = createMergeContext(defaultStrategy);
|
|
28
|
+
mergedContent = deepMerge(structuredClone(baseContent), rawRepo.content, ctx);
|
|
29
|
+
mergedContent = stripMergeDirectives(mergedContent);
|
|
30
|
+
}
|
|
31
|
+
// Step 3: Interpolate env vars
|
|
32
|
+
mergedContent = interpolateEnvVars(mergedContent, { strict: true });
|
|
33
|
+
expandedRepos.push({
|
|
34
|
+
git: gitUrl,
|
|
35
|
+
content: mergedContent,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
fileName: raw.fileName,
|
|
41
|
+
repos: expandedRepos,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Validates raw config structure before normalization.
|
|
4
|
+
* @throws Error if validation fails
|
|
5
|
+
*/
|
|
6
|
+
export function validateRawConfig(config) {
|
|
7
|
+
if (!config.fileName) {
|
|
8
|
+
throw new Error("Config missing required field: fileName");
|
|
9
|
+
}
|
|
10
|
+
// Validate fileName doesn't allow path traversal
|
|
11
|
+
if (config.fileName.includes("..") || isAbsolute(config.fileName)) {
|
|
12
|
+
throw new Error(`Invalid fileName: must be a relative path without '..' components`);
|
|
13
|
+
}
|
|
14
|
+
// Validate fileName doesn't contain control characters that could bypass shell escaping
|
|
15
|
+
if (/[\n\r\0]/.test(config.fileName)) {
|
|
16
|
+
throw new Error(`Invalid fileName: cannot contain newlines or null bytes`);
|
|
17
|
+
}
|
|
18
|
+
if (!config.repos || !Array.isArray(config.repos)) {
|
|
19
|
+
throw new Error("Config missing required field: repos (must be an array)");
|
|
20
|
+
}
|
|
21
|
+
const validStrategies = ["replace", "append", "prepend"];
|
|
22
|
+
if (config.mergeStrategy !== undefined &&
|
|
23
|
+
!validStrategies.includes(config.mergeStrategy)) {
|
|
24
|
+
throw new Error(`Invalid mergeStrategy: ${config.mergeStrategy}. Must be one of: ${validStrategies.join(", ")}`);
|
|
25
|
+
}
|
|
26
|
+
if (config.content !== undefined &&
|
|
27
|
+
(typeof config.content !== "object" ||
|
|
28
|
+
config.content === null ||
|
|
29
|
+
Array.isArray(config.content))) {
|
|
30
|
+
throw new Error("Root content must be an object");
|
|
31
|
+
}
|
|
32
|
+
const hasRootContent = config.content !== undefined;
|
|
33
|
+
for (let i = 0; i < config.repos.length; i++) {
|
|
34
|
+
const repo = config.repos[i];
|
|
35
|
+
if (!repo.git) {
|
|
36
|
+
throw new Error(`Repo at index ${i} missing required field: git`);
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(repo.git) && repo.git.length === 0) {
|
|
39
|
+
throw new Error(`Repo at index ${i} has empty git array`);
|
|
40
|
+
}
|
|
41
|
+
if (!hasRootContent && !repo.content) {
|
|
42
|
+
throw new Error(`Repo at index ${i} missing required field: content (no root-level content defined)`);
|
|
43
|
+
}
|
|
44
|
+
if (repo.override && !repo.content) {
|
|
45
|
+
throw new Error(`Repo ${getGitDisplayName(repo.git)} has override: true but no content defined`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function getGitDisplayName(git) {
|
|
50
|
+
if (Array.isArray(git)) {
|
|
51
|
+
return git[0] || "unknown";
|
|
52
|
+
}
|
|
53
|
+
return git;
|
|
54
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ArrayMergeStrategy } from "./merge.js";
|
|
2
|
+
export { convertContentToString } from "./config-formatter.js";
|
|
2
3
|
export interface RawRepoConfig {
|
|
3
4
|
git: string | string[];
|
|
4
5
|
content?: Record<string, unknown>;
|
|
@@ -19,4 +20,3 @@ export interface Config {
|
|
|
19
20
|
repos: RepoConfig[];
|
|
20
21
|
}
|
|
21
22
|
export declare function loadConfig(filePath: string): Config;
|
|
22
|
-
export declare function convertContentToString(content: Record<string, unknown>, fileName: string): string;
|
package/dist/config.js
CHANGED
|
@@ -1,97 +1,22 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { parse
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
// =============================================================================
|
|
8
|
-
function validateRawConfig(config) {
|
|
9
|
-
if (!config.fileName) {
|
|
10
|
-
throw new Error("Config missing required field: fileName");
|
|
11
|
-
}
|
|
12
|
-
if (!config.repos || !Array.isArray(config.repos)) {
|
|
13
|
-
throw new Error("Config missing required field: repos (must be an array)");
|
|
14
|
-
}
|
|
15
|
-
const hasRootContent = config.content !== undefined;
|
|
16
|
-
for (let i = 0; i < config.repos.length; i++) {
|
|
17
|
-
const repo = config.repos[i];
|
|
18
|
-
if (!repo.git) {
|
|
19
|
-
throw new Error(`Repo at index ${i} missing required field: git`);
|
|
20
|
-
}
|
|
21
|
-
if (!hasRootContent && !repo.content) {
|
|
22
|
-
throw new Error(`Repo at index ${i} missing required field: content (no root-level content defined)`);
|
|
23
|
-
}
|
|
24
|
-
if (repo.override && !repo.content) {
|
|
25
|
-
throw new Error(`Repo ${getGitDisplayName(repo.git)} has override: true but no content defined`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function getGitDisplayName(git) {
|
|
30
|
-
if (Array.isArray(git)) {
|
|
31
|
-
return git[0] || "unknown";
|
|
32
|
-
}
|
|
33
|
-
return git;
|
|
34
|
-
}
|
|
35
|
-
// =============================================================================
|
|
36
|
-
// Normalization Pipeline
|
|
37
|
-
// =============================================================================
|
|
38
|
-
function normalizeConfig(raw) {
|
|
39
|
-
const baseContent = raw.content ?? {};
|
|
40
|
-
const defaultStrategy = raw.mergeStrategy ?? "replace";
|
|
41
|
-
const expandedRepos = [];
|
|
42
|
-
for (const rawRepo of raw.repos) {
|
|
43
|
-
// Step 1: Expand git arrays
|
|
44
|
-
const gitUrls = Array.isArray(rawRepo.git) ? rawRepo.git : [rawRepo.git];
|
|
45
|
-
for (const gitUrl of gitUrls) {
|
|
46
|
-
// Step 2: Compute merged content
|
|
47
|
-
let mergedContent;
|
|
48
|
-
if (rawRepo.override) {
|
|
49
|
-
// Override mode: use only repo content
|
|
50
|
-
mergedContent = stripMergeDirectives(structuredClone(rawRepo.content));
|
|
51
|
-
}
|
|
52
|
-
else if (!rawRepo.content) {
|
|
53
|
-
// No repo content: use root content as-is
|
|
54
|
-
mergedContent = structuredClone(baseContent);
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
// Merge mode: deep merge base + overlay
|
|
58
|
-
const ctx = createMergeContext(defaultStrategy);
|
|
59
|
-
mergedContent = deepMerge(structuredClone(baseContent), rawRepo.content, ctx);
|
|
60
|
-
mergedContent = stripMergeDirectives(mergedContent);
|
|
61
|
-
}
|
|
62
|
-
// Step 3: Interpolate env vars
|
|
63
|
-
mergedContent = interpolateEnvVars(mergedContent, { strict: true });
|
|
64
|
-
expandedRepos.push({
|
|
65
|
-
git: gitUrl,
|
|
66
|
-
content: mergedContent,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
fileName: raw.fileName,
|
|
72
|
-
repos: expandedRepos,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
2
|
+
import { parse } from "yaml";
|
|
3
|
+
import { validateRawConfig } from "./config-validator.js";
|
|
4
|
+
import { normalizeConfig } from "./config-normalizer.js";
|
|
5
|
+
// Re-export formatter functions for backwards compatibility
|
|
6
|
+
export { convertContentToString } from "./config-formatter.js";
|
|
75
7
|
// =============================================================================
|
|
76
8
|
// Public API
|
|
77
9
|
// =============================================================================
|
|
78
10
|
export function loadConfig(filePath) {
|
|
79
11
|
const content = readFileSync(filePath, "utf-8");
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
function detectOutputFormat(fileName) {
|
|
85
|
-
const ext = fileName.toLowerCase().split(".").pop();
|
|
86
|
-
if (ext === "yaml" || ext === "yml") {
|
|
87
|
-
return "yaml";
|
|
12
|
+
let rawConfig;
|
|
13
|
+
try {
|
|
14
|
+
rawConfig = parse(content);
|
|
88
15
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const format = detectOutputFormat(fileName);
|
|
93
|
-
if (format === "yaml") {
|
|
94
|
-
return stringify(content, { indent: 2 });
|
|
16
|
+
catch (error) {
|
|
17
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18
|
+
throw new Error(`Failed to parse YAML config at ${filePath}: ${message}`);
|
|
95
19
|
}
|
|
96
|
-
|
|
20
|
+
validateRawConfig(rawConfig);
|
|
21
|
+
return normalizeConfig(rawConfig);
|
|
97
22
|
}
|
package/dist/git-ops.d.ts
CHANGED
|
@@ -1,27 +1,49 @@
|
|
|
1
|
+
import { CommandExecutor } from "./command-executor.js";
|
|
1
2
|
export interface GitOpsOptions {
|
|
2
3
|
workDir: string;
|
|
3
4
|
dryRun?: boolean;
|
|
5
|
+
executor?: CommandExecutor;
|
|
6
|
+
/** Number of retries for network operations (default: 3) */
|
|
7
|
+
retries?: number;
|
|
4
8
|
}
|
|
5
9
|
export declare class GitOps {
|
|
6
10
|
private workDir;
|
|
7
11
|
private dryRun;
|
|
12
|
+
private executor;
|
|
13
|
+
private retries;
|
|
8
14
|
constructor(options: GitOpsOptions);
|
|
9
15
|
private exec;
|
|
16
|
+
/**
|
|
17
|
+
* Run a command with retry logic for transient failures.
|
|
18
|
+
* Used for network operations like clone, fetch, push.
|
|
19
|
+
*/
|
|
20
|
+
private execWithRetry;
|
|
21
|
+
/**
|
|
22
|
+
* Validates that a file path doesn't escape the workspace directory.
|
|
23
|
+
* @returns The resolved absolute file path
|
|
24
|
+
* @throws Error if path traversal is detected
|
|
25
|
+
*/
|
|
26
|
+
private validatePath;
|
|
10
27
|
cleanWorkspace(): void;
|
|
11
|
-
clone(gitUrl: string): void
|
|
12
|
-
createBranch(branchName: string): void
|
|
28
|
+
clone(gitUrl: string): Promise<void>;
|
|
29
|
+
createBranch(branchName: string): Promise<void>;
|
|
13
30
|
writeFile(fileName: string, content: string): void;
|
|
14
31
|
/**
|
|
15
32
|
* Checks if writing the given content would result in changes.
|
|
16
33
|
* Works in both normal and dry-run modes by comparing content directly.
|
|
17
34
|
*/
|
|
18
35
|
wouldChange(fileName: string, content: string): boolean;
|
|
19
|
-
hasChanges(): boolean
|
|
20
|
-
commit(message: string): void
|
|
21
|
-
push(branchName: string): void
|
|
22
|
-
getDefaultBranch(): {
|
|
36
|
+
hasChanges(): Promise<boolean>;
|
|
37
|
+
commit(message: string): Promise<void>;
|
|
38
|
+
push(branchName: string): Promise<void>;
|
|
39
|
+
getDefaultBranch(): Promise<{
|
|
23
40
|
branch: string;
|
|
24
41
|
method: string;
|
|
25
|
-
}
|
|
42
|
+
}>;
|
|
26
43
|
}
|
|
27
44
|
export declare function sanitizeBranchName(fileName: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Validates a user-provided branch name against git's naming rules.
|
|
47
|
+
* @throws Error if the branch name is invalid
|
|
48
|
+
*/
|
|
49
|
+
export declare function validateBranchName(branchName: string): void;
|