code-foundry 1.4.1 → 1.5.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/.github/actions/codeql/action.yml +4 -0
- package/.github/dependabot.yml +4 -2
- package/.github/workflows/cloudflare-deploy.yml +98 -0
- package/.github/workflows/codeql.yml +4 -0
- package/.github/workflows/release.yml +10 -6
- package/.oxlintrc.json +1 -1
- package/AGENTS.md +16 -1
- package/CHANGELOG.md +14 -0
- package/README.md +12 -10
- package/docs/CONFIGURATION.md +15 -3
- package/docs/RELEASES.md +15 -14
- package/docs/WORKFLOWS.md +23 -17
- package/package.json +1 -1
- package/src/commands/doctor.mjs +7 -2
- package/src/commands/fleet.mjs +15 -3
- package/src/commands/sync.mjs +139 -21
- package/src/runtime.mjs +16 -37
|
@@ -25,5 +25,9 @@ runs:
|
|
|
25
25
|
uses: github/codeql-action/analyze@v4
|
|
26
26
|
with:
|
|
27
27
|
category: ${{ inputs.category }}
|
|
28
|
+
# Pin pull-request uploads to the contributor's head SHA/ref so a
|
|
29
|
+
# moving merge ref cannot race the required code-scanning context.
|
|
30
|
+
ref: ${{ github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
|
31
|
+
sha: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
|
28
32
|
upload-database: false
|
|
29
33
|
wait-for-processing: false
|
package/.github/dependabot.yml
CHANGED
|
@@ -29,8 +29,10 @@ updates:
|
|
|
29
29
|
applies-to: version-updates
|
|
30
30
|
patterns: ['*']
|
|
31
31
|
ignore:
|
|
32
|
-
# TypeScript 7
|
|
33
|
-
#
|
|
32
|
+
# TypeScript 7 (the native compiler) is supported by the oxlint/oxfmt
|
|
33
|
+
# toolchain; the tsconfig must use NodeNext or bundler resolution since
|
|
34
|
+
# legacy node10 resolution was removed. Held with 5.9 until each
|
|
35
|
+
# repository's tsconfig is verified on 7.
|
|
34
36
|
- dependency-name: 'typescript'
|
|
35
37
|
versions: ['>=7.0.0']
|
|
36
38
|
# Held with TypeScript 5.9 above.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
name: Code Foundry Cloudflare Deploy
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
inputs:
|
|
6
|
+
mode:
|
|
7
|
+
description: 'Deploy mode: preview (wrangler versions upload) or production (wrangler deploy).'
|
|
8
|
+
required: false
|
|
9
|
+
type: string
|
|
10
|
+
default: preview
|
|
11
|
+
working-directory:
|
|
12
|
+
description: 'Directory containing the wrangler configuration.'
|
|
13
|
+
required: false
|
|
14
|
+
type: string
|
|
15
|
+
default: '.'
|
|
16
|
+
runner:
|
|
17
|
+
required: false
|
|
18
|
+
type: string
|
|
19
|
+
default: ubuntu-latest
|
|
20
|
+
wrangler-version:
|
|
21
|
+
required: false
|
|
22
|
+
type: string
|
|
23
|
+
default: 'latest'
|
|
24
|
+
environment:
|
|
25
|
+
description: 'GitHub deployment environment. Defaults to Preview/Production based on the mode.'
|
|
26
|
+
required: false
|
|
27
|
+
type: string
|
|
28
|
+
default: ''
|
|
29
|
+
secrets:
|
|
30
|
+
CLOUDFLARE_API_TOKEN:
|
|
31
|
+
required: true
|
|
32
|
+
CLOUDFLARE_ACCOUNT_ID:
|
|
33
|
+
required: true
|
|
34
|
+
|
|
35
|
+
permissions:
|
|
36
|
+
contents: read
|
|
37
|
+
deployments: write
|
|
38
|
+
|
|
39
|
+
jobs:
|
|
40
|
+
deploy:
|
|
41
|
+
name: Cloudflare Deploy / ${{ inputs.mode }}
|
|
42
|
+
if: vars.CI_BILLING_PAUSED != 'true'
|
|
43
|
+
runs-on: ${{ inputs.runner }}
|
|
44
|
+
timeout-minutes: 20
|
|
45
|
+
steps:
|
|
46
|
+
- name: Checkout
|
|
47
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
48
|
+
with:
|
|
49
|
+
persist-credentials: false
|
|
50
|
+
|
|
51
|
+
- name: Deploy to Cloudflare
|
|
52
|
+
id: deploy
|
|
53
|
+
working-directory: ${{ inputs.working-directory }}
|
|
54
|
+
env:
|
|
55
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
56
|
+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
57
|
+
WRANGLER_VERSION: ${{ inputs.wrangler-version }}
|
|
58
|
+
MODE: ${{ inputs.mode }}
|
|
59
|
+
run: |
|
|
60
|
+
set -euo pipefail
|
|
61
|
+
npx -y wrangler@"$WRANGLER_VERSION" --version
|
|
62
|
+
if [ "$MODE" = "production" ]; then
|
|
63
|
+
npx -y wrangler@"$WRANGLER_VERSION" deploy 2>&1 | tee deploy-output.txt
|
|
64
|
+
elif [ "$MODE" = "preview" ]; then
|
|
65
|
+
npx -y wrangler@"$WRANGLER_VERSION" versions upload --message "preview ${GITHUB_SHA::7}" 2>&1 | tee deploy-output.txt
|
|
66
|
+
else
|
|
67
|
+
echo "Unsupported mode: $MODE (use preview or production)" >&2
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
# Prefer version preview URLs; fall back to the first workers.dev URL.
|
|
71
|
+
url=$(grep -Eo 'https://[a-zA-Z0-9][a-zA-Z0-9.-]*\.workers\.dev' deploy-output.txt | tail -1 || true)
|
|
72
|
+
echo "url=$url" >> "$GITHUB_OUTPUT"
|
|
73
|
+
|
|
74
|
+
- name: Record GitHub deployment
|
|
75
|
+
env:
|
|
76
|
+
GH_TOKEN: ${{ github.token }}
|
|
77
|
+
MODE: ${{ inputs.mode }}
|
|
78
|
+
ENVIRONMENT_OVERRIDE: ${{ inputs.environment }}
|
|
79
|
+
DEPLOY_URL: ${{ steps.deploy.outputs.url }}
|
|
80
|
+
run: |
|
|
81
|
+
set -euo pipefail
|
|
82
|
+
if [ "$MODE" != "production" ] && [ "$MODE" != "preview" ]; then
|
|
83
|
+
echo "Unsupported mode: $MODE" >&2
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
environment="${ENVIRONMENT_OVERRIDE:-$([ "$MODE" = "production" ] && echo Production || echo Preview)}"
|
|
87
|
+
body=$(jq -n \
|
|
88
|
+
--arg ref "$GITHUB_SHA" \
|
|
89
|
+
--arg environment "$environment" \
|
|
90
|
+
--argjson production "$([ "$MODE" = "production" ] && echo true || echo false)" \
|
|
91
|
+
'{ref: $ref, environment: $environment, auto_merge: false, production_environment: $production, required_contexts: []}')
|
|
92
|
+
deployment=$(gh api "repos/$GITHUB_REPOSITORY/deployments" --input - <<< "$body" --jq .id)
|
|
93
|
+
status_args=(-f state=success -f description="Cloudflare $MODE deploy (${GITHUB_SHA::7})")
|
|
94
|
+
if [ -n "$DEPLOY_URL" ]; then
|
|
95
|
+
status_args+=(-f environment_url="$DEPLOY_URL" -f log_url="$DEPLOY_URL")
|
|
96
|
+
fi
|
|
97
|
+
gh api -X POST "repos/$GITHUB_REPOSITORY/deployments/$deployment/statuses" "${status_args[@]}" --jq .state
|
|
98
|
+
echo "Recorded $environment deployment for ${GITHUB_SHA::7}"
|
|
@@ -285,6 +285,10 @@ jobs:
|
|
|
285
285
|
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
|
|
286
286
|
with:
|
|
287
287
|
category: /language:rust/${{ steps.scope.outputs.scope_id }}
|
|
288
|
+
# Keep uploads attached to the PR head rather than the mutable
|
|
289
|
+
# merge ref used by GitHub's pull_request event.
|
|
290
|
+
ref: ${{ github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
|
291
|
+
sha: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
|
288
292
|
upload-database: false
|
|
289
293
|
wait-for-processing: false
|
|
290
294
|
- name: Not applicable
|
|
@@ -89,17 +89,21 @@ jobs:
|
|
|
89
89
|
let releaseType = config.release_type || 'auto'
|
|
90
90
|
let npmPublish = config.npm_publish || 'false'
|
|
91
91
|
// The merge audit topology requires rebase for Release Please version
|
|
92
|
-
// pull requests into main, matching the linear-history ruleset.
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
92
|
+
// pull requests into main, matching the linear-history ruleset. The
|
|
93
|
+
// direct topology may also opt into squash: a single-commit release
|
|
94
|
+
// PR squashes to the identical tree and release-please recommends
|
|
95
|
+
// squash. Release automation never defaults to a merge method: an
|
|
96
|
+
// unset or unsupported release_merge_strategy fails the release job
|
|
97
|
+
// before any pull request is created or merged.
|
|
98
|
+
const gitWorkflow = String(config.git_workflow || 'direct').trim().toLowerCase()
|
|
96
99
|
const releaseMergeStrategy = String(config.release_merge_strategy || '').trim().toLowerCase()
|
|
97
100
|
if (releaseType === 'auto') {
|
|
98
101
|
releaseType = fs.existsSync('package.json') ? 'node' : fs.existsSync('pyproject.toml') ? 'python' : fs.existsSync('Cargo.toml') ? 'rust' : fs.existsSync('version.txt') ? 'simple' : 'none'
|
|
99
102
|
}
|
|
100
103
|
if (!['node', 'python', 'rust', 'simple', 'none'].includes(releaseType)) throw new Error(`Unsupported release_type: ${releaseType}`)
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
const allowedStrategies = gitWorkflow === 'staging-release' ? ['rebase'] : ['rebase', 'squash']
|
|
105
|
+
if (!allowedStrategies.includes(releaseMergeStrategy)) {
|
|
106
|
+
throw new Error(`release_merge_strategy must be "rebase"${gitWorkflow === 'staging-release' ? '' : ' or "squash" (direct topology)'} for automated release merges; got ${releaseMergeStrategy || '(unset; release automation never defaults to merge)'}`)
|
|
103
107
|
}
|
|
104
108
|
if (releaseType === 'none' || !fs.existsSync('package.json')) npmPublish = 'false'
|
|
105
109
|
let legacyReleaseType = ''
|
package/.oxlintrc.json
CHANGED
package/AGENTS.md
CHANGED
|
@@ -77,6 +77,21 @@ This repository uses the `staging-release` workflow: topic branches **squash** i
|
|
|
77
77
|
|
|
78
78
|
Merge only with the repository's canonical method. Never merge with `--admin`, never default or auto-select a merge method, and never use a method the branch ruleset does not allow. When in doubt, prefer the merge button's configured method and verify the ruleset after merging. Check `.github/CONTRIBUTING.md` for the complete flow and merge table.
|
|
79
79
|
|
|
80
|
+
### Branch and commit policy
|
|
81
|
+
|
|
82
|
+
Branch rulesets enforce deletions, force-pushes, required status checks, pull
|
|
83
|
+
requests, conversation resolution, and linear history where the repository's
|
|
84
|
+
plan supports them. Mirror those rules even where the plan cannot enforce
|
|
85
|
+
them:
|
|
86
|
+
|
|
87
|
+
- Branch from the default branch using
|
|
88
|
+
`feat/*`, `fix/*`, `chore/*`, `refactor/*`, `docs/*`, or `test/*` names.
|
|
89
|
+
- Never push directly to protected branches; open a pull request.
|
|
90
|
+
- Use Conventional Commit subjects (`feat:`, `fix:`, `chore:`, …); Release
|
|
91
|
+
Please depends on them to version releases.
|
|
92
|
+
- Keep pull requests focused; merge with the canonical method only after
|
|
93
|
+
required checks pass.
|
|
94
|
+
|
|
80
95
|
## Toolchain and dependencies
|
|
81
96
|
|
|
82
97
|
- Follow `toolchain: auto` in `.github/code-foundry.yml`; use native tools by
|
|
@@ -112,7 +127,7 @@ Run focused tests first, then the complete applicable set for release, security,
|
|
|
112
127
|
|
|
113
128
|
At minimum:
|
|
114
129
|
|
|
115
|
-
- TypeScript/JavaScript: Oxfmt formatting, Oxlint linting, type-check, build, and Bun's native test runner for unit/integration tests; use the project's native browser runner for E2E tests. Repositories
|
|
130
|
+
- TypeScript/JavaScript: Oxfmt formatting, Oxlint linting, type-check, build, and Bun's native test runner for unit/integration tests; use the project's native browser runner for E2E tests. Repositories using a different linter or formatter keep full control through their own `lint`/`format` scripts, which the runtime honors.
|
|
116
131
|
- Do not add Vitest. Preserve specialized native runners such as Matchstick for The Graph and Hardhat for smart contracts.
|
|
117
132
|
- Rust: default rustfmt, Clippy with warnings treated as errors, check, unit/integration tests, and dependency audit
|
|
118
133
|
- Python: Ruff formatting and linting, compile or type checks, pytest, coverage, and dependency audit
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.5.0...v1.5.1) (2026-09-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* sync-caller-runtime-pin ([#484](https://github.com/0xPlayerOne/code-foundry/issues/484)) ([e1a216a](https://github.com/0xPlayerOne/code-foundry/commit/e1a216a9dcebd040ef0eacdb20791f360a921908))
|
|
9
|
+
|
|
10
|
+
## [1.5.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.4.1...v1.5.0) (2026-09-07)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **release:** squash release merges, Cloudflare deploy workflow, tooling neutrality ([6a847eb](https://github.com/0xPlayerOne/code-foundry/commit/6a847eb0a3f78dbd343b7b189f32543d621b4b8a))
|
|
16
|
+
|
|
3
17
|
## [1.4.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.4.0...v1.4.1) (2026-09-07)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -74,7 +74,8 @@ The standard workflow triggers are:
|
|
|
74
74
|
|
|
75
75
|
Jobs are language-aware and skip irrelevant setup while remaining visible as
|
|
76
76
|
successful required checks. TypeScript uses Oxlint, Oxfmt, and Bun's native
|
|
77
|
-
test runner (
|
|
77
|
+
test runner (repositories using another linter or formatter keep full control
|
|
78
|
+
through their own `lint`/`format` scripts, which the runtime honors).
|
|
78
79
|
Rust uses `rustfmt`, Clippy with warnings as errors, and native
|
|
79
80
|
Cargo tests. Python uses Ruff, uv/pip-compatible setup, and native Python
|
|
80
81
|
tests. Solidity projects retain their native toolchain and test runner.
|
|
@@ -90,15 +91,16 @@ See [Workflow and CI conventions](docs/WORKFLOWS.md) for triggers, required
|
|
|
90
91
|
checks, runners, coverage, caching, and custom workflow extensions.
|
|
91
92
|
|
|
92
93
|
The contribution policy defaults to the `direct` workflow: feature PRs squash
|
|
93
|
-
into `main`, and Release Please version PRs
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
into `main`, and Release Please version PRs use the configured
|
|
95
|
+
`release_merge_strategy` (rebase by default, or squash when opted in).
|
|
96
|
+
Repositories with a preview/staging environment opt into
|
|
97
|
+
`git_workflow: staging-release`, where feature PRs squash into `staging`, the
|
|
98
|
+
promotion PR rebases into `main` (`merge_strategy: rebase`), and Release Please
|
|
99
|
+
version PRs rebase into `main`. Release automation never defaults to a merge
|
|
100
|
+
method and never merges with `--admin`; `code-foundry doctor` and
|
|
101
|
+
`code-foundry sync` fail closed on any strategy outside the selected topology.
|
|
102
|
+
GitHub Stacks is not part of this topology and does not reduce the required
|
|
103
|
+
workflow runs.
|
|
102
104
|
|
|
103
105
|
## Releases and publishing
|
|
104
106
|
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -50,7 +50,7 @@ repository manifests and source
|
|
|
50
50
|
| `license` | `gpl-3.0-or-later`, `agpl-3.0-or-later`, `apache-2.0`, `mit`, `preserve`, `none` | License policy; new repositories default to GPLv3 |
|
|
51
51
|
| `git_workflow` | `direct` (default), `staging-release` | Branch/release model; `direct` opens feature branches into `main`, `staging-release` promotes `staging` into `main` |
|
|
52
52
|
| `merge_strategy` | `rebase` | Promotion merge method for `staging` → `main`; only enforced by the `staging-release` topology |
|
|
53
|
-
| `release_merge_strategy` | `rebase`
|
|
53
|
+
| `release_merge_strategy` | `rebase`, `squash` (direct topology only) | Merge method for Release Please version PRs into `main`; release automation fails closed on anything else |
|
|
54
54
|
| `runner` fields | GitHub runner names | Per-workflow runner policy |
|
|
55
55
|
|
|
56
56
|
Supported features are `ci`, `codeql`, `security`, `test`, `draft-pr`,
|
|
@@ -77,8 +77,9 @@ present.
|
|
|
77
77
|
- `direct` (default): feature branches open pull requests directly into
|
|
78
78
|
`main`. Validation and security scans run on every PR. No `staging` branch
|
|
79
79
|
exists, no promotion caller is generated, and `merge_strategy` is not
|
|
80
|
-
enforced.
|
|
81
|
-
|
|
80
|
+
enforced. Release Please version PRs use `release_merge_strategy` (rebase by
|
|
81
|
+
default; squash is also allowed). Dependabot updates target `main`. This is
|
|
82
|
+
the right choice when a repository has no preview or staging environment.
|
|
82
83
|
- `staging-release` (opt-in): feature branches squash into `staging`, a
|
|
83
84
|
promotion PR rebases validated changes into `main` (`merge_strategy:
|
|
84
85
|
rebase`), and Release Please version PRs rebase into `main`
|
|
@@ -115,3 +116,14 @@ Each scoped shard must contain tracked Rust source. Code Foundry rejects
|
|
|
115
116
|
absolute paths, parent traversal, duplicates, empty scopes, and more than eight
|
|
116
117
|
shards. Do not split a single crate by arbitrary non-Rust directories: use
|
|
117
118
|
`["all"]` when complete, non-overlapping source scopes are not available.
|
|
119
|
+
|
|
120
|
+
## Cloudflare Workers deployments
|
|
121
|
+
|
|
122
|
+
Repositories that deploy to Cloudflare Workers can opt into GitHub-native
|
|
123
|
+
deployments (Preview/Production environments with deployment statuses, like
|
|
124
|
+
Vercel's integration) by adding a small caller for the runtime's reusable
|
|
125
|
+
`cloudflare-deploy.yml` workflow. The workflow runs `wrangler versions upload`
|
|
126
|
+
for pull-request previews and `wrangler deploy` for production, records a
|
|
127
|
+
GitHub deployment plus status with the workers.dev URL, and respects
|
|
128
|
+
`CI_BILLING_PAUSED`. It requires the `CLOUDFLARE_API_TOKEN` and
|
|
129
|
+
`CLOUDFLARE_ACCOUNT_ID` secrets in the consumer repository.
|
package/docs/RELEASES.md
CHANGED
|
@@ -24,10 +24,11 @@ topology, feature and fix branches land on `staging` with **squash** merges,
|
|
|
24
24
|
the `staging` → `main` promotion PR merges with **rebase** (`merge_strategy:
|
|
25
25
|
rebase`), and Release Please version PRs merge with **rebase**
|
|
26
26
|
(`release_merge_strategy: rebase`). In the `direct` topology, feature and fix
|
|
27
|
-
branches squash straight into `main` and
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
branches squash straight into `main` and Release Please version PRs use the
|
|
28
|
+
configured `release_merge_strategy` (**rebase** by default, or **squash** when
|
|
29
|
+
opted in). `merge_strategy` is not enforced. Release automation never defaults
|
|
30
|
+
to a merge method and never merges with `--admin`: `staging-release` accepts
|
|
31
|
+
only rebase for release PRs, while `direct` accepts rebase or squash.
|
|
31
32
|
|
|
32
33
|
The release workflow opens or updates a versioned PR after changes reach
|
|
33
34
|
`main`. Merging that PR updates the changelog, creates the Git tag and GitHub
|
|
@@ -45,21 +46,21 @@ release_type: auto # auto, node, python, rust, simple, or none
|
|
|
45
46
|
npm_publish: false # true only for an npm package
|
|
46
47
|
git_workflow: direct # direct (default) or staging-release
|
|
47
48
|
merge_strategy: rebase # staging-release only: staging -> main promotion PRs rebase
|
|
48
|
-
release_merge_strategy: rebase # required
|
|
49
|
+
release_merge_strategy: rebase # required; direct also allows squash
|
|
49
50
|
```
|
|
50
51
|
|
|
51
52
|
`git_workflow: staging-release` is opt-in; without it, repositories use the
|
|
52
53
|
`direct` flow and no promotion PR exists. When the staging-release topology is
|
|
53
54
|
selected, `merge_strategy` applies to promotion PRs (`staging` into `main`)
|
|
54
55
|
and `release_merge_strategy` to Release Please version PRs; feature PRs into
|
|
55
|
-
`staging` use squash merges. `code-foundry doctor
|
|
56
|
-
reject any non-`rebase` `merge_strategy`
|
|
57
|
-
|
|
58
|
-
release
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
`staging` use squash merges. `code-foundry doctor`, `code-foundry sync`, and
|
|
57
|
+
the release workflow reject any non-`rebase` `merge_strategy` or release
|
|
58
|
+
strategy in `staging-release`; `direct` accepts `rebase` or `squash` for
|
|
59
|
+
release PRs and never falls back to `merge`. Both keep `main` fully linear,
|
|
60
|
+
which is what makes the post-release reconciliation possible. The final
|
|
61
|
+
branch trees are inspected before mutation; validated main-only changes are
|
|
62
|
+
inherited by `staging`, while unpromoted staging work is replayed on top of
|
|
63
|
+
`main`.
|
|
63
64
|
|
|
64
65
|
Promotion never opens a pull request directly from a divergent `staging`
|
|
65
66
|
history. The workflow creates or refreshes a deterministic promotion branch
|
|
@@ -135,6 +136,6 @@ already passed).
|
|
|
135
136
|
|
|
136
137
|
1. Merge tested changes into `main` (direct: feature PRs; staging-release: promote `staging` into `main`).
|
|
137
138
|
2. Review the generated Release Please PR and changelog.
|
|
138
|
-
3. Merge the release PR with the repository's configured `release_merge_strategy` (**rebase
|
|
139
|
+
3. Merge the release PR with the repository's configured `release_merge_strategy` (**rebase** by default; **squash** is also valid for direct topology).
|
|
139
140
|
4. Confirm the GitHub Release and any package publication.
|
|
140
141
|
5. staging-release only: synchronize `staging` with the new `main` release commit.
|
package/docs/WORKFLOWS.md
CHANGED
|
@@ -100,15 +100,17 @@ succeeds.
|
|
|
100
100
|
## Merge methods
|
|
101
101
|
|
|
102
102
|
The merge audit pins one merge method per transition. `code-foundry doctor`
|
|
103
|
-
and `code-foundry sync`
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
103
|
+
and `code-foundry sync` validate release strategy against the repository
|
|
104
|
+
topology. Staging-release release PRs require `rebase`; direct release PRs
|
|
105
|
+
allow `rebase` or `squash`. The release workflow fails closed on any other
|
|
106
|
+
strategy and never falls back to `merge`.
|
|
107
|
+
|
|
108
|
+
| Transition | Merge method | Enforcement |
|
|
109
|
+
| ---------------------------------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
110
|
+
| Feature/fix PR into `main` (direct topology) | Squash | Contribution policy; see `CONTRIBUTING.md` |
|
|
111
|
+
| Feature/fix PR into `staging` (staging-release topology) | Squash | Contribution policy; see `CONTRIBUTING.md` |
|
|
112
|
+
| `staging` → `main` promotion PR (staging-release topology) | Rebase (`merge_strategy: rebase`) | Code Foundry creates a one-commit head with `main` as its parent and the exact validated `staging` tree; `merge_strategy` must be `rebase`, and merge commits are rejected |
|
|
113
|
+
| Release Please version PR into `main` | Configured (`release_merge_strategy`: rebase default; squash direct only) | Release automation fails closed on unsupported topology/strategy; never defaults to `merge`, never uses `--admin` |
|
|
112
114
|
|
|
113
115
|
The promotion rows above apply only to `staging-release`; `direct`
|
|
114
116
|
repositories never generate a promotion caller and `merge_strategy` is not
|
|
@@ -123,14 +125,18 @@ additional required checks after earlier ones have passed (for example
|
|
|
123
125
|
merge policy-blocked. The mergeability poll is bounded and fails closed on
|
|
124
126
|
conflicts or timeout; releases without an automation token remain manual.
|
|
125
127
|
|
|
126
|
-
Keeping `main` linear — rebase promotions and
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
Keeping `main` linear — rebase promotions and release PRs in the
|
|
129
|
+
`staging-release` topology, or a single-commit rebase/squash release PR in the
|
|
130
|
+
`direct` topology — is what lets the post-release reconciliation fast-forward
|
|
131
|
+
or replay `staging` safely. `direct` repositories have no reconciliation step:
|
|
132
|
+
releases merge straight into `main` with the configured
|
|
133
|
+
`release_merge_strategy`.
|
|
134
|
+
|
|
135
|
+
Protect `main` with the aggregate `Validation / Gate`. Require squash for
|
|
136
|
+
feature/fix pull requests and permit the configured Release Please method:
|
|
137
|
+
rebase in `staging-release`, or rebase/squash in `direct`. In the
|
|
138
|
+
`staging-release` topology, protect `staging` the same way with a single GitHub
|
|
139
|
+
Actions integration path. That path uses the
|
|
134
140
|
GitHub Actions integration token by default, and optionally an SSH deploy key
|
|
135
141
|
when `STAGING_DEPLOY_KEY` is configured. The deploy key is required only when
|
|
136
142
|
a personal-repository ruleset for `staging` enforces a Deploy Key bypass for
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
|
|
5
5
|
"homepage": "https://github.com/0xPlayerOne/code-foundry#readme",
|
|
6
6
|
"bugs": {
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -122,9 +122,14 @@ export function doctor(root, options = {}) {
|
|
|
122
122
|
)
|
|
123
123
|
}
|
|
124
124
|
const releaseMergeStrategy = config.release_merge_strategy ?? ''
|
|
125
|
-
|
|
125
|
+
const allowedReleaseStrategies =
|
|
126
|
+
workflow === 'staging-release' ? ['rebase'] : ['rebase', 'squash']
|
|
127
|
+
if (
|
|
128
|
+
includesValue(features, 'release') &&
|
|
129
|
+
!allowedReleaseStrategies.includes(releaseMergeStrategy)
|
|
130
|
+
) {
|
|
126
131
|
error(
|
|
127
|
-
`release_merge_strategy must be "rebase" for automated release merges; got "${releaseMergeStrategy || '(unset; release automation never defaults to merge)'}".`
|
|
132
|
+
`release_merge_strategy must be "rebase"${workflow === 'staging-release' ? '' : ' or "squash" (direct topology)'} for automated release merges; got "${releaseMergeStrategy || '(unset; release automation never defaults to merge)'}".`
|
|
128
133
|
)
|
|
129
134
|
}
|
|
130
135
|
for (const name of ['validation', 'draft-pr', 'release-pr', 'release']) {
|
package/src/commands/fleet.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } fro
|
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { tmpdir } from 'node:os'
|
|
6
6
|
import { spawnSync } from 'node:child_process'
|
|
7
|
-
import { syncRepository } from './sync.mjs'
|
|
7
|
+
import { readPackageVersion, syncRepository } from './sync.mjs'
|
|
8
8
|
|
|
9
9
|
/** @typedef {{ path: string, repository: string, runtimeRef: string, dirty: boolean, configured: boolean, gitWorkflow: string }} FleetRepository */
|
|
10
10
|
|
|
@@ -32,6 +32,13 @@ export function discoverRepositories(root) {
|
|
|
32
32
|
|
|
33
33
|
/** @param {string} root @param {string} source @param {{ createPr?: boolean, dryRun?: boolean, force?: boolean, version: string, exclude?: string[] }} options */
|
|
34
34
|
export function upgradeFleet(root, source, options) {
|
|
35
|
+
const sourceVersion = `v${readPackageVersion(source)}`
|
|
36
|
+
const version = options.version ?? sourceVersion
|
|
37
|
+
if (options.version && options.version !== sourceVersion) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`fleet upgrade target ${options.version} does not match the runtime source checkout (${sourceVersion}); refresh the code-foundry checkout to ${options.version} before upgrading so the rendered callers and the declared runtime agree.`
|
|
40
|
+
)
|
|
41
|
+
}
|
|
35
42
|
const repositories = discoverRepositories(root)
|
|
36
43
|
const report = []
|
|
37
44
|
for (const repository of repositories) {
|
|
@@ -80,7 +87,7 @@ export function upgradeFleet(root, source, options) {
|
|
|
80
87
|
})
|
|
81
88
|
continue
|
|
82
89
|
}
|
|
83
|
-
report.push(upgradeRepository(repository, source,
|
|
90
|
+
report.push(upgradeRepository(repository, source, version))
|
|
84
91
|
}
|
|
85
92
|
console.log(JSON.stringify(report, null, 2))
|
|
86
93
|
return report
|
|
@@ -121,7 +128,12 @@ function upgradeRepository(repository, source, version) {
|
|
|
121
128
|
status: 'skipped',
|
|
122
129
|
reason: add.stderr.trim() || 'unable to create isolated worktree',
|
|
123
130
|
}
|
|
124
|
-
const result = syncRepository({
|
|
131
|
+
const result = syncRepository({
|
|
132
|
+
target: temporary,
|
|
133
|
+
source,
|
|
134
|
+
force: false,
|
|
135
|
+
runtimeRef: version,
|
|
136
|
+
})
|
|
125
137
|
const configFile = join(temporary, '.github/code-foundry.yml')
|
|
126
138
|
if (existsSync(configFile)) {
|
|
127
139
|
const current = readFileSync(configFile, 'utf8')
|
package/src/commands/sync.mjs
CHANGED
|
@@ -96,7 +96,7 @@ const legacyFiles = [
|
|
|
96
96
|
'.github/licenses/AGPL-3.0-or-later.txt',
|
|
97
97
|
]
|
|
98
98
|
|
|
99
|
-
/** @typedef {{ target: string, source: string, dryRun?: boolean, force?: boolean, init?: boolean }} SyncOptions */
|
|
99
|
+
/** @typedef {{ target: string, source: string, dryRun?: boolean, force?: boolean, init?: boolean, runtimeRef?: string }} SyncOptions */
|
|
100
100
|
|
|
101
101
|
/** @param {SyncOptions} options */
|
|
102
102
|
export function syncRepository(options) {
|
|
@@ -137,7 +137,11 @@ export function syncRepository(options) {
|
|
|
137
137
|
const features = configured(config.features, 'all')
|
|
138
138
|
const runtimeRepository = configured(config.runtime_repository, '0xPlayerOne/code-foundry')
|
|
139
139
|
const sourceRuntimeRef = `v${readPackageVersion(source)}`
|
|
140
|
-
|
|
140
|
+
// An explicit runtime ref (fleet upgrade) is authoritative: the rendered
|
|
141
|
+
// callers and the config pin must agree with it, otherwise an upgrade would
|
|
142
|
+
// declare one runtime while shipping another.
|
|
143
|
+
const targetRuntimeRef = options.runtimeRef ?? sourceRuntimeRef
|
|
144
|
+
let runtimeRef = options.runtimeRef ?? configured(config.runtime_ref, sourceRuntimeRef)
|
|
141
145
|
const toolchain = configured(config.toolchain, 'auto')
|
|
142
146
|
const overlays = overlayPolicy(target, config)
|
|
143
147
|
const rustCodeql = validateRustCodeqlConfig(config)
|
|
@@ -161,23 +165,33 @@ export function syncRepository(options) {
|
|
|
161
165
|
)
|
|
162
166
|
}
|
|
163
167
|
const releaseMergeStrategy = configured(config.release_merge_strategy, '')
|
|
164
|
-
if (includesValue(features, 'release')
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
if (includesValue(features, 'release')) {
|
|
169
|
+
// Staging-release reconciliations depend on rebase promotions and rebase
|
|
170
|
+
// release commits; the direct topology has no reconciliation step, so it
|
|
171
|
+
// may also squash Release Please version PRs (a single-commit release PR
|
|
172
|
+
// squashes to the identical tree, and release-please recommends squash).
|
|
173
|
+
const allowedReleaseStrategies =
|
|
174
|
+
workflow === 'staging-release' ? ['rebase'] : ['rebase', 'squash']
|
|
175
|
+
if (!allowedReleaseStrategies.includes(releaseMergeStrategy)) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Unsupported release_merge_strategy: ${releaseMergeStrategy || '(unset)'}; release automation requires rebase (or squash in the direct topology) for Release Please version pull requests and never defaults to merge.`
|
|
178
|
+
)
|
|
179
|
+
}
|
|
168
180
|
}
|
|
169
181
|
const changed = []
|
|
170
182
|
|
|
171
183
|
// Keep normal semver pins current during sync while preserving intentional
|
|
172
|
-
// refs such as `main`, `staging`, or a custom immutable SHA.
|
|
184
|
+
// refs such as `main`, `staging`, or a custom immutable SHA. An explicit
|
|
185
|
+
// runtime ref (fleet upgrade) is authoritative and overrides even those so
|
|
186
|
+
// the rendered callers and the config pin land on the same runtime.
|
|
173
187
|
if (
|
|
174
188
|
existingConfig.runtime_ref &&
|
|
175
|
-
|
|
176
|
-
|
|
189
|
+
existingConfig.runtime_ref !== targetRuntimeRef &&
|
|
190
|
+
(options.runtimeRef !== undefined || /^v\d+\.\d+\.\d+$/.test(existingConfig.runtime_ref))
|
|
177
191
|
) {
|
|
178
|
-
runtimeRef =
|
|
192
|
+
runtimeRef = targetRuntimeRef
|
|
179
193
|
const current = readFileSync(configPath, 'utf8')
|
|
180
|
-
const updated = current.replace(/^runtime_ref:\s*.*$/m, `runtime_ref: ${
|
|
194
|
+
const updated = current.replace(/^runtime_ref:\s*.*$/m, `runtime_ref: ${targetRuntimeRef}`)
|
|
181
195
|
if (updated !== current) {
|
|
182
196
|
changed.push('.github/code-foundry.yml')
|
|
183
197
|
writeOrReport(configPath, updated, dryRun)
|
|
@@ -643,7 +657,7 @@ const DIRECT_DOC_REPLACEMENTS = {
|
|
|
643
657
|
],
|
|
644
658
|
[
|
|
645
659
|
'This repository uses the `staging-release` workflow: topic branches **squash** into `staging`, a promotion PR **rebases** validated changes into `main` (`merge_strategy: rebase`), and the Release Please version PR **rebases** into `main` (`release_merge_strategy: rebase`). Feature PRs land on `staging` with squash merges; promotion and release PRs land on `main` with rebase merges. Re-align `staging` with `main` after a release when needed.',
|
|
646
|
-
'This repository uses the `direct` workflow: topic branches **squash** directly into `main`, and the Release Please version PR
|
|
660
|
+
'This repository uses the `direct` workflow: topic branches **squash** directly into `main`, and the Release Please version PR lands on `main` with the configured `release_merge_strategy` (rebase by default, or squash when opted in). Feature PRs land on `main` with squash merges. No integration branch exists; all pull requests target `main`.',
|
|
647
661
|
],
|
|
648
662
|
],
|
|
649
663
|
'.github/CONTRIBUTING.md': [
|
|
@@ -661,7 +675,7 @@ const DIRECT_DOC_REPLACEMENTS = {
|
|
|
661
675
|
],
|
|
662
676
|
[
|
|
663
677
|
'The Git workflow is `staging-release`: topic branches **squash** into `staging`, a promotion PR **rebases** validated changes into `main` (`merge_strategy: rebase`), and the Release Please version PR **rebases** into `main` (`release_merge_strategy: rebase`). Release automation never defaults to a merge method and never merges with `--admin`; `code-foundry doctor` and `code-foundry sync` fail closed on any other merge strategy. Re-align `staging` with `main` after a release when needed.',
|
|
664
|
-
'The Git workflow is `direct`: topic branches **squash** directly into `main`, and the Release Please version PR
|
|
678
|
+
'The Git workflow is `direct`: topic branches **squash** directly into `main`, and the Release Please version PR lands on `main` with the configured `release_merge_strategy` (rebase by default, or squash when opted in). Release automation never defaults to a merge method and never merges with `--admin`; `code-foundry doctor` and `code-foundry sync` fail closed on any other release merge strategy. Feature branches never touch `staging`; repositories with a preview/staging environment opt into `git_workflow: staging-release` explicitly.',
|
|
665
679
|
],
|
|
666
680
|
[
|
|
667
681
|
'git switch staging\ngit pull --ff-only origin staging',
|
|
@@ -687,7 +701,7 @@ const DIRECT_DOC_REPLACEMENTS = {
|
|
|
687
701
|
],
|
|
688
702
|
[
|
|
689
703
|
'| Change | Target | Merge method | Merge gate |\n| ---------------------------- | --------- | ----------------------------------------------- | --------------------------------------------------------- |\n| Working branch | `staging` | Squash | All applicable required checks pass |\n| `staging` → `main` promotion | `main` | Rebase (`merge_strategy`) | Current staging checks, release review, and rollout notes |\n| Release Please version PR | `main` | Rebase (`release_merge_strategy`, fails closed) | Validation gate and release policy pass |\n',
|
|
690
|
-
'| Change | Target | Merge method | Merge gate |\n|----------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| Working branch | `main` | Squash | All applicable required checks pass |\n| Release Please version PR | `main` |
|
|
704
|
+
'| Change | Target | Merge method | Merge gate |\n|----------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| Working branch | `main` | Squash | All applicable required checks pass |\n| Release Please version PR | `main` | Configured `release_merge_strategy` (rebase default, or squash in the direct topology) | Validation gate and release policy pass |',
|
|
691
705
|
],
|
|
692
706
|
['1. Create a focused branch from `staging`.', '1. Create a focused branch from `main`.'],
|
|
693
707
|
],
|
|
@@ -821,6 +835,103 @@ function mergeGitignore(baseline, existing) {
|
|
|
821
835
|
: baseline
|
|
822
836
|
}
|
|
823
837
|
|
|
838
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
839
|
+
function isJsonObject(value) {
|
|
840
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/** @param {string} source @returns {string} */
|
|
844
|
+
function stripJsonComments(source) {
|
|
845
|
+
const output = []
|
|
846
|
+
let inString = false
|
|
847
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
848
|
+
const char = source[index]
|
|
849
|
+
const next = source[index + 1]
|
|
850
|
+
if (inString) {
|
|
851
|
+
output.push(char)
|
|
852
|
+
if (char === '\\' && next !== undefined) {
|
|
853
|
+
output.push(next)
|
|
854
|
+
index += 1
|
|
855
|
+
} else if (char === '"') {
|
|
856
|
+
inString = false
|
|
857
|
+
}
|
|
858
|
+
continue
|
|
859
|
+
}
|
|
860
|
+
if (char === '"') {
|
|
861
|
+
inString = true
|
|
862
|
+
output.push(char)
|
|
863
|
+
continue
|
|
864
|
+
}
|
|
865
|
+
if (char === '/' && next === '/') {
|
|
866
|
+
output.push(' ', ' ')
|
|
867
|
+
index += 2
|
|
868
|
+
while (index < source.length && source[index] !== '\n' && source[index] !== '\r') {
|
|
869
|
+
output.push(' ')
|
|
870
|
+
index += 1
|
|
871
|
+
}
|
|
872
|
+
index -= 1
|
|
873
|
+
continue
|
|
874
|
+
}
|
|
875
|
+
if (char === '/' && next === '*') {
|
|
876
|
+
output.push(' ', ' ')
|
|
877
|
+
index += 2
|
|
878
|
+
while (index < source.length) {
|
|
879
|
+
const commentChar = source[index]
|
|
880
|
+
const commentNext = source[index + 1]
|
|
881
|
+
if (commentChar === '*' && commentNext === '/') {
|
|
882
|
+
output.push(' ', ' ')
|
|
883
|
+
index += 1
|
|
884
|
+
break
|
|
885
|
+
}
|
|
886
|
+
output.push(commentChar === '\n' || commentChar === '\r' ? commentChar : ' ')
|
|
887
|
+
index += 1
|
|
888
|
+
}
|
|
889
|
+
continue
|
|
890
|
+
}
|
|
891
|
+
output.push(char)
|
|
892
|
+
}
|
|
893
|
+
return output.join('')
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** @param {string} source @returns {string} */
|
|
897
|
+
function stripJsonTrailingCommas(source) {
|
|
898
|
+
const output = []
|
|
899
|
+
let inString = false
|
|
900
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
901
|
+
const char = source[index]
|
|
902
|
+
if (inString) {
|
|
903
|
+
output.push(char)
|
|
904
|
+
if (char === '\\') {
|
|
905
|
+
const escaped = source[index + 1]
|
|
906
|
+
if (escaped !== undefined) {
|
|
907
|
+
output.push(escaped)
|
|
908
|
+
index += 1
|
|
909
|
+
}
|
|
910
|
+
} else if (char === '"') {
|
|
911
|
+
inString = false
|
|
912
|
+
}
|
|
913
|
+
continue
|
|
914
|
+
}
|
|
915
|
+
if (char === '"') {
|
|
916
|
+
inString = true
|
|
917
|
+
output.push(char)
|
|
918
|
+
continue
|
|
919
|
+
}
|
|
920
|
+
if (char === ',') {
|
|
921
|
+
let nextIndex = index + 1
|
|
922
|
+
while (/\s/.test(source[nextIndex] ?? '')) nextIndex += 1
|
|
923
|
+
if (source[nextIndex] === '}' || source[nextIndex] === ']') continue
|
|
924
|
+
}
|
|
925
|
+
output.push(char)
|
|
926
|
+
}
|
|
927
|
+
return output.join('')
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/** @param {string} source @returns {unknown} */
|
|
931
|
+
function parseJsonc(source) {
|
|
932
|
+
return JSON.parse(stripJsonTrailingCommas(stripJsonComments(source)))
|
|
933
|
+
}
|
|
934
|
+
|
|
824
935
|
/**
|
|
825
936
|
* Merge a baseline Oxc config (.oxfmtrc.json / .oxlintrc.json) with the
|
|
826
937
|
* consumer's current config. The baseline owns every key it defines, except
|
|
@@ -828,7 +939,9 @@ function mergeGitignore(baseline, existing) {
|
|
|
828
939
|
* migrations or by the repository) are preserved so repeated syncs never
|
|
829
940
|
* churn them. Keys the baseline does not define (e.g. a repository-owned
|
|
830
941
|
* `overrides` block) belong to the consumer and are preserved as well, so
|
|
831
|
-
* a sync never silently drops repository-owned configuration.
|
|
942
|
+
* a sync never silently drops repository-owned configuration. Consumer
|
|
943
|
+
* category values are merged last so explicit category overrides survive the
|
|
944
|
+
* baseline's default category levels.
|
|
832
945
|
*
|
|
833
946
|
* When the merged semantics already match the consumer file, the exact
|
|
834
947
|
* existing bytes are returned untouched: rewriting canonical JSON here
|
|
@@ -840,23 +953,28 @@ function mergeIgnorePatternsConfig(baseline, existing) {
|
|
|
840
953
|
/** @type {Record<string, any>} */
|
|
841
954
|
let consumer = {}
|
|
842
955
|
try {
|
|
843
|
-
|
|
844
|
-
if (
|
|
845
|
-
|
|
956
|
+
const parsed = parseJsonc(existing)
|
|
957
|
+
if (!isJsonObject(parsed)) return baseline
|
|
958
|
+
consumer = parsed
|
|
846
959
|
} catch {
|
|
847
960
|
return baseline
|
|
848
961
|
}
|
|
849
962
|
/** @type {Record<string, any>} */
|
|
850
963
|
let config = {}
|
|
851
964
|
try {
|
|
852
|
-
|
|
853
|
-
if (
|
|
965
|
+
const parsed = parseJsonc(baseline)
|
|
966
|
+
if (!isJsonObject(parsed)) return baseline
|
|
967
|
+
config = parsed
|
|
854
968
|
} catch {
|
|
855
969
|
return baseline
|
|
856
970
|
}
|
|
857
971
|
/** @type {Record<string, any>} */
|
|
858
972
|
const merged = { ...config }
|
|
859
973
|
for (const [key, value] of Object.entries(consumer)) {
|
|
974
|
+
if (key === 'categories' && isJsonObject(merged.categories) && isJsonObject(value)) {
|
|
975
|
+
merged.categories = { ...merged.categories, ...value }
|
|
976
|
+
continue
|
|
977
|
+
}
|
|
860
978
|
if (!(key in merged)) merged[key] = value
|
|
861
979
|
}
|
|
862
980
|
const extra = Array.isArray(consumer.ignorePatterns) ? consumer.ignorePatterns : []
|
|
@@ -1006,7 +1124,7 @@ function renderConfigLine(key, value) {
|
|
|
1006
1124
|
return needsQuotes ? `${key}: '${value.replace(/'/g, "''")}'` : `${key}: ${value}`
|
|
1007
1125
|
}
|
|
1008
1126
|
/** @param {string} root */
|
|
1009
|
-
function readPackageVersion(root) {
|
|
1127
|
+
export function readPackageVersion(root) {
|
|
1010
1128
|
try {
|
|
1011
1129
|
return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version ?? '0.0.0'
|
|
1012
1130
|
} catch {
|
package/src/runtime.mjs
CHANGED
|
@@ -327,34 +327,11 @@ function hasDependencyManifest(ecosystem) {
|
|
|
327
327
|
return false
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
/**
|
|
331
|
-
* Detect repository-owned ESLint setup: an eslint dependency (or script
|
|
332
|
-
* reference), or a tracked flat/legacy config file. Repositories without any
|
|
333
|
-
* of these have nothing for the fallback runner to enforce, so lint skips
|
|
334
|
-
* instead of forcing a network fetch of ESLint in CI.
|
|
335
|
-
* @returns {boolean}
|
|
336
|
-
*/
|
|
337
|
-
function hasEslintSetup() {
|
|
338
|
-
const pkg = readPackage() ?? {}
|
|
339
|
-
const scripts = pkg.scripts ?? {}
|
|
340
|
-
const dependencies = {
|
|
341
|
-
...pkg.dependencies,
|
|
342
|
-
...pkg.devDependencies,
|
|
343
|
-
...pkg.optionalDependencies,
|
|
344
|
-
...pkg.peerDependencies,
|
|
345
|
-
}
|
|
346
|
-
if (dependencies.eslint) return true
|
|
347
|
-
if (Object.values(scripts).some((value) => /\beslint\b/.test(String(value)))) return true
|
|
348
|
-
return repositoryTestFiles().some((file) =>
|
|
349
|
-
/(^|\/)(\.eslintrc[^/]*|eslint\.config\.[^/]*)$/.test(file)
|
|
350
|
-
)
|
|
351
|
-
}
|
|
352
|
-
|
|
353
330
|
/**
|
|
354
331
|
* Detect repository-owned Oxlint setup: an oxlint dependency (or script
|
|
355
332
|
* reference) or a tracked `.oxlintrc.json` / `oxlint.config.*` file. Oxlint
|
|
356
|
-
* is the baseline's preferred JavaScript linter;
|
|
357
|
-
*
|
|
333
|
+
* is the baseline's preferred JavaScript linter; repositories without one
|
|
334
|
+
* fall back to their own `lint` script if defined.
|
|
358
335
|
* @returns {boolean}
|
|
359
336
|
*/
|
|
360
337
|
function hasOxlintSetup() {
|
|
@@ -376,8 +353,8 @@ function hasOxlintSetup() {
|
|
|
376
353
|
/**
|
|
377
354
|
* Detect repository-owned Oxfmt setup: an oxfmt dependency (or script
|
|
378
355
|
* reference) or a tracked `.oxfmtrc.json` / `oxfmt.config.*` file. Oxfmt is
|
|
379
|
-
* the baseline's
|
|
380
|
-
*
|
|
356
|
+
* the baseline's formatter; repositories using a different formatter opt out
|
|
357
|
+
* through their own `format`/`fmt` scripts, which the script fallback honors.
|
|
381
358
|
* @returns {boolean}
|
|
382
359
|
*/
|
|
383
360
|
function hasOxfmtSetup() {
|
|
@@ -415,12 +392,13 @@ function ci(task) {
|
|
|
415
392
|
if (
|
|
416
393
|
!scripted &&
|
|
417
394
|
(hasLanguage('typescript') || hasLanguage('javascript')) &&
|
|
418
|
-
hasRootJavascriptProject()
|
|
395
|
+
hasRootJavascriptProject() &&
|
|
396
|
+
hasOxfmtSetup()
|
|
419
397
|
) {
|
|
420
|
-
// Oxfmt is the
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
398
|
+
// Oxfmt is the baseline's formatter. Repositories that use a different
|
|
399
|
+
// formatter keep full control through their own `format`/`fmt` scripts,
|
|
400
|
+
// which the runScript fallback above already honors.
|
|
401
|
+
runTool('oxfmt', ['--check', '.'])
|
|
424
402
|
}
|
|
425
403
|
if (hasLanguage('python') && hasRootPythonProject()) runTool('ruff', ['format', '--check', '.'])
|
|
426
404
|
if (hasLanguage('rust') && hasRootRustProject()) run('cargo', ['fmt', '--check'])
|
|
@@ -431,12 +409,13 @@ function ci(task) {
|
|
|
431
409
|
if (
|
|
432
410
|
!scripted &&
|
|
433
411
|
(hasLanguage('typescript') || hasLanguage('javascript')) &&
|
|
434
|
-
hasRootJavascriptProject()
|
|
412
|
+
hasRootJavascriptProject() &&
|
|
413
|
+
hasOxlintSetup()
|
|
435
414
|
) {
|
|
436
|
-
// Oxlint is the
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
415
|
+
// Oxlint is the baseline's linter. Repositories that use a different
|
|
416
|
+
// linter keep full control through their own `lint` script, which the
|
|
417
|
+
// runScript fallback above already honors.
|
|
418
|
+
runTool('oxlint', [])
|
|
440
419
|
}
|
|
441
420
|
if (hasLanguage('python') && hasRootPythonProject()) runTool('ruff', ['check', '.'])
|
|
442
421
|
if (hasLanguage('rust') && hasRootRustProject())
|