code-foundry 0.34.2 → 0.34.4

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.
@@ -286,7 +286,11 @@ jobs:
286
286
  reconcile:
287
287
  name: Release / Reconcile
288
288
  needs: release
289
- if: needs.release.result == 'success'
289
+ # A normal promotion push can successfully run Release Please without
290
+ # creating a release. Reconciliation is only meaningful after a release
291
+ # commit/tag is actually created; otherwise the promotion's source changes
292
+ # are incorrectly rejected as non-metadata commits.
293
+ if: needs.release.result == 'success' && needs.release.outputs.release_created == 'true'
290
294
  runs-on: ${{ inputs.runner }}
291
295
  timeout-minutes: 15
292
296
  permissions:
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.34.4](https://github.com/0xPlayerOne/code-foundry/compare/v0.34.3...v0.34.4) (2026-08-02)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * split native test suites by category ([93c9ede](https://github.com/0xPlayerOne/code-foundry/commit/93c9ede6e202a790befe1ee0e57633e3d01dc428))
9
+
10
+ ## [0.34.3](https://github.com/0xPlayerOne/code-foundry/compare/v0.34.2...v0.34.3) (2026-08-02)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **release:** reconcile only after creating a release ([#317](https://github.com/0xPlayerOne/code-foundry/issues/317)) ([#318](https://github.com/0xPlayerOne/code-foundry/issues/318)) ([6f2b00c](https://github.com/0xPlayerOne/code-foundry/commit/6f2b00c5f3fe27e56b448671902bf6f5e33cca1f))
16
+
3
17
  ## [0.34.2](https://github.com/0xPlayerOne/code-foundry/compare/v0.34.1...v0.34.2) (2026-08-01)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "0.34.2",
3
+ "version": "0.34.4",
4
4
  "description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",
@@ -44,6 +44,7 @@
44
44
  "format:check": "node --check src/cli.mjs",
45
45
  "lint": "node --check src/cli.mjs",
46
46
  "test": "node --test",
47
+ "test:unit": "node --test",
47
48
  "type-check": "tsc --project jsconfig.json",
48
49
  "pack:check": "npm pack --dry-run",
49
50
  "prepublishOnly": "npm test && npm run pack:check"
@@ -52,7 +53,7 @@
52
53
  "access": "public"
53
54
  },
54
55
  "devDependencies": {
55
- "@types/node": "^24.0.0",
56
- "typescript": "^5.8.3"
56
+ "@types/node": "^26.0.0",
57
+ "typescript": "^7.0.2"
57
58
  }
58
59
  }
@@ -0,0 +1,39 @@
1
+ // @ts-check
2
+
3
+ const TEST_FILE_PATTERN = /(?:\.(?:test|spec|unit|integration|smoke|e2e)\.(?:[cm]?[jt]sx?|py)|(?:^|\/)test_[^/]+\.py$|(?:^|\/)tests\/.*\.rs$)/i
4
+
5
+ /** @param {string} file */
6
+ export function isTestFile(file) {
7
+ return TEST_FILE_PATTERN.test(file.replaceAll('\\', '/'))
8
+ }
9
+
10
+ /** @param {string} file */
11
+ export function testCategory(file) {
12
+ const normalized = file.replaceAll('\\', '/').toLowerCase()
13
+ const base = normalized.split('/').at(-1) ?? normalized
14
+
15
+ if (/(^|\/)(e2e|end-to-end|acceptance)(\/|$)|(?:^|[._-])(e2e|acceptance)(?:[._-]|$)/.test(normalized)) {
16
+ return 'e2e'
17
+ }
18
+ if (/(^|\/)(integration)(\/|$)|(?:^|[._-])integration(?:[._-]|$)/.test(normalized)) {
19
+ return 'integration'
20
+ }
21
+ if (/(^|\/)(smoke|live)(\/|$)|(?:^|[._-])(smoke|live)(?:[._-]|$)/.test(normalized)) {
22
+ return 'smoke'
23
+ }
24
+
25
+ if (normalized.endsWith('.rs') && /(^|\/)tests\//.test(normalized)) {
26
+ return 'integration'
27
+ }
28
+
29
+ // Rust files under tests/ compile as integration-test binaries. An explicit
30
+ // suffix still lets smoke and integration names win when they are present.
31
+ if (base.endsWith('_smoke.rs')) return 'smoke'
32
+ if (base.endsWith('_integration.rs')) return 'integration'
33
+ return 'unit'
34
+ }
35
+
36
+ /** @param {string[]} files @param {'unit'|'integration'|'e2e'|'smoke'} task */
37
+ export function classifyTestFiles(files, task) {
38
+ return files.filter((file) => isTestFile(file) && testCategory(file) === task)
39
+ }
package/src/runtime.mjs CHANGED
@@ -6,6 +6,7 @@ import { resolve } from 'node:path'
6
6
  import { spawnSync } from 'node:child_process'
7
7
  import { detectPackageManager, resolveProfile } from './lib/profile.mjs'
8
8
  import { configured, readConfig } from './lib/config.mjs'
9
+ import { classifyTestFiles } from './lib/test-discovery.mjs'
9
10
  import { classifyValidationMode, evaluateValidationGate } from './lib/validation-policy.mjs'
10
11
  import { readReleaseConfig, validateGeneratedReleaseDiff } from './lib/release-policy.mjs'
11
12
 
