Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
125 changes: 125 additions & 0 deletions .claude/skills/authoring-clint-updates/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
name: authoring-clint-updates
description: Use this skill when a frontend change in this Craft base repo (statikbe/craft) needs to ship as a Clint update so existing projects receive it. Invoke when the user mentions creating/filling a Clint update, an update folder, update.json, CHANGELOG.md, the update manifest/index.json, or "ship this frontend fix to projects". It analyzes the git diff and produces update.json (modify + findAndReplace) and CHANGELOG.md, then regenerates the manifest.
---

# Authoring Clint Updates

Clint distributes frontend changes to existing projects through **update folders**. Editing `frontend/` only updates the template for *new* clones — existing projects receive a change **only** when it also ships as an update that the manifest lists. So whenever a frontend change should propagate, author an update alongside it.

Full reference: `docs/src/frontend/clint/updates.md` and `docs/src/frontend/clint/configuration.md`.

## When an update is needed

Author an update when the change touches shared frontend code/assets, root config (`vite.config*.js`, root `package.json`), or shared `templates/` that downstream projects use. Skip it for purely project-specific files or for clint-internal changes.

## Procedure

### 1. Determine the change set

Look at what changed (against the base branch, usually `develop`):

```bash
git diff --name-status develop...HEAD -- frontend/ templates/ vite.config*.js package.json composer.json
# or, for uncommitted work:
git status --short
git diff --stat
```

Read the actual diffs of the changed files so you can write an accurate changelog and spot template/class renames that need a `findAndReplace`.

### 2. Classify each changed path

Read `clint/cli.config.json` → `frontend.frontendExcludeFromSync` and classify every changed file. Exclude entries are: directory names (`css`, `icons`, `img`), path prefixes (`js/components-site`), and regexes wrapped in slashes (`/js/.*.ts$/`).

| Where the file lives | How it propagates |
| --- | --- |
| Under `frontend/`, **not** matching an exclude pattern | Synced automatically by the full frontend sync — just ensure the update has a `frontend` block. Do **not** list it in `modify`. |
| Under `frontend/`, **matching** an exclude pattern (e.g. a `js/components-site/*` file, or a `.ts` under `js`) | Excluded from the normal sync — **must** be listed in `frontend.modify` to force-sync it. Paths are relative to `frontend/`. |
| Root-level (e.g. `vite.config.js`, root `package.json`) | List in `root.modify` (path relative to repo root). |
| A class/string rename across `templates/` or code | Use a `findAndReplace` rule instead of (or in addition to) a file sync. |
| Documentation / advisory only | **Record-only** update — omit both `frontend` and `root` blocks. |

> **Note:** the frontend sync copies the whole `frontend/` tree (minus excludes) from the pinned channel, so non-excluded files don't need enumerating — the `modify` list exists specifically to *also* pull excluded files.

### 3. Scaffold the folder

From `clint/` (build first if `dist/` is stale: `yarn build-cli`):

```bash
node dist/cli.js update:new "<concise title>"
```

Creates `clint/updates/<YYYYMMDD>-<kebab-title>/` with a prefilled `update.json` and a `CHANGELOG.md` template. The folder name is the update `id`.

### 4. Fill `update.json`

```json
{
"id": "20260612-fix-card-spacing",
"title": "Fix card spacing",
"description": "Corrects the gap between cards in the grid component.",
"date": "2026-06-12",
"issues": [612],
"pr": null,
"requires": [],
"frontend": {
"modify": ["js/components-site/card.component.ts"],
"findAndReplace": [
{ "files": ["../templates/**/*.twig"], "from": "/gap-3/g", "to": "gap-4" }
]
},
"root": {
"modify": ["vite.config.js"]
}
}
```

- `id` **must** equal the folder name; `issues`/`pr` are for linking only (never ordering).
- `requires`: list another update's id only if this one genuinely depends on it landing first.
- `frontend.modify` / `root.modify`: only the paths identified as excluded/root in step 2.
- Omit a block entirely when unused; omit **both** for a record-only update.

