Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .claude/settings-hugo.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"plansDirectory": ".claude/plans",
"permissions": {
"allow": [
"Bash(git status:*)",
Expand Down Expand Up @@ -30,7 +31,9 @@
"Bash(touch:*)",
"Bash(python3 .agents/skills/update-copyright/scripts/update_copyright.py:*)",
"Bash(./config/pull)",
"Bash(./config/migrate)"
"Bash(./config/migrate)",
"Skill(pre-pr)",
"Skill(pre-pr:*)"
],
"deny": [
"Bash(git push:*)",
Expand Down
6 changes: 5 additions & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"plansDirectory": ".claude/plans",
Comment thread
alexander-yevsyukov marked this conversation as resolved.
"permissions": {
"allow": [
"Edit(version.gradle.kts)",
Expand Down Expand Up @@ -32,8 +33,11 @@
"Bash(mkdir:*)",
"Bash(touch:*)",
"Bash(python3 .agents/skills/update-copyright/scripts/update_copyright.py:*)",
"Bash(.agents/skills/version-bumped/scripts/version-bumped.sh)",
"Bash(./config/pull)",
"Bash(./config/migrate)"
"Bash(./config/migrate)",
"Skill(pre-pr)",
"Skill(pre-pr:*)"
],
"deny": [
"Bash(git reset --hard:*)",
Expand Down
10 changes: 0 additions & 10 deletions .claude/settings.local.json

This file was deleted.

6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ __pycache__/

# Claude working files
/.claude/worktrees/
# Ephemeral plan-mode scratch (durable task docs live in `.agents/tasks/`).
/.claude/plans/

# Personal, per-developer Claude Code settings overrides (never committed;
# the distributed `.claude/settings.json` is the shared, committed layer).
/.claude/settings.local.json

# Auto-downloaded Lychee binary used by the `check-links` skill.
/.agents/skills/check-links/.cache/
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ The `pull` script also wires up AI-agent configuration:
the submodule on the first run and floats it to the latest `agents@master` (checked out
*on* `master`, not a detached `HEAD`) on every subsequent run — so shared skills update
everywhere with **no file churn** in consumer pull requests.
* `.claude/settings.json` — the permission allowlist distributed by `config` (Hugo-only
repos receive a Hugo-tuned variant). JVM and mixed repos also get `settings.local.json`;
Hugo-only repos do not.
* `.claude/settings.json` — the shared, committed permission allowlist distributed by
`config` (Hugo-only repos receive a Hugo-tuned variant). `.claude/settings.local.json`
is **not** distributed: it is Claude Code's gitignored, per-developer personal-override
layer, so `pull` never creates, overwrites, or deletes it.
* `.junie/guidelines.md` — JetBrains Junie guidelines.

Per-repo content is never overwritten: `docs/project.md` (linked from `.agents/project.md`),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ package io.spine.dependency.local
@Suppress("ConstPropertyName", "unused")
object ToolBase {
const val group = Spine.toolsGroup
const val version = "2.0.0-SNAPSHOT.402"
const val dogfoodingVersion = "2.0.0-SNAPSHOT.402"
const val version = "2.0.0-SNAPSHOT.403"
const val dogfoodingVersion = "2.0.0-SNAPSHOT.403"

const val lib = "$group:tool-base:$version"
const val classicCodegen = "$group:classic-codegen:$version"
Expand Down
10 changes: 8 additions & 2 deletions migrate
Original file line number Diff line number Diff line change
Expand Up @@ -207,17 +207,23 @@ 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
rm -f ../.claude/settings.local.json
else
cp .claude/settings.json ../.claude/settings.json
cp .claude/settings.local.json ../.claude/settings.local.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.
Expand Down
53 changes: 53 additions & 0 deletions scripts/test-update-gitignore.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ git -C "$consumer" config user.name test
printf '%s\n' '!gradle-wrapper.jar'
printf '%s\n' '!debug.log'
printf '%s\n' '*.log'
# A retired baseline negation an earlier first-migration baked into repo-local;
# the steady-state merge must strip it so `.idea/misc.xml` stays ignored.
printf '%s\n' '!.idea/misc.xml'
# A consumer comment that merely BEGINS like the legacy secret trailer, plus a
# real entry after it. The legacy match is exact, so this comment is preserved
# and the entry survives (regression guard for the start-anchored to-EOF drop).
Expand All @@ -103,6 +106,7 @@ mkdir -p generated && : > generated/secret.gpg
: > info.log
: > creds.secret.properties.gpg
mkdir -p keep-me-ignored && : > keep-me-ignored/x
mkdir -p .idea && : > .idea/misc.xml

# (1) Every decrypted credential must be ignored despite the repo-local negations.
for f in "${secrets[@]}"; do
Expand Down Expand Up @@ -196,6 +200,21 @@ else
f-ail "look-alike legacy comment triggered to-EOF drop; 'keep-me-ignored/' lost"
fi

# (3h) A retired baseline negation baked into repo-local (`!.idea/misc.xml`) must be
# stripped so the baseline's `.idea/*.xml` ignore wins. The secret trailer does NOT
# cover it (misc.xml is not a secret glob), so without stripping it would re-expose the
# per-developer IDEA project file.
if git check-ignore -q .idea/misc.xml; then
pass "retired '!.idea/misc.xml' stripped from repo-local (.idea/misc.xml stays ignored)"
else
f-ail "retired '!.idea/misc.xml' survived in repo-local (.idea/misc.xml re-exposed)"
fi
if grep -qxF '!.idea/misc.xml' "$consumer/.gitignore"; then
f-ail "retired '!.idea/misc.xml' still present in the merged .gitignore"
else
pass "retired '!.idea/misc.xml' removed from the merged .gitignore"
fi

# --- (4) Idempotency: re-running yields byte-identical output. -----------------
cp "$consumer/.gitignore" "$work/first.gitignore"
( cd "$consumer/config" && bash "$script" )
Expand Down Expand Up @@ -312,6 +331,40 @@ else
diff "$work/c3-first.gitignore" "$consumer3/.gitignore" >&2 || true
fi

# --- (8) First-migration retired negation: a legacy raw copy carrying the OLD ------
# baseline's `!.idea/misc.xml`. The bootstrap keeps consumer negations, so without the
# retired-negation filter this stale `!` lands in repo-local (after the managed block)
# and re-includes `.idea/misc.xml`. It must be stripped; a genuine consumer line beside
# it must still survive.
consumer4="$work/consumer4"
mkdir -p "$consumer4/config"
cp "$baseline" "$consumer4/config/.gitignore"
git -C "$consumer4" init -q
git -C "$consumer4" config user.email test@example.com
git -C "$consumer4" config user.name test
# Legacy raw copy of an OLDER baseline: current baseline + the retired negation it used
# to carry + a genuine consumer line. No managed markers.
{ cat "$baseline"; printf '%s\n' '!.idea/misc.xml' 'my-own-cache/'; } > "$consumer4/.gitignore"

( cd "$consumer4/config" && bash "$script" )

mkdir -p "$consumer4/.idea" && : > "$consumer4/.idea/misc.xml"
if git -C "$consumer4" check-ignore -q .idea/misc.xml; then
pass "first-migration strips retired '!.idea/misc.xml' (.idea/misc.xml stays ignored)"
else
f-ail "first-migration kept retired '!.idea/misc.xml' (.idea/misc.xml re-exposed)"
fi
if git -C "$consumer4" check-ignore -q my-own-cache/x; then
pass "first-migration preserved the genuine consumer line beside the filtered negation"
else
f-ail "first-migration dropped the genuine consumer line"
fi
if grep -qxF '!.idea/misc.xml' "$consumer4/.gitignore"; then
f-ail "first-migration left the retired '!.idea/misc.xml' in the merged file"
else
pass "first-migration removed the retired '!.idea/misc.xml' from the merged file"
fi

echo
if [ "$fail" -eq 0 ]; then
echo "OK: all update_gitignore regression checks passed."
Expand Down
51 changes: 36 additions & 15 deletions scripts/update-gitignore.sh
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ secret_end='# <<< secret ignores <<<'
legacy_local_label='# --- repo-local entries (preserved across ./config/pull) ---'
legacy_secret_label='# --- secret ignores re-asserted last so a repo-local negation cannot un-ignore a credential ---'

# Negations config once shipped as baseline `!`-re-inclusions but has since retired.
# A legacy raw-copy consumer (first-migration) — or an already-migrated consumer whose
# repo-local was written by an earlier first-migration — still carries them. Left in the
# repo-local region they sit AFTER the managed baseline and, gitignore being
# last-match-wins, re-include a path the current baseline now ignores. Strip them so the
# baseline's ignore wins. First entry: `!.idea/misc.xml` (the current baseline dropped its
# `!.idea/misc.xml` re-inclusion to keep the per-machine IDEA project file untracked).
# Newline-separated; extend as further negations are retired.
retired_negations='!.idea/misc.xml'

# Positive secret patterns from the baseline's Secrets section: the span from the
# `# Secrets` header to the closing `!*.gpg`, excluding comments and negations.
# (The `# Secrets` header and the trailing `!*.gpg` are asserted as invariants by
Expand Down Expand Up @@ -131,16 +141,20 @@ fi
if grep -qxF "$base_begin" "$dest"; then
# STEADY STATE — the file already carries our markers. Repo-local entries are
# everything OUTSIDE the managed blocks, preserved VERBATIM and IN ORDER (no
# de-duplication, no filtering). Stripped here: the baseline block, the secrets
# block, the repo-local markers themselves (their CONTENT is what we keep), and
# any legacy pre-marker scaffolding written by intermediate script versions (the
# de-duplication, no reordering). Stripped here: the baseline block, the secrets
# block, the repo-local markers themselves (their CONTENT is what we keep), any
# legacy pre-marker scaffolding written by intermediate script versions (the
# `legacy_local_label` and a `legacy_secret_label` trailer that always ran to
# end-of-file), matched EXACTLY — so an already-migrated consumer converts to the
# marker format exactly once, and a look-alike consumer comment is never dropped.
# end-of-file), matched EXACTLY, and any retired baseline negation (see
# `retired_negations`) that would otherwise re-include a now-ignored path — so an
# already-migrated consumer converts to the marker format exactly once, and a
# look-alike consumer comment is never dropped.
awk -v bb="$base_begin" -v eb="$base_end" \
-v sb="$secret_begin" -v se="$secret_end" \
-v lb="$local_begin" -v le="$local_end" \
-v lll="$legacy_local_label" -v lsl="$legacy_secret_label" '
-v lll="$legacy_local_label" -v lsl="$legacy_secret_label" \
-v rn="$retired_negations" '
BEGIN { k = split(rn, r, "\n"); for (i = 1; i <= k; i++) if (r[i] != "") retired[r[i]] = 1 }
$0 == bb { inb = 1; next }
inb { if ($0 == eb) inb = 0; next }
$0 == sb { ins = 1; next }
Expand All @@ -149,23 +163,30 @@ if grep -qxF "$base_begin" "$dest"; then
legacy { next }
$0 == lb || $0 == le { next }
$0 == lll { next }
$0 in retired { next }
{ print }
' "$dest" | trim_blank_edges > "$locals"
else
# FIRST MIGRATION — no markers yet (a legacy raw `cp` of the baseline, possibly
# with the consumer's own lines appended). One-time best-effort bootstrap: drop
# lines duplicating a baseline POSITIVE pattern (already in the managed block),
# keep every negation (a consumer may re-state an exception such as
# `!gradle-wrapper.jar` after their own broad ignore) and every custom line.
# Keeping negations means a raw baseline copy carries the baseline's OWN
# negations (e.g. `!*.gpg`) into the repo-local block on this one pull — cosmetic
# and harmless (they re-include already-included paths), and dropping them would
# reintroduce the order-sensitive de-dup this rewrite removed, so we don't.
# After this run writes the markers, the exact steady-state path above takes
# over. `|| true`: a pure raw copy leaves no custom lines, and `grep -v` exits 1
# when it selects nothing — not an error here.
# `!gradle-wrapper.jar` after their own broad ignore) and every custom line —
# EXCEPT a retired baseline negation (see `retired_negations`), which a raw copy
# of an older baseline still carries and which, kept in repo-local, would
# re-include a path the current baseline now ignores (e.g. `!.idea/misc.xml`).
# Keeping the other negations means a raw baseline copy also carries the baseline's
# still-current OWN negations (e.g. `!*.gpg`) into the repo-local block on this one
# pull — cosmetic and harmless (they re-include already-included paths), and dropping
# them wholesale would reintroduce the order-sensitive de-dup this rewrite removed,
# so we drop only the retired set. After this run writes the markers, the exact
# steady-state path above takes over. `|| true`: a pure raw copy leaves no custom
# lines, and `grep -v` exits 1 when it selects nothing — not an error here.
{ grep -vxF -f <(grep -v '^!' "$src") "$dest" || true; } \
| awk '!seen[$0]++' | trim_blank_edges > "$locals"
| awk -v rn="$retired_negations" \
'BEGIN { k = split(rn, r, "\n"); for (i = 1; i <= k; i++) if (r[i] != "") retired[r[i]] = 1 }
$0 in retired { next } !seen[$0]++' \
| trim_blank_edges > "$locals"
fi

# Fail closed: re-asserting secrets only matters when there ARE repo-local lines
Expand Down
Loading