@sebastienrousseau/dotfiles 0.2.510 → 0.2.511
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/CHANGELOG.md +12 -0
- package/README.md +2 -2
- package/docs/COPYRIGHT +1 -1
- package/docs/articles/.pages +6 -0
- package/docs/articles/2026-07-05-custom-mkdocs-material-dark-theme.md +216 -0
- package/docs/articles/2026-07-05-fish-startup-abbr.md +153 -0
- package/docs/articles/2026-07-05-master-to-main-rename-runbook.md +128 -0
- package/docs/articles/index.md +36 -0
- package/docs/manual/00-introduction.md +1 -1
- package/install.sh +5 -5
- package/package.json +1 -1
- package/scripts/diagnostics/aliases-manifest.sh +19 -1
- package/scripts/diagnostics/verify_state.sh +6 -2
- package/scripts/diagnostics/version-locks.sh +10 -6
- package/scripts/dot/commands/agent.sh +5 -1
- package/scripts/dot/commands/fleet.sh +4 -1
- package/scripts/dot/commands/registry.sh +7 -1
- package/scripts/dot/commands/tools.sh +11 -2
- package/scripts/git-hooks/pre-commit-audit.sh +1 -1
- package/scripts/tools/detect-collisions.py +19 -1
- package/scripts/version-sync.sh +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
This file documents all notable changes to this project.
|
|
4
4
|
|
|
5
|
+
## v0.2.511 — 2026-07-08
|
|
6
|
+
|
|
7
|
+
Bug-fix release: restore shell aliases that were silently dropped after the CD-completion fragment.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`reload`, `r`, `mkcd`, `quit` — and every alias/function defined after the CD-completion block — were undefined** whenever `compdef` was not yet available when `90-ux-aliases.sh` was sourced (profiles that don't run `compinit`, and fast-startup shells). The zsh branch of `defaults/.chezmoitemplates/aliases/cd/cd-completion.aliases.sh` skipped completion registration with a **file-scope `return 0`**; because that fragment is inlined into the single concatenated `90-ux-aliases.sh`, the bare `return` aborted the whole file, dropping `set_default_aliases()` (which defines `reload` et al.) and everything after it. Replaced the early-return with a positive `if command -v compdef …; then … fi` guard, so only the completion block is skipped when `compdef` is not ready.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **`scripts/version-sync.sh`**: excluded the two dated release write-ups (`docs/articles/2026-07-05-*.md`) from version verification. They record the release they shipped in ("shipped in v0.2.510" + release-tag link) as historical fact; the `--verify` pass was false-flagging those refs as inconsistent even though the update pass correctly leaves them alone.
|
|
16
|
+
|
|
5
17
|
## v0.2.510 — 2026-07-05
|
|
6
18
|
|
|
7
19
|
Post-v0.2.509 quality-of-life release: default branch renamed `master` → `main`, docs site relit with a custom dark + terminal-green MkDocs Material theme, shells warmed up (fish alias→abbr, zsh PATH prune), font bumped to 20pt across every terminal template, themes rebuilt from wallpapers.
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://github.com/sebastienrousseau/dotfiles/actions"><img src="https://img.shields.io/github/actions/workflow/status/sebastienrousseau/dotfiles/ci.yml?style=for-the-badge&logo=githubactions&logoColor=white" alt="Build" /></a>
|
|
13
|
-
<a href="https://github.com/sebastienrousseau/dotfiles/releases/latest"><img src="https://img.shields.io/badge/Version-v0.2.
|
|
13
|
+
<a href="https://github.com/sebastienrousseau/dotfiles/releases/latest"><img src="https://img.shields.io/badge/Version-v0.2.511-blue?style=for-the-badge&logo=semanticrelease&logoColor=white" alt="Version" /></a>
|
|
14
14
|
<a href="https://github.com/sebastienrousseau/dotfiles/releases"><img src="https://img.shields.io/github/downloads/sebastienrousseau/dotfiles/total?style=for-the-badge&logo=github&logoColor=white" alt="Downloads" /></a>
|
|
15
15
|
<a href="https://codespaces.new/sebastienrousseau/dotfiles"><img src="https://img.shields.io/badge/Open%20in-Codespaces-blue?style=for-the-badge&logo=github&logoColor=white" alt="Open in GitHub Codespaces" /></a>
|
|
16
16
|
<a href="https://scorecard.dev/viewer/?uri=github.com/sebastienrousseau/dotfiles"><img src="https://img.shields.io/ossf-scorecard/github.com/sebastienrousseau/dotfiles?style=for-the-badge&logo=linuxfoundation&logoColor=white&label=OpenSSF%20Scorecard" alt="OpenSSF Scorecard" /></a>
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
curl -fsSL -o /tmp/dotfiles-install.sh \
|
|
55
|
-
https://raw.githubusercontent.com/sebastienrousseau/dotfiles/v0.2.
|
|
55
|
+
https://raw.githubusercontent.com/sebastienrousseau/dotfiles/v0.2.511/install.sh
|
|
56
56
|
echo "d5a04c5e2813a93a63c8ecce9655cf3d107f6068862c6eba84a92cf22f801c7e /tmp/dotfiles-install.sh" \
|
|
57
57
|
| shasum -a 256 -c
|
|
58
58
|
bash /tmp/dotfiles-install.sh
|
package/docs/COPYRIGHT
CHANGED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Custom Documentation Sites in 2026: Building a Distinctive Dark-Themed Developer Reference on MkDocs Material Without Forking"
|
|
3
|
+
description: A three-file recipe for a bespoke MkDocs Material theme — terminal-green on near-black, custom hero + card grid, hash-locked build. No custom_dir, no fork.
|
|
4
|
+
date: 2026-07-05
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Custom Documentation Sites in 2026: Building a Distinctive Dark-Themed Developer Reference on MkDocs Material Without Forking
|
|
8
|
+
|
|
9
|
+
*Documentation is the public API surface of an open-source project; the difference between the default Material theme and a bespoke palette is the difference between "reads like every other project" and "reads like this specific project".*
|
|
10
|
+
|
|
11
|
+
*Sebastien Rousseau · Published 5 Jul 2026 · 11 min read*
|
|
12
|
+
|
|
13
|
+
## Why Documentation Aesthetics Matter in 2026 #
|
|
14
|
+
|
|
15
|
+
An open-source project's documentation site is the first surface an evaluator touches — before the README, before the release notes, before the code. In 2026, the density of developer-tool competition means that visual differentiation carries measurable weight: does the site feel like a curated product, or does it feel like a Jekyll-Cayman default from 2019? The felt distinction shapes adoption decisions before the reader has read a single sentence.
|
|
16
|
+
|
|
17
|
+
The reference standard for "distinctive open-source documentation" is [docs.n8n.io](https://docs.n8n.io) — dark, polished, opinionated, immediately identifiable as n8n's. n8n runs on GitBook, a commercial SaaS. Most open-source projects, [.dotfiles](https://doc.dotfiles.io) included, cannot justify GitBook's licensing but can invest in MkDocs Material customisation to achieve equivalent visual distinction on an open-source stack.
|
|
18
|
+
|
|
19
|
+
This article documents the migration of [doc.dotfiles.io](https://doc.dotfiles.io) from Jekyll's Cayman theme (the default when GitHub Pages serves markdown without a MkDocs configuration) to a fully customised MkDocs Material theme with a terminal-green-on-near-black palette, custom typography, and a hero + card-grid landing page.
|
|
20
|
+
|
|
21
|
+
## The Custom Docs Theme 2026 Architecture Lens #
|
|
22
|
+
|
|
23
|
+
MkDocs Material's customisation surface has four distinct layers, each with different capabilities and constraints:
|
|
24
|
+
|
|
25
|
+
| Layer | Design Decision | Why It Matters | Risk if Mishandled |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| **Palette declaration** | `mkdocs.yml` `theme.palette.primary: custom` + `accent: custom` | Signals to MkDocs Material that CSS custom properties will drive the palette rather than a named preset (green, teal, indigo, etc.) | Named presets constrain the palette to Material Design's colour tokens; `custom` unlocks arbitrary hex values via CSS variables |
|
|
28
|
+
| **CSS custom-property overrides** | `docs/stylesheets/extra.css` sets `--md-primary-fg-color`, `--md-accent-fg-color`, `--md-default-bg-color`, `--md-code-bg-color` under `[data-md-color-scheme="slate"]` | The single source of truth for the palette. Every Material component reads from these variables | Overriding component-level CSS rules rather than variables creates unmaintainable per-component drift as MkDocs Material upgrades |
|
|
29
|
+
| **Component restyling** | Same `extra.css` overrides selectors like `.md-header`, `.md-nav__link`, `.grid.cards > ul > li` | Where the "feels bespoke" work happens — spacing, borders, hover states, gradients, backdrop blur | Under-styling produces "Material Design in a different colour"; over-styling drifts away from Material's ergonomic defaults |
|
|
30
|
+
| **Landing-page markup** | `docs/index.md` with Front-Matter `hide: [navigation, toc]` + `<section class="dot-hero">` + `<div class="grid cards" markdown>` | The homepage is the highest-impact surface; it should not look like a table of contents | Default MkDocs Material index reads like documentation; a custom hero reads like a product page |
|
|
31
|
+
|
|
32
|
+
## Key Documentation-UX Signals #
|
|
33
|
+
|
|
34
|
+
| Signal | Operational Benchmark | Reference | Technical Platform Implementation |
|
|
35
|
+
|---|---|---|---|
|
|
36
|
+
| **Time-to-first-CTA** | Hero action button visible above the fold in ≤ 100 ms of first-paint | Landing-page conversion norms | Custom hero section with primary and secondary CTAs immediately below the site title |
|
|
37
|
+
| **Palette Distinctiveness** | Primary accent color is unique to the project, not a Material Design preset | Brand recognition | CSS custom property overrides on `[data-md-color-scheme]` selectors |
|
|
38
|
+
| **Reading Contrast** | WCAG AA compliance on all text-on-background pairs | Accessibility gate | `--md-default-fg-color` (`#e4e7ec`) on `--md-default-bg-color` (`#0b0e14`) = 15.6:1 ratio |
|
|
39
|
+
| **Cognitive Load per Section** | Feature-card grid on landing page (visual chunking) rather than a bulleted link list | Landing-page ergonomics | `<div class="grid cards" markdown>` + 8 cards with material icons |
|
|
40
|
+
| **Build Reproducibility** | `mkdocs.yml` + `docs/stylesheets/extra.css` under version control; hashes locked in `requirements-docs.txt` | Supply-chain hygiene | `pip-compile --generate-hashes` + `pip install --require-hashes` in the Pages workflow |
|
|
41
|
+
| **Cache Cost at Edge** | CDN TTL respected; theme changes propagate to `doc.<domain>` within 10 minutes | Deployment latency | Cloudflare (or equivalent) `max-age=600` on the site |
|
|
42
|
+
|
|
43
|
+
## Diagnosis: What "Default MkDocs Material" Leaves on the Table #
|
|
44
|
+
|
|
45
|
+
An out-of-the-box MkDocs Material site with `primary: teal, accent: teal` and no `extra_css` is visually acceptable — but it is one of many thousand acceptable sites that look identically acceptable. The named presets are constrained to Google's Material Design palette; the sidebar, header, and content surface all read as "Material default".
|
|
46
|
+
|
|
47
|
+
For a project positioning itself as "an opinionated developer platform, not a library", the visual signal that the docs are *the product's* docs — not somebody else's — is a marketing surface, not a decoration. The lift is contained to three files:
|
|
48
|
+
|
|
49
|
+
- `mkdocs.yml` — palette selector configuration
|
|
50
|
+
- `docs/stylesheets/extra.css` — the actual palette + typography + component overrides
|
|
51
|
+
- `docs/index.md` — hero + card grid replacing the default index
|
|
52
|
+
|
|
53
|
+
No template overrides (`custom_dir`), no plugin authoring, no JavaScript. All the customisation lives in files MkDocs Material is explicitly designed to consume.
|
|
54
|
+
|
|
55
|
+
## Remediation: The Three-File Custom Theme #
|
|
56
|
+
|
|
57
|
+
### `mkdocs.yml` Palette Configuration
|
|
58
|
+
|
|
59
|
+
The magic value that unlocks CSS-driven colours is `primary: custom` (and `accent: custom`). Under `[data-md-color-scheme="slate"]`, MkDocs Material's dark variant, every component reads its colours from CSS custom properties that we get to define.
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
theme:
|
|
63
|
+
name: material
|
|
64
|
+
font:
|
|
65
|
+
text: Inter
|
|
66
|
+
code: JetBrains Mono
|
|
67
|
+
palette:
|
|
68
|
+
- media: "(prefers-color-scheme: dark)"
|
|
69
|
+
scheme: slate
|
|
70
|
+
primary: custom
|
|
71
|
+
accent: custom
|
|
72
|
+
- media: "(prefers-color-scheme: light)"
|
|
73
|
+
scheme: default
|
|
74
|
+
primary: custom
|
|
75
|
+
accent: custom
|
|
76
|
+
features:
|
|
77
|
+
- navigation.tabs
|
|
78
|
+
- navigation.tabs.sticky
|
|
79
|
+
- navigation.footer
|
|
80
|
+
- content.code.copy
|
|
81
|
+
|
|
82
|
+
extra_css:
|
|
83
|
+
- stylesheets/extra.css
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The `navigation.tabs.sticky` feature keeps the top-level navigation visible on scroll, which pairs with the hero + card grid to keep the site feeling like a product page rather than a scrolling article.
|
|
87
|
+
|
|
88
|
+
### `docs/stylesheets/extra.css` — Palette + Component Overrides
|
|
89
|
+
|
|
90
|
+
The stylesheet is organised in five zones: CSS custom properties for the palette, typography, dark-scheme palette overrides, per-component restyling, and the custom hero + grid-card styles.
|
|
91
|
+
|
|
92
|
+
```css
|
|
93
|
+
:root {
|
|
94
|
+
--dot-green: #7ee787; /* terminal green — primary */
|
|
95
|
+
--dot-green-bright: #b0f5b7; /* hover / focused */
|
|
96
|
+
--dot-green-dim: #4a9153; /* muted */
|
|
97
|
+
--dot-bg: #0b0e14; /* near-black base */
|
|
98
|
+
--dot-bg-elev: #111621; /* elevated card */
|
|
99
|
+
--dot-fg: #e4e7ec;
|
|
100
|
+
--dot-fg-muted: #94a3b8;
|
|
101
|
+
--dot-border: #1f2937;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
[data-md-color-scheme="slate"] {
|
|
105
|
+
--md-default-bg-color: var(--dot-bg);
|
|
106
|
+
--md-default-fg-color: var(--dot-fg);
|
|
107
|
+
--md-primary-fg-color: var(--dot-green);
|
|
108
|
+
--md-accent-fg-color: var(--dot-green-bright);
|
|
109
|
+
--md-typeset-a-color: var(--dot-green);
|
|
110
|
+
--md-code-bg-color: #161b26;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The choice of `#7ee787` (GitHub's terminal green) as the accent is deliberate: it reads as "developer tool" to the target audience without being GitHub-branded, and the WCAG contrast on the `#0b0e14` background exceeds 12:1 for text and 4.5:1 for the accent-on-background — comfortably above AA thresholds.
|
|
115
|
+
|
|
116
|
+
Component overrides are targeted at the highest-impact surfaces:
|
|
117
|
+
|
|
118
|
+
```css
|
|
119
|
+
/* Header: blurred backdrop-saturate for a floating feel */
|
|
120
|
+
.md-header {
|
|
121
|
+
background-color: rgba(11, 14, 20, 0.92);
|
|
122
|
+
backdrop-filter: saturate(180%) blur(12px);
|
|
123
|
+
border-bottom: 1px solid var(--dot-border);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Grid cards on landing page — hover lift + accent glow */
|
|
127
|
+
.md-typeset .grid.cards > :is(ul, ol) > li {
|
|
128
|
+
background: var(--dot-bg-elev);
|
|
129
|
+
border: 1px solid var(--dot-border);
|
|
130
|
+
border-radius: 12px;
|
|
131
|
+
transition: transform 180ms ease, border-color 180ms ease;
|
|
132
|
+
}
|
|
133
|
+
.md-typeset .grid.cards > :is(ul, ol) > li:hover {
|
|
134
|
+
transform: translateY(-2px);
|
|
135
|
+
border-color: rgba(126, 231, 135, 0.35);
|
|
136
|
+
box-shadow: 0 8px 24px -8px rgba(0, 0, 0, 0.5);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `docs/index.md` — Hero + Grid Cards
|
|
141
|
+
|
|
142
|
+
The landing page is not documentation — it is a product surface. MkDocs Material's `md_in_html` extension allows Markdown to nest inside a custom HTML section:
|
|
143
|
+
|
|
144
|
+
```markdown
|
|
145
|
+
---
|
|
146
|
+
hide:
|
|
147
|
+
- navigation
|
|
148
|
+
- toc
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
<section class="dot-hero" markdown>
|
|
152
|
+
|
|
153
|
+
# .dotfiles
|
|
154
|
+
|
|
155
|
+
<p class="tagline">Cross-platform, signed, local-first dotfiles…</p>
|
|
156
|
+
|
|
157
|
+
<div class="buttons">
|
|
158
|
+
<a class="primary" href="guides/INSTALL/">Install →</a>
|
|
159
|
+
<a href="https://github.com/sebastienrousseau/dotfiles">GitHub</a>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
</section>
|
|
163
|
+
|
|
164
|
+
## What's inside
|
|
165
|
+
|
|
166
|
+
<div class="grid cards" markdown>
|
|
167
|
+
|
|
168
|
+
- :material-console:{ .lg .middle } **Multi-shell parity**
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
Bash, Zsh, Fish, Nushell — same aliases, functions, prompt, and completions.
|
|
173
|
+
|
|
174
|
+
[→ Shell hub](https://doc.dotfiles.io/reference/UTILS/)
|
|
175
|
+
|
|
176
|
+
</div>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The `hide: [navigation, toc]` front-matter removes the sidebar and right-column table-of-contents on this page only — the landing page gets the full canvas, sub-pages retain the standard docs layout.
|
|
180
|
+
|
|
181
|
+
## Verification: Build, Deploy, Contrast #
|
|
182
|
+
|
|
183
|
+
The custom theme adds ~12 KB of CSS to the built site. Local `mkdocs build --clean` completes in ~2 seconds. The Pages workflow uses hash-locked dependencies (`pip install --require-hashes -r requirements-docs.txt`) to keep the build reproducible across MkDocs Material and its 30+ transitive dependencies.
|
|
184
|
+
|
|
185
|
+
Accessibility contrast measured on the deployed site:
|
|
186
|
+
|
|
187
|
+
- Body text (`#e4e7ec` on `#0b0e14`) — **15.6:1** (WCAG AAA)
|
|
188
|
+
- Accent (`#7ee787` on `#0b0e14`) — **12.4:1** (WCAG AAA)
|
|
189
|
+
- Muted text (`#94a3b8` on `#0b0e14`) — **7.2:1** (WCAG AAA)
|
|
190
|
+
|
|
191
|
+
Cloudflare CDN cache invalidation after Pages deploy: ~10 minutes on `max-age=600`. Fetching the GitHub Pages origin (`sebastienrousseau.github.io/dotfiles/`) reflects the new theme immediately; the CDN-fronted custom domain propagates within one cache TTL.
|
|
192
|
+
|
|
193
|
+
## Return on Resilience #
|
|
194
|
+
|
|
195
|
+
| Metric | Before (Jekyll Cayman) | After (Custom MkDocs Material) |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| Page weight | 2.9 KB | 14.8 KB (compressed 4.2 KB) |
|
|
198
|
+
| Time to visible hero | ~800 ms (no hero) | ~150 ms |
|
|
199
|
+
| WCAG AA compliance | Passes body text; hero absent | Passes AAA on all text pairs |
|
|
200
|
+
| Landing-page CTAs | 0 (link list only) | 4 (primary + 3 secondary) |
|
|
201
|
+
| Visual differentiation from default | Zero | Distinctive palette + hero + grid |
|
|
202
|
+
| Build reproducibility | Jekyll on `github-pages` gem (unpinned transitive deps) | `pip install --require-hashes` (fully locked) |
|
|
203
|
+
|
|
204
|
+
## Takeaways #
|
|
205
|
+
|
|
206
|
+
1. **`primary: custom` + `extra_css` beats theme forking.** No `custom_dir`, no Jinja templates, no plugin authoring. All the customisation lives in files MkDocs Material is explicitly designed to consume.
|
|
207
|
+
|
|
208
|
+
2. **CSS custom properties are the maintainable seam.** Overriding `--md-primary-fg-color` scales; overriding `.md-header a.md-header__button:not(...)` selectors doesn't.
|
|
209
|
+
|
|
210
|
+
3. **Treat the landing page as a product surface, not documentation.** Front-matter `hide: [navigation, toc]` unlocks the full canvas. Hero + card grid + tabbed quick-start reads as a product page.
|
|
211
|
+
|
|
212
|
+
4. **Lock the docs-build supply chain.** `pip-compile --generate-hashes` + `pip install --require-hashes` closes the Scorecard `PinnedDependenciesID` alert and prevents transitive-dep drift in Pages deploys.
|
|
213
|
+
|
|
214
|
+
5. **Measure contrast, not just aesthetics.** WCAG AAA on body text is achievable with the right palette; it's not a tradeoff against distinctiveness.
|
|
215
|
+
|
|
216
|
+
The reference implementation landed as [PR #960](https://github.com/sebastienrousseau/dotfiles/pull/960) and is live at [doc.dotfiles.io](https://doc.dotfiles.io); the theme source lives at [`docs/stylesheets/extra.css`](https://github.com/sebastienrousseau/dotfiles/blob/main/docs/stylesheets/extra.css).
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Fish Startup in 2026: Cutting Interactive Shell Latency by Half with abbr on Multi-Shell Dotfiles"
|
|
3
|
+
description: Diagnosing and remediating a 232 ms fish cold-start on the .dotfiles multi-shell bridge — one printf format change, one chezmoi hook, half the latency.
|
|
4
|
+
date: 2026-07-05
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Fish Startup in 2026: Cutting Interactive Shell Latency by Half with `abbr` on Multi-Shell Dotfiles
|
|
8
|
+
|
|
9
|
+
*Interactive shells have become the primary interface for AI-augmented development; the difference between a 120 ms and a 230 ms first prompt compounds into measurable engineering-hour loss across a global fleet.*
|
|
10
|
+
|
|
11
|
+
*Sebastien Rousseau · Published 5 Jul 2026 · 10 min read*
|
|
12
|
+
|
|
13
|
+
## Why Interactive Shell Latency Matters in 2026 #
|
|
14
|
+
|
|
15
|
+
The AI-augmented developer opens a terminal dozens of times a day. In fleets running Claude Code, Codex CLI, GitHub Copilot CLI, or agentic frameworks that spawn subshells for every tool call, shell startup latency stops being a personal-comfort metric and becomes a **platform-engineering signal**.
|
|
16
|
+
|
|
17
|
+
The [.dotfiles reference framework](https://github.com/sebastienrousseau/dotfiles) treats sub-second shell startup as an SLO alongside SLSA-signed releases and MCP boundary enforcement. When a shell exceeds its budget, the framework's `dot health` command reports it as a failing check, not a warning — because a slow prompt on a workstation running 18 concurrent AI agents is a supply-chain-throughput problem, not an aesthetic one.
|
|
18
|
+
|
|
19
|
+
This article walks through the diagnosis and fix that took Fish cold-start latency from **231 ms → 119 ms** — a 48% reduction — on a workstation carrying ~900 bridged bash aliases into Fish for cross-shell parity. The remediation is a one-line change to a code-generation printf statement, plus a chezmoi hook that moves the cost out of the interactive path.
|
|
20
|
+
|
|
21
|
+
## The Multi-Shell Bridge 2026 Architecture Lens #
|
|
22
|
+
|
|
23
|
+
Cross-shell parity — the same aliases, functions, environment, and completions across bash, Zsh, Fish, and Nushell — is a distinct architectural property of a mature dotfiles framework. Each layer of that bridge carries its own performance tax:
|
|
24
|
+
|
|
25
|
+
| Layer | Design Decision | Why It Matters | Risk if Mishandled |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| **Source of truth** | Bash-hosted alias library (~900 entries) sourced by `zsh` and `bash` natively | Single canonical location prevents drift; matches how most upstream tooling assumes aliases live | Duplication across shells silently diverges; users on Fish or Nushell get a subtly different alias set |
|
|
28
|
+
| **Fish bridge** | `bash --norc --noprofile` subshell dumps `alias -p`, output translated and cached to `~/.cache/fish/bash-aliases.fish` | Fish has no `bash`-sourcing primitive; the bridge is unavoidable | Bridge runs at every shell start unless cached; cache invalidation timing determines the felt cost |
|
|
29
|
+
| **Cache format** | `abbr --add NAME -- VALUE` (this article's change) instead of `alias NAME=VALUE` | Fish's `alias` builtin allocates a function per entry (~183 µs each × 900 entries = ~165 ms); `abbr` is a command-line-time expansion at ~40 µs | Choosing `alias` for the cache format silently caps Fish cold-start at ~230 ms even on a warm cache |
|
|
30
|
+
| **Cache invalidation** | Compare source mtime + first-line format marker | Alias sources change on every `chezmoi apply`, invalidating the cache and forcing regen on the next shell — the exact moment the user opens a terminal to try their changes | Cache regen on the interactive path punishes the shell that opens right after configuration changes |
|
|
31
|
+
| **Pre-warm hook** | `run_onchange_after_` chezmoi hook rebuilds the cache during apply | Moves the ~200 ms regen cost off the user's first prompt into the apply step | Absence of a pre-warm hook makes the first post-apply shell feel broken |
|
|
32
|
+
|
|
33
|
+
## Key Interactive Shell Performance Signals #
|
|
34
|
+
|
|
35
|
+
| Signal | Operational Benchmark | Reference | Technical Platform Implementation |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| **Fish cold-start** | ≤ 200 ms first-prompt latency | Interactive-response threshold (Nielsen 1993, still the industry norm) | `hyperfine --warmup 2 'fish -i -c exit'` in CI; regression fails a PR if the median crosses threshold |
|
|
38
|
+
| **Fish warm-start** | ≤ 130 ms after cache is populated | Delta between cold/warm reveals cache-invalidation cost | Same command with `--warmup 3`; the mean tracks the fully-cached shell path |
|
|
39
|
+
| **First-post-apply latency** | Warm-shell parity — no cliff after `chezmoi apply` | Signals that regeneration lives outside the shell hot path | `chezmoi apply && hyperfine 'fish -i -c exit'` — cold and warm should be within noise |
|
|
40
|
+
| **Cache-format compatibility** | Auto-heal path when upgrading between cache formats | Ensures long-lived workstations don't inherit stale caches on framework upgrade | Staleness check compares first line of cache to expected format marker |
|
|
41
|
+
| **Bridge throughput** | ~40 µs per abbreviated entry, ~183 µs per aliased entry | Fish internals — measured, not documented | Choice of `abbr` over `alias` in the cache-emission format string |
|
|
42
|
+
|
|
43
|
+
## Diagnosis: Where the Milliseconds Went #
|
|
44
|
+
|
|
45
|
+
`fish --profile-startup=/tmp/f.prof -i -c 'exit'` emits a per-command trace with self-time and cumulative time. Sorted by cumulative time descending, one line dominated the warm-start budget:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Time (µs) Sum (µs) Command
|
|
49
|
+
1687 132211 ----> source "$_alias_cache"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**132 ms of a 231 ms budget** — 57% — spent sourcing a single cache file. Everything else (starship prompt initialisation, mise activation, atuin history bindings, direnv hooks) added up cleanly to the remaining ~85 ms.
|
|
53
|
+
|
|
54
|
+
The cache file itself was well-formed and cache-invalidation was working correctly. The problem lived at the primitive level: what does `alias name='value'` cost when Fish parses and installs it? Ran in isolation:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
$ hyperfine --warmup 2 "fish -c 'source ~/.cache/fish/bash-aliases.fish'"
|
|
58
|
+
Time (mean ± σ): 170.8 ms ± 11.3 ms
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**170 ms just to source 878 alias lines.** Fish's `alias` isn't a shell-level string substitution — it's a function factory. The invocation `alias ll='eza -la --icons'` roughly desugars to:
|
|
62
|
+
|
|
63
|
+
```fish
|
|
64
|
+
function ll --wraps='eza -la --icons' --description 'alias ll=eza -la --icons'
|
|
65
|
+
eza -la --icons $argv
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Every call parses the definition, allocates a function object, installs it in the function table, records the description, and wires the `--wraps` for tab completion. Approximately 183 µs per entry. At 878 entries, the maths is uncompromising: 878 × 183 µs = 161 ms.
|
|
70
|
+
|
|
71
|
+
The cost is O(1) per call, but the call count is the problem, and there is no batching path in Fish's `alias` implementation.
|
|
72
|
+
|
|
73
|
+
## Remediation: `abbr --add` as a Cross-Shell-Bridge Primitive #
|
|
74
|
+
|
|
75
|
+
Fish exposes two ways to give a short name to a longer command:
|
|
76
|
+
|
|
77
|
+
- **`alias`** — function factory. Available in interactive shells, scripts, pipes, subshells, and inside other functions. Costs a function allocation on every source.
|
|
78
|
+
- **`abbr --add`** — abbreviation. Expanded at the interactive command line the moment the user types the abbreviation and hits space or enter. Not available in scripts (they need functions). No function allocation on installation.
|
|
79
|
+
|
|
80
|
+
For bash-alias bridges, the tradeoff is invisible: users don't call `ll` from inside a Fish script — they'd write a proper Fish function for that use case. Abbreviations for this workload are a strict upgrade: identical interactive UX, no function-table pressure, and — as a side benefit — they show the user what actually runs when they type the abbreviation, which improves shell literacy.
|
|
81
|
+
|
|
82
|
+
The code change is a single printf format string in the cache-generator:
|
|
83
|
+
|
|
84
|
+
```diff
|
|
85
|
+
- printf "alias %s=%s\n" "$name" "$val"
|
|
86
|
+
+ printf "abbr --add %s -- %s\n" "$name" "$val"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Measured in isolation:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
Benchmark 1: fish -c 'source alias-cache.fish'
|
|
93
|
+
Time (mean ± σ): 170.8 ms ± 11.3 ms
|
|
94
|
+
|
|
95
|
+
Benchmark 2: fish -c 'source abbr-cache.fish'
|
|
96
|
+
Time (mean ± σ): 34.0 ms ± 0.9 ms
|
|
97
|
+
|
|
98
|
+
Summary
|
|
99
|
+
abbr-cache.fish ran 5.02 ± 0.36 times faster than alias-cache.fish
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**5× faster. 137 ms saved on every warm shell start.** End-to-end Fish latency dropped from 231 ms → 119 ms.
|
|
103
|
+
|
|
104
|
+
## The Second-Order Bug: Cache Invalidation Timing #
|
|
105
|
+
|
|
106
|
+
Solving the warm case revealed a distinct failure mode: the *first* Fish shell opened after `chezmoi apply` still measured ~306 ms. The apply step writes new versions of the underlying bash alias source files (updated mtimes). The staleness check inside the Fish bridge sees `source.mtime > cache.mtime`, throws the cache away, and rebuilds it — spawning a subshell, sourcing 40 KB of bash, iterating 878 lines. **~200 ms.**
|
|
107
|
+
|
|
108
|
+
The shell that pays this cost is whichever one the user opens first, which is almost always the shell they opened *because* they wanted to see the effect of the apply.
|
|
109
|
+
|
|
110
|
+
The remediation is architectural, not algorithmic. The regeneration is moved off the interactive path and onto the apply itself via a chezmoi `run_onchange_after_` script:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
#!/usr/bin/env bash
|
|
114
|
+
# Source-hash retrigger keys — script re-runs when any of these change:
|
|
115
|
+
# 90-ux-aliases.sh.tmpl: {{ include "…/90-ux-aliases.sh.tmpl" | sha256sum }}
|
|
116
|
+
# 91-ux-aliases-lazy.sh.tmpl: {{ include "…/91-ux-aliases-lazy.sh.tmpl" | sha256sum }}
|
|
117
|
+
# aliases.fish.tmpl: {{ include "…/aliases.fish.tmpl" | sha256sum }}
|
|
118
|
+
|
|
119
|
+
command -v fish >/dev/null 2>&1 || exit 0
|
|
120
|
+
|
|
121
|
+
rm -f "${HOME}/.cache/fish/bash-aliases.fish"
|
|
122
|
+
fish -i -c 'exit' >/dev/null 2>&1 || true
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The hook invalidates the cache, spawns a throwaway interactive Fish so the bridge's existing regen path fires, then exits. The user's next actual shell finds a valid, up-to-date cache. The 200 ms cost lands where the user expects "compilation work" to happen — during the apply — not on the terminal they open ten seconds later.
|
|
126
|
+
|
|
127
|
+
Cold-start Fish after apply: **306 ms → 120 ms.**
|
|
128
|
+
|
|
129
|
+
## Return on Resilience #
|
|
130
|
+
|
|
131
|
+
At a workstation opening 40 shells per day, saving 112 ms per shell reclaims 4.5 seconds daily; 22 minutes annually. At a small engineering org of 50 developers with the same profile, that's 18 engineering-hours reclaimed per year — measurable but modest.
|
|
132
|
+
|
|
133
|
+
The stronger case is qualitative. Interactive-latency perception is nonlinear: at ~200 ms the user consciously notices lag; at ~120 ms the shell feels immediate. Once the felt lag is gone, the developer stops flinching before opening a terminal — which changes the frequency and length of exploratory shell work, which changes the shape of what they do at the CLI.
|
|
134
|
+
|
|
135
|
+
| Metric | Before | After | Delta |
|
|
136
|
+
|---|---|---|---|
|
|
137
|
+
| Fish warm-start (median) | 231 ms | 119 ms | −112 ms (−48%) |
|
|
138
|
+
| Fish cold-start after apply | 306 ms | 120 ms | −186 ms (−61%) |
|
|
139
|
+
| Cache source cost | 170 ms | 34 ms | −136 ms (−80%) |
|
|
140
|
+
| CI regression threshold | ✗ 231 > 200 | ✓ 119 ≤ 200 | Now under budget |
|
|
141
|
+
| Full test suite | 4703 tests, 0 fail | 4703 tests, 0 fail | Zero regressions |
|
|
142
|
+
|
|
143
|
+
## Takeaways #
|
|
144
|
+
|
|
145
|
+
1. **Profile every interactive shell in CI.** Fish, Zsh, Nushell, Bash — each has its own primitives with different costs. Treat first-prompt latency as a signal with a threshold, not a comfort metric.
|
|
146
|
+
|
|
147
|
+
2. **Prefer `abbr` for interactive-only bridged aliases in Fish.** The distinction between `abbr` (line-time expansion) and `alias` (function factory) is documented; the 5× cost distinction is not. If your users don't call the alias from inside a Fish script — and for bridged bash aliases they don't — `abbr` is a strict upgrade.
|
|
148
|
+
|
|
149
|
+
3. **Move cache regeneration off the interactive path.** Any cache invalidated by a configuration-management action (chezmoi, ansible, dotbot) should be regenerated by that same action, not by whichever shell opens next.
|
|
150
|
+
|
|
151
|
+
4. **Version the cache format itself, not just the source.** The staleness check should include a format marker so upgrading users don't inherit stale caches by mtime luck.
|
|
152
|
+
|
|
153
|
+
The reference implementation lives on `main` at [sebastienrousseau/dotfiles](https://github.com/sebastienrousseau/dotfiles); the change landed as [PR #963](https://github.com/sebastienrousseau/dotfiles/pull/963) and [PR #964](https://github.com/sebastienrousseau/dotfiles/pull/964), shipped in [v0.2.510](https://github.com/sebastienrousseau/dotfiles/releases/tag/v0.2.510).
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Renaming master to main in 2026: A Zero-Downtime Runbook"
|
|
3
|
+
description: A supply-chain-safe procedure for renaming the default branch of a mature open-source repository — 96 in-repo edits, a grace-period mirror workflow, zero broken install URLs.
|
|
4
|
+
date: 2026-07-05
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Renaming `master` to `main` in 2026: A Zero-Downtime Runbook for a Repository at the Heart of an Open-Source Supply Chain
|
|
8
|
+
|
|
9
|
+
*Renaming the default branch of a mature repository is a supply-chain event, not a cosmetic one; done wrong, it strands `curl \| bash` install commands, breaks CI, and severs external distribution channels.*
|
|
10
|
+
|
|
11
|
+
*Sebastien Rousseau · Published 5 Jul 2026 · 12 min read*
|
|
12
|
+
|
|
13
|
+
## Why Default-Branch Naming Matters in 2026 #
|
|
14
|
+
|
|
15
|
+
Every open-source project with a public install path publishes a URL of the form `raw.githubusercontent.com/<owner>/<repo>/<branch>/install.sh`. Every downstream consumer who has copied that URL — into a README, a Slack message, a devcontainer, a company wiki, a StackOverflow answer, a Homebrew tap, an AUR PKGBUILD, an internal Ansible playbook — has taken an implicit dependency on that branch name. Renaming the branch is a **breaking change to the project's public API surface**, whether the maintainer intended one or not.
|
|
16
|
+
|
|
17
|
+
The industry moved off `master` as the default branch name years ago; new repositories default to `main`. Existing repositories, however, live with a naming inconsistency that becomes actively confusing when a maintainer has multiple projects — some on `main`, some still on `master`. The migration is unavoidable, but the operational risk profile is significant enough that most maintainers put it off indefinitely.
|
|
18
|
+
|
|
19
|
+
This article documents the migration of the [.dotfiles](https://github.com/sebastienrousseau/dotfiles) repository — 60+ files with hardcoded branch references, four external distribution channels, three CI providers tracking the default branch, and a public install command bookmarked by an unknown number of downstream users. The migration was **zero-downtime**: no install command broke, no CI job failed, no downstream integration degraded. The blueprint is generalisable to any medium-complexity open-source repository.
|
|
20
|
+
|
|
21
|
+
## The Branch-Rename 2026 Architecture Lens #
|
|
22
|
+
|
|
23
|
+
A default-branch rename is not a single operation. It's a sequence of coordinated changes across a defined dependency graph, each with its own migration mechanism:
|
|
24
|
+
|
|
25
|
+
| Layer | Design Decision | Why It Matters | Risk if Mishandled |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| **GitHub metadata** | Native rename via Settings → Branches or `POST /repos/{owner}/{repo}/branches/{branch}/rename` | GitHub auto-migrates default-branch setting, open PR targets, branch-protection assignment, ruleset targeting, and Pages source | Renaming via manual `git push :old-name` + `git push new-name` skips the auto-migration and orphans PRs |
|
|
28
|
+
| **In-repo workflow triggers** | `.github/workflows/*.yml` `branches:` lists updated pre-rename | Workflows that trigger on `push:` or `pull_request:` targeting the old name silently stop firing after rename | Migration PR itself under-tested — the workflows it edits no longer fire on it |
|
|
29
|
+
| **Grace-period mirror** | New workflow fast-forwards `master` from `main` on every push | Preserves `raw.githubusercontent.com/…/master/…` URLs for downstream consumers who cannot be reached | External `curl \| bash` install commands return HTTP 404 the moment `master` ceases to exist |
|
|
30
|
+
| **Documentation URIs** | `mkdocs.yml` `edit_uri`, README install commands, docs prose | GitHub redirects `github.com/…/blob/master/…` automatically; `raw.githubusercontent.com/…/master/…` does **not** redirect | Docs site edit buttons point at nonexistent branches; install commands 404 |
|
|
31
|
+
| **Rulesets as code** | `.github/rulesets/<branch>.json` file renamed alongside the JSON `target.include` | Machine-readable ruleset files that reference `refs/heads/master` misalign with GitHub's auto-migrated state | Configuration drift between the in-repo policy source of truth and GitHub's live enforcement |
|
|
32
|
+
| **Test-suite assertions** | Regression tests that asserted `/master/` URLs in README updated to assert `/main/` | Tests written before rename fail *after* rename in the exact commit that fixes them | Migration PR shows red CI, blocking merge |
|
|
33
|
+
| **External distribution** | Homebrew tap, Scoop bucket, AUR PKGBUILD — pinned to release tags, not branches | Version-pinning insulates external distribution from branch renames | Branch-pinned distribution manifests break silently on rename |
|
|
34
|
+
|
|
35
|
+
## Key Branch-Rename Migration Signals #
|
|
36
|
+
|
|
37
|
+
| Signal | Operational Benchmark | Reference | Technical Platform Implementation |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| **URL Reachability Post-Rename** | `HTTP 200` on both `main/install.sh` and `master/install.sh` for the full grace period | External-consumer continuity | Mirror workflow: `on: push: branches: [main]` → `git push origin main:refs/heads/master` |
|
|
40
|
+
| **CI Coverage on Migration PR** | Workflow-trigger `branches:` list transitionally includes both `main` and `master` | Migration PR must be tested against the current default | `pull_request: branches: [main, master]` — remove `master` once the mirror retires |
|
|
41
|
+
| **In-Repo URL Consistency** | Zero remaining `/master/` URIs on the head branch, excluding intentional mirror-preservation strings | Doc/install correctness | `git grep 'raw.githubusercontent.com/.../master/'` returns empty (or only test-fixture strings) |
|
|
42
|
+
| **Ruleset File Alignment** | `.github/rulesets/*.json` filename matches its `target.include` refspec | Config-as-code hygiene | File rename via `git mv` + JSON `target.include` update in the same commit |
|
|
43
|
+
| **Grace-Period Retirement Marker** | Calendar-visible tracking item with an explicit sunset date | Operational-debt visibility | GitHub issue with target date in title + calendar event (`.ics` or Google Calendar quick-add URL) |
|
|
44
|
+
|
|
45
|
+
## Diagnosis: Enumerating the Blast Radius #
|
|
46
|
+
|
|
47
|
+
Before touching a single file, an authoritative audit surfaces the full inventory of `master` references. On the .dotfiles repository, that surfaced **~88 mechanical replacements across 60 files**, grouped as:
|
|
48
|
+
|
|
49
|
+
- **24 GitHub Actions workflow files** — trigger lists, `github.ref_name == 'master'` conditionals, `--base master` PR-creating steps, inline `@SHA # master` comments describing what commit was pinned
|
|
50
|
+
- **7 `raw.githubusercontent.com/.../master/…` URLs** — README install command, `install.sh` (referencing itself in its own comment header), `bin/dot-bootstrap`, `docs/index.md`, install guide, MkDocs edit_uri, chezmoi-data JSON `$id` field
|
|
51
|
+
- **~25 documentation files** — `github.com/…/blob/master/…` references in operations runbooks, security docs, architecture decision records
|
|
52
|
+
- **1 ruleset-as-code file** — `.github/rulesets/master.json` with a `target.include: refs/heads/master` field
|
|
53
|
+
- **1 regression test** — asserting the README contains `/master/` (a red-team catch: this test PROTECTS against accidental rename, which now needs its assertion inverted)
|
|
54
|
+
- **4 legitimately-kept references** — the `gbd` bulk-branch-delete script with a `main|master` whitelist regex, the `git-primary-branch` shell function's fallback path, the release-branch check in `scripts/ops/release.sh`, and Scorecard-linter fixtures demonstrating `@master` as an anti-pattern
|
|
55
|
+
|
|
56
|
+
## Remediation Sequence #
|
|
57
|
+
|
|
58
|
+
The rename is executed as a five-phase sequence, each with a distinct commit or GitHub operation:
|
|
59
|
+
|
|
60
|
+
**Phase 1 — Pre-migration content preparation.** A single pull request rewrites all in-repo `master` references while `master` is still the default branch. This PR must merge before any GitHub-side rename. Workflow `pull_request:` triggers gain `[main, master]` (paired) so the PR itself triggers CI against the current default. The ruleset file is renamed via `git mv` and its JSON target updated. The regression test's assertion is inverted from "must contain `/master/`" to "must contain `/main/`".
|
|
61
|
+
|
|
62
|
+
**Phase 2 — GitHub-side rename.** Via UI (`Settings → Branches → Rename`) or API (`POST /repos/{owner}/{repo}/branches/master/rename`). GitHub auto-migrates default-branch setting, PR targets, branch-protection assignment, ruleset targeting, Pages source. Blocked if a branch-protection *rule pattern* targeting the new name already exists — delete the empty rule first via GraphQL:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
gh api graphql -f query='
|
|
66
|
+
mutation {
|
|
67
|
+
deleteBranchProtectionRule(input: { branchProtectionRuleId: "..." }) {
|
|
68
|
+
clientMutationId
|
|
69
|
+
}
|
|
70
|
+
}'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Phase 3 — Mirror workflow activation.** Trigger the pre-committed mirror workflow via `workflow_dispatch` or a small push to `main`. The workflow performs a fast-forward `git push origin main:refs/heads/master`, recreating `master` as a passive mirror. From this point forward, every push to `main` mirrors automatically.
|
|
74
|
+
|
|
75
|
+
**Phase 4 — Local clone update (per-workstation).** Every developer with an active clone runs:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
git branch -m master main
|
|
79
|
+
git fetch origin
|
|
80
|
+
git branch -u origin/main main
|
|
81
|
+
git remote set-head origin -a
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Phase 5 — Retirement scheduling.** A GitHub issue with a title-embedded target date (`[2027-07-05] Retire master mirror + tighten workflow triggers to main-only`) plus a calendar reminder ensures the grace-period mirror doesn't become permanent operational debt.
|
|
85
|
+
|
|
86
|
+
## Verification: URLs, CI, Docs Site #
|
|
87
|
+
|
|
88
|
+
Post-rename smoke tests:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
$ curl -sI https://raw.githubusercontent.com/sebastienrousseau/dotfiles/main/install.sh | head -1
|
|
92
|
+
HTTP/2 200
|
|
93
|
+
$ curl -sI https://raw.githubusercontent.com/sebastienrousseau/dotfiles/master/install.sh | head -1
|
|
94
|
+
HTTP/2 200
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Both branch names resolve during the grace period. When the mirror is retired in 12 months, `/master/` returns 404 by design — but by then no active install command should still reference it.
|
|
98
|
+
|
|
99
|
+
CI status post-rename: on the .dotfiles repository, the migration PR (`#961`) ran **70 checks green, 0 failed**, driven by the `[main, master]` trigger-list transitional configuration. The follow-up test-coverage PR (`#963`) confirmed no downstream test regression.
|
|
100
|
+
|
|
101
|
+
Docs site: MkDocs `edit_uri` — updated from `edit/master/docs/` to `edit/main/docs/` — resolves correctly. `github.com/…/blob/master/…` links continue to work via GitHub's built-in redirect; `raw.githubusercontent.com/…/master/…` works via the mirror.
|
|
102
|
+
|
|
103
|
+
## Return on Resilience #
|
|
104
|
+
|
|
105
|
+
The commercial value of a supply-chain-safe rename is defensive, not offensive: nothing new is built, but nothing existing breaks.
|
|
106
|
+
|
|
107
|
+
| Metric | Result |
|
|
108
|
+
|---|---|
|
|
109
|
+
| Files updated in migration PR | 97 (96 edits + 1 rename + 1 new workflow) |
|
|
110
|
+
| Grace-period external URL uptime | 100% (both `/main/` and `/master/` return HTTP 200) |
|
|
111
|
+
| CI checks post-rename | 70 green, 0 failed |
|
|
112
|
+
| Downstream distribution channels broken | 0 (Homebrew tap, Scoop bucket, AUR pinned to tags) |
|
|
113
|
+
| Local-clone update commands | 4 lines, ~5 seconds per workstation |
|
|
114
|
+
| Operational debt introduced | 1 mirror workflow, retired via calendar-scheduled task 2027-07-05 |
|
|
115
|
+
|
|
116
|
+
## Takeaways #
|
|
117
|
+
|
|
118
|
+
1. **Land the in-repo edits before the GitHub-side rename.** Merging Phase 1 while `master` is still default gives you both a working CI baseline and a rehearsal.
|
|
119
|
+
|
|
120
|
+
2. **Add the mirror workflow before renaming, not after.** The interval between the GitHub rename and the mirror's first fast-forward push is the window during which `raw.githubusercontent.com/…/master/…` returns 404. Minimising that window is a matter of ordering.
|
|
121
|
+
|
|
122
|
+
3. **Update `pull_request:` triggers transitionally.** `branches: [main, master]` covers the migration PR itself (which targets the pre-rename default) and every future PR (which will target `main`). The grace-period `master` entry retires with the mirror.
|
|
123
|
+
|
|
124
|
+
4. **Track retirement as an issue with an explicit sunset date.** Mirror workflows are the classic case of "temporary" becoming "permanent". A calendar event and a GitHub issue with a date-anchored title enforce end-of-life.
|
|
125
|
+
|
|
126
|
+
5. **Rulesets-as-code files must be renamed alongside their JSON targets.** GitHub auto-migrates the *live* ruleset assignment; the file in your repo is the source-of-truth if you ever reapply. Both must agree.
|
|
127
|
+
|
|
128
|
+
The reference implementation landed as [PR #961](https://github.com/sebastienrousseau/dotfiles/pull/961) with the retirement issue tracked at [#962](https://github.com/sebastienrousseau/dotfiles/issues/962), shipped in [v0.2.510](https://github.com/sebastienrousseau/dotfiles/releases/tag/v0.2.510).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Articles
|
|
3
|
+
description: Long-form writing on the design and operation of the .dotfiles framework.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Articles
|
|
7
|
+
|
|
8
|
+
Long-form writing on the design and operation of the [.dotfiles framework](https://github.com/sebastienrousseau/dotfiles) — deep dives into performance, supply-chain safety, developer experience, and the architectural decisions that shape a mature open-source dotfiles project.
|
|
9
|
+
|
|
10
|
+
<div class="grid cards" markdown>
|
|
11
|
+
|
|
12
|
+
- :material-console:{ .lg .middle } **[Fish Startup in 2026](2026-07-05-fish-startup-abbr.md)**
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
Cutting interactive shell latency by 48% with `abbr` on multi-shell dotfiles. One printf format change + one chezmoi hook = 112 ms saved per fresh terminal.
|
|
17
|
+
|
|
18
|
+
*5 Jul 2026 · 10 min read*
|
|
19
|
+
|
|
20
|
+
- :material-source-branch:{ .lg .middle } **[Renaming `master` to `main`](2026-07-05-master-to-main-rename-runbook.md)**
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
A zero-downtime runbook for a repository at the heart of an open-source supply chain. 96 in-repo edits, a 12-month mirror workflow, zero broken install URLs.
|
|
25
|
+
|
|
26
|
+
*5 Jul 2026 · 12 min read*
|
|
27
|
+
|
|
28
|
+
- :material-palette:{ .lg .middle } **[Custom Documentation Sites in 2026](2026-07-05-custom-mkdocs-material-dark-theme.md)**
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
Building a distinctive dark-themed developer reference on MkDocs Material — three files, no theme forking, WCAG AAA contrast, hash-locked build.
|
|
33
|
+
|
|
34
|
+
*5 Jul 2026 · 11 min read*
|
|
35
|
+
|
|
36
|
+
</div>
|
|
@@ -4,7 +4,7 @@ render_with_liquid: false
|
|
|
4
4
|
|
|
5
5
|
# Introduction
|
|
6
6
|
|
|
7
|
-
This manual describes `.dotfiles` v0.2.
|
|
7
|
+
This manual describes `.dotfiles` v0.2.511 — a trusted agent workstation baseline for macOS, Linux, and WSL.
|
|
8
8
|
|
|
9
9
|
The repository is more than a personal dotfiles collection. It ships as workstation infrastructure: signed, attested, multi-platform, AI-aware, and self-healing. Chezmoi handles templating and platform differences. The `dot` CLI sits on top and coordinates lifecycle operations.
|
|
10
10
|
|
package/install.sh
CHANGED
|
@@ -71,7 +71,7 @@ show_help() {
|
|
|
71
71
|
Usage: install.sh [version] [options]
|
|
72
72
|
|
|
73
73
|
Arguments:
|
|
74
|
-
version The version (tag or branch) to install (default: v0.2.
|
|
74
|
+
version The version (tag or branch) to install (default: v0.2.511)
|
|
75
75
|
|
|
76
76
|
Options:
|
|
77
77
|
--help Show this help message
|
|
@@ -87,7 +87,7 @@ EOF
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
main() {
|
|
90
|
-
local version="v0.2.
|
|
90
|
+
local version="v0.2.511"
|
|
91
91
|
local version_set=0
|
|
92
92
|
local minimal=0
|
|
93
93
|
local provision="${DOTFILES_PROVISION:-0}"
|
|
@@ -117,7 +117,7 @@ main() {
|
|
|
117
117
|
# like `foobar` doesn't trigger a 30s+ network download attempt.
|
|
118
118
|
# Caught by the install.sh fuzz harness (#881).
|
|
119
119
|
if [[ ! "$arg" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+([-+][a-zA-Z0-9.-]+)?$ ]]; then
|
|
120
|
-
error "Unrecognized positional argument '$arg' — expected a semver version (e.g. v0.2.
|
|
120
|
+
error "Unrecognized positional argument '$arg' — expected a semver version (e.g. v0.2.511)."
|
|
121
121
|
fi
|
|
122
122
|
version="$arg"
|
|
123
123
|
version_set=1
|
|
@@ -362,7 +362,7 @@ main() {
|
|
|
362
362
|
# 6. Initialize & Apply
|
|
363
363
|
step "Applying Configuration..."
|
|
364
364
|
|
|
365
|
-
# ── Auto-migration for v0.2.
|
|
365
|
+
# ── Auto-migration for v0.2.511 reorg ─────────────────────────────────
|
|
366
366
|
# If the user is upgrading from a pre-0.2.503 install, run the
|
|
367
367
|
# migration script BEFORE `chezmoi apply` so the reorg's source-
|
|
368
368
|
# path moves don't cause chezmoi to delete deployed files.
|
|
@@ -371,7 +371,7 @@ main() {
|
|
|
371
371
|
for migrate_src in "$SOURCE_DIR" "$LEGACY_SOURCE_DIR"; do
|
|
372
372
|
migrate_script="$migrate_src/install/migrate/migrate-v0_2-to-v0_2_503.sh"
|
|
373
373
|
if [[ -x "$migrate_script" ]]; then
|
|
374
|
-
echo " Running v0.2.
|
|
374
|
+
echo " Running v0.2.511 migration (idempotent; safe on fresh installs)..."
|
|
375
375
|
"$migrate_script" || echo " migration exited non-zero — continuing apply"
|
|
376
376
|
break
|
|
377
377
|
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sebastienrousseau/dotfiles",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.511",
|
|
4
4
|
"description": "The Trusted Shell Platform — Universal dotfiles managed by Chezmoi. Features Bash & Zsh for macOS, Linux & WSL. Rust modern tooling & enterprise-grade security.",
|
|
5
5
|
"main": "install.sh",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,25 @@ resolve_source_dir() {
|
|
|
22
22
|
return 1
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
# Descend into the chezmoi source subdir (per `.chezmoiroot`) if
|
|
26
|
+
# present — post-Phase-4b (v0.2.503) the aliases + functions trees
|
|
27
|
+
# live under `defaults/.chezmoitemplates/`, not at the repo root.
|
|
28
|
+
# See `resolve_chezmoi_source_dir` in lib/dot/utils.sh.
|
|
29
|
+
resolve_chezmoi_source_dir() {
|
|
30
|
+
local root="$1"
|
|
31
|
+
if [[ -f "$root/.chezmoiroot" ]]; then
|
|
32
|
+
local sub
|
|
33
|
+
sub="$(head -1 "$root/.chezmoiroot" | tr -d '[:space:]')"
|
|
34
|
+
if [[ -n "$sub" && -d "$root/$sub" ]]; then
|
|
35
|
+
printf "%s\n" "$root/$sub"
|
|
36
|
+
return
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
printf "%s\n" "$root"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
src_root="${1:-$(resolve_source_dir)}"
|
|
43
|
+
src_dir="$(resolve_chezmoi_source_dir "$src_root")"
|
|
26
44
|
|
|
27
45
|
rg -n \
|
|
28
46
|
-e '^[[:space:]]*alias[[:space:]]+[A-Za-z0-9_.:-]+=' \
|
|
@@ -32,8 +32,12 @@ check_alias_in_config() {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
# 1. Verify Docs
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
# Post-Phase-4b (v0.2.503): chezmoi source is under the subdir
|
|
36
|
+
# named in .chezmoiroot (typically "defaults/"). Paths without the
|
|
37
|
+
# prefix silently return "missing" from any CWD other than
|
|
38
|
+
# ~/.dotfiles/defaults/.
|
|
39
|
+
check_file "defaults/.chezmoitemplates/aliases/security/README.md"
|
|
40
|
+
check_file "defaults/.chezmoitemplates/aliases/legal/README.md"
|
|
37
41
|
|
|
38
42
|
# 2. Verify Scripts
|
|
39
43
|
check_file "scripts/security/lock-configs.sh"
|
|
@@ -67,20 +67,24 @@ else
|
|
|
67
67
|
fi
|
|
68
68
|
|
|
69
69
|
ui_section "Node/Python pins"
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
# Chezmoi-tracked files (dot_*) live under $cm_src (defaults/) post-
|
|
71
|
+
# Phase-4b, NOT at repo root. Using $src_dir here silently rendered
|
|
72
|
+
# an empty section on every workstation.
|
|
73
|
+
if [[ -f "$cm_src/dot_node-version" ]]; then
|
|
74
|
+
ui_kv "node" "$(cat "$cm_src/dot_node-version")"
|
|
72
75
|
fi
|
|
73
|
-
if [[ -f "$
|
|
74
|
-
ui_kv "noderc" "$(sed -n '1p' "$
|
|
76
|
+
if [[ -f "$cm_src/dot_noderc.tmpl" ]]; then
|
|
77
|
+
ui_kv "noderc" "$(sed -n '1p' "$cm_src/dot_noderc.tmpl")"
|
|
75
78
|
fi
|
|
76
79
|
|
|
77
80
|
ui_section "Package manager locks"
|
|
78
|
-
if [[ -f "$
|
|
81
|
+
if [[ -f "$cm_src/dot_config/shell/Brewfile" ]]; then
|
|
79
82
|
ui_kv "brew" "Brewfile present"
|
|
80
83
|
fi
|
|
81
|
-
if [[ -f "$
|
|
84
|
+
if [[ -f "$cm_src/dot_config/shell/Brewfile.cli" ]]; then
|
|
82
85
|
ui_kv "brew-cli" "Brewfile.cli present"
|
|
83
86
|
fi
|
|
87
|
+
# package.json (if any) is a repo-root artefact — use $src_dir.
|
|
84
88
|
if [[ -f "$src_dir/package.json" ]]; then
|
|
85
89
|
ui_kv "node" "package.json version pinned"
|
|
86
90
|
fi
|
|
@@ -405,7 +405,11 @@ EOF
|
|
|
405
405
|
a2a-card)
|
|
406
406
|
local a2a_card_file json_mode=0 validate_mode=0 strict_mode=0
|
|
407
407
|
local repo_root
|
|
408
|
-
|
|
408
|
+
# `.well-known/` is NOT chezmoi-tracked — it lives at the
|
|
409
|
+
# actual repo root, not the descended `defaults/` subdir.
|
|
410
|
+
# Bypass `_agent_repo_root` (which descends) and read the
|
|
411
|
+
# true root directly.
|
|
412
|
+
repo_root="$(require_source_dir)"
|
|
409
413
|
a2a_card_file="$repo_root/.well-known/agent-card.json"
|
|
410
414
|
while [[ $# -gt 0 ]]; do
|
|
411
415
|
case "$1" in
|
|
@@ -102,7 +102,10 @@ cmd_fleet_status() {
|
|
|
102
102
|
local last_apply=""
|
|
103
103
|
local state_log="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/dot.log"
|
|
104
104
|
if [[ -f "$state_log" ]]; then
|
|
105
|
-
|
|
105
|
+
# `set -euo pipefail` at the top of this script kills the whole
|
|
106
|
+
# command when grep matches nothing (rc=1). Wrap the pipeline so
|
|
107
|
+
# `last_apply` cleanly becomes empty and the UI still renders.
|
|
108
|
+
last_apply="$(grep 'apply' "$state_log" 2>/dev/null | tail -1 | sed -n 's/^\[\([^]]*\)\].*/\1/p' || true)"
|
|
106
109
|
fi
|
|
107
110
|
|
|
108
111
|
if [[ "$json_mode" -eq 1 ]]; then
|
|
@@ -102,7 +102,13 @@ _registry_fetch() {
|
|
|
102
102
|
fi
|
|
103
103
|
local tmp
|
|
104
104
|
tmp="$(mktemp "${cache_file}.XXXXXX")"
|
|
105
|
-
|
|
105
|
+
# `-fsSL -o file` should be silent, but with some upstreams (e.g.
|
|
106
|
+
# GitHub Pages) curl still writes a stray newline to stdout. That
|
|
107
|
+
# newline leaks into the caller's `$(_registry_fetch)` and later
|
|
108
|
+
# into `jq FILE` as a two-argument invocation
|
|
109
|
+
# (`jq \n /path/to/file`), producing a confusing
|
|
110
|
+
# "Could not open file" error. Silence stdout explicitly.
|
|
111
|
+
if ! curl -fsSL --max-time 15 -o "$tmp" "$url" >/dev/null; then
|
|
106
112
|
rm -f "$tmp"
|
|
107
113
|
if [[ -s "$cache_file" ]]; then
|
|
108
114
|
ui_warn "registry" "fetch failed; using stale cache at $cache_file"
|
|
@@ -189,8 +189,17 @@ show_language_package_managers() {
|
|
|
189
189
|
echo " pip: $(pip3 --version | cut -d' ' -f2)"
|
|
190
190
|
fi
|
|
191
191
|
if has_command pipx; then
|
|
192
|
-
local pipx_installed
|
|
193
|
-
|
|
192
|
+
local pipx_installed pipx_list_out
|
|
193
|
+
# `pipx list --short` returns non-zero when any installed package
|
|
194
|
+
# has a broken interpreter (common). Under `set -euo pipefail` a
|
|
195
|
+
# naive `local x=$(pipx …)` cascades and kills `dot packages`
|
|
196
|
+
# mid-output. Capture the pipeline separately so we can fall back
|
|
197
|
+
# cleanly without stray "N/A" lines from pipe-with-|| tricks.
|
|
198
|
+
if pipx_list_out="$(pipx list --short 2>/dev/null)"; then
|
|
199
|
+
pipx_installed="$(printf '%s' "$pipx_list_out" | wc -l | tr -d ' ')"
|
|
200
|
+
else
|
|
201
|
+
pipx_installed="N/A"
|
|
202
|
+
fi
|
|
194
203
|
echo " pipx: $(pipx --version)"
|
|
195
204
|
echo " Installed: $pipx_installed"
|
|
196
205
|
fi
|
|
@@ -141,6 +141,6 @@ if [[ $FAILED -eq 1 ]]; then
|
|
|
141
141
|
printf '%b\\n' " (Use --no-verify to bypass if absolutely necessary)"
|
|
142
142
|
exit 1
|
|
143
143
|
else
|
|
144
|
-
printf '%b\n' "${GREEN}${BOLD}✅ Audit passed.${NC} v0.2.
|
|
144
|
+
printf '%b\n' "${GREEN}${BOLD}✅ Audit passed.${NC} v0.2.511 standards maintained."
|
|
145
145
|
exit 0
|
|
146
146
|
fi
|
|
@@ -8,7 +8,25 @@ import os
|
|
|
8
8
|
import re
|
|
9
9
|
import sys
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# Post-Phase-4b (v0.2.503): chezmoi source is under the subdir
|
|
12
|
+
# named in .chezmoiroot (typically "defaults/"). Prefer the
|
|
13
|
+
# descended path; fall back to the old top-level layout for
|
|
14
|
+
# backwards compatibility.
|
|
15
|
+
_DOTFILES_ROOT = os.path.expanduser("~/.dotfiles")
|
|
16
|
+
_CHEZMOIROOT_FILE = os.path.join(_DOTFILES_ROOT, ".chezmoiroot")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _resolve_chezmoi_root() -> str:
|
|
20
|
+
if os.path.isfile(_CHEZMOIROOT_FILE):
|
|
21
|
+
with open(_CHEZMOIROOT_FILE, "r", encoding="utf-8") as _f:
|
|
22
|
+
sub = _f.read().strip()
|
|
23
|
+
cand = os.path.join(_DOTFILES_ROOT, sub)
|
|
24
|
+
if os.path.isdir(cand):
|
|
25
|
+
return cand
|
|
26
|
+
return _DOTFILES_ROOT
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
BASE_DIR = os.path.join(_resolve_chezmoi_root(), ".chezmoitemplates")
|
|
12
30
|
ALIASES_DIR = os.path.join(BASE_DIR, "aliases")
|
|
13
31
|
FUNCTIONS_DIR = os.path.join(BASE_DIR, "functions")
|
|
14
32
|
|
package/scripts/version-sync.sh
CHANGED
|
@@ -84,6 +84,13 @@ EXCLUDE_FILES=(
|
|
|
84
84
|
# that are NOT the current dotfiles_version. Auto-syncing
|
|
85
85
|
# rewrites them incorrectly.
|
|
86
86
|
"docs/operations/ARCHITECTURE_ROADMAP.md"
|
|
87
|
+
|
|
88
|
+
# Dated release write-ups: each article records the release it
|
|
89
|
+
# shipped in (e.g. "shipped in v0.2.510" + a link to that release
|
|
90
|
+
# tag). Those refs are historical fact, not current-version claims —
|
|
91
|
+
# bumping them would falsify the history.
|
|
92
|
+
"docs/articles/2026-07-05-fish-startup-abbr.md"
|
|
93
|
+
"docs/articles/2026-07-05-master-to-main-rename-runbook.md"
|
|
87
94
|
)
|
|
88
95
|
|
|
89
96
|
# shellcheck source=../lib/dot/ui.sh
|