**findAndReplace rules — must be idempotent.** A rule's `from` must match only the *pre-change* state so a re-run (after a failed/partial apply) is a no-op. Prefer renames over appends.
- `"from": "/pattern/g"` → global regex (all occurrences).
- `"from": "plain text"` → literal string, **first occurrence only**.
- Targets that don't exist now fail loudly — don't point at files a project may not have unless that's intended.

### 5. Write `CHANGELOG.md`

Summarize from the actual diff. Sections (include those that apply): `## Summary`, `## Highlights`, `## Added`, `## Changed`, `## Fixed`, and a top-level `# Manual intervention` for anything that can't be automated (custom components, project-specific CSS/JS/Twig, required `yarn install`/`yarn build`).

```markdown
# Fix card spacing

**Release date:** 2026-06-12

## Summary

Corrects the gap between cards in the grid component.

## Changed

- `gap-3` → `gap-4` on the card grid (automated)

# Manual intervention

> ⚠️ **ATTENTION**:
> Re-check any custom card layouts that override the grid gap.
```

### 6. Regenerate the manifest and commit

```bash
node dist/cli.js update:index # assigns seq, validates, writes clint/updates/index.json
```

The generator fails if a folder lacks `update.json`/`CHANGELOG.md`, `id` ≠ folder name, or JSON is invalid. **Never hand-edit `index.json`.** Commit the regenerated `index.json` together with the update folder and the real frontend change in the same PR (target `develop`). CI (`clint-update-index`) re-validates; merge to `develop` publishes to the `clint-updates` channel.

## Checklist before finishing

- [ ] The real frontend change and the update folder are in the same change set.
- [ ] Excluded/root files are in `modify`; non-excluded files are not.
- [ ] Every `findAndReplace.from` matches only the pre-change state (idempotent).
- [ ] `CHANGELOG.md` reflects the diff and flags manual steps.
- [ ] `update:index` ran clean and `index.json` is committed.
5 changes: 4 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@
/docs/ export-ignore

# Auto detect text files and perform LF normalization
* text=auto
* text=auto

# Clint applied-update log: union-merge so two developers' independent appends never conflict
frontend/.clint-applied merge=union
42 changes: 42 additions & 0 deletions .github/workflows/clint-publish-updates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: clint-publish-updates

# Publishes the Clint update channel. `develop` is the source of truth; this advances the
# `clint-updates` branch (the ref consumers fetch the manifest, update folders, and frontend
# content from) to develop's commit whenever the fetched content changes.
#
# clint-updates is a derived publish branch, not a development branch, so a force-update to
# develop's SHA is the intended behaviour — it keeps the channel an exact, consistent snapshot of
# develop. Do not protect clint-updates or this job cannot publish. To gate releases behind a human
# step later, switch the trigger to `master` or to tags instead of `develop`.

on:
workflow_dispatch:
push:
branches: [develop]
paths:
- 'clint/updates/**'
- 'frontend/**'

permissions:
contents: write

concurrency:
group: clint-publish-updates
cancel-in-progress: false

jobs:
publish:
name: Publish develop to clint-updates
runs-on: ubuntu-latest
if: github.repository == 'statikbe/craft'
steps:
- name: Checkout develop
uses: actions/checkout@v4
with:
ref: develop
fetch-depth: 0

- name: Force-update clint-updates to this commit
run: |
echo "Publishing $(git rev-parse --short HEAD) to clint-updates"
git push origin HEAD:refs/heads/clint-updates --force
91 changes: 91 additions & 0 deletions .github/workflows/clint-update-index.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: clint-update-index

# Validates the Clint frontend-update folders and enforces that clint/updates/index.json
# (the published manifest consumers fetch) is regenerated and committed.
#
# index.json is NEVER hand-edited: authors run `node clint/dist/cli.js update:index` and commit
# the result. This job (a) fails on any structural error in an update folder — missing update.json
# or CHANGELOG.md, id != folder name, invalid JSON — via the generator's own exit code, and
# (b) fails when the committed manifest is stale relative to the folders on the branch.
#
# This model needs no bot write access and no merge queue: two concurrent PRs that both touch
# updates produce an ordinary merge conflict on index.json, which the author resolves by re-running
# the generator. If you later adopt the council's "CI assigns seq on merge" model instead, replace
# the validate step with a regenerate-and-commit step gated behind a serialized merge queue.

