code-foundry 0.28.0 → 0.29.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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/commands/sync.mjs +19 -0
- package/src/lib/release-manifest.mjs +121 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.29.0](https://github.com/0xPlayerOne/code-foundry/compare/v0.28.0...v0.29.0) (2026-07-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **release:** detect mixed-language release manifests ([f2daa61](https://github.com/0xPlayerOne/code-foundry/commit/f2daa616f2d8cfd2883f960823e862768454fbd6))
|
|
9
|
+
|
|
3
10
|
## [0.28.0](https://github.com/0xPlayerOne/code-foundry/compare/v0.27.18...v0.28.0) (2026-07-29)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
package/src/commands/sync.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
5
5
|
import { spawnSync } from 'node:child_process'
|
|
6
6
|
import { detectLanguages, detectPackageManager, detectProfile } from '../lib/profile.mjs'
|
|
7
7
|
import { configured, includesValue, readConfig } from '../lib/config.mjs'
|
|
8
|
+
import { buildReleaseConfig } from '../lib/release-manifest.mjs'
|
|
8
9
|
|
|
9
10
|
const standardFiles = [
|
|
10
11
|
'.editorconfig', '.gitattributes', '.gitignore', 'release-please-config.json',
|
|
@@ -82,6 +83,9 @@ export function syncRepository(options) {
|
|
|
82
83
|
if ((file === 'LICENSE' || file === 'NOTICE') && license === 'none') continue
|
|
83
84
|
if (file === '.github/CODEOWNERS' && existsSync(destination)) continue
|
|
84
85
|
let content = readFileSync(sourceFile)
|
|
86
|
+
if (file === 'release-please-config.json') {
|
|
87
|
+
content = Buffer.from(renderReleaseConfig(target, sourceFile))
|
|
88
|
+
}
|
|
85
89
|
if (file.endsWith('.yml') && file.startsWith('.github/workflows/')) {
|
|
86
90
|
content = Buffer.from(renderWorkflow(content.toString('utf8'), config, runtimeRepository, runtimeRef))
|
|
87
91
|
}
|
|
@@ -133,6 +137,21 @@ export function syncRepository(options) {
|
|
|
133
137
|
return { changed, config }
|
|
134
138
|
}
|
|
135
139
|
|
|
140
|
+
/** @param {string} target @param {string} sourceFile @returns {string} */
|
|
141
|
+
function renderReleaseConfig(target, sourceFile) {
|
|
142
|
+
let baseline = {}
|
|
143
|
+
try { baseline = JSON.parse(readFileSync(sourceFile, 'utf8')) }
|
|
144
|
+
catch { baseline = {} }
|
|
145
|
+
const destination = join(target, 'release-please-config.json')
|
|
146
|
+
let existing = baseline
|
|
147
|
+
if (existsSync(destination)) {
|
|
148
|
+
try { existing = JSON.parse(readFileSync(destination, 'utf8')) }
|
|
149
|
+
catch { existing = baseline }
|
|
150
|
+
}
|
|
151
|
+
const merged = { ...baseline, ...existing }
|
|
152
|
+
return `${JSON.stringify(buildReleaseConfig(target, merged), null, 2)}\n`
|
|
153
|
+
}
|
|
154
|
+
|
|
136
155
|
/** @param {string} file @param {string} languages @param {string} features */
|
|
137
156
|
function shouldInclude(file, languages, features) {
|
|
138
157
|
if (file === 'ruff.toml') return includesValue(languages, 'python')
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
const ignored = new Set(['.git', '.code-foundry', '.venv', 'node_modules', 'target', 'vendor', 'dist', 'build'])
|
|
7
|
+
|
|
8
|
+
/** @typedef {{ directory: string, manifest: string, releaseType: 'node'|'python'|'rust', packageName?: string, extraFiles: string[] }} ReleasePackage */
|
|
9
|
+
|
|
10
|
+
/** @param {string} root @returns {ReleasePackage[]} */
|
|
11
|
+
export function detectReleasePackages(root) {
|
|
12
|
+
/** @type {ReleasePackage[]} */
|
|
13
|
+
const packages = []
|
|
14
|
+
walk(root, (file) => {
|
|
15
|
+
const name = file.split('/').pop()
|
|
16
|
+
const directory = file.includes('/') ? file.slice(0, file.lastIndexOf('/')) : '.'
|
|
17
|
+
if (name === 'package.json') {
|
|
18
|
+
const parsed = readJson(join(root, file))
|
|
19
|
+
packages.push({ directory, manifest: file, releaseType: 'node', packageName: typeof parsed?.name === 'string' ? parsed.name : undefined, extraFiles: detectExtraFiles(root, directory) })
|
|
20
|
+
} else if (name === 'Cargo.toml') {
|
|
21
|
+
packages.push({ directory, manifest: file, releaseType: 'rust', packageName: tomlPackageName(join(root, file)), extraFiles: detectExtraFiles(root, directory) })
|
|
22
|
+
} else if (name === 'pyproject.toml') {
|
|
23
|
+
packages.push({ directory, manifest: file, releaseType: 'python', packageName: pyprojectName(join(root, file)), extraFiles: detectExtraFiles(root, directory) })
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
return packages.sort((a, b) => a.directory.localeCompare(b.directory))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Merge automatic package and extra-file detection into a Release Please
|
|
31
|
+
* configuration without discarding repository-owned settings.
|
|
32
|
+
* @param {string} root
|
|
33
|
+
* @param {Record<string, any>} existing
|
|
34
|
+
*/
|
|
35
|
+
export function buildReleaseConfig(root, existing = {}) {
|
|
36
|
+
const packages = detectReleasePackages(root)
|
|
37
|
+
const result = { ...existing }
|
|
38
|
+
if (!packages.length) return result
|
|
39
|
+
if (packages.length === 1 && packages[0].directory === '.') {
|
|
40
|
+
result['release-type'] ??= packages[0].releaseType
|
|
41
|
+
/** @type {any[]} */
|
|
42
|
+
const extra = Array.isArray(result['extra-files']) ? result['extra-files'] : []
|
|
43
|
+
result['extra-files'] = extra
|
|
44
|
+
for (const file of packages[0].extraFiles) {
|
|
45
|
+
if (!extra.some((entry) => (typeof entry === 'string' ? entry : entry?.path) === file)) extra.push(file)
|
|
46
|
+
}
|
|
47
|
+
return result
|
|
48
|
+
}
|
|
49
|
+
/** @type {Record<string, any>} */
|
|
50
|
+
const configuredPackages = { ...(result.packages ?? {}) }
|
|
51
|
+
for (const entry of packages) {
|
|
52
|
+
const current = { ...(configuredPackages[entry.directory] ?? {}) }
|
|
53
|
+
current['release-type'] ??= entry.releaseType
|
|
54
|
+
if (entry.packageName) current['package-name'] ??= entry.packageName
|
|
55
|
+
/** @type {any[]} */
|
|
56
|
+
const extra = Array.isArray(current['extra-files']) ? [...current['extra-files']] : []
|
|
57
|
+
for (const file of entry.extraFiles) if (!extra.some((item) => (typeof item === 'string' ? item : item?.path) === file)) extra.push(file)
|
|
58
|
+
if (extra.length) current['extra-files'] = extra
|
|
59
|
+
configuredPackages[entry.directory] = current
|
|
60
|
+
}
|
|
61
|
+
result.packages = configuredPackages
|
|
62
|
+
delete result['release-type']
|
|
63
|
+
return result
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @param {string} root @param {Record<string, any>} config @returns {string[]} */
|
|
67
|
+
export function validateReleaseConfig(root, config) {
|
|
68
|
+
const detected = detectReleasePackages(root)
|
|
69
|
+
if (!detected.length) return []
|
|
70
|
+
const configured = config.packages && typeof config.packages === 'object' ? config.packages : null
|
|
71
|
+
if (detected.length > 1 && !configured) return ['mixed-language or multi-package repositories require release-please packages configuration']
|
|
72
|
+
if (!configured) return []
|
|
73
|
+
const errors = []
|
|
74
|
+
for (const entry of detected) {
|
|
75
|
+
const item = configured[entry.directory]
|
|
76
|
+
if (!item) errors.push(`release-please packages is missing ${entry.directory}`)
|
|
77
|
+
else if (item['release-type'] !== entry.releaseType) errors.push(`${entry.directory} release-type should be ${entry.releaseType}`)
|
|
78
|
+
}
|
|
79
|
+
return errors
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** @param {string} root @param {(file: string) => void} visit */
|
|
83
|
+
function walk(root, visit) {
|
|
84
|
+
/** @param {string} directory */
|
|
85
|
+
function descend(directory) {
|
|
86
|
+
for (const entry of readdirSync(join(root, directory), { withFileTypes: true })) {
|
|
87
|
+
if (entry.isDirectory() && ignored.has(entry.name)) continue
|
|
88
|
+
const path = directory ? `${directory}/${entry.name}` : entry.name
|
|
89
|
+
if (entry.isDirectory()) descend(path)
|
|
90
|
+
else visit(path)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
descend('')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @param {string} root @param {string} directory @returns {string[]} */
|
|
97
|
+
function detectExtraFiles(root, directory) {
|
|
98
|
+
const candidates = ['version.txt', 'VERSION', '.version', 'version.json', 'src/version.ts', 'src/version.js', 'src/version.py', 'src/version.rs']
|
|
99
|
+
return candidates
|
|
100
|
+
.map((file) => directory === '.' ? file : `${directory}/${file}`)
|
|
101
|
+
.filter((file) => existsSync(join(root, file)))
|
|
102
|
+
.map((file) => directory === '.' ? file : file.slice(directory.length + 1))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @param {string} file @returns {any} */
|
|
106
|
+
function readJson(file) {
|
|
107
|
+
try { return JSON.parse(readFileSync(file, 'utf8')) }
|
|
108
|
+
catch { return {} }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** @param {string} file @returns {string|undefined} */
|
|
112
|
+
function tomlPackageName(file) {
|
|
113
|
+
const match = readFileSync(file, 'utf8').match(/^name\s*=\s*["']([^"']+)["']/m)
|
|
114
|
+
return match?.[1]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** @param {string} file @returns {string|undefined} */
|
|
118
|
+
function pyprojectName(file) {
|
|
119
|
+
const match = readFileSync(file, 'utf8').match(/^(?:name|name\s*)\s*=\s*["']([^"']+)["']/m)
|
|
120
|
+
return match?.[1]
|
|
121
|
+
}
|