@regressionproof/snapshotter 0.3.1 → 0.3.3
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/CHANGELOG.md +16 -0
- package/package.json +9 -2
- package/src/.spruce/errors/errors.types.ts +52 -0
- package/src/.spruce/errors/options.types.ts +10 -0
- package/src/.spruce/errors/regressionproofSnapshotter/execCommandFailed.schema.ts +33 -0
- package/src/.spruce/schemas/fields/fieldClassMap.ts +6 -0
- package/src/.spruce/schemas/fields/fields.types.ts +1 -0
- package/src/.spruce/settings.json +3 -1
- package/src/errors/SpruceError.ts +29 -0
- package/src/errors/execCommandFailed.builder.ts +19 -0
- package/src/git.ts +37 -10
- package/src/snapshot.ts +38 -14
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.3.3](https://github.com/sprucelabsai-community/regressionproof/compare/v0.3.2...v0.3.3) (2026-01-13)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @regressionproof/snapshotter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [0.3.2](https://github.com/sprucelabsai-community/regressionproof/compare/v0.3.1...v0.3.2) (2026-01-13)
|
|
15
|
+
|
|
16
|
+
**Note:** Version bump only for package @regressionproof/snapshotter
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
## [0.3.1](https://github.com/sprucelabsai-community/regressionproof/compare/v0.3.0...v0.3.1) (2026-01-13)
|
|
7
23
|
|
|
8
24
|
**Note:** Version bump only for package @regressionproof/snapshotter
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@regressionproof/snapshotter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"@sprucelabs/esm-postbuild": "^9.0.13",
|
|
50
50
|
"@sprucelabs/jest-json-reporter": "^10.0.19",
|
|
51
51
|
"@sprucelabs/resolve-path-aliases": "^4.0.13",
|
|
52
|
+
"@sprucelabs/spruce-skill-utils": "^34.0.3",
|
|
52
53
|
"@sprucelabs/test": "^11.1.0",
|
|
53
54
|
"@sprucelabs/test-utils": "^7.2.3",
|
|
54
55
|
"@types/node": "^25.0.6",
|
|
@@ -84,5 +85,11 @@
|
|
|
84
85
|
"^#spruce/(.*)$": "<rootDir>/build/.spruce/$1"
|
|
85
86
|
}
|
|
86
87
|
},
|
|
87
|
-
"
|
|
88
|
+
"dependencies": {
|
|
89
|
+
"@sprucelabs/error": "^8.1.2",
|
|
90
|
+
"@sprucelabs/schema": "^33.1.3",
|
|
91
|
+
"@sprucelabs/spruce-core-schemas": "^42.1.3",
|
|
92
|
+
"@sprucelabs/spruce-skill-utils": "^34.0.3"
|
|
93
|
+
},
|
|
94
|
+
"gitHead": "bd2c5801a5d27ec36938d59dbfff50b32a390b87"
|
|
88
95
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { default as SchemaEntity } from '@sprucelabs/schema'
|
|
2
|
+
import * as SpruceSchema from '@sprucelabs/schema'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export declare namespace SpruceErrors.RegressionproofSnapshotter {
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export interface ExecCommandFailed {
|
|
12
|
+
|
|
13
|
+
/** . The command that was attempted */
|
|
14
|
+
'command': string
|
|
15
|
+
|
|
16
|
+
'stdout'?: string | undefined | null
|
|
17
|
+
|
|
18
|
+
'stderr'?: string | undefined | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ExecCommandFailedSchema extends SpruceSchema.Schema {
|
|
22
|
+
id: 'execCommandFailed',
|
|
23
|
+
namespace: 'RegressionproofSnapshotter',
|
|
24
|
+
name: 'Exec command failed',
|
|
25
|
+
fields: {
|
|
26
|
+
/** . The command that was attempted */
|
|
27
|
+
'command': {
|
|
28
|
+
type: 'text',
|
|
29
|
+
isRequired: true,
|
|
30
|
+
hint: 'The command that was attempted',
|
|
31
|
+
options: undefined
|
|
32
|
+
},
|
|
33
|
+
/** . */
|
|
34
|
+
'stdout': {
|
|
35
|
+
type: 'text',
|
|
36
|
+
options: undefined
|
|
37
|
+
},
|
|
38
|
+
/** . */
|
|
39
|
+
'stderr': {
|
|
40
|
+
type: 'text',
|
|
41
|
+
options: undefined
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ExecCommandFailedEntity = SchemaEntity<SpruceErrors.RegressionproofSnapshotter.ExecCommandFailedSchema>
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SpruceErrors } from "#spruce/errors/errors.types"
|
|
2
|
+
import { ErrorOptions as ISpruceErrorOptions} from "@sprucelabs/error"
|
|
3
|
+
|
|
4
|
+
export interface ExecCommandFailedErrorOptions extends SpruceErrors.RegressionproofSnapshotter.ExecCommandFailed, ISpruceErrorOptions {
|
|
5
|
+
code: 'EXEC_COMMAND_FAILED'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type ErrorOptions = | ExecCommandFailedErrorOptions
|
|
9
|
+
|
|
10
|
+
export default ErrorOptions
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SchemaRegistry } from '@sprucelabs/schema'
|
|
2
|
+
import { SpruceErrors } from '../errors.types'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const execCommandFailedSchema: SpruceErrors.RegressionproofSnapshotter.ExecCommandFailedSchema = {
|
|
7
|
+
id: 'execCommandFailed',
|
|
8
|
+
namespace: 'RegressionproofSnapshotter',
|
|
9
|
+
name: 'Exec command failed',
|
|
10
|
+
fields: {
|
|
11
|
+
/** . The command that was attempted */
|
|
12
|
+
'command': {
|
|
13
|
+
type: 'text',
|
|
14
|
+
isRequired: true,
|
|
15
|
+
hint: 'The command that was attempted',
|
|
16
|
+
options: undefined
|
|
17
|
+
},
|
|
18
|
+
/** . */
|
|
19
|
+
'stdout': {
|
|
20
|
+
type: 'text',
|
|
21
|
+
options: undefined
|
|
22
|
+
},
|
|
23
|
+
/** . */
|
|
24
|
+
'stderr': {
|
|
25
|
+
type: 'text',
|
|
26
|
+
options: undefined
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
SchemaRegistry.getInstance().trackSchema(execCommandFailedSchema)
|
|
32
|
+
|
|
33
|
+
export default execCommandFailedSchema
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FieldDefinitions, FieldDefinitionMap, FieldValueTypeGeneratorMap, FieldMap } from '@sprucelabs/schema'
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import BaseSpruceError from '@sprucelabs/error'
|
|
2
|
+
import ErrorOptions from '#spruce/errors/options.types'
|
|
3
|
+
|
|
4
|
+
export default class SpruceError extends BaseSpruceError<ErrorOptions> {
|
|
5
|
+
/** an easy to understand version of the errors */
|
|
6
|
+
public friendlyMessage(): string {
|
|
7
|
+
const { options } = this
|
|
8
|
+
let message
|
|
9
|
+
switch (options?.code) {
|
|
10
|
+
case 'EXEC_COMMAND_FAILED':
|
|
11
|
+
message = `The command "${options.command}" failed to execute.
|
|
12
|
+
|
|
13
|
+
Stdout:
|
|
14
|
+
${options.stdout ?? '<no stdout>'}
|
|
15
|
+
|
|
16
|
+
Stderr:
|
|
17
|
+
${options.stderr ?? '<no stderr>'}`
|
|
18
|
+
break
|
|
19
|
+
default:
|
|
20
|
+
message = super.friendlyMessage()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const fullMessage = options.friendlyMessage
|
|
24
|
+
? options.friendlyMessage
|
|
25
|
+
: message
|
|
26
|
+
|
|
27
|
+
return fullMessage
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { buildErrorSchema } from '@sprucelabs/schema'
|
|
2
|
+
|
|
3
|
+
export default buildErrorSchema({
|
|
4
|
+
id: 'execCommandFailed',
|
|
5
|
+
name: 'Exec command failed',
|
|
6
|
+
fields: {
|
|
7
|
+
command: {
|
|
8
|
+
type: 'text',
|
|
9
|
+
isRequired: true,
|
|
10
|
+
hint: 'The command that was attempted',
|
|
11
|
+
},
|
|
12
|
+
stdout: {
|
|
13
|
+
type: 'text',
|
|
14
|
+
},
|
|
15
|
+
stderr: {
|
|
16
|
+
type: 'text',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
})
|
package/src/git.ts
CHANGED
|
@@ -2,47 +2,74 @@ import { exec } from 'child_process'
|
|
|
2
2
|
import { existsSync } from 'fs'
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import { promisify } from 'util'
|
|
5
|
+
import { Log } from '@sprucelabs/spruce-skill-utils'
|
|
6
|
+
import SpruceError from './errors/SpruceError.js'
|
|
5
7
|
import { RemoteOptions } from './snapshotter.types.js'
|
|
6
8
|
|
|
7
9
|
const execAsync = promisify(exec)
|
|
8
10
|
|
|
9
|
-
export async function gitCommit(
|
|
11
|
+
export async function gitCommit(
|
|
12
|
+
mirrorPath: string,
|
|
13
|
+
log?: Log
|
|
14
|
+
): Promise<boolean> {
|
|
10
15
|
const gitDir = path.join(mirrorPath, '.git')
|
|
11
16
|
|
|
12
17
|
if (!existsSync(gitDir)) {
|
|
13
|
-
await
|
|
18
|
+
await execOrThrow(`git -C "${mirrorPath}" init`, log)
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
await
|
|
21
|
+
await execOrThrow(`git -C "${mirrorPath}" add -A`, log)
|
|
17
22
|
|
|
18
|
-
const { stdout } = await
|
|
23
|
+
const { stdout } = await execOrThrow(
|
|
19
24
|
`git -C "${mirrorPath}" status --porcelain`
|
|
20
25
|
)
|
|
21
26
|
if (!stdout.trim()) {
|
|
27
|
+
log?.info('No changes detected for commit', mirrorPath)
|
|
22
28
|
return false
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
const message = `Snapshot ${new Date().toISOString()}`
|
|
26
|
-
await
|
|
32
|
+
await execOrThrow(`git -C "${mirrorPath}" commit -m "${message}"`, log)
|
|
27
33
|
return true
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
export async function gitPush(
|
|
31
37
|
mirrorPath: string,
|
|
32
|
-
remote: RemoteOptions
|
|
38
|
+
remote: RemoteOptions,
|
|
39
|
+
log?: Log
|
|
33
40
|
): Promise<void> {
|
|
34
41
|
const authedUrl = remote.url.replace('://', `://${remote.token}@`)
|
|
35
42
|
|
|
36
43
|
try {
|
|
37
|
-
await
|
|
38
|
-
await
|
|
44
|
+
await execOrThrow(`git -C "${mirrorPath}" remote get-url origin`, log)
|
|
45
|
+
await execOrThrow(
|
|
39
46
|
`git -C "${mirrorPath}" remote set-url origin "${authedUrl}"`
|
|
40
47
|
)
|
|
41
48
|
} catch {
|
|
42
|
-
await
|
|
49
|
+
await execOrThrow(
|
|
43
50
|
`git -C "${mirrorPath}" remote add origin "${authedUrl}"`
|
|
44
51
|
)
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
await
|
|
54
|
+
await execOrThrow(`git -C "${mirrorPath}" push -u origin HEAD`, log)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function execOrThrow(
|
|
58
|
+
command: string,
|
|
59
|
+
log?: Log
|
|
60
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
61
|
+
try {
|
|
62
|
+
return await execAsync(command)
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const error = err as Error & { stdout?: string; stderr?: string }
|
|
65
|
+
const stdout = error.stdout ?? ''
|
|
66
|
+
const stderr = error.stderr ?? ''
|
|
67
|
+
log?.error('Command failed', command, stderr)
|
|
68
|
+
throw new SpruceError({
|
|
69
|
+
code: 'EXEC_COMMAND_FAILED',
|
|
70
|
+
command,
|
|
71
|
+
stdout,
|
|
72
|
+
stderr,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
48
75
|
}
|
package/src/snapshot.ts
CHANGED
|
@@ -1,27 +1,51 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from 'fs'
|
|
2
2
|
import path from 'path'
|
|
3
|
+
import { buildLog } from '@sprucelabs/spruce-skill-utils'
|
|
3
4
|
import { gitCommit, gitPush } from './git.js'
|
|
4
5
|
import { SnapshotOptions } from './snapshotter.types.js'
|
|
5
6
|
import { syncFiles } from './sync.js'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
class Snapshotter {
|
|
9
|
+
private log = buildLog('Snapshotter')
|
|
10
|
+
|
|
11
|
+
public async snapshot(options: SnapshotOptions): Promise<boolean> {
|
|
12
|
+
const sourcePath = options.sourcePath ?? process.cwd()
|
|
13
|
+
const { mirrorPath, testResults, remote } = options
|
|
14
|
+
|
|
15
|
+
this.log.info('Starting snapshot', sourcePath, mirrorPath)
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await syncFiles(sourcePath, mirrorPath)
|
|
19
|
+
this.log.info('Files synced', mirrorPath)
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
const snapshotterDir = path.join(mirrorPath, '.snapshotter')
|
|
22
|
+
mkdirSync(snapshotterDir, { recursive: true })
|
|
23
|
+
writeFileSync(
|
|
24
|
+
path.join(snapshotterDir, 'testResults.json'),
|
|
25
|
+
JSON.stringify(testResults, null, 2)
|
|
26
|
+
)
|
|
27
|
+
this.log.info('Test results saved', snapshotterDir)
|
|
12
28
|
|
|
13
|
-
|
|
14
|
-
mkdirSync(snapshotterDir, { recursive: true })
|
|
15
|
-
writeFileSync(
|
|
16
|
-
path.join(snapshotterDir, 'testResults.json'),
|
|
17
|
-
JSON.stringify(testResults, null, 2)
|
|
18
|
-
)
|
|
29
|
+
const committed = await gitCommit(mirrorPath, this.log)
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
if (!committed) {
|
|
32
|
+
this.log.info('No changes to commit', mirrorPath)
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
this.log.info('Commit created, pushing', remote.url)
|
|
37
|
+
await gitPush(mirrorPath, remote, this.log)
|
|
38
|
+
this.log.info('Push completed', remote.url)
|
|
39
|
+
|
|
40
|
+
return true
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
43
|
+
this.log.error('Snapshot failed', message)
|
|
44
|
+
throw err
|
|
45
|
+
}
|
|
24
46
|
}
|
|
47
|
+
}
|
|
25
48
|
|
|
26
|
-
|
|
49
|
+
export async function snapshot(options: SnapshotOptions): Promise<boolean> {
|
|
50
|
+
return new Snapshotter().snapshot(options)
|
|
27
51
|
}
|