on:
workflow_dispatch:
pull_request:
paths:
- 'clint/updates/**'
- 'clint/src/**'
- 'clint/package.json'
- '.github/workflows/clint-update-index.yml'
push:
branches: [develop, master]
paths:
- 'clint/updates/**'
- 'clint/src/**'
- 'clint/package.json'
- '.github/workflows/clint-update-index.yml'

permissions:
contents: read

concurrency:
group: clint-update-index-${{ github.ref }}
cancel-in-progress: true

jobs:
validate:
name: Validate update manifest
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: yarn
cache-dependency-path: clint/yarn.lock

- name: Install dependencies
working-directory: clint
run: yarn install --frozen-lockfile

- name: Build CLI
working-directory: clint
run: yarn build-cli

- name: Validate folders and regenerate manifest
working-directory: clint
run: |
# Snapshot the committed manifest (if any) before the generator overwrites it.
if [ -f updates/index.json ]; then
cp updates/index.json /tmp/index.committed.json
else
echo '{"updates":[]}' > /tmp/index.committed.json
fi

# The generator exits non-zero on any structural error (missing update.json / CHANGELOG.md,
# id != folder name, invalid JSON), which fails this step.
node dist/cli.js update:index

- name: Ensure committed manifest is current
working-directory: clint
run: |
# Compare the regenerated manifest against the committed one, ignoring the volatile
# generatedAt timestamp — only the ordered `updates` data is contractual.
if ! diff \
<(jq -S 'del(.generatedAt)' /tmp/index.committed.json) \
<(jq -S 'del(.generatedAt)' updates/index.json) > /dev/null; then
echo "::error file=clint/updates/index.json::clint/updates/index.json is out of date. Run 'node clint/dist/cli.js update:index' and commit the result."
echo "--- committed (ignoring generatedAt)"
jq -S 'del(.generatedAt)' /tmp/index.committed.json
echo "--- expected from current folders"
jq -S 'del(.generatedAt)' updates/index.json
exit 1
fi
echo "✅ clint/updates/index.json is current."
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ clint/public/tmp/*
clint/public/excel/*
clint/dist/*
public/favicon/site/dummy-*.js

# Clint applied-update state: the line list is committed; the audit sidecar + lock are local-only
frontend/.clint-applied.meta.json
frontend/.clint.lock
5 changes: 4 additions & 1 deletion clint/cli.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
"updatePath": "clint/updates"
},
"frontend": {
"packagePath": "../package.json",
"packagePath": "../frontend/package.json",
"packageGitUrl": "https://raw.githubusercontent.com/statikbe/craft/refs/heads/master/frontend/package.json",
"indexGitUrl": "https://raw.githubusercontent.com/statikbe/craft/refs/heads/clint-updates/clint/updates/index.json",
"updateRepo": "git@github.com:statikbe/craft.git",
"updateRef": "clint-updates",
"updatePath": "clint/updates",
"frontendPath": "frontend",
"appliedStatePath": "../frontend/.clint-applied",
"frontendExcludeFromSync": ["css", "icons", "img", "js/components-site", "/js/.*.ts$/"]
}
}
10 changes: 8 additions & 2 deletions clint/example_update.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"id": "20260101-example-update",
"title": "Example update",
"description": "This is a test update to verify the update mechanism.",
"date": "2026-01-01",
"issues": [],
"pr": null,
"requires": [],
"frontend": {
"modify": ["path_to_file_relative_to_frontend"]
},
Expand All @@ -8,8 +14,8 @@
"findAndReplace": [
{
"files": ["../templates/**/*.twig"],
"from": "/text/g",
"to": "changed-text"
"from": "/old-class-name/g",
"to": "new-class-name"
}
]
}
Expand Down
Loading
Loading