From adf1f5fe35c57272d1f083d5f87f05ecfaeeff77 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Wed, 9 Sep 2026 11:27:22 +0200 Subject: [PATCH] fix(blacksmith): treat a tailscale/headscale join as a network credential; time out on the slowest healthy run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First real run of the skill on a client repo. The inventory suggested migrating a nightly that joins the client's LAN through a headscale pre-auth key — the regex only knew WireGuard. A tailscale/headscale join is the same thing: a network-scoped credential that should keep originating where it does today. The timeout rule also said "1.5 × the slowest run", which on that repo was a 56-minute cache-restore stall — the exact thing the migration removes. The slowest *healthy* run is the right anchor. Co-Authored-By: Claude Fable 5.1 --- .claude-plugin/plugin.json | 2 +- skills/blacksmith/SKILL.md | 4 ++-- skills/blacksmith/scripts/inventory.sh | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a9922b8..50d88ad 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "fsh", "description": "Full Stack House Claude Code skills (idea brainstorming & kickoff, prior-art research, delivery workflow, bug/flake hunting, iterative fresh-eyes review loops, change explanation, hands-on test drives, spec/ticket/PR polishing, documentation & agent-instruction audit, CI runner migration to Blacksmith, status reporting, design, document redlines)", - "version": "1.7.0", + "version": "1.7.1", "author": { "name": "Full Stack House", "url": "https://fullstack.house" diff --git a/skills/blacksmith/SKILL.md b/skills/blacksmith/SKILL.md index 4d7a87f..3148834 100644 --- a/skills/blacksmith/SKILL.md +++ b/skills/blacksmith/SKILL.md @@ -54,7 +54,7 @@ One question decides most rows: **does this job build, run or test the app, and - Short, and neither builds nor runs the app: the branch-protection required check, `if: always()` joiners, `notify-failure`, PR commenters, `paths-filter` jobs, cancel-workflows, labelers, deployment bookkeeping, config computation. Seconds of runtime, so nothing to gain — and the merge gate in particular should sit on the most boring infrastructure available. - Waits on an external system: `kubectl rollout status`, Cloud Run deploys, health checks, cron pollers. Faster cores buy nothing, and there is no reason to hand a third-party runner the cluster kubeconfig for no gain. - `self-hosted` or an unknown custom label. It is on that hardware for a reason — cluster LAN, on-disk kubeconfig. -- Machine-local or network-scoped credentials: `KUBECONFIG` pointing at a file on the runner, a WireGuard peer config, SSH to a host that allowlists the runner's IP. +- Machine-local or network-scoped credentials: `KUBECONFIG` pointing at a file on the runner, a WireGuard or tailscale/headscale join into someone's LAN, SSH to a host that allowlists the runner's IP. - **A self-triggering workflow that has been dormant** (lists itself in `paths:` and hasn't run in 60+ days — the inventory flags these). Listing itself is normal; dormancy is the problem: your edit runs it for the first time in months, and a pre-existing failure then looks like yours. If it fails on something unrelated to runners, revert that file to byte-identical, report the failure, and don't fix it here — not even with a comment in the file, because the comment is an edit and re-fires it. **Ask once**, in a single `AskUserQuestion`, with the numbers beside each item: @@ -98,7 +98,7 @@ One question decides most rows: **does this job build, run or test the app, and Keep any "free runner disk space" step — it still guards the image's own size. Multi-platform builds: one job per platform on the matching runner (`…-arm` for arm64), no QEMU. -**Every migrated job gets `timeout-minutes`** if it lacks one. A stuck VM on either vendor otherwise burns GitHub's 6-hour default. Value: 1.5 × the slowest run in the baseline, rounded up, minimum 10. +**Every migrated job gets `timeout-minutes`** if it lacks one. A stuck VM on either vendor otherwise burns GitHub's 6-hour default. Value: 1.5 × the slowest *healthy* run in the baseline, rounded up, minimum 10 — not the slowest run outright, which may be the very stall you are removing. **`.github/actionlint.yaml`** from [`templates/actionlint.yaml`](./templates/actionlint.yaml), `{{LABELS}}` = the labels you introduced (one ` - ` line each); no repo has one before this, so create it. actionlint otherwise reads the labels as typos. diff --git a/skills/blacksmith/scripts/inventory.sh b/skills/blacksmith/scripts/inventory.sh index 46a2ff4..b3abadc 100755 --- a/skills/blacksmith/scripts/inventory.sh +++ b/skills/blacksmith/scripts/inventory.sh @@ -144,7 +144,8 @@ for f in files: r'\bssh\b', r'\bscp\b', r'rollout status', r'\bkubectl\b'], runs)) health = flag('health-check', hit([r'rollout status', r'curl[^\n]*(health|ready)', r'wait-for'], runs)) local_cred = flag('machine-local-cred', hit([r'KUBECONFIG[\'"]?:\s*[\'"]?/', r'ci-kubeconfig'], envs)) - netcred = flag('network-cred', hit([r'wireguard', r'wg-connect', r'WG_CONFIG', r'openvpn'], + netcred = flag('network-cred', hit([r'wireguard', r'wg-connect', r'WG_CONFIG', r'openvpn', + r'tailscale', r'headscale', r'TS_AUTHKEY'], uses + runs + envs + withs)) gate = flag('always/failure-if', bool(re.search(r'always\(\)|failure\(\)', cond))) glue = flag('glue-action', hit([r'slackapi/', r'sticky-pull-request-comment', r'dorny/paths-filter',