Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3112b4ce92 | ||
|
|
51a8c25b4d | ||
|
|
d40169f2b7 | ||
|
|
d4f0416dc5 | ||
|
|
abd81f939a | ||
|
|
e42d2ba9cb | ||
|
|
68cebc0bbb |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "skills",
|
||||
"description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.",
|
||||
"version": "0.6.1",
|
||||
"version": "0.7.0",
|
||||
"author": {
|
||||
"name": "Mathias",
|
||||
"url": "https://git.d-ma.be/mathias"
|
||||
|
||||
@@ -1,23 +1,52 @@
|
||||
name: release
|
||||
name: CI
|
||||
|
||||
# Auto-tag every push to main with a semver bump derived from the latest
|
||||
# commit footer:
|
||||
# Bump-Type: major → vX.0.0
|
||||
# Bump-Type: minor → v0.Y.0
|
||||
# (default) → v0.0.Z (patch)
|
||||
# Two jobs, in order:
|
||||
#
|
||||
# Skips when the latest commit is itself the auto-tagger (commit message
|
||||
# contains "[skip-release]") to prevent recursion. Tag is pushed back
|
||||
# to the repo using the GITEA_TOKEN provided by act_runner.
|
||||
# gate — `task check` (structure, frontmatter, manifest/tag parity) plus
|
||||
# `task test` (hooks, doctor, release-skip). Runs on every push and
|
||||
# every PR. Before this existed the suite ran nowhere: CI reported
|
||||
# green having tested nothing (#22).
|
||||
#
|
||||
# tag — auto-tags a push to main with a semver bump derived from the latest
|
||||
# commit footer:
|
||||
# Bump-Type: major → vX.0.0
|
||||
# Bump-Type: minor → v0.Y.0
|
||||
# (default) → v0.0.Z (patch)
|
||||
# Skips when the commit carries the `Skip-Release: true` trailer (the
|
||||
# tagger's own manifest bump) to prevent recursion. Gated on `gate`,
|
||||
# so a red check cannot ship a tag.
|
||||
|
||||
on:
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
branches: ["**"]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
gate:
|
||||
name: Check / Test
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# `task check` compares plugin.json's version to `git describe`;
|
||||
# a shallow checkout has no tags and would skip that assertion.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Verify toolchain
|
||||
run: task --version
|
||||
|
||||
- name: Structure gate
|
||||
run: task check
|
||||
|
||||
- name: Shell test suite
|
||||
run: task test
|
||||
|
||||
tag:
|
||||
name: Tag release
|
||||
needs: gate
|
||||
runs-on: self-hosted
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -72,16 +101,19 @@ jobs:
|
||||
manifest=.claude-plugin/plugin.json
|
||||
version="${TAG#v}"
|
||||
current=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1)
|
||||
git tag -a "$TAG" -m "release $TAG"
|
||||
|
||||
if [ "$current" != "$version" ]; then
|
||||
sed -i "s/\"version\"[[:space:]]*:[[:space:]]*\"$current\"/\"version\": \"$version\"/" "$manifest"
|
||||
git add "$manifest"
|
||||
# The trailer below stops this push from re-triggering the tagger.
|
||||
git commit -m "chore(release): plugin.json version $version" -m "Skip-Release: true"
|
||||
git push origin HEAD:main
|
||||
# Commit and tag go up in ONE push: the gate job runs `task check`
|
||||
# on the bump commit, and that asserts manifest == latest tag. Two
|
||||
# separate pushes leave a window where it legitimately fails.
|
||||
git push origin HEAD:main "$TAG"
|
||||
echo "bumped $manifest to $version"
|
||||
else
|
||||
git push origin "$TAG"
|
||||
fi
|
||||
|
||||
git tag -a "$TAG" -m "release $TAG"
|
||||
# gitea-actions checkout uses an x-access-token URL; reuse it.
|
||||
git push origin "$TAG"
|
||||
echo "pushed $TAG"
|
||||
@@ -30,6 +30,7 @@ This index lists all available engineering skills. Load the full SKILL.md on dem
|
||||
| `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing |
|
||||
| `stage-gate-review` | Stage-gate product governance (go/no-go council) | Preparing a Product Council ask; building a go/no-go checklist; gate review |
|
||||
| `web-shot` | Screenshot a running web UI via a koala k3s Playwright job | Visual verification of a running UI when no local browser exists |
|
||||
| `cad-babysit` | Focused human-in-the-loop session working CAD's stuck queue (read-only) | Picking up whatever `cad stuck` flags, with a human present |
|
||||
| `superpowers-brainstorming` | Structured divergent brainstorming | Generating or expanding a set of ideas |
|
||||
| `superpowers-writing-plans` | Writing implementation plans | Planning substantial work before coding |
|
||||
| `superpowers-executing-plans` | Executing a written plan step by step | Running an approved plan to completion |
|
||||
|
||||
@@ -109,6 +109,15 @@ do
|
||||
continue
|
||||
fi
|
||||
|
||||
# A real directory where a link belongs holds content nothing manages: it
|
||||
# cannot receive new skills and silently shadows the checkout. Distinguished
|
||||
# from the foreign-checkout case below because the fix differs — move it
|
||||
# aside and re-run install.sh, rather than re-point a link.
|
||||
if [ ! -L "$dir" ] && [ -d "$dir" ]; then
|
||||
add FAIL unmanaged "$dir is a real directory, not a link into the checkout — it holds $(count_skills "$dir") unmanaged skill(s); move it aside and re-run install.sh"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Resolve to a physical path and confirm it lands inside THIS checkout.
|
||||
# A link that resolves to a different checkout is the dangerous case: it
|
||||
# works, so nothing complains, while serving stale content.
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
---
|
||||
name: cad-babysit
|
||||
description: Focused human-in-the-loop session working CAD's stuck queue (cad-stuck output). Read-only — diagnoses, never dispatches, arms a label, or closes an issue. Use when picking up whatever CAD has run into trouble on, with a human present.
|
||||
---
|
||||
|
||||
# CAD Babysit
|
||||
|
||||
## Overview
|
||||
|
||||
A deliberately narrow session: pick up whatever CAD has run into trouble on, work it with a
|
||||
human present, and stop. This is not an autonomous supervisor — it is a babysitter with a
|
||||
person in the room, and that is the point, not a limitation to route around later.
|
||||
|
||||
**Core principle:** an unattended supervisor reproduces the exact failure it exists to catch.
|
||||
`dispatch` ran dead for eight weeks because it exited 0 and nobody noticed (`dispatch#33`); a
|
||||
long-running watcher process that dies quietly has the same property one level up, and
|
||||
manufactures false confidence while doing it. With a human present, **the human is the
|
||||
liveness check** — that removes the objection entirely and needs no new machinery.
|
||||
|
||||
This skill is the ritual around `cad stuck` (`mathias/cad#14`), not a replacement for it.
|
||||
Without that query it would be a checklist with no input; the query without this skill is
|
||||
data nobody reads carefully. Neither closes the loop alone.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Picking up CAD's stuck queue with a human present and time set aside to work it
|
||||
- After `dispatch` (or any executor) reports a failure and it is not immediately obvious
|
||||
whether it is an infrastructure fault or a real task outcome
|
||||
- Reviewing the "awaiting human" queue (issues CI has verified but nobody has closed)
|
||||
|
||||
**Do not use for:**
|
||||
- Unattended/scheduled runs. This skill assumes a human is present for its entire duration —
|
||||
if nobody is watching, its central safety property (human-as-liveness-check) does not hold.
|
||||
- Dispatching, arming a redispatch label, or closing an issue. This skill only ever diagnoses;
|
||||
see Iron Laws.
|
||||
|
||||
## Iron Laws
|
||||
|
||||
1. **Read-only, always.** No dispatching, no arming labels (including `status:redispatch`), no
|
||||
closing or reopening an issue. A reviewer that can act on its own findings has no
|
||||
independent check — control stays where it already is.
|
||||
2. **Every finding cites a locator.** A run id, commit sha, comment permalink, or a specific
|
||||
log line. A finding without one is an unfalsifiable opinion about a machine — do not emit
|
||||
it, and do not soften this into "probably" language instead of dropping it.
|
||||
3. **Re-derive, never recall.** State lives in Gitea and Actions, not in this session. Start
|
||||
cold every time: run `cad stuck` fresh, do not carry forward a prior session's worklist.
|
||||
Sessions get compacted and lose exactly the cross-run history that makes a stale worklist
|
||||
dangerous to trust.
|
||||
4. **Distinguish "blocked" from "awaiting human".** `cad stuck` already makes this split
|
||||
(`stuck` vs `awaiting_human`); preserve it in every summary. The `awaiting_human` class is
|
||||
the human gate working, not a bottleneck — do not report it as one.
|
||||
5. **Anything that is the operator's call surfaces as `DECISION NEEDED`, never gets decided
|
||||
for them.** See Output Format.
|
||||
|
||||
## Process
|
||||
|
||||
### Phase 1 — Query (re-derive the worklist)
|
||||
|
||||
Run `cad stuck` against the estate (the `dispatch-repos.txt` scope, or whatever repo set the
|
||||
operator names). This is the only input. Do not substitute a memory of a prior run, a brain
|
||||
note, or this issue's own worked examples — all three can be stale within the same day (a
|
||||
taxonomy row's cited live example can stop reproducing between when it was written and when
|
||||
you run the query, because the underlying state genuinely changed; that is the design working,
|
||||
not a bug to paper over).
|
||||
|
||||
Group the output by class exactly as `cad stuck` returns it: `stuck` first, `awaiting_human`
|
||||
second. Pick ONE row to work per pass — this skill is deliberately narrow, not a full-estate
|
||||
sweep.
|
||||
|
||||
### Phase 2 — Read the issue's own comments FIRST
|
||||
|
||||
Before anything else, before touching Actions or pod logs: read every comment on the issue,
|
||||
oldest first. This step exists because it was skipped once and it cost a session — a confident
|
||||
wrong root cause was produced from pod logs while the correct diagnosis sat in the issue's own
|
||||
comment thread from nine days earlier (`cad-atlas#10`). The rule "read issue comments before
|
||||
working" was already written down elsewhere and was not followed; it is written down again
|
||||
here because that is exactly the kind of already-known thing a rushed session skips.
|
||||
|
||||
What to look for: the bot's own report comments (routing decision, ground-truth verdict,
|
||||
attempt count), and any human comment that already contains the diagnosis.
|
||||
|
||||
### Phase 3 — THEN the Actions job logs
|
||||
|
||||
Only after Phase 2 turns up nothing conclusive, read the CI job logs:
|
||||
|
||||
```
|
||||
GET /repos/{owner}/{repo}/actions/runs # find the run, match on .run_number
|
||||
GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs # returns 200, full text, secrets masked
|
||||
```
|
||||
|
||||
This endpoint works. A prior note claiming it 404s was wrong and cost a diagnosis the day this
|
||||
skill was written — do not rediscover that. Get the run id from `/actions/runs`, not from the
|
||||
`run_number` shown in a comment (they are different fields; match on `.run_number` to find the
|
||||
internal `id` the jobs endpoint needs).
|
||||
|
||||
This is where the two most valuable findings of the day this skill is drawn from lived: an
|
||||
agent issuing the identical refused command five times and stopping (never trying the tools it
|
||||
was actually permitted to use), and a different model, same denial, writing a Go program to
|
||||
route around it. Neither is visible from the issue comments alone — no metric produces either
|
||||
of them. They need a reader of the raw transcript.
|
||||
|
||||
### Phase 4 — Separate infrastructure fault from task outcome
|
||||
|
||||
A run that died before turn 1 says nothing about the issue itself. Before concluding anything
|
||||
about the WORK, check whether the run reached the model at all:
|
||||
|
||||
- Zero turns, a 2-3 record transcript, a "could not start" comment, or a claim label with no
|
||||
attempt comment behind it → infrastructure fault. This is not evidence the issue is hard, ill
|
||||
specified, or that the assigned model is unsuitable — it is evidence something upstream
|
||||
(credit, a key, MCP connectivity, the executor binary) is broken.
|
||||
- A run that reached the model, drove tools, and stopped on a ceiling, a denial loop, or a
|
||||
genuine task failure → a real task outcome, worth diagnosing on its own terms.
|
||||
|
||||
Four claim labels were burned this exact way in one estate sweep and the tracker read them as
|
||||
four handled issues (`dispatch#33`). Get this distinction wrong and everything downstream is
|
||||
about the wrong thing.
|
||||
|
||||
### Phase 5 — Decide, or say `DECISION NEEDED`
|
||||
|
||||
For each row worked, produce one of:
|
||||
|
||||
- **A closed diagnosis** — cause identified, cited to a locator, no operator judgment call
|
||||
required (e.g. "infrastructure fault, credit exhausted, see comment #N — retry is automatic
|
||||
on the next cycle, no action needed").
|
||||
- **A `DECISION NEEDED`** — anything that is genuinely the operator's call: whether to close a
|
||||
verified-but-open issue, whether to raise a ceiling, whether to abandon an issue that keeps
|
||||
hitting the same denial. Never make this call on the operator's behalf.
|
||||
|
||||
This skill's OWN last act, every time, is to stop — not to dispatch, not to redispatch, not to
|
||||
close. Ending a pass is itself the enforcement of Iron Law 1.
|
||||
|
||||
## Output Format
|
||||
|
||||
For a closed diagnosis:
|
||||
|
||||
```
|
||||
DIAGNOSIS: <repo>#<issue> — <category from cad stuck, e.g. ceiling_hit>
|
||||
CAUSE: <one sentence, cites the mechanism>
|
||||
LOCATOR: <url or run id / log line>
|
||||
ACTION: <what happens automatically, or "none — informational">
|
||||
```
|
||||
|
||||
For anything requiring a human call:
|
||||
|
||||
```
|
||||
DECISION NEEDED: <repo>#<issue>
|
||||
QUESTION: <the exact thing only the operator can decide>
|
||||
OPTIONS:
|
||||
1. <option> — <consequence>
|
||||
2. <option> — <consequence>
|
||||
RECOMMENDATION: <your pick, with reasoning — but the operator decides>
|
||||
BLOCKS: <what stays stuck until this is answered, if anything>
|
||||
```
|
||||
|
||||
End every pass with a one-line summary: `N diagnosed, M decisions needed, stopping.`
|
||||
|
||||
## Worked Example
|
||||
|
||||
`cad stuck` returns `mathias/cad-atlas#10 [near_attempt_cap] 4 dispatch attempts recorded, at
|
||||
or above the near-cap threshold (4 of MaxAttempts=5).`
|
||||
|
||||
Phase 2 (issue comments): the issue's own thread already explains the pattern — repeated
|
||||
`status:redispatch` re-arming without the label being cleared after a completed attempt.
|
||||
|
||||
Phase 3: not needed — Phase 2 was conclusive.
|
||||
|
||||
Phase 4: each of the 4 attempts reached the model and ran (not an infrastructure fault) — a
|
||||
real re-fire pattern, not a claim-label artifact.
|
||||
|
||||
Phase 5 output:
|
||||
|
||||
```
|
||||
DECISION NEEDED: mathias/cad-atlas#10
|
||||
QUESTION: one more automatic attempt remains before MaxAttempts=5 gives up permanently. Let
|
||||
it fire, or intervene now (fix the underlying cause, or manually close/redirect the issue)?
|
||||
OPTIONS:
|
||||
1. Let it fire — costs one more headless run; if it fails the same way, dispatch gives up
|
||||
automatically and posts a terminal comment. No action needed until then.
|
||||
2. Intervene now — diagnose why redispatch keeps re-arming (likely: the watcher isn't
|
||||
clearing RedispatchLabel after a serviced run) and fix that before the cap is spent on
|
||||
a symptom rather than the cause.
|
||||
RECOMMENDATION: option 2. The pattern (redispatch not self-clearing after a completed run) is
|
||||
already documented as a known operational hazard (dispatch#54) — spending the last attempt
|
||||
on it again teaches nothing new.
|
||||
BLOCKS: nothing else; this issue's own progress.
|
||||
```
|
||||
|
||||
`1 diagnosed, 1 decision needed, stopping.`
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why It Fails |
|
||||
|---|---|
|
||||
| Diagnosing from pod/Actions logs before reading issue comments | The exact mistake that cost a session — the answer was already written down nine days earlier. |
|
||||
| Reporting `awaiting_human` rows as a backlog or bottleneck | It is the human gate working. Misreporting it manufactures urgency that isn't there and trains the operator to distrust the queue. |
|
||||
| Treating a zero-turn, could-not-start run as evidence about the issue | It is an infrastructure fault. Four claim labels were burned exactly this way and read as four handled issues. |
|
||||
| Deciding a `DECISION NEEDED` case on the operator's behalf "to save time" | Removes the one property that makes this skill safe to run unattended-adjacent: the human decides, this skill only prepares the decision. |
|
||||
| Applying `status:redispatch`, closing an issue, or adding any label from this skill | Control stays where it is. This skill has no write path and should never be given one. |
|
||||
| Carrying forward a worklist from an earlier session instead of re-running `cad stuck` | State changes between sessions (see the worked note on `brain-gardener#190` in `cad#14`'s own acceptance run — its trigger label was gone hours later). A stale worklist gives false confidence, which is the specific failure mode this whole skill exists to avoid. |
|
||||
|
||||
## Honest Limitation
|
||||
|
||||
This skill encodes the *repeatable* diagnostic sequence: read comments, then logs, separate
|
||||
fault from outcome, verify by exit code, re-derive rather than recall. It reliably catches the
|
||||
taxonomy `cad stuck` already knows about. It will not, by itself, produce a genuinely novel
|
||||
finding the way an unhurried human-plus-agent reading session did the day this skill was
|
||||
written — that came from noticing things (an agent editing the file that authorizes it, an
|
||||
issue body recommending a command the policy forbids) that no checklist anticipates. Expect
|
||||
this skill to be reliable on known failure modes and to miss the next new one. That is an
|
||||
acceptable trade as long as nobody mistakes a clean pass through this skill for "nothing is
|
||||
wrong" — it means "nothing on the known list is wrong."
|
||||
|
||||
## Brain MCP Integration
|
||||
|
||||
**At session start:**
|
||||
- Run `brain_query` for the domain (`cad`, `dispatch`, the specific repo) to check whether a
|
||||
finding this session is about to produce has already been diagnosed and written up. Do not
|
||||
re-diagnose a known pattern from scratch every session.
|
||||
|
||||
**After a genuinely novel finding (not already in the taxonomy or the brain):**
|
||||
- `brain_write` the pattern, generalized beyond the specific issue — the failure mode, not the
|
||||
issue number. Do not write routine, already-known-taxonomy findings; that is what `cad
|
||||
stuck`'s own output already records, durably, in Gitea.
|
||||
|
||||
**Never:**
|
||||
- Write a `DECISION NEEDED` outcome to the brain before the operator has actually decided —
|
||||
that would record a guess as a resolution.
|
||||
|
||||
### Logging
|
||||
|
||||
Call `session_log` once at the end of every phase to record the outcome.
|
||||
|
||||
**At end of each phase:**
|
||||
- `session_log` with `{skill: "cad-babysit", phase: "<phase-name>", final_status: "pass" |
|
||||
"fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root:
|
||||
"<absolute path>"}`
|
||||
|
||||
**Phases for this skill:** query, read-comments, read-actions-logs, classify-fault-vs-outcome,
|
||||
decide
|
||||
|
||||
**Status semantics:**
|
||||
- `pass` — the phase's intended outcome was reached (worklist re-derived; comments read;
|
||||
fault/outcome separated; a diagnosis or `DECISION NEEDED` was produced).
|
||||
- `fail` — the phase could not be completed (e.g. `cad stuck` errored, Gitea was unreachable).
|
||||
- `skip` — phase intentionally skipped (e.g. Phase 3 skipped because Phase 2 was conclusive).
|
||||
|
||||
## Mode 2 Routing Note
|
||||
|
||||
Phases 2-4 (reading comments/logs, classifying fault vs outcome) are judgment-heavy and should
|
||||
stay Mode 1. Phase 1 (running `cad stuck` and grouping its output) is mechanical and a Mode 2
|
||||
routing candidate once Plan 6 ships the routing pod.
|
||||
|
||||
## Cross-References
|
||||
|
||||
- Depends on `mathias/cad#14`'s `cad stuck` query — this skill is the ritual around it, not a
|
||||
replacement.
|
||||
- Load `debug` for the hypothesis-generation discipline if Phase 4's fault/outcome split is
|
||||
itself ambiguous.
|
||||
- Load `secret-hygiene` before reading raw Actions job logs if the repo is client-tagged —
|
||||
logs are secret-masked by Gitea but a pasted excerpt into a summary is not.
|
||||
- `mathias/cad-atlas#11` renders the same `cad stuck` output as a pipeline overlay — read that
|
||||
instead of this skill's text output when a visual "where is everything stuck" view is wanted
|
||||
over working one row in depth.
|
||||
+33
-38
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: gitea-ci
|
||||
description: End-to-end Gitea CI/CD setup with self-hosted act_runner, quality gate pipeline, buildctl image build, k3s deploy, and GitHub mirror. Covers act_runner-specific gotchas that are not documented anywhere.
|
||||
description: End-to-end Gitea CI/CD setup with self-hosted act_runner, quality gate pipeline, shared buildctl image-build script, k3s deploy, and GitHub mirror. Covers act_runner-specific gotchas that are not documented anywhere.
|
||||
---
|
||||
|
||||
# Gitea CI/CD Pipeline Setup
|
||||
@@ -133,44 +133,39 @@ jobs:
|
||||
echo "version-tag=${REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# infra#309: do NOT paste a build implementation here. The estate has
|
||||
# exactly one, mathias/infra's ci-templates/build-image.sh, and each
|
||||
# repo carries a byte-identical copy at .gitea/build-image.sh that a
|
||||
# drift gate keeps current.
|
||||
#
|
||||
# Copying the implementation is what made the buildah->buildctl
|
||||
# migration (infra#241) an eleven-repo sprint, and what left this very
|
||||
# skill handing out the broken version for weeks after the app repos
|
||||
# were fixed. Per-repo differences are INPUTS, not edits to the copy.
|
||||
#
|
||||
# To adopt in a new repo:
|
||||
# 1. add the repo to mathias/infra ci-templates/adopters.txt
|
||||
# 2. run `task ci:sync` in mathias/infra -- it writes the copy
|
||||
# 3. commit .gitea/build-image.sh in your repo
|
||||
# 4. call it as below
|
||||
#
|
||||
# Skipping step 1 leaves the copy unmanaged: it works today and falls
|
||||
# silently behind the next time the canonical file changes.
|
||||
#
|
||||
# Inputs: IMAGE (required, usually the workflow-level env), SHA_TAG,
|
||||
# VERSION_TAG, GIT_SHA, IMAGE_SOURCE, BUILD_ARGS, EXTRA_LABELS,
|
||||
# CONTEXT, DOCKERFILE_DIR, DOCKERFILE_NAME, PUSH_LATEST. Read the
|
||||
# canonical file's header for the full list.
|
||||
#
|
||||
# `bash <script>`, not a direct exec: a file written through the Gitea
|
||||
# API lands 0644, and CI that depends on a file mode is a trap.
|
||||
- name: Build and push to local registry
|
||||
run: |
|
||||
set -e
|
||||
# buildctl, NOT buildah (infra#241). The runner is a pod with
|
||||
# `capabilities: drop [ALL]` and `allowPrivilegeEscalation: false`,
|
||||
# so rootless buildah cannot remap UID/GID for a fresh base-image
|
||||
# pull and the pod has no OCI runtime -- every `RUN` step fails.
|
||||
# buildctl is a thin client: it hands the build to the host's
|
||||
# buildkitd over a socket, so the privileged work happens on the
|
||||
# host and the pod's securityContext is not in the picture.
|
||||
# The failure is LATENT -- warm base-image layers mask it, so a
|
||||
# buildah step can look fine for weeks and then fail on a cold pull.
|
||||
BUILDKIT_HOST="unix:///run/buildkit/buildkitd.sock"
|
||||
REGISTRY="localhost:5000"
|
||||
REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}"
|
||||
TAR_FILE="$(mktemp)"
|
||||
trap 'rm -f "$TAR_FILE"' EXIT
|
||||
|
||||
buildctl --addr "$BUILDKIT_HOST" build \
|
||||
--frontend dockerfile.v0 \
|
||||
--local context=. \
|
||||
--local dockerfile=. \
|
||||
--opt label:org.opencontainers.image.revision="${{ github.sha }}" \
|
||||
--opt label:org.opencontainers.image.source="${{ github.repositoryUrl }}" \
|
||||
--output type=oci,dest="$TAR_FILE"
|
||||
|
||||
# buildctl has no registry-push equivalent to `buildah push`; skopeo
|
||||
# copies the OCI archive up, once per tag.
|
||||
skopeo copy --dest-tls-verify=false \
|
||||
oci-archive:"$TAR_FILE" docker://${REF}
|
||||
skopeo copy --dest-tls-verify=false \
|
||||
oci-archive:"$TAR_FILE" docker://${REGISTRY}/${{ env.IMAGE }}:latest
|
||||
if [ -n "${{ steps.meta.outputs.version-tag }}" ]; then
|
||||
skopeo copy --dest-tls-verify=false \
|
||||
oci-archive:"$TAR_FILE" \
|
||||
docker://${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.version-tag }}
|
||||
fi
|
||||
echo "✓ Image pushed to ${REF}"
|
||||
env:
|
||||
SHA_TAG: ${{ steps.meta.outputs.sha-tag }}
|
||||
VERSION_TAG: ${{ steps.meta.outputs.version-tag }}
|
||||
GIT_SHA: ${{ github.sha }}
|
||||
IMAGE_SOURCE: ${{ github.repositoryUrl }}
|
||||
run: bash .gitea/build-image.sh
|
||||
|
||||
# No smoke test step. The one this skill used to recommend ran
|
||||
# `sudo k3s ctr run` against the host's containerd -- that worked when
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/bin/sh
|
||||
# Tests for the CI gate workflow.
|
||||
#
|
||||
# The regression these exist for: this repo had a release workflow that
|
||||
# auto-tagged every push to main, and a `task check` / `task test` suite that
|
||||
# nothing ever ran. CI reported green having tested nothing — a gate that
|
||||
# fails *open*. These assertions pin the wiring so it cannot silently regress.
|
||||
|
||||
set -u
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
SUT="$ROOT/.gitea/workflows/ci.yml"
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; }
|
||||
no() { fail=$((fail + 1)); printf ' FAIL %s\n' "$1"; }
|
||||
|
||||
if [ ! -f "$SUT" ]; then
|
||||
printf 'FAIL: %s does not exist\n' "$SUT"
|
||||
exit 1
|
||||
fi
|
||||
ok 'ci.yml exists'
|
||||
|
||||
# A pull_request trigger is what gives tbd_ship a CI run to read on the head
|
||||
# commit of a branch; without one it fails closed and never merges (#22).
|
||||
grep -qE '^ pull_request:' "$SUT" \
|
||||
&& ok 'runs on pull_request' \
|
||||
|| no 'runs on pull_request'
|
||||
|
||||
grep -qE '^ +push:' "$SUT" \
|
||||
&& ok 'runs on push' \
|
||||
|| no 'runs on push'
|
||||
|
||||
grep -qE '^ +- *(run: *)?task check$|task check' "$SUT" \
|
||||
&& ok 'gate runs task check' \
|
||||
|| no 'gate runs task check'
|
||||
|
||||
grep -qE 'task test' "$SUT" \
|
||||
&& ok 'gate runs task test' \
|
||||
|| no 'gate runs task test'
|
||||
|
||||
# The point of the gate: tagging must not happen on a red check.
|
||||
tag_job=$(awk '/^ tag:/{f=1} f&&/^ [a-z][a-z_-]*:/&&!/^ tag:/{f=0} f' "$SUT")
|
||||
printf '%s' "$tag_job" | grep -qE '^ +needs: *gate' \
|
||||
&& ok 'tag job needs the gate' \
|
||||
|| no 'tag job needs the gate'
|
||||
|
||||
# The old workflow file must be gone, or two workflows race on the same push
|
||||
# and the ungated one still tags.
|
||||
[ ! -f "$ROOT/.gitea/workflows/release.yml" ] \
|
||||
&& ok 'ungated release.yml removed' \
|
||||
|| no 'ungated release.yml removed'
|
||||
|
||||
printf '\nci_gate: %d passed, %d failed\n' "$pass" "$fail"
|
||||
[ "$fail" -eq 0 ]
|
||||
@@ -112,6 +112,19 @@ mkdir -p "$f/home/.claude/skills"
|
||||
ln -sfn "$f/ck/skills/alpha" "$f/home/.claude/skills/alpha"
|
||||
assert_fails "$f" 'legacy' 'leftover per-skill Claude symlinks are reported'
|
||||
|
||||
# --- 7b. harness dir is a REAL directory, not a link into the checkout -------
|
||||
# Found on flamingo 2026-08-26: ~/.config/crush/skills held real directories
|
||||
# from a pre-plugin install. The old message read "resolves to
|
||||
# <path>, outside the active checkout" — naming the directory as its own
|
||||
# resolution target, which is true but useless. It must say the dir is not a
|
||||
# link at all, because the fix (move it aside, re-run install.sh) is different
|
||||
# from the fix for a link into a foreign checkout.
|
||||
f=$(new_fixture realdir)
|
||||
mkdir -p "$f/home/.agents" "$f/home/.config/crush/skills/alpha"
|
||||
ln -sfn "$f/ck/skills" "$f/home/.agents/skills"
|
||||
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/home/.config/crush/skills/alpha/SKILL.md"
|
||||
assert_fails "$f" 'not a link' 'real directory is reported as unmanaged, not as "outside"'
|
||||
|
||||
# --- 7. dangling link inside a harness dir (not the canonical link itself) --
|
||||
# Exercises the per-entry scan rather than the canonical-link check: this is
|
||||
# the shape the 243 dead links actually had.
|
||||
|
||||
Reference in New Issue
Block a user