@@ -126,6 +127,19 @@ function capture(command, args = []) {
126
127
  return result.status === 0 ? result.stdout.trim() : ''
127
128
  }
128
129
 
130
+ /** @returns {string[]} */
131
+ function repositoryTestFiles() {
132
+ return capture('git', ['ls-files'])
133
+ .split(/\r?\n/)
134
+ .map((file) => file.trim())
135
+ .filter(Boolean)
136
+ }
137
+
138
+ /** @param {'unit'|'integration'|'e2e'|'smoke'} task */
139
+ function taskTestFiles(task) {
140
+ return classifyTestFiles(repositoryTestFiles(), task)
141
+ }
142
+
129
143
  /** @param {string[]} args @returns {[string|null, string[]]} */
130
144
  function packageCommand(args) {
131
145
  switch (packageManager) {
@@ -212,8 +226,16 @@ function relevant(task) {
212
226
  return (js && hasRootJavascriptProject()) || (python && hasRootPythonProject()) || (rust && hasRootRustProject())
213
227
  }
214
228
  if (['unit', 'integration', 'e2e', 'smoke'].includes(task)) {
215
- return (js && hasRootJavascriptProject()) || (python && hasRootPythonProject()) ||
216
- (rust && hasRootRustProject()) || (hasLanguage('solidity') && hasRootJavascriptProject())
229
+ if (taskTestFiles(/** @type {'unit'|'integration'|'e2e'|'smoke'} */ (task)).length > 0) return true
230
+ // A project with inline Rust/Python/JavaScript unit tests is still a unit
231
+ // test target even when it has no conventional test file path. Other
232
+ // categories must have an explicit script or discoverable test files so
233
+ // their workflow jobs become skipped instead of successful no-ops.
234
+ if (task === 'unit') {
235
+ return (js && hasRootJavascriptProject()) || (python && hasRootPythonProject()) ||
236
+ (rust && hasRootRustProject()) || (hasLanguage('solidity') && hasRootJavascriptProject())
237
+ }
238
+ return false
217
239
  }
218
240
  return true
219
241
  }
@@ -274,17 +296,49 @@ function ci(task) {
274
296
  smoke: ['test:smoke', 'smoke'],
275
297
  }
276
298
  const scripts = scriptsByTask[task]
277
- if (scripts) runScript(scripts)
278
- if (hasLanguage('rust') && hasRootRustProject()) run('cargo', ['test'])
299
+ const scripted = scripts ? runScript(scripts) : false
300
+ const testFiles = taskTestFiles(/** @type {'unit'|'integration'|'e2e'|'smoke'} */ (task))
301
+ const javascriptTests = testFiles.filter((file) => /\.(?:[cm]?[jt]sx?)$/i.test(file))
302
+ const pythonTests = testFiles.filter((file) => /\.py$/i.test(file))
303
+ const rustTests = testFiles.filter((file) => /\.rs$/i.test(file))
304
+
305
+ // A repository script is authoritative for that package. When it is absent,
306
+ // use Bun's native runner with only the discovered files for the requested
307
+ // category; this prevents smoke/integration files from being re-run as unit
308
+ // tests and avoids no-op category jobs.
309
+ if (!scripted && (hasLanguage('typescript') || hasLanguage('javascript') || hasLanguage('solidity')) && hasRootJavascriptProject()) {
310
+ if (javascriptTests.length > 0 || task === 'unit') run('bun', ['test', ...javascriptTests])
311
+ }
312
+
279
313
  if (hasLanguage('python') && hasRootPythonProject()) {
280
314
  const python = existsSync(resolve(root, '.venv/bin/python')) ? resolve(root, '.venv/bin/python') : 'python'
281
- const integrationTests = resolve(root, 'tests/integration')
282
- if (task !== 'integration' || existsSync(integrationTests)) {
283
- run(python, ['-m', 'pytest', ...(task === 'integration' ? ['tests/integration'] : [])])
315
+ if (pythonTests.length > 0 || task === 'unit') run(python, ['-m', 'pytest', ...pythonTests])
316
+ }
317
+
318
+ if (hasLanguage('rust') && hasRootRustProject()) {
319
+ if (task === 'unit') {
320
+ if (existsSync(resolve(root, 'src/lib.rs'))) run('cargo', ['test', '--lib'])
321
+ if (existsSync(resolve(root, 'src/main.rs'))) run('cargo', ['test', '--bin', packageName()])
322
+ if (!existsSync(resolve(root, 'src/lib.rs')) && !existsSync(resolve(root, 'src/main.rs'))) run('cargo', ['test'])
323
+ } else {
324
+ for (const file of rustTests) {
325
+ const match = file.match(/^tests\/(.+)\.rs$/)
326
+ if (match && !match[1].includes('/')) run('cargo', ['test', '--test', match[1]])
327
+ }
284
328
  }
285
329
  }
286
330
  }
287
331
 
332
+ function packageName() {
333
+ const cargo = readFileSafe(resolve(root, 'Cargo.toml'))
334
+ return cargo?.match(/^name\s*=\s*"([^"]+)"/m)?.[1] ?? 'app'
335
+ }
336
+
337
+ /** @param {string} file */
338
+ function readFileSafe(file) {
339
+ try { return readFileSync(file, 'utf8') } catch { return null }
340
+ }
341
+
288
342
  /** @param {string} task @param {string} ecosystem */
289
343
  function security(task, ecosystem) {
290
344
  if (task === 'profile') {