@versu/core 0.4.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 +241 -0
- package/dist/adapters/gradle/constants.d.ts +13 -0
- package/dist/adapters/gradle/constants.d.ts.map +1 -0
- package/dist/adapters/gradle/constants.js +12 -0
- package/dist/adapters/gradle/gradle-project-information.d.ts +19 -0
- package/dist/adapters/gradle/gradle-project-information.d.ts.map +1 -0
- package/dist/adapters/gradle/gradle-project-information.js +233 -0
- package/dist/adapters/gradle/init-project-information.gradle.kts +163 -0
- package/dist/adapters/gradle/services/gradle-adapter-identifier.d.ts +21 -0
- package/dist/adapters/gradle/services/gradle-adapter-identifier.d.ts.map +1 -0
- package/dist/adapters/gradle/services/gradle-adapter-identifier.js +44 -0
- package/dist/adapters/gradle/services/gradle-module-detector.d.ts +19 -0
- package/dist/adapters/gradle/services/gradle-module-detector.d.ts.map +1 -0
- package/dist/adapters/gradle/services/gradle-module-detector.js +28 -0
- package/dist/adapters/gradle/services/gradle-module-system-factory.d.ts +26 -0
- package/dist/adapters/gradle/services/gradle-module-system-factory.d.ts.map +1 -0
- package/dist/adapters/gradle/services/gradle-module-system-factory.js +29 -0
- package/dist/adapters/gradle/services/gradle-version-update-strategy.d.ts +23 -0
- package/dist/adapters/gradle/services/gradle-version-update-strategy.d.ts.map +1 -0
- package/dist/adapters/gradle/services/gradle-version-update-strategy.js +38 -0
- package/dist/adapters/project-information.d.ts +62 -0
- package/dist/adapters/project-information.d.ts.map +1 -0
- package/dist/adapters/project-information.js +1 -0
- package/dist/changelog/index.d.ts +14 -0
- package/dist/changelog/index.d.ts.map +1 -0
- package/dist/changelog/index.js +132 -0
- package/dist/config/index.d.ts +122 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +117 -0
- package/dist/factories/adapter-identifier-registry.d.ts +12 -0
- package/dist/factories/adapter-identifier-registry.d.ts.map +1 -0
- package/dist/factories/adapter-identifier-registry.js +24 -0
- package/dist/factories/module-system-factory.d.ts +10 -0
- package/dist/factories/module-system-factory.d.ts.map +1 -0
- package/dist/factories/module-system-factory.js +18 -0
- package/dist/git/index.d.ts +355 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/index.js +702 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/semver/index.d.ts +86 -0
- package/dist/semver/index.d.ts.map +1 -0
- package/dist/semver/index.js +186 -0
- package/dist/services/adapter-identifier-registry.d.ts +38 -0
- package/dist/services/adapter-identifier-registry.d.ts.map +1 -0
- package/dist/services/adapter-identifier-registry.js +59 -0
- package/dist/services/adapter-identifier.d.ts +31 -0
- package/dist/services/adapter-identifier.d.ts.map +1 -0
- package/dist/services/adapter-identifier.js +1 -0
- package/dist/services/adapter-metadata-provider.d.ts +51 -0
- package/dist/services/adapter-metadata-provider.d.ts.map +1 -0
- package/dist/services/adapter-metadata-provider.js +66 -0
- package/dist/services/changelog-generator.d.ts +16 -0
- package/dist/services/changelog-generator.d.ts.map +1 -0
- package/dist/services/changelog-generator.js +28 -0
- package/dist/services/commit-analyzer.d.ts +47 -0
- package/dist/services/commit-analyzer.d.ts.map +1 -0
- package/dist/services/commit-analyzer.js +89 -0
- package/dist/services/configuration-loader.d.ts +23 -0
- package/dist/services/configuration-loader.d.ts.map +1 -0
- package/dist/services/configuration-loader.js +79 -0
- package/dist/services/configuration-validator.d.ts +16 -0
- package/dist/services/configuration-validator.d.ts.map +1 -0
- package/dist/services/configuration-validator.js +24 -0
- package/dist/services/git-operations.d.ts +16 -0
- package/dist/services/git-operations.d.ts.map +1 -0
- package/dist/services/git-operations.js +89 -0
- package/dist/services/module-detector.d.ts +24 -0
- package/dist/services/module-detector.d.ts.map +1 -0
- package/dist/services/module-detector.js +1 -0
- package/dist/services/module-registry.d.ts +45 -0
- package/dist/services/module-registry.d.ts.map +1 -0
- package/dist/services/module-registry.js +57 -0
- package/dist/services/module-system-factory.d.ts +27 -0
- package/dist/services/module-system-factory.d.ts.map +1 -0
- package/dist/services/module-system-factory.js +1 -0
- package/dist/services/version-applier.d.ts +26 -0
- package/dist/services/version-applier.d.ts.map +1 -0
- package/dist/services/version-applier.js +63 -0
- package/dist/services/version-bumper.d.ts +159 -0
- package/dist/services/version-bumper.d.ts.map +1 -0
- package/dist/services/version-bumper.js +291 -0
- package/dist/services/version-manager.d.ts +68 -0
- package/dist/services/version-manager.d.ts.map +1 -0
- package/dist/services/version-manager.js +94 -0
- package/dist/services/version-update-strategy.d.ts +18 -0
- package/dist/services/version-update-strategy.d.ts.map +1 -0
- package/dist/services/version-update-strategy.js +1 -0
- package/dist/services/versu-runner.d.ts +46 -0
- package/dist/services/versu-runner.d.ts.map +1 -0
- package/dist/services/versu-runner.js +188 -0
- package/dist/utils/banner.d.ts +2 -0
- package/dist/utils/banner.d.ts.map +1 -0
- package/dist/utils/banner.js +12 -0
- package/dist/utils/commits.d.ts +12 -0
- package/dist/utils/commits.d.ts.map +1 -0
- package/dist/utils/commits.js +24 -0
- package/dist/utils/file.d.ts +7 -0
- package/dist/utils/file.d.ts.map +1 -0
- package/dist/utils/file.js +19 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/logger.d.ts +14 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +22 -0
- package/dist/utils/properties.d.ts +24 -0
- package/dist/utils/properties.d.ts.map +1 -0
- package/dist/utils/properties.js +94 -0
- package/dist/utils/version.d.ts +3 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +4 -0
- package/dist/utils/versioning.d.ts +9 -0
- package/dist/utils/versioning.d.ts.map +1 -0
- package/dist/utils/versioning.js +20 -0
- package/package.json +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# @versu/core - Core Library
|
|
2
|
+
|
|
3
|
+
The core business logic powering VERSU (Version Engine for Repo Semantic Evolution). This package is completely framework-agnostic and can be integrated into any TypeScript/JavaScript project, CI/CD system, or custom tooling.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @versu/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { VersuRunner } from '@versu/core';
|
|
15
|
+
|
|
16
|
+
const runner = new VersuRunner({
|
|
17
|
+
repoRoot: '/path/to/repository',
|
|
18
|
+
adapter: 'gradle', // Optional - auto-detected if not specified
|
|
19
|
+
dryRun: false,
|
|
20
|
+
pushTags: true,
|
|
21
|
+
pushChanges: true,
|
|
22
|
+
generateChangelog: true
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const result = await runner.run();
|
|
26
|
+
|
|
27
|
+
console.log(`Bumped: ${result.bumped}`);
|
|
28
|
+
console.log(`Changed modules:`, result.changedModules);
|
|
29
|
+
console.log(`Created tags:`, result.createdTags);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For detailed pre-release configuration and examples, see [PRERELEASE.md](./PRERELEASE.md).
|
|
33
|
+
|
|
34
|
+
## VersuRunner API
|
|
35
|
+
|
|
36
|
+
### Options
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
interface RunnerOptions {
|
|
40
|
+
repoRoot: string; // Path to repository root
|
|
41
|
+
adapter?: string; // Language adapter (auto-detected if not specified)
|
|
42
|
+
dryRun?: boolean; // Preview changes without writing (default: false)
|
|
43
|
+
pushTags?: boolean; // Push version tags (default: true)
|
|
44
|
+
prereleaseMode?: boolean; // Generate pre-release versions (default: false)
|
|
45
|
+
prereleaseId?: string; // Pre-release identifier: alpha, beta, rc, etc. (default: 'alpha')
|
|
46
|
+
bumpUnchanged?: boolean; // Bump modules with no changes in prerelease mode (default: false)
|
|
47
|
+
addBuildMetadata?: boolean; // Add build metadata with short SHA (default: false)
|
|
48
|
+
timestampVersions?: boolean; // Use timestamp-based prerelease IDs (default: false)
|
|
49
|
+
appendSnapshot?: boolean; // Add -SNAPSHOT suffix (Gradle only) (default: false)
|
|
50
|
+
pushChanges?: boolean; // Commit and push version changes (default: true)
|
|
51
|
+
generateChangelog?: boolean; // Generate changelogs (default: true)
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Result
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
interface RunnerResult {
|
|
59
|
+
bumped: boolean; // Whether any version was updated
|
|
60
|
+
changedModules: Array<{
|
|
61
|
+
name: string; // Module name
|
|
62
|
+
from: string; // Previous version
|
|
63
|
+
to: string; // New version
|
|
64
|
+
}>;
|
|
65
|
+
createdTags: string[]; // Git tags that were created
|
|
66
|
+
changelogPaths: string[]; // Generated changelog file paths
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
VERSU core uses [cosmiconfig](https://github.com/davidtheclark/cosmiconfig) for configuration loading and [Zod](https://github.com/colinhacks/zod) for validation.
|
|
73
|
+
|
|
74
|
+
### Supported Configuration Files
|
|
75
|
+
|
|
76
|
+
1. `package.json` (in a `"versu"` property)
|
|
77
|
+
2. `.versurc.json`
|
|
78
|
+
3. `.versurc.yaml` / `.versurc.yml`
|
|
79
|
+
4. `.versurc.js` or `versu.config.js` (JavaScript)
|
|
80
|
+
|
|
81
|
+
### Configuration Example
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"defaultBump": "patch",
|
|
86
|
+
"commitTypes": {
|
|
87
|
+
"feat": "minor",
|
|
88
|
+
"fix": "patch",
|
|
89
|
+
"perf": "patch",
|
|
90
|
+
"docs": "ignore"
|
|
91
|
+
},
|
|
92
|
+
"dependencyRules": {
|
|
93
|
+
"onMajorOfDependency": "minor",
|
|
94
|
+
"onMinorOfDependency": "patch",
|
|
95
|
+
"onPatchOfDependency": "none"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Adapters
|
|
101
|
+
|
|
102
|
+
### Gradle Adapter
|
|
103
|
+
|
|
104
|
+
Built-in support for Gradle projects (Groovy & Kotlin DSL).
|
|
105
|
+
|
|
106
|
+
**Features:**
|
|
107
|
+
|
|
108
|
+
- Multi-module project detection
|
|
109
|
+
- Version management through root `gradle.properties`
|
|
110
|
+
- Dependency detection
|
|
111
|
+
- Both Groovy and Kotlin DSL support
|
|
112
|
+
|
|
113
|
+
**Version Format:**
|
|
114
|
+
|
|
115
|
+
```properties
|
|
116
|
+
# Root module
|
|
117
|
+
version=1.0.0
|
|
118
|
+
|
|
119
|
+
# Submodules
|
|
120
|
+
core.version=2.1.0
|
|
121
|
+
api.version=1.5.0
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Creating Custom Adapters
|
|
125
|
+
|
|
126
|
+
To add support for new project types, implement a language adapter following the pattern in `src/adapters/gradle/`:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import {
|
|
130
|
+
AdapterIdentifier,
|
|
131
|
+
ModuleDetector,
|
|
132
|
+
VersionUpdateStrategy,
|
|
133
|
+
ModuleSystemFactory,
|
|
134
|
+
AdapterMetadata,
|
|
135
|
+
RawProjectInformation,
|
|
136
|
+
ProcessedModuleChange
|
|
137
|
+
} from '@versu/core';
|
|
138
|
+
|
|
139
|
+
// 1. Adapter identifier for auto-detection
|
|
140
|
+
class MyAdapterIdentifier implements AdapterIdentifier {
|
|
141
|
+
readonly metadata: AdapterMetadata = {
|
|
142
|
+
id: 'my-adapter',
|
|
143
|
+
capabilities: { supportsSnapshots: false }
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
async accept(projectRoot: string): Promise<boolean> {
|
|
147
|
+
// Check for adapter-specific files
|
|
148
|
+
return await fileExists(path.join(projectRoot, 'my-build-file'));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 2. Module detector for discovering project structure
|
|
153
|
+
class MyModuleDetector implements ModuleDetector {
|
|
154
|
+
async detectModules(projectRoot: string): Promise<RawProjectInformation> {
|
|
155
|
+
// Discover and return modules and dependencies
|
|
156
|
+
return {
|
|
157
|
+
modules: [
|
|
158
|
+
{ name: 'root', path: projectRoot, version: '1.0.0' },
|
|
159
|
+
{ name: 'module-a', path: join(projectRoot, 'module-a'), version: '2.0.0' }
|
|
160
|
+
],
|
|
161
|
+
dependencies: [
|
|
162
|
+
{ from: 'module-a', to: 'root' }
|
|
163
|
+
]
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 3. Version update strategy for applying changes
|
|
169
|
+
class MyVersionUpdateStrategy implements VersionUpdateStrategy {
|
|
170
|
+
async applyVersionUpdates(
|
|
171
|
+
changes: ProcessedModuleChange[],
|
|
172
|
+
projectRoot: string
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
// Apply version changes to build files
|
|
175
|
+
for (const change of changes) {
|
|
176
|
+
// Update version in your project's format
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 4. Module system factory to tie it all together
|
|
182
|
+
class MyModuleSystemFactory implements ModuleSystemFactory {
|
|
183
|
+
createDetector(): ModuleDetector {
|
|
184
|
+
return new MyModuleDetector();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
createVersionUpdateStrategy(): VersionUpdateStrategy {
|
|
188
|
+
return new MyVersionUpdateStrategy();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Then register your adapter:
|
|
194
|
+
|
|
195
|
+
1. Add to `src/factories/module-system-factory.ts` in the `createModuleSystemFactory` function
|
|
196
|
+
2. Add to `src/services/adapter-identifier-registry.ts` in the registry initialization
|
|
197
|
+
|
|
198
|
+
## Development
|
|
199
|
+
|
|
200
|
+
### Building
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# From monorepo root
|
|
204
|
+
npm run build
|
|
205
|
+
|
|
206
|
+
# Or from core package
|
|
207
|
+
cd packages/core
|
|
208
|
+
npm run build
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Testing
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# From monorepo root
|
|
215
|
+
npm test
|
|
216
|
+
|
|
217
|
+
# Or from core package
|
|
218
|
+
cd packages/core
|
|
219
|
+
npm test
|
|
220
|
+
npm run test:coverage
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Publishing
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
npm publish --workspace packages/core --access public
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Related Packages
|
|
230
|
+
|
|
231
|
+
- **[@versu/cli](../cli)** - Command-line interface
|
|
232
|
+
- **[@versu/action](../action)** - GitHub Actions integration
|
|
233
|
+
|
|
234
|
+
## Requirements
|
|
235
|
+
|
|
236
|
+
- **Node.js**: >= 20
|
|
237
|
+
- **TypeScript**: >= 5.0 (if using TypeScript)
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT License - see [LICENSE](../../LICENSE) for details.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Standard filename for Gradle project properties file ('gradle.properties'). */
|
|
2
|
+
export declare const GRADLE_PROPERTIES_FILE = "gradle.properties";
|
|
3
|
+
/** Standard filename for Gradle build script using Groovy DSL ('build.gradle'). */
|
|
4
|
+
export declare const GRADLE_BUILD_FILE = "build.gradle";
|
|
5
|
+
/** Standard filename for Gradle build script using Kotlin DSL ('build.gradle.kts'). */
|
|
6
|
+
export declare const GRADLE_BUILD_KTS_FILE = "build.gradle.kts";
|
|
7
|
+
/** Standard filename for Gradle settings file using Groovy DSL ('settings.gradle'). */
|
|
8
|
+
export declare const GRADLE_SETTINGS_FILE = "settings.gradle";
|
|
9
|
+
/** Standard filename for Gradle settings file using Kotlin DSL ('settings.gradle.kts'). */
|
|
10
|
+
export declare const GRADLE_SETTINGS_KTS_FILE = "settings.gradle.kts";
|
|
11
|
+
/** Unique identifier for the Gradle adapter ('gradle'). */
|
|
12
|
+
export declare const GRADLE_ID = "gradle";
|
|
13
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/adapters/gradle/constants.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAE1D,mFAAmF;AACnF,eAAO,MAAM,iBAAiB,iBAAiB,CAAC;AAEhD,uFAAuF;AACvF,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AAExD,uFAAuF;AACvF,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AAEtD,2FAA2F;AAC3F,eAAO,MAAM,wBAAwB,wBAAwB,CAAC;AAE9D,2DAA2D;AAC3D,eAAO,MAAM,SAAS,WAAW,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Standard filename for Gradle project properties file ('gradle.properties'). */
|
|
2
|
+
export const GRADLE_PROPERTIES_FILE = 'gradle.properties';
|
|
3
|
+
/** Standard filename for Gradle build script using Groovy DSL ('build.gradle'). */
|
|
4
|
+
export const GRADLE_BUILD_FILE = 'build.gradle';
|
|
5
|
+
/** Standard filename for Gradle build script using Kotlin DSL ('build.gradle.kts'). */
|
|
6
|
+
export const GRADLE_BUILD_KTS_FILE = 'build.gradle.kts';
|
|
7
|
+
/** Standard filename for Gradle settings file using Groovy DSL ('settings.gradle'). */
|
|
8
|
+
export const GRADLE_SETTINGS_FILE = 'settings.gradle';
|
|
9
|
+
/** Standard filename for Gradle settings file using Kotlin DSL ('settings.gradle.kts'). */
|
|
10
|
+
export const GRADLE_SETTINGS_KTS_FILE = 'settings.gradle.kts';
|
|
11
|
+
/** Unique identifier for the Gradle adapter ('gradle'). */
|
|
12
|
+
export const GRADLE_ID = 'gradle';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ProjectInformation, RawProjectInformation } from '../project-information.js';
|
|
2
|
+
/**
|
|
3
|
+
* Executes Gradle to collect raw project structure information.
|
|
4
|
+
* Runs gradlew with init script to output JSON containing module hierarchy, versions, and dependencies.
|
|
5
|
+
* @param projectRoot - Absolute path to the Gradle project root directory
|
|
6
|
+
* @param outputFile - Path to output JSON file to be generated
|
|
7
|
+
* @returns Promise resolving to raw project information as JSON
|
|
8
|
+
* @throws {Error} If initialization script not found or Gradle execution fails
|
|
9
|
+
*/
|
|
10
|
+
export declare function getRawProjectInformation(projectRoot: string, outputFile: string): Promise<RawProjectInformation>;
|
|
11
|
+
/**
|
|
12
|
+
* Transforms raw project information into structured, queryable format.
|
|
13
|
+
* Normalizes modules, identifies root, parses versions, and maps dependencies.
|
|
14
|
+
* @param projectInformation - Raw project information from Gradle
|
|
15
|
+
* @returns Structured ProjectInformation with normalized data
|
|
16
|
+
* @throws {Error} If no root module found in hierarchy
|
|
17
|
+
*/
|
|
18
|
+
export declare function getProjectInformation(projectInformation: RawProjectInformation): ProjectInformation;
|
|
19
|
+
//# sourceMappingURL=gradle-project-information.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gradle-project-information.d.ts","sourceRoot":"","sources":["../../../src/adapters/gradle/gradle-project-information.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AA4H1G;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAuEtH;AA2CD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,kBAAkB,EAAE,qBAAqB,GAAG,kBAAkB,CA6CnG"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import path, { join } from 'path';
|
|
2
|
+
import { createInitialVersion, parseSemVer } from '../../semver/index.js';
|
|
3
|
+
import { exists } from '../../utils/file.js';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import crypto from 'crypto';
|
|
8
|
+
import fg from 'fast-glob';
|
|
9
|
+
import { parseProperties } from '../../utils/properties.js';
|
|
10
|
+
import { logger } from '../../utils/logger.js';
|
|
11
|
+
/**
|
|
12
|
+
* Name of the Gradle wrapper script file.
|
|
13
|
+
* Ensures consistent builds without requiring pre-installed Gradle.
|
|
14
|
+
*/
|
|
15
|
+
const GRADLE_WRAPPER = 'gradlew';
|
|
16
|
+
/**
|
|
17
|
+
* Relative path to the Gradle initialization script within the action.
|
|
18
|
+
* Injected into Gradle to collect project structure information as JSON.
|
|
19
|
+
*/
|
|
20
|
+
const GRADLE_INIT_SCRIPT = './init-project-information.gradle.kts';
|
|
21
|
+
/**
|
|
22
|
+
* Finds all Gradle build files recursively under the project root.
|
|
23
|
+
* Searches for settings.gradle, settings.gradle.kts, build.gradle, and build.gradle.kts files.
|
|
24
|
+
* @param projectRoot - Absolute path to the Gradle project root directory
|
|
25
|
+
* @returns Promise resolving to array of relative paths to Gradle build files
|
|
26
|
+
*/
|
|
27
|
+
async function findGradleFiles(projectRoot) {
|
|
28
|
+
const patterns = [
|
|
29
|
+
'**/settings.gradle',
|
|
30
|
+
'**/settings.gradle.kts',
|
|
31
|
+
'**/build.gradle',
|
|
32
|
+
'**/build.gradle.kts'
|
|
33
|
+
];
|
|
34
|
+
const files = await fg(patterns, {
|
|
35
|
+
cwd: projectRoot,
|
|
36
|
+
absolute: false,
|
|
37
|
+
ignore: ['**/node_modules/**', '**/build/**', '**/.gradle/**']
|
|
38
|
+
});
|
|
39
|
+
// Sort for consistent ordering
|
|
40
|
+
return files.sort();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Computes SHA-256 hash of all Gradle build files.
|
|
44
|
+
* Used to detect changes in project configuration that would invalidate cached information.
|
|
45
|
+
* @param projectRoot - Absolute path to the Gradle project root directory
|
|
46
|
+
* @returns Promise resolving to hexadecimal hash string
|
|
47
|
+
*/
|
|
48
|
+
async function computeGradleFilesHash(projectRoot) {
|
|
49
|
+
const files = await findGradleFiles(projectRoot);
|
|
50
|
+
const hash = crypto.createHash('sha256');
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const content = await fs.readFile(join(projectRoot, file), 'utf-8');
|
|
53
|
+
hash.update(file); // Include file path for uniqueness
|
|
54
|
+
hash.update(content);
|
|
55
|
+
}
|
|
56
|
+
return hash.digest('hex');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Executes the Gradle wrapper script to generate project information.
|
|
60
|
+
* Runs gradlew with initialization script to create the project-information.json file.
|
|
61
|
+
* @param projectRoot - Absolute path to the Gradle project root directory
|
|
62
|
+
* @param outputFile - Path to output JSON file to be generated
|
|
63
|
+
* @throws {Error} If initialization script not found or Gradle execution fails
|
|
64
|
+
*/
|
|
65
|
+
async function executeGradleScript(projectRoot, outputFile) {
|
|
66
|
+
logger.info(`⚙️ Executing Gradle to collect project information...`);
|
|
67
|
+
const gradlew = join(projectRoot, GRADLE_WRAPPER);
|
|
68
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
69
|
+
const initScriptPath = join(dirname, GRADLE_INIT_SCRIPT);
|
|
70
|
+
// Check if init script exists
|
|
71
|
+
const scriptExists = await exists(initScriptPath);
|
|
72
|
+
if (!scriptExists) {
|
|
73
|
+
throw new Error(`Init script not found at ${initScriptPath}. ` +
|
|
74
|
+
`Please create the ${GRADLE_INIT_SCRIPT} file.`);
|
|
75
|
+
}
|
|
76
|
+
// Prepare Gradle command arguments
|
|
77
|
+
const args = [
|
|
78
|
+
'--quiet', // Suppress non-error output for clean JSON
|
|
79
|
+
'--console=plain', // Disable ANSI formatting
|
|
80
|
+
'--init-script', // Inject initialization script
|
|
81
|
+
initScriptPath,
|
|
82
|
+
'structure', // Custom task that outputs project structure
|
|
83
|
+
`-PprojectInfoOutput=${outputFile}`
|
|
84
|
+
];
|
|
85
|
+
// Execute Gradle wrapper with the prepared arguments
|
|
86
|
+
const result = await execa(gradlew, args, {
|
|
87
|
+
cwd: projectRoot, // Run from project root
|
|
88
|
+
reject: false // Handle non-zero exit codes ourselves
|
|
89
|
+
});
|
|
90
|
+
// Check for Gradle execution failure
|
|
91
|
+
if (result.exitCode !== 0) {
|
|
92
|
+
throw new Error(`Gradle command failed with exit code ${result.exitCode}: ${result.stderr}`);
|
|
93
|
+
}
|
|
94
|
+
logger.info(`✅ Gradle project information generated at ${outputFile}.`);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Executes Gradle to collect raw project structure information.
|
|
98
|
+
* Runs gradlew with init script to output JSON containing module hierarchy, versions, and dependencies.
|
|
99
|
+
* @param projectRoot - Absolute path to the Gradle project root directory
|
|
100
|
+
* @param outputFile - Path to output JSON file to be generated
|
|
101
|
+
* @returns Promise resolving to raw project information as JSON
|
|
102
|
+
* @throws {Error} If initialization script not found or Gradle execution fails
|
|
103
|
+
*/
|
|
104
|
+
export async function getRawProjectInformation(projectRoot, outputFile) {
|
|
105
|
+
// Step 1: Check if project-information.json exists
|
|
106
|
+
const fileExists = await exists(outputFile);
|
|
107
|
+
let data = {};
|
|
108
|
+
let executeScript = true;
|
|
109
|
+
// Compute hash of all Gradle build files
|
|
110
|
+
const currentHash = await computeGradleFilesHash(projectRoot);
|
|
111
|
+
logger.debug(`🔍 Computed Gradle files hash: ${currentHash}`);
|
|
112
|
+
if (fileExists) {
|
|
113
|
+
logger.info(`💾 Cached project information found at ${outputFile}. Validating cache...`);
|
|
114
|
+
// Step 2: File exists, check cache validity
|
|
115
|
+
try {
|
|
116
|
+
const fileContent = await fs.readFile(outputFile, 'utf-8');
|
|
117
|
+
const cachedData = JSON.parse(fileContent);
|
|
118
|
+
// Step 2.1: Compare hashes
|
|
119
|
+
if (cachedData.hash === currentHash) {
|
|
120
|
+
logger.info(`✅ Cache is valid. Using cached project information.`);
|
|
121
|
+
// Cache hit - use cached data
|
|
122
|
+
executeScript = false;
|
|
123
|
+
data = cachedData.data;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
logger.debug(`❌ Cache is invalid. Cached hash: ${cachedData.hash}`);
|
|
127
|
+
logger.info(`🔄 Gradle files changed, regenerating project information...`);
|
|
128
|
+
}
|
|
129
|
+
// Cache miss - hash mismatch, need to regenerate
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
// If there's any error reading/parsing cached file, regenerate
|
|
133
|
+
logger.warning(`⚠️ Failed to read cached project information: ${error}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (executeScript) {
|
|
137
|
+
// Step 3: File doesn't exist or cache is invalid - execute Gradle script
|
|
138
|
+
const outputFile = join(projectRoot, 'build', 'project-information.json');
|
|
139
|
+
await executeGradleScript(projectRoot, outputFile);
|
|
140
|
+
// Verify that the output file was created
|
|
141
|
+
const fileExistsAfterExec = await exists(outputFile);
|
|
142
|
+
if (!fileExistsAfterExec) {
|
|
143
|
+
throw new Error(`Expected output file not found at ${outputFile}. ` +
|
|
144
|
+
`Ensure that the Gradle init script is correctly generating the project information.`);
|
|
145
|
+
}
|
|
146
|
+
// Read the output file content
|
|
147
|
+
const fileContent = await fs.readFile(outputFile, 'utf-8');
|
|
148
|
+
// Parse JSON output from Gradle
|
|
149
|
+
data = JSON.parse(fileContent.trim() || '{}');
|
|
150
|
+
}
|
|
151
|
+
// Compute hash and save with cache information
|
|
152
|
+
const cachedData = {
|
|
153
|
+
hash: currentHash,
|
|
154
|
+
data
|
|
155
|
+
};
|
|
156
|
+
// Read gradle.properites and add version
|
|
157
|
+
const projectInformation = await getInformationWithVersions(projectRoot, data);
|
|
158
|
+
if (executeScript) {
|
|
159
|
+
// Write back to file with hash for future cache validation
|
|
160
|
+
await fs.writeFile(outputFile, JSON.stringify(cachedData, null, 2), 'utf-8');
|
|
161
|
+
}
|
|
162
|
+
return projectInformation;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Reads gradle.properties to extract module versions and augment raw project information.
|
|
166
|
+
* @param projectRoot - Absolute path to the Gradle project root directory
|
|
167
|
+
* @param projectInformation - Gradle project information without versions
|
|
168
|
+
* @returns Promise resolving to augmented RawProjectInformation with versions
|
|
169
|
+
*/
|
|
170
|
+
async function getInformationWithVersions(projectRoot, projectInformation) {
|
|
171
|
+
const gradlePropertiesFile = join(projectRoot, 'gradle.properties');
|
|
172
|
+
const gradlePropertiesExists = await exists(gradlePropertiesFile);
|
|
173
|
+
const result = {};
|
|
174
|
+
let moduleVersions = new Map();
|
|
175
|
+
if (gradlePropertiesExists) {
|
|
176
|
+
moduleVersions = await parseProperties(gradlePropertiesFile);
|
|
177
|
+
for (const [moduleId, module] of Object.entries(projectInformation)) {
|
|
178
|
+
const version = moduleVersions.get(module.versionProperty);
|
|
179
|
+
const resultVersion = version ? version : undefined;
|
|
180
|
+
result[moduleId] = {
|
|
181
|
+
...module,
|
|
182
|
+
version: resultVersion,
|
|
183
|
+
declaredVersion: resultVersion !== undefined
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Transforms raw project information into structured, queryable format.
|
|
191
|
+
* Normalizes modules, identifies root, parses versions, and maps dependencies.
|
|
192
|
+
* @param projectInformation - Raw project information from Gradle
|
|
193
|
+
* @returns Structured ProjectInformation with normalized data
|
|
194
|
+
* @throws {Error} If no root module found in hierarchy
|
|
195
|
+
*/
|
|
196
|
+
export function getProjectInformation(projectInformation) {
|
|
197
|
+
const moduleIds = Object.keys(projectInformation);
|
|
198
|
+
const modules = new Map();
|
|
199
|
+
// Find root module by looking for the one with type 'root'
|
|
200
|
+
let rootModule;
|
|
201
|
+
for (const [moduleId, rawModule] of Object.entries(projectInformation)) {
|
|
202
|
+
if (rawModule.type === 'root') {
|
|
203
|
+
rootModule = moduleId;
|
|
204
|
+
}
|
|
205
|
+
// Create normalized Module object
|
|
206
|
+
const module = {
|
|
207
|
+
id: moduleId,
|
|
208
|
+
name: rawModule.name,
|
|
209
|
+
path: rawModule.path,
|
|
210
|
+
type: rawModule.type,
|
|
211
|
+
affectedModules: new Set(rawModule.affectedModules),
|
|
212
|
+
// Parse version if present, otherwise create initial version
|
|
213
|
+
version: rawModule.version === undefined ?
|
|
214
|
+
createInitialVersion() :
|
|
215
|
+
parseSemVer(rawModule.version),
|
|
216
|
+
declaredVersion: rawModule.declaredVersion,
|
|
217
|
+
};
|
|
218
|
+
if ('versionProperty' in rawModule) {
|
|
219
|
+
module['versionProperty'] = rawModule.versionProperty;
|
|
220
|
+
}
|
|
221
|
+
modules.set(moduleId, module);
|
|
222
|
+
}
|
|
223
|
+
// Validate that a root module was found
|
|
224
|
+
if (!rootModule) {
|
|
225
|
+
throw new Error('No root module found in hierarchy. ' +
|
|
226
|
+
'Every project hierarchy must contain exactly one module with type "root".');
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
moduleIds,
|
|
230
|
+
modules,
|
|
231
|
+
rootModule
|
|
232
|
+
};
|
|
233
|
+
}
|