-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathmigrate
More file actions
437 lines (385 loc) · 20.5 KB
/
Copy pathmigrate
File metadata and controls
437 lines (385 loc) · 20.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# Project type detection
# ---------------------------------------------------------------------------
# Detect if the target repo has a Hugo documentation site.
# Hugo config may live directly in the site root or in a `config/` or
# `config/_default/` subdirectory (both layouts are valid Hugo conventions).
IS_HUGO_DOCS=false
for hugo_dir in docs site; do
for hugo_cfg in hugo.toml hugo.yaml \
config/hugo.toml config/hugo.yaml \
config/_default/hugo.toml config/_default/hugo.yaml; do
if [ -f "../$hugo_dir/$hugo_cfg" ]; then
IS_HUGO_DOCS=true
echo "Detected Hugo documentation site."
break 2
fi
done
done
# Detect if the target repo is a JVM (Gradle) repository.
# The Spine dependency declarations directory is the reliable marker —
# convenience Gradle files alone can exist in non-JVM repos.
# Note: on a first-time configure of a brand-new JVM repo the directory will
# not yet exist; running migrate a second time (after the initial buildSrc
# copy) will pick it up.
IS_JVM=false
if [ -d "../buildSrc/src/main/kotlin/io/spine/dependency" ]; then
IS_JVM=true
echo "Detected JVM repository."
fi
# ---------------------------------------------------------------------------
# Shared agent content — skills, scripts, guidelines, Claude commands/agents.
#
# Done FIRST, before any files are copied: `adopt-shared-agents` verifies the
# `SpineEventEngine/agents` remote is reachable before it mutates anything, so an
# offline / no-access failure aborts the whole migrate before the parent repo is
# touched (no partial migration). The content is NOT copied — it is a floating
# submodule at `.agents/shared` exposed via symlinks. The script is idempotent: it
# bootstraps a new consumer, converts one with the old copied files, or floats an
# existing one to `agents@master`. Repo-local `.agents/memory` and `.agents/tasks`
# are left untouched (no `rm -rf .agents`).
# ---------------------------------------------------------------------------
# Pass the canonical remote explicitly. `adopt-shared-agents` honours an
# `AGENTS_URL` environment variable, but `migrate` runs automatically as part of
# `./config/pull`; an ambient `AGENTS_URL` (in a shell or CI env) must not
# silently redirect a consumer to an unintended fork. An explicit argument wins
# over the env var, keeping automatic pulls deterministic.
AGENTS_URL_CANONICAL="https://github.com/SpineEventEngine/agents.git"
echo "Setting up shared agent content (.agents/shared submodule + symlinks)"
if ! ( cd .. && bash ./config/adopt-shared-agents "$AGENTS_URL_CANONICAL" ); then
echo "ERROR: shared-agents setup failed — aborting to avoid a half-migrated repo." >&2
echo " Fix the cause reported above and re-run \`./config/pull\`." >&2
exit 1
fi
# ---------------------------------------------------------------------------
# Common — applies to every repository
# ---------------------------------------------------------------------------
# Copies the file or directory passed as the first parameter to the upper
# directory, only if such a file or directory does not yet exist there.
function initialize() {
if [ ! -e ../"$1" ]; then
echo "Creating $1"
cp -R "$1" ..
fi
}
echo "Updating IDEA configuration"
# Preserve a project's `.idea/misc.xml` (do not overwrite it). It is project-local
# — the per-project JDK name plus IDEA's own churn — so it is ignored and untracked
# further below; letting this shared `.idea` overlay clobber it with config's copy
# would defeat that (the reset is git-silent, but still resets the consumer's JDK
# on every pull). Config's copy still seeds a consumer that has none yet, carrying
# the shared `EntryPointsManager` / nullness defaults; an existing project-local
# copy wins. Same approach as `module.gradle.kts` below.
DEST_MISC="../.idea/misc.xml"
MISC_PRESERVE_TMP=""
if [ -f "$DEST_MISC" ]; then
echo "Preserving existing \`.idea/misc.xml\`"
MISC_PRESERVE_TMP=$(mktemp -t misc.XXXXXX)
cp -a "$DEST_MISC" "$MISC_PRESERVE_TMP"
fi
# Preserve a project's `.idea/copyright/profiles_settings.xml` (do not overwrite it).
# It selects the default copyright profile, which is project-specific: proprietary
# repositories keep `TeamDev Proprietary`, while the open-source SDK repositories keep
# `TeamDev Open-Source`. Letting config's copy clobber it silently re-licenses the
# consumer's headers (the `update-copyright` hook then applies the wrong notice on the
# next edit). Config's copy still seeds a consumer that has none yet; an existing
# project-local copy wins. Unlike `.idea/misc.xml`, this preservation fails closed
# (aborts on a copy error): a silently switched default copyright profile re-licenses
# the consumer's source headers, so we must never continue past a failure here.
DEST_COPYRIGHT="../.idea/copyright/profiles_settings.xml"
COPYRIGHT_PRESERVE_TMP=""
if [ -f "$DEST_COPYRIGHT" ]; then
echo "Preserving existing \`.idea/copyright/profiles_settings.xml\`"
COPYRIGHT_PRESERVE_TMP=$(mktemp -t copyright.XXXXXX)
cp -a "$DEST_COPYRIGHT" "$COPYRIGHT_PRESERVE_TMP" \
|| { echo "ERROR: failed to back up '$DEST_COPYRIGHT' — aborting so the consumer's default copyright profile is not silently switched." >&2; exit 1; }
fi
cp -R .idea ..
# Restore preserved `.idea/misc.xml`, if any.
if [ -n "$MISC_PRESERVE_TMP" ] && [ -f "$MISC_PRESERVE_TMP" ]; then
echo "Restoring existing \`.idea/misc.xml\`"
cp -a "$MISC_PRESERVE_TMP" "$DEST_MISC"
rm -f "$MISC_PRESERVE_TMP"
fi
# Restore preserved `.idea/copyright/profiles_settings.xml`, if any. Fails closed for
# the same reason as the preservation above — ensure the parent dir exists and abort
# if the restore copy fails, so the pull never silently changes the default profile.
if [ -n "$COPYRIGHT_PRESERVE_TMP" ] && [ -f "$COPYRIGHT_PRESERVE_TMP" ]; then
echo "Restoring existing \`.idea/copyright/profiles_settings.xml\`"
mkdir -p "$(dirname "$DEST_COPYRIGHT")"
cp -a "$COPYRIGHT_PRESERVE_TMP" "$DEST_COPYRIGHT" \
|| { echo "ERROR: failed to restore '$DEST_COPYRIGHT' — aborting so the pull does not silently change the default copyright profile." >&2; exit 1; }
rm -f "$COPYRIGHT_PRESERVE_TMP"
fi
echo "Updating Contributor's Guide"
# CONTRIBUTING.md is identical org-wide and static; copy it only if the repo
# does not already have one (don't clobber a deliberately customized copy).
initialize CONTRIBUTING.md
echo "Updating Contributor Covenant"
cp CODE_OF_CONDUCT.md ..
echo "Updating \`AGENTS.md\`"
cp AGENTS.md ..
echo "Updating \`CLAUDE.md\`"
cp CLAUDE.md ..
echo "Updating \`init-submodules\`"
# A plain tracked file at the consumer ROOT — deliberately NOT inside a submodule
# — so a fresh `git worktree` always checks it out and can bootstrap the `config`
# and `.agents/shared` submodules that a worktree starts without. A `SessionStart`
# hook in `.claude/settings.json` runs it automatically.
cp init-submodules ..
chmod +x ../init-submodules
echo "Updating Junie guidelines"
# Copy only the tracked guideline file. The `.junie/skills` symlink is created
# by `adopt-shared-agents` (run earlier in this script), and any repo-local
# `.junie/memory` is left untouched rather than wiped.
mkdir -p ../.junie
cp .junie/guidelines.md ../.junie/guidelines.md
initialize .gitattributes
echo "Merging .gitignore (shared baseline + preserved repo-local entries)"
# Fail closed: if the merge cannot write the consumer `.gitignore`, abort the pull
# rather than continue and silently leave the secret-ignore patterns uninstalled.
bash scripts/update-gitignore.sh \
|| { echo "ERROR: '.gitignore' merge failed — aborting so secret ignores are not silently skipped." >&2; exit 1; }
mkdir -p ../.github/workflows
echo "Updating GitHub Copilot instructions"
cp .github/copilot-instructions.md ../.github/copilot-instructions.md
# ---------------------------------------------------------------------------
# Repo-specific workflow overrides
#
# A consumer repository sometimes needs a CI workflow that diverges from the
# uniform one `config` distributes — e.g. `gcloud-java` decrypts a service-account
# key and skips the Datastore-emulator suites on the Windows runner. Such a repo
# keeps its own variant under a distinct name (e.g. `build-on-ubuntu-gcloud.yml`)
# and marks it with a directive comment — ignored by GitHub Actions — naming the
# distributed file it stands in for:
#
# # config:replaces build-on-ubuntu.yml
#
# `migrate` then refrains from copying that generic workflow here, so only the
# repo-specific variant runs. It does NOT delete a generic file the consumer has
# already committed: that one-time cleanup is left to the developer.
# ---------------------------------------------------------------------------
# Names of distributed workflows a repo-specific variant stands in for, gathered
# from `config:replaces` directives in the consumer's own workflows (one per line).
# The directive is a YAML *comment*, so the pattern is anchored to a comment line
# (optional leading whitespace, then `#`) — this avoids an accidental match on the
# literal string appearing inside a workflow's values. The filename is the last
# whitespace-separated field of the match, regardless of spacing after `#`.
OVERRIDDEN=$(grep -rhoE '^[[:space:]]*#[[:space:]]*config:replaces[[:space:]]+[A-Za-z0-9._-]+' \
../.github/workflows 2>/dev/null | awk '{print $NF}' | sort -u)
if [ -n "$OVERRIDDEN" ]; then
echo "Repo-specific workflow overrides detected; not distributing:"
printf '%s\n' "$OVERRIDDEN" | while IFS= read -r wf; do
echo " $wf"
done
fi
# Whether the distributed workflow named by $1 is replaced by a repo-specific
# variant in the consumer (per a `config:replaces` directive).
function is_overridden() {
printf '%s\n' "$OVERRIDDEN" | grep -qxF "$1"
}
# Copies every workflow from the source directory ($1) into the consumer's
# `.github/workflows`, skipping any the consumer has chosen to replace.
function copy_workflows() {
local f name
for f in "$1"/*; do
[ -e "$f" ] || continue
name=$(basename "$f")
if is_overridden "$name"; then
echo "Skipping '$name' (replaced by a repo-specific workflow)"
continue
fi
cp -a "$f" ../.github/workflows/
done
}
# ---------------------------------------------------------------------------
# Secret-scan workflow — distributed to EVERY repository (JVM, Hugo, or plain) so
# a leaked credential fails CI even when the local nets are bypassed. Lives in the
# common section (before the type-specific branches) and honors the same
# `config:replaces` override mechanism as the other workflows.
# ---------------------------------------------------------------------------
echo "Updating secret-scan workflow"
mkdir -p ../.github/workflows
if is_overridden secret-scan.yml; then
echo "Skipping 'secret-scan.yml' (replaced by a repo-specific workflow)"
else
cp .github/workflows/secret-scan.yml ../.github/workflows/
fi
# ---------------------------------------------------------------------------
# Claude settings — the commands/agents/skills come from the submodule above;
# only the permission settings are repo configuration distributed by `config`.
#
# `settings.json` is the SHARED, committed permission layer (org-wide defaults);
# for a Hugo-only repo the Hugo-tuned `settings-hugo.json` is applied under that
# name. `settings.local.json` is deliberately NOT distributed: in Claude Code it
# is the gitignored, per-developer personal-override layer (precedence
# user < project < local), so a `./config/pull` must never create, overwrite, or
# delete it — doing any of these wiped a developer's personal overrides or forced
# the file into Git. Org-wide permissions belong in the two shared templates above.
# ---------------------------------------------------------------------------
echo "Updating Claude settings"
mkdir -p ../.claude
if [ "$IS_HUGO_DOCS" = "true" ] && [ "$IS_JVM" = "false" ]; then
# Hugo-only repos use the Hugo-tuned permission set.
cp .claude/settings-hugo.json ../.claude/settings.json
else
cp .claude/settings.json ../.claude/settings.json
fi
# settings-hugo.json is a config-internal template (applied as settings.json for
# Hugo-only repos), never distributed as-is; drop any stale copy an older migrate left.
rm -f ../.claude/settings-hugo.json
# ---------------------------------------------------------------------------
# Hugo-specific
# ---------------------------------------------------------------------------
if [ "$IS_HUGO_DOCS" = "true" ]; then
echo "Updating lychee.toml"
cp lychee.toml ..
echo "Updating GitHub workflows (Hugo)"
if is_overridden check-links.yml; then
echo "Skipping 'check-links.yml' (replaced by a repo-specific workflow)"
else
cp .github/workflows/check-links.yml ../.github/workflows/
fi
# On a Hugo-only repo, remove any JVM workflows that may be present from
# a previous non-Hugo pull. On a repo that is also JVM, leave them — the
# JVM section below will keep them current.
if [ "$IS_JVM" = "false" ]; then
rm -f ../.github/workflows/build-on-ubuntu.yml
rm -f ../.github/workflows/build-on-windows.yml
rm -f ../.github/workflows/ensure-reports-updated.yml
rm -f ../.github/workflows/increment-guard.yml
rm -f ../.github/workflows/publish.yml
rm -f ../.github/workflows/remove-obsolete-artifacts-from-packages.yaml
rm -f ../.github/workflows/detekt-code-analysis.yml
rm -f ../.github/workflows/gradle-wrapper-validation.yml
fi
fi
# ---------------------------------------------------------------------------
# JVM-specific
# ---------------------------------------------------------------------------
if [ "$IS_JVM" = "true" ]; then
echo "Updating Codecov settings"
cp .codecov.yml ..
cp -a gradle.properties ..
# NOTE: `.gitignore` is intentionally NOT copied here. It is merged once, for
# every repo, by `scripts/update-gitignore.sh` in the common section above —
# preserving repo-local entries instead of clobbering them.
echo "Updating Gradle \`buildSrc\`"
# Preserve an existing project's `module.gradle.kts` (do not overwrite it).
DEST_MODULE="../buildSrc/src/main/kotlin/module.gradle.kts"
PRESERVE_TMP=""
if [ -f "$DEST_MODULE" ]; then
echo "Preserving existing \`module.gradle.kts\`"
PRESERVE_TMP=$(mktemp -t module.XXXXXX)
cp -a "$DEST_MODULE" "$PRESERVE_TMP"
fi
cp -R buildSrc ..
# Restore preserved `module.gradle.kts`, if any.
if [ -n "$PRESERVE_TMP" ] && [ -f "$PRESERVE_TMP" ]; then
echo "Restoring existing \`module.gradle.kts\`"
cp -a "$PRESERVE_TMP" "$DEST_MODULE"
rm -f "$PRESERVE_TMP"
fi
# Remove dependency objects that `config` has retired. `cp -R buildSrc ..`
# overlays the current sources but never deletes files dropped from `config`,
# so a consumer that received these earlier keeps a stale copy that no longer
# compiles against the rest of `buildSrc`. ProtoData was superseded by Spine
# Compiler (`io.spine.compiler`); McJava by CoreJvm Compiler (`io.spine.core-jvm`).
echo "Removing retired dependency objects (ProtoData, McJava)"
rm -f ../buildSrc/src/main/kotlin/io/spine/dependency/local/ProtoData.kt
rm -f ../buildSrc/src/main/kotlin/io/spine/dependency/local/McJava.kt
# Remove the retired vanilla-JaCoCo script plugins for the same reason as the
# dependency objects above: `config` no longer ships them, but `cp -R buildSrc ..`
# cannot delete a consumer's stale copy from an earlier pull. Coverage is now
# configured via Kover by the `jvm-module` / `kmp-module` conventions; the
# `raise-coverage` skill migrates existing consumers. See
# .agents/skills/raise-coverage/references/migrate-to-kover.md.
echo "Removing retired JaCoCo script plugins (jacoco-kotlin-jvm, jacoco-kmm-jvm)"
rm -f ../buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts
rm -f ../buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts
echo "Updating GitHub workflows (JVM)"
copy_workflows .github-workflows
copy_workflows .github/workflows
rm -f ../.github/workflows/detekt-code-analysis.yml # config-only workflow
echo "Updating Gradle Wrapper"
cp -R ./gradle ..
cp gradlew ..
cp gradlew.bat ..
fi
cd ..
# ---------------------------------------------------------------------------
# Route Git hooks to the shared, version-controlled hooks directory so the
# secret-scan `pre-commit` hook guards EVERY commit — any agent, any human. The
# path resolves through `.agents/scripts` -> the shared `agents` submodule, so the
# hook floats with it and needs no per-clone copy. Set only when `core.hooksPath`
# is unset or already ours, so a repo's own hooks configuration is never hijacked.
# ---------------------------------------------------------------------------
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
desired_hooks=".agents/scripts/git-hooks"
current_hooks=$(git config --local --get core.hooksPath 2>/dev/null || true)
if [ -z "$current_hooks" ] || [ "$current_hooks" = "$desired_hooks" ]; then
git config --local core.hooksPath "$desired_hooks"
echo "Git hooks routed to '$desired_hooks' (secret-scan pre-commit active)."
else
echo "Leaving existing core.hooksPath ('$current_hooks') untouched." >&2
fi
fi
# ---------------------------------------------------------------------------
# Make `.idea/misc.xml` project-local. It carries the per-project JDK name and
# IDEA's own churn (entry-point list indices, external-storage toggles), so a
# tracked copy collides with the consumer's local IDE state on every
# `./config/pull`. Two steps:
#
# 1. Drop any surviving `!.idea/misc.xml` negation. The shared baseline no
# longer rescues the file, but `update-gitignore.sh` preserves a legacy
# raw-copied `.gitignore`'s negations into the repo-local block, so the
# retired rescue can outlive the baseline and — `.gitignore` being
# last-match-wins — un-ignore the file, leaving it `?? .idea/misc.xml`
# (which `git add -A` re-adds) instead of ignored. The merge already ran
# above; scrub the negation from its result.
# 2. Untrack any copy an earlier pull committed. `--cached` keeps the working
# file; `--force` overrides `git rm`'s up-to-date check so a consumer that
# staged `misc.xml` IDE churn (its index differing from both HEAD and the
# work tree) is still migrated — otherwise `git rm` fails and, as `migrate`
# runs without `set -e`, silently leaves the file tracked. The `git ls-files`
# guard makes a re-run a quiet no-op.
# ---------------------------------------------------------------------------
if [ -f .gitignore ] && grep -qxF '!.idea/misc.xml' .gitignore; then
echo "Dropping the retired '!.idea/misc.xml' negation from .gitignore"
gi_tmp=$(mktemp ./.gitignore.XXXXXX)
if grep -vxF '!.idea/misc.xml' .gitignore > "$gi_tmp"; then
mv "$gi_tmp" .gitignore
else
rm -f "$gi_tmp"
fi
fi
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
&& git ls-files --error-unmatch .idea/misc.xml >/dev/null 2>&1; then
echo "Untracking '.idea/misc.xml' (now project-local, git-ignored)"
git rm --cached --force --quiet .idea/misc.xml
fi
if [ "$IS_JVM" = "true" ]; then
# Remove stale root-level dependency reports. The `pom.xml` and
# `dependencies.md` dependency reports are now generated under
# `docs/dependencies` (see `PomGenerator` and `LicenseReporter` in
# `buildSrc`); the legacy copies left at the repository root are stale
# duplicates that the Codex and Copilot CI reviewers flag on every pull
# request. In a Gradle repo a root `pom.xml` is only ever this generated
# report (never a real Maven build file), so deleting it is safe. `git rm`
# stages the deletion when the file is tracked — the usual case, since the
# reports were committed — so the pull's own commit carries the removal; an
# untracked working-tree artifact falls back to `rm -f`. Both guards make a
# re-run a quiet no-op once the files are gone.
for stale_report in pom.xml dependencies.md; do
if git ls-files --error-unmatch "$stale_report" >/dev/null 2>&1; then
echo "Removing stale root dependency report '$stale_report' (now under docs/dependencies)"
git rm --force --quiet "$stale_report"
elif [ -f "$stale_report" ]; then
echo "Removing stale root dependency report '$stale_report' (now under docs/dependencies)"
rm -f "$stale_report"
fi
done
echo "Adding \`buildSrc\` sources to Git..."
git add ./buildSrc/src
fi