code-foundry 0.27.17 → 0.28.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/.github/workflows/release.yml +67 -2
- package/.github/workflows/release_self-ci.yml +2 -0
- package/CHANGELOG.md +14 -0
- package/docs/RELEASES.md +12 -5
- package/package.json +1 -1
- package/src/cli.mjs +21 -2
- package/src/commands/release.mjs +64 -0
- package/src/lib/release-policy.mjs +108 -0
|
@@ -8,6 +8,16 @@ on:
|
|
|
8
8
|
required: false
|
|
9
9
|
type: string
|
|
10
10
|
default: ubuntu-slim
|
|
11
|
+
runtime-repository:
|
|
12
|
+
description: Repository containing the Code Foundry runtime.
|
|
13
|
+
required: false
|
|
14
|
+
type: string
|
|
15
|
+
default: 0xPlayerOne/code-foundry
|
|
16
|
+
runtime-ref:
|
|
17
|
+
description: Code Foundry runtime tag or ref.
|
|
18
|
+
required: false
|
|
19
|
+
type: string
|
|
20
|
+
default: v0.27.18
|
|
11
21
|
|
|
12
22
|
permissions:
|
|
13
23
|
contents: write
|
|
@@ -71,6 +81,16 @@ jobs:
|
|
|
71
81
|
`release_type=${releaseType}\nlegacy_release_type=${legacyReleaseType}\nnpm_publish=${npmPublish}\n`,
|
|
72
82
|
)
|
|
73
83
|
NODE
|
|
84
|
+
- name: Detect release credentials
|
|
85
|
+
id: credentials
|
|
86
|
+
env:
|
|
87
|
+
RELEASE_PLEASE_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
|
88
|
+
run: |
|
|
89
|
+
if [ -n "$RELEASE_PLEASE_TOKEN" ]; then
|
|
90
|
+
echo "auto_merge=true" >> "$GITHUB_OUTPUT"
|
|
91
|
+
else
|
|
92
|
+
echo "auto_merge=false" >> "$GITHUB_OUTPUT"
|
|
93
|
+
fi
|
|
74
94
|
- name: Release Please
|
|
75
95
|
id: release
|
|
76
96
|
if: steps.profile.outputs.release_type != 'none'
|
|
@@ -79,10 +99,14 @@ jobs:
|
|
|
79
99
|
token: ${{ secrets.RELEASE_PLEASE_TOKEN || github.token }}
|
|
80
100
|
config-file: release-please-config.json
|
|
81
101
|
release-type: ${{ steps.profile.outputs.legacy_release_type }}
|
|
102
|
+
- name: Leave release pull request for manual merge
|
|
103
|
+
if: steps.release.outputs.prs_created == 'true' && steps.credentials.outputs.auto_merge != 'true'
|
|
104
|
+
run: |
|
|
105
|
+
echo "::notice title=Manual release merge required::Release Please created or updated a version pull request. RELEASE_PLEASE_TOKEN is not configured, so Code Foundry skipped guarded auto-merge. Configure the token before merging when downstream release workflows must run automatically."
|
|
82
106
|
- name: Merge generated version pull requests
|
|
83
|
-
if: steps.release.outputs.prs_created == 'true'
|
|
107
|
+
if: steps.release.outputs.prs_created == 'true' && steps.credentials.outputs.auto_merge == 'true'
|
|
84
108
|
env:
|
|
85
|
-
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN
|
|
109
|
+
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
|
86
110
|
run: |
|
|
87
111
|
set -euo pipefail
|
|
88
112
|
|
|
@@ -154,6 +178,47 @@ jobs:
|
|
|
154
178
|
--delete-branch
|
|
155
179
|
done
|
|
156
180
|
|
|
181
|
+
reconcile:
|
|
182
|
+
name: Release / Reconcile
|
|
183
|
+
needs: release
|
|
184
|
+
if: needs.release.outputs.release_created == 'true'
|
|
185
|
+
runs-on: ${{ inputs.runner }}
|
|
186
|
+
timeout-minutes: 15
|
|
187
|
+
permissions:
|
|
188
|
+
contents: write
|
|
189
|
+
pull-requests: write
|
|
190
|
+
env:
|
|
191
|
+
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || github.token }}
|
|
192
|
+
steps:
|
|
193
|
+
- name: Checkout repository
|
|
194
|
+
uses: actions/checkout@v7
|
|
195
|
+
with:
|
|
196
|
+
fetch-depth: 0
|
|
197
|
+
- name: Checkout runtime
|
|
198
|
+
uses: actions/checkout@v7
|
|
199
|
+
with:
|
|
200
|
+
repository: ${{ inputs.runtime-repository }}
|
|
201
|
+
ref: ${{ inputs.runtime-ref }}
|
|
202
|
+
path: .code-foundry
|
|
203
|
+
sparse-checkout: |
|
|
204
|
+
src
|
|
205
|
+
release-please-config.json
|
|
206
|
+
- name: Check staging branch
|
|
207
|
+
id: staging
|
|
208
|
+
run: |
|
|
209
|
+
if git ls-remote --exit-code origin refs/heads/staging >/dev/null 2>&1; then
|
|
210
|
+
echo "exists=true" >> "$GITHUB_OUTPUT"
|
|
211
|
+
git fetch --no-tags origin main staging
|
|
212
|
+
else
|
|
213
|
+
echo "exists=false" >> "$GITHUB_OUTPUT"
|
|
214
|
+
fi
|
|
215
|
+
- name: Reconcile release metadata
|
|
216
|
+
if: steps.staging.outputs.exists == 'true'
|
|
217
|
+
run: node .code-foundry/src/cli.mjs release reconcile --github --base main --head staging
|
|
218
|
+
- name: Skip without staging
|
|
219
|
+
if: steps.staging.outputs.exists != 'true'
|
|
220
|
+
run: echo 'No staging branch exists; release reconciliation is not applicable.'
|
|
221
|
+
|
|
157
222
|
npm:
|
|
158
223
|
name: Release / Publish npm
|
|
159
224
|
needs: release
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.28.0](https://github.com/0xPlayerOne/code-foundry/compare/v0.27.18...v0.28.0) (2026-07-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **release:** reconcile staging after release metadata ([4de1ad6](https://github.com/0xPlayerOne/code-foundry/commit/4de1ad64bd9276344368934ab555e6cb31a90784))
|
|
9
|
+
|
|
10
|
+
## [0.27.18](https://github.com/0xPlayerOne/code-foundry/compare/v0.27.17...v0.27.18) (2026-07-29)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **release:** degrade gracefully without release token ([69ac43a](https://github.com/0xPlayerOne/code-foundry/commit/69ac43acfa2813495b8da0535942afbbedae3017))
|
|
16
|
+
|
|
3
17
|
## [0.27.17](https://github.com/0xPlayerOne/code-foundry/compare/v0.27.16...v0.27.17) (2026-07-29)
|
|
4
18
|
|
|
5
19
|
|
package/docs/RELEASES.md
CHANGED
|
@@ -29,8 +29,8 @@ body. Existing `CHANGELOG.md` history remains repository-owned.
|
|
|
29
29
|
Set these values in `.github/code-foundry.yml`:
|
|
30
30
|
|
|
31
31
|
```yaml
|
|
32
|
-
release_type: auto
|
|
33
|
-
npm_publish: false
|
|
32
|
+
release_type: auto # auto, node, python, rust, simple, or none
|
|
33
|
+
npm_publish: false # true only for an npm package
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
`auto` selects a supported manifest. Use `simple` with `version.txt` for a
|
|
@@ -39,9 +39,16 @@ not release automatically.
|
|
|
39
39
|
|
|
40
40
|
## Pull request permissions
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
Release Please falls back to the repository's `GITHUB_TOKEN` when a
|
|
43
|
+
`RELEASE_PLEASE_TOKEN` secret is unavailable. In that mode, Code Foundry opens
|
|
44
|
+
or updates the version pull request, leaves it for manual merge, and completes
|
|
45
|
+
the release job successfully.
|
|
46
|
+
|
|
47
|
+
Configure a narrowly scoped `RELEASE_PLEASE_TOKEN` repository or organization
|
|
48
|
+
secret to enable guarded automatic merging and downstream workflows triggered
|
|
49
|
+
by the resulting release. The token needs `contents`, `issues`, and
|
|
50
|
+
`pull-requests` write permissions. Code Foundry validates every changed path in
|
|
51
|
+
the generated version pull request before using the token to merge it.
|
|
45
52
|
|
|
46
53
|
## Operational checklist
|
|
47
54
|
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
import { resolve } from 'node:path'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
import { doctor } from './commands/doctor.mjs'
|
|
7
|
+
import { reconcileRelease } from './commands/release.mjs'
|
|
7
8
|
import { syncRepository } from './commands/sync.mjs'
|
|
8
9
|
|
|
9
10
|
const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)))
|
|
10
11
|
|
|
11
|
-
/** @typedef {{ target: string, dryRun: boolean, force: boolean }} Options */
|
|
12
|
+
/** @typedef {{ target: string, dryRun: boolean, force: boolean, github: boolean, base: string, head: string }} Options */
|
|
12
13
|
/** @typedef {{ command: string, options: Options }} ParsedArgs */
|
|
13
14
|
|
|
14
15
|
const usage = `code-foundry — initialize and maintain agent-ready repositories
|
|
@@ -17,6 +18,7 @@ Usage:
|
|
|
17
18
|
npx code-foundry init [--target PATH]
|
|
18
19
|
npx code-foundry sync [--target PATH]
|
|
19
20
|
npx code-foundry doctor [--target PATH]
|
|
21
|
+
npx code-foundry release reconcile [--github] [--base BRANCH] [--head BRANCH]
|
|
20
22
|
|
|
21
23
|
The repository configuration lives in .github/code-foundry.yml.
|
|
22
24
|
init detects the repository, creates that file, and renders the baseline.
|
|
@@ -26,6 +28,9 @@ Options:
|
|
|
26
28
|
--target PATH Repository directory (default: current directory)
|
|
27
29
|
--dry-run Preview changes without writing files
|
|
28
30
|
--force Replace protected standard documents
|
|
31
|
+
--github Apply a verified fast-forward through GitHub
|
|
32
|
+
--base BRANCH Release source branch (default: main)
|
|
33
|
+
--head BRANCH Branch to reconcile (default: staging)
|
|
29
34
|
-h, --help Show this help
|
|
30
35
|
`
|
|
31
36
|
|
|
@@ -40,7 +45,12 @@ function parseArgs(argv) {
|
|
|
40
45
|
const first = argv[0]
|
|
41
46
|
const hasCommand = Boolean(first && !first.startsWith('-'))
|
|
42
47
|
const command = hasCommand ? /** @type {string} */ (argv.shift()) : 'init'
|
|
43
|
-
const options = { target: process.cwd(), dryRun: false, force: false }
|
|
48
|
+
const options = { target: process.cwd(), dryRun: false, force: false, github: false, base: 'main', head: 'staging' }
|
|
49
|
+
|
|
50
|
+
if (command === 'release') {
|
|
51
|
+
const subcommand = argv.shift()
|
|
52
|
+
if (subcommand !== 'reconcile') fail(`unknown release command: ${subcommand ?? '(missing)'}; use release reconcile`)
|
|
53
|
+
}
|
|
44
54
|
|
|
45
55
|
while (argv.length) {
|
|
46
56
|
const arg = argv.shift()
|
|
@@ -54,6 +64,9 @@ function parseArgs(argv) {
|
|
|
54
64
|
options.target = value
|
|
55
65
|
} else if (arg === '--dry-run') options.dryRun = true
|
|
56
66
|
else if (arg === '--force') options.force = true
|
|
67
|
+
else if (arg === '--github') options.github = true
|
|
68
|
+
else if (arg === '--base') options.base = argv.shift() ?? fail('--base requires a branch')
|
|
69
|
+
else if (arg === '--head') options.head = argv.shift() ?? fail('--head requires a branch')
|
|
57
70
|
else fail(`unknown option: ${arg}; run --help for the supported options`)
|
|
58
71
|
}
|
|
59
72
|
|
|
@@ -83,6 +96,12 @@ function main() {
|
|
|
83
96
|
console.error(`code-foundry: ${error instanceof Error ? error.message : String(error)}`)
|
|
84
97
|
process.exitCode = 1
|
|
85
98
|
}
|
|
99
|
+
} else if (command === 'release') {
|
|
100
|
+
try {
|
|
101
|
+
reconcileRelease(target, options)
|
|
102
|
+
} catch (error) {
|
|
103
|
+
fail(error instanceof Error ? error.message : String(error))
|
|
104
|
+
}
|
|
86
105
|
}
|
|
87
106
|
else fail(`unknown command: ${command}`)
|
|
88
107
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { join, resolve } from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { approvedReleaseFiles, classifyReconciliation, readReleaseConfig } from '../lib/release-policy.mjs'
|
|
7
|
+
|
|
8
|
+
/** @typedef {{ target: string, dryRun: boolean, github: boolean, base: string, head: string }} ReleaseOptions */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Reconcile staging after Release Please updates main. The local mode is
|
|
12
|
+
* deterministic and suitable for CI; --github applies only a verified
|
|
13
|
+
* fast-forward through the GitHub API and otherwise fails closed.
|
|
14
|
+
* @param {string} root
|
|
15
|
+
* @param {ReleaseOptions} options
|
|
16
|
+
*/
|
|
17
|
+
export function reconcileRelease(root, options) {
|
|
18
|
+
const target = resolve(root)
|
|
19
|
+
const base = options.base || 'main'
|
|
20
|
+
const head = options.head || 'staging'
|
|
21
|
+
const mainSha = git(target, ['rev-parse', `origin/${base}`]) || git(target, ['rev-parse', base])
|
|
22
|
+
const stagingSha = git(target, ['rev-parse', `origin/${head}`]) || git(target, ['rev-parse', head])
|
|
23
|
+
const mergeBaseSha = git(target, ['merge-base', mainSha, stagingSha])
|
|
24
|
+
const mainChangedPaths = diffNames(target, mergeBaseSha, mainSha)
|
|
25
|
+
const stagingChangedPaths = diffNames(target, mergeBaseSha, stagingSha)
|
|
26
|
+
const plan = classifyReconciliation({
|
|
27
|
+
mainSha,
|
|
28
|
+
stagingSha,
|
|
29
|
+
mergeBaseSha,
|
|
30
|
+
mainChangedPaths,
|
|
31
|
+
stagingChangedPaths,
|
|
32
|
+
allowed: approvedReleaseFiles(readReleaseConfig(target)),
|
|
33
|
+
})
|
|
34
|
+
console.log(JSON.stringify({ base, head, ...plan }, null, 2))
|
|
35
|
+
if (plan.action === 'fail') throw new Error(plan.reason + (plan.unexpected?.length ? ` Unexpected paths: ${plan.unexpected.join(', ')}` : ''))
|
|
36
|
+
if (plan.action !== 'fast-forward' || !options.github || options.dryRun) return plan
|
|
37
|
+
if (!process.env.GITHUB_REPOSITORY) throw new Error('GITHUB_REPOSITORY is required for --github reconciliation.')
|
|
38
|
+
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN
|
|
39
|
+
if (!token) throw new Error('GH_TOKEN or GITHUB_TOKEN is required for --github reconciliation.')
|
|
40
|
+
const result = spawnSync('gh', [
|
|
41
|
+
'api', '--method', 'PATCH', `repos/${process.env.GITHUB_REPOSITORY}/git/refs/heads/${head}`,
|
|
42
|
+
'-f', `sha=${plan.targetSha}`, '-F', 'force=false',
|
|
43
|
+
], { cwd: target, stdio: 'inherit', env: { ...process.env, GH_TOKEN: token } })
|
|
44
|
+
if (result.status !== 0) throw new Error(`GitHub refused the protected fast-forward of ${head}.`)
|
|
45
|
+
return plan
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {string} root @param {string[]} args @returns {string} */
|
|
49
|
+
function git(root, args) {
|
|
50
|
+
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' })
|
|
51
|
+
return result.status === 0 ? result.stdout.trim() : ''
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @param {string} root @param {string} from @param {string} to @returns {string[]} */
|
|
55
|
+
function diffNames(root, from, to) {
|
|
56
|
+
if (!from || !to || from === to) return []
|
|
57
|
+
const result = spawnSync('git', ['diff', '--name-only', from, to], { cwd: root, encoding: 'utf8' })
|
|
58
|
+
return result.status === 0 ? result.stdout.split(/\r?\n/).filter(Boolean) : []
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @param {string} root */
|
|
62
|
+
export function releaseConfigExists(root) {
|
|
63
|
+
return existsSync(join(resolve(root), 'release-please-config.json'))
|
|
64
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_RELEASE_FILES = new Set([
|
|
7
|
+
'.release-please-manifest.json',
|
|
8
|
+
'CHANGELOG.md',
|
|
9
|
+
'Cargo.lock',
|
|
10
|
+
'Cargo.toml',
|
|
11
|
+
'bun.lock',
|
|
12
|
+
'bun.lockb',
|
|
13
|
+
'package-lock.json',
|
|
14
|
+
'package.json',
|
|
15
|
+
'pnpm-lock.yaml',
|
|
16
|
+
'pyproject.toml',
|
|
17
|
+
'uv.lock',
|
|
18
|
+
'version.txt',
|
|
19
|
+
'yarn.lock',
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
/** @param {string} root @returns {Record<string, unknown>} */
|
|
23
|
+
export function readReleaseConfig(root) {
|
|
24
|
+
const file = join(root, 'release-please-config.json')
|
|
25
|
+
if (!existsSync(file)) return {}
|
|
26
|
+
try { return JSON.parse(readFileSync(file, 'utf8')) }
|
|
27
|
+
catch { return {} }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Return the release files allowed to change during an automated reconciliation.
|
|
32
|
+
* Package manifests are scoped beneath their Release Please package directory.
|
|
33
|
+
* @param {Record<string, unknown>} config
|
|
34
|
+
* @returns {Set<string>}
|
|
35
|
+
*/
|
|
36
|
+
export function approvedReleaseFiles(config = {}) {
|
|
37
|
+
const allowed = new Set(DEFAULT_RELEASE_FILES)
|
|
38
|
+
addExtraFiles(allowed, config['extra-files'])
|
|
39
|
+
const packages = config.packages
|
|
40
|
+
if (packages && typeof packages === 'object' && !Array.isArray(packages)) {
|
|
41
|
+
for (const [directory, value] of Object.entries(packages)) {
|
|
42
|
+
const prefix = directory === '.' ? '' : directory.replace(/\/$/, '')
|
|
43
|
+
for (const file of DEFAULT_RELEASE_FILES) addPath(allowed, prefix, file)
|
|
44
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
45
|
+
addExtraFiles(allowed, value['extra-files'], prefix)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return allowed
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @param {Set<string>} allowed @param {unknown} entries @param {string} [prefix] */
|
|
53
|
+
function addExtraFiles(allowed, entries, prefix = '') {
|
|
54
|
+
if (!Array.isArray(entries)) return
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const path = typeof entry === 'string' ? entry : entry && typeof entry === 'object' && 'path' in entry ? entry.path : ''
|
|
57
|
+
if (typeof path === 'string' && path) addPath(allowed, prefix, path)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @param {Set<string>} allowed @param {string} prefix @param {string} path */
|
|
62
|
+
function addPath(allowed, prefix, path) {
|
|
63
|
+
const clean = path.replace(/^\.\//, '').replace(/\\/g, '/')
|
|
64
|
+
allowed.add(prefix ? `${prefix}/${clean}` : clean)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @param {string[]} paths @param {Set<string>} allowed @returns {string[]} */
|
|
68
|
+
export function unexpectedReleasePaths(paths, allowed) {
|
|
69
|
+
return [...new Set(paths.map((path) => path.trim()).filter(Boolean))]
|
|
70
|
+
.filter((path) => !allowed.has(path))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Classify the relationship between main and staging. A fast-forward is safe
|
|
75
|
+
* only when main is the ancestor of staging or staging is the ancestor of main
|
|
76
|
+
* and the intervening main files are release metadata.
|
|
77
|
+
* @param {{ mainSha: string, stagingSha: string, mergeBaseSha?: string, mainChangedPaths?: string[], stagingChangedPaths?: string[], allowed?: Set<string> }} input
|
|
78
|
+
*/
|
|
79
|
+
export function classifyReconciliation(input) {
|
|
80
|
+
const {
|
|
81
|
+
mainSha,
|
|
82
|
+
stagingSha,
|
|
83
|
+
mergeBaseSha = '',
|
|
84
|
+
mainChangedPaths = [],
|
|
85
|
+
stagingChangedPaths = [],
|
|
86
|
+
allowed = approvedReleaseFiles(),
|
|
87
|
+
} = input
|
|
88
|
+
if (!mainSha || !stagingSha) return { action: 'fail', reason: 'Missing branch SHA.' }
|
|
89
|
+
if (mainSha === stagingSha) return { action: 'aligned', reason: 'Branches already point at the same commit.' }
|
|
90
|
+
if (mainSha === mergeBaseSha) {
|
|
91
|
+
return { action: 'none', reason: 'staging contains commits that are not on main; no release-only reconciliation is needed.' }
|
|
92
|
+
}
|
|
93
|
+
const unexpectedMain = unexpectedReleasePaths(mainChangedPaths, allowed)
|
|
94
|
+
const unexpectedStaging = unexpectedReleasePaths(stagingChangedPaths, allowed)
|
|
95
|
+
if (unexpectedMain.length || unexpectedStaging.length) {
|
|
96
|
+
return {
|
|
97
|
+
action: 'fail',
|
|
98
|
+
reason: 'Branch divergence includes repository code or configuration.',
|
|
99
|
+
unexpected: [...new Set([...unexpectedMain, ...unexpectedStaging])],
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (stagingSha === mergeBaseSha) {
|
|
103
|
+
return { action: 'fast-forward', targetSha: mainSha, reason: 'main only added approved release metadata.' }
|
|
104
|
+
}
|
|
105
|
+
return { action: 'pull-request', targetSha: mainSha, reason: 'Branches diverged, but only approved release metadata changed.' }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { DEFAULT_RELEASE_FILES }
|