Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b188965161 | ||
|
|
ae66840a03 | ||
|
|
e7e514390b | ||
|
|
087685f2e3 | ||
|
|
83b069afba | ||
|
|
a699ad8cbd | ||
|
|
a42ec18f40 | ||
|
|
46ff1acdfe | ||
|
|
3112b4ce92 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "skills",
|
"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.",
|
"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.7.0",
|
"version": "0.8.3",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Mathias",
|
"name": "Mathias",
|
||||||
"url": "https://git.d-ma.be/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
|
# Two jobs, in order:
|
||||||
# commit footer:
|
|
||||||
# Bump-Type: major → vX.0.0
|
|
||||||
# Bump-Type: minor → v0.Y.0
|
|
||||||
# (default) → v0.0.Z (patch)
|
|
||||||
#
|
#
|
||||||
# Skips when the latest commit is itself the auto-tagger (commit message
|
# gate — `task check` (structure, frontmatter, manifest/tag parity) plus
|
||||||
# contains "[skip-release]") to prevent recursion. Tag is pushed back
|
# `task test` (hooks, doctor, release-skip). Runs on every push and
|
||||||
# to the repo using the GITEA_TOKEN provided by act_runner.
|
# 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:
|
push:
|
||||||
branches:
|
branches: ["**"]
|
||||||
- main
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tag:
|
gate:
|
||||||
|
name: Check / Test
|
||||||
runs-on: self-hosted
|
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:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -72,16 +101,19 @@ jobs:
|
|||||||
manifest=.claude-plugin/plugin.json
|
manifest=.claude-plugin/plugin.json
|
||||||
version="${TAG#v}"
|
version="${TAG#v}"
|
||||||
current=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1)
|
current=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1)
|
||||||
|
git tag -a "$TAG" -m "release $TAG"
|
||||||
|
|
||||||
if [ "$current" != "$version" ]; then
|
if [ "$current" != "$version" ]; then
|
||||||
sed -i "s/\"version\"[[:space:]]*:[[:space:]]*\"$current\"/\"version\": \"$version\"/" "$manifest"
|
sed -i "s/\"version\"[[:space:]]*:[[:space:]]*\"$current\"/\"version\": \"$version\"/" "$manifest"
|
||||||
git add "$manifest"
|
git add "$manifest"
|
||||||
# The trailer below stops this push from re-triggering the tagger.
|
# 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 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"
|
echo "bumped $manifest to $version"
|
||||||
|
else
|
||||||
|
git push origin "$TAG"
|
||||||
fi
|
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"
|
echo "pushed $TAG"
|
||||||
+12
-8
@@ -19,9 +19,11 @@ This index lists all available engineering skills. Load the full SKILL.md on dem
|
|||||||
| `gitea-ci` | Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror | Setting up or debugging CI on this infra |
|
| `gitea-ci` | Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror | Setting up or debugging CI on this infra |
|
||||||
| `debug` | Hypothesis-first debugging discipline | Any test failure or bug whose cause is not immediately obvious |
|
| `debug` | Hypothesis-first debugging discipline | Any test failure or bug whose cause is not immediately obvious |
|
||||||
| `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project |
|
| `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project |
|
||||||
|
| `experiment-spec` | Rigorous experiment spec — falsifiable hypothesis, quantitative acceptance, null-result protocol | Scoping a research/ML phase instead of a feature |
|
||||||
| `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI |
|
| `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI |
|
||||||
| `session-retrospective` | Surface non-obvious learnings from a session log | After a coding session ends, before context is lost |
|
| `session-retrospective` | Surface non-obvious learnings from a session log | After a coding session ends, before context is lost |
|
||||||
| `trainer` | Two-phase brain curation (score candidates, write past quality gate) | Periodic brain pruning and curation |
|
| `trainer` | Two-phase brain curation (score candidates, write past quality gate) | Periodic brain pruning and curation |
|
||||||
|
| `dream` | Reflective consolidation pass over an agent's memory directory | Pruning/merging memory files, rebuilding a memory index |
|
||||||
| `grill-me` | Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem | Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod |
|
| `grill-me` | Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem | Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod |
|
||||||
| `own-the-task` | Delegation ownership — Intake → Brief → Plan → Execute | A task is handed over open-ended; "take this off my plate"; before a plan exists |
|
| `own-the-task` | Delegation ownership — Intake → Brief → Plan → Execute | A task is handed over open-ended; "take this off my plate"; before a plan exists |
|
||||||
| `telos-load` | Load TELOS intention substrate at session start | Starting any koala session; before architectural decisions; CAD pipeline entry |
|
| `telos-load` | Load TELOS intention substrate at session start | Starting any koala session; before architectural decisions; CAD pipeline entry |
|
||||||
@@ -107,14 +109,16 @@ problem-analysis
|
|||||||
│
|
│
|
||||||
├── spec-driven-dev (PM-kickoff lifecycle)
|
├── spec-driven-dev (PM-kickoff lifecycle)
|
||||||
│ │
|
│ │
|
||||||
│ └── feature-spec (per-feature, mid-implementation)
|
│ ├── feature-spec (per-feature, mid-implementation)
|
||||||
│ │
|
│ │ │
|
||||||
│ └── planning
|
│ │ └── planning
|
||||||
│ │
|
│ │ │
|
||||||
│ └── atdd ──→ tdd (foundation)
|
│ │ └── atdd ──→ tdd (foundation)
|
||||||
│ ├──→ debug (when tests fail)
|
│ │ ├──→ debug (when tests fail)
|
||||||
│ ├──→ clean-code (REFACTOR phase)
|
│ │ ├──→ clean-code (REFACTOR phase)
|
||||||
│ └──→ test-design (review)
|
│ │ └──→ test-design (review)
|
||||||
|
│ │
|
||||||
|
│ └── experiment-spec (research/ML phase, instead of feature-spec)
|
||||||
│
|
│
|
||||||
└── planning
|
└── planning
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -94,7 +94,12 @@ tasks:
|
|||||||
count=$((count + 1))
|
count=$((count + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
# 3. Plugin manifest version must match the latest tag, or installers
|
# 3. SKILLS_INDEX.md lists exactly the skills that exist. Delegated
|
||||||
|
# to bin/check-index so the rule is covered by
|
||||||
|
# tests/check_index_test.sh rather than living untested here.
|
||||||
|
sh '{{.TASKFILE_DIR}}/bin/check-index' '{{.TASKFILE_DIR}}' || fail=1
|
||||||
|
|
||||||
|
# 4. Plugin manifest version must match the latest tag, or installers
|
||||||
# never see the update (Claude Code gates updates on this field).
|
# never see the update (Claude Code gates updates on this field).
|
||||||
if [ -f '{{.PLUGIN_MANIFEST}}' ]; then
|
if [ -f '{{.PLUGIN_MANIFEST}}' ]; then
|
||||||
manifest_v=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' '{{.PLUGIN_MANIFEST}}' | head -1)
|
manifest_v=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' '{{.PLUGIN_MANIFEST}}' | head -1)
|
||||||
|
|||||||
Executable
+42
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# check-index — asserts SKILLS_INDEX.md lists exactly the skills that exist.
|
||||||
|
#
|
||||||
|
# The index is hand-maintained and drifts silently in both directions: a skill
|
||||||
|
# ships with no row (`dream` and `experiment-spec` did — their rows lived on
|
||||||
|
# branches squash-merged after a directory reorg, so the SKILL.md landed and
|
||||||
|
# the row did not, #15), or a row outlives the skill it names. Neither errors
|
||||||
|
# anywhere; the library just misreports itself to every harness that reads it.
|
||||||
|
#
|
||||||
|
# Usage: check-index [checkout-root] (defaults to this script's repo root)
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=${1:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}
|
||||||
|
INDEX="$ROOT/SKILLS_INDEX.md"
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
if [ ! -f "$INDEX" ]; then
|
||||||
|
echo "ERROR: missing SKILLS_INDEX.md" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A row is a table line whose first cell is the skill name in backticks.
|
||||||
|
listed=$(sed -n 's/^|[[:space:]]*`\([^`]*\)`.*/\1/p' "$INDEX" | sort -u)
|
||||||
|
|
||||||
|
for dir in "$ROOT"/skills/*/; do
|
||||||
|
name=$(basename "$dir")
|
||||||
|
[ -f "$dir/SKILL.md" ] || continue
|
||||||
|
printf '%s\n' "$listed" | grep -qx "$name" || {
|
||||||
|
echo "ERROR: skills/$name has no row in SKILLS_INDEX.md" >&2
|
||||||
|
fail=1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
for name in $listed; do
|
||||||
|
[ -f "$ROOT/skills/$name/SKILL.md" ] || {
|
||||||
|
echo "ERROR: SKILLS_INDEX.md lists '$name', which has no skills/$name/SKILL.md" >&2
|
||||||
|
fail=1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
exit $fail
|
||||||
+12
-5
@@ -50,11 +50,18 @@ if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)op\s+document\s+get\b'; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Raw env/secret-file dumps (infra#228/#229 root cause).
|
# Raw env/secret-file dumps (infra#228/#229 root cause).
|
||||||
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)env(\s|$).*\|.*grep'; then
|
#
|
||||||
deny "Blocked: 'env | grep' prints matching values in full. Use an existence-only check: [ -n \"\$VAR\" ] && echo set"
|
# `env` is a dump only when nothing but options and VAR=value assignments
|
||||||
fi
|
# follow it. `env -u VAR cmd` and `env FOO=bar cmd` EXEC a command with a
|
||||||
if printf '%s' "$CMD" | grep -Eq '(^|[;&|])[[:space:]]*env[[:space:]]*($|[;&|])'; then
|
# modified environment and print nothing themselves — piping those into grep
|
||||||
deny "Blocked: bare 'env' prints every variable's value, including secrets. Use an existence-only check: [ -n \"\$VAR\" ] && echo set"
|
# filters the command's own output, not the environment (#25). The earlier
|
||||||
|
# pattern matched any `env` token followed anywhere later by a pipe into
|
||||||
|
# grep, which blocked the documented 1Password write path
|
||||||
|
# (`env -u OP_SERVICE_ACCOUNT_TOKEN op item create`) and cost a workaround.
|
||||||
|
# Denying only the "no command word follows" shape also catches `env FOO=bar`
|
||||||
|
# with no command, which the two previous rules let through.
|
||||||
|
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|[[:space:]])env([[:space:]]+-[iu][[:space:]]*[A-Za-z_][A-Za-z0-9_]*|[[:space:]]+-[iu]|[[:space:]]+[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*)*[[:space:]]*($|[|;&])'; then
|
||||||
|
deny "Blocked: this 'env' prints every variable's value, including secrets (no command word follows it, so it dumps rather than execs). Use an existence-only check: [ -n \"\$VAR\" ] && echo set. Note 'env -u VAR cmd' and 'env FOO=bar cmd' are allowed — they run a command."
|
||||||
fi
|
fi
|
||||||
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)printenv\b'; then
|
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)printenv\b'; then
|
||||||
deny "Blocked: printenv dumps all env values. Use: [ -n \"\$VAR\" ] && echo set"
|
deny "Blocked: printenv dumps all env values. Use: [ -n \"\$VAR\" ] && echo set"
|
||||||
|
|||||||
@@ -22,18 +22,28 @@ Screenshot a locally-running web UI without installing a browser on the agent's
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
`shot.sh` ships beside this SKILL.md, so resolve it from wherever the harness
|
||||||
|
installed the skill rather than from a checkout path — an absolute `~/dev/...`
|
||||||
|
went stale the first time the repo moved and cost a caller three hand-rolled
|
||||||
|
Job attempts before they found the working script (#26).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Claude Code installs the plugin; Crush/Pi/OpenCode get ~/.agents/skills.
|
||||||
|
SHOT=$(ls ~/.claude/plugins/marketplaces/*/skills/web-shot/shot.sh \
|
||||||
|
~/.agents/skills/web-shot/shot.sh 2>/dev/null | head -1 || true)
|
||||||
|
[ -n "$SHOT" ] || { echo "web-shot not installed on this host: run install.sh or the plugin install" >&2; exit 1; }
|
||||||
|
|
||||||
# auto-detect Tailscale hostname, screenshot port 8099 root
|
# auto-detect Tailscale hostname, screenshot port 8099 root
|
||||||
~/dev/.skills/web-shot/shot.sh --port 8099
|
"$SHOT" --port 8099
|
||||||
|
|
||||||
# explicit URL
|
# explicit URL
|
||||||
~/dev/.skills/web-shot/shot.sh --url http://flamingo:5173
|
"$SHOT" --url http://flamingo:5173
|
||||||
|
|
||||||
# capture a search state too (types into input[name=q])
|
# capture a search state too (types into input[name=q])
|
||||||
~/dev/.skills/web-shot/shot.sh --port 8181 --search agentsquad
|
"$SHOT" --port 8181 --search agentsquad
|
||||||
|
|
||||||
# different output dir
|
# different output dir
|
||||||
~/dev/.skills/web-shot/shot.sh --port 3000 --out /tmp/shots
|
"$SHOT" --port 3000 --out /tmp/shots
|
||||||
```
|
```
|
||||||
|
|
||||||
## Flags
|
## Flags
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Tests for bin/check-index.
|
||||||
|
#
|
||||||
|
# The regression these exist for: SKILLS_INDEX.md is maintained by hand, and
|
||||||
|
# two skills (`dream`, `experiment-spec`) shipped without a row — the index
|
||||||
|
# rows lived on feature branches that were squash-merged after a directory
|
||||||
|
# reorg, so the SKILL.md landed and the row did not (#15, #22). Nothing
|
||||||
|
# errored; the index just quietly under-reported the library.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
SUT="$ROOT/bin/check-index"
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
WORK=${TMPDIR:-/tmp}/check_index_test.$$
|
||||||
|
|
||||||
|
if [ ! -f "$SUT" ]; then
|
||||||
|
printf 'FAIL: %s does not exist\n' "$SUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() { rm -rf "$WORK"; }
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
# new_fixture <name> — a checkout with skills/alpha and skills/beta.
|
||||||
|
new_fixture() {
|
||||||
|
_f="$WORK/$1"
|
||||||
|
mkdir -p "$_f/skills/alpha" "$_f/skills/beta"
|
||||||
|
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$_f/skills/alpha/SKILL.md"
|
||||||
|
printf -- '---\nname: beta\ndescription: b\n---\n' > "$_f/skills/beta/SKILL.md"
|
||||||
|
printf '%s' "$_f"
|
||||||
|
}
|
||||||
|
|
||||||
|
index() {
|
||||||
|
_dir=$1; shift
|
||||||
|
printf '# Engineering Skills Index\n\n| Skill | Description | Use when |\n|---|---|---|\n' > "$_dir/SKILLS_INDEX.md"
|
||||||
|
for _n in "$@"; do
|
||||||
|
printf '| `%s` | desc | when |\n' "$_n" >> "$_dir/SKILLS_INDEX.md"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# expect <0|1> <label> <fixture> [grep-for]
|
||||||
|
expect() {
|
||||||
|
_want=$1; _label=$2; _fx=$3; _needle=${4:-}
|
||||||
|
_out=$(sh "$SUT" "$_fx" 2>&1); _rc=$?
|
||||||
|
if [ "$_rc" -ne "$_want" ]; then
|
||||||
|
fail=$((fail + 1)); printf ' FAIL %s (want rc=%s got %s)\n%s\n' "$_label" "$_want" "$_rc" "$_out"; return
|
||||||
|
fi
|
||||||
|
if [ -n "$_needle" ] && ! printf '%s' "$_out" | grep -q "$_needle"; then
|
||||||
|
fail=$((fail + 1)); printf ' FAIL %s (rc ok, but report never names %s)\n%s\n' "$_label" "$_needle" "$_out"; return
|
||||||
|
fi
|
||||||
|
pass=$((pass + 1)); printf ' ok %s\n' "$_label"
|
||||||
|
}
|
||||||
|
|
||||||
|
fx=$(new_fixture complete); index "$fx" alpha beta
|
||||||
|
expect 0 'every skill has a row' "$fx"
|
||||||
|
|
||||||
|
fx=$(new_fixture missing); index "$fx" alpha
|
||||||
|
expect 1 'a skill with no row is reported' "$fx" beta
|
||||||
|
|
||||||
|
fx=$(new_fixture stale); index "$fx" alpha beta gamma
|
||||||
|
expect 1 'a row for a skill that does not exist is reported' "$fx" gamma
|
||||||
|
|
||||||
|
fx=$(new_fixture noindex)
|
||||||
|
expect 1 'a missing index is reported' "$fx" SKILLS_INDEX.md
|
||||||
|
|
||||||
|
printf '\ncheck_index: %d passed, %d failed\n' "$pass" "$fail"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
@@ -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 ]
|
||||||
@@ -61,6 +61,17 @@ expect deny 'op read "op://vault/item/field"' 'op read'
|
|||||||
expect deny 'env | grep -i gitea' 'env piped to grep'
|
expect deny 'env | grep -i gitea' 'env piped to grep'
|
||||||
expect deny 'env' 'bare env'
|
expect deny 'env' 'bare env'
|
||||||
expect deny 'printenv' 'printenv'
|
expect deny 'printenv' 'printenv'
|
||||||
|
expect deny 'env -i | grep -i token' 'env -i with no command is still a dump'
|
||||||
|
expect deny 'env FOO=bar' 'env with only assignments and no command is a dump'
|
||||||
|
|
||||||
|
# --- allow: `env VAR=x cmd` / `env -u VAR cmd` RUN a command (#25) ---
|
||||||
|
# These print nothing themselves; a pipe into grep filters the command's own
|
||||||
|
# output, not the environment. Blocking them cost a documented workaround
|
||||||
|
# ((unset VAR; cmd)) on the 1Password write path, which is the shape below.
|
||||||
|
expect allow 'env -u ANTHROPIC_API_KEY claude --debug -p "ok" 2>&1 | grep -i plugin' 'env -u VAR runs a command'
|
||||||
|
expect allow 'env FOO=bar ./script.sh | grep x' 'env VAR=x runs a command'
|
||||||
|
expect allow 'env -u OP_SERVICE_ACCOUNT_TOKEN op item create - < item.json' 'env -u on the 1P write path'
|
||||||
|
expect deny 'env -u FOO ./run.sh; env | grep -i secret' 'a real dump later in the same line still denies'
|
||||||
|
|
||||||
# --- deny: kubernetes secret material ---
|
# --- deny: kubernetes secret material ---
|
||||||
expect deny 'kubectl get secret brain-app -o yaml' 'kubectl get secret -o yaml'
|
expect deny 'kubectl get secret brain-app -o yaml' 'kubectl get secret -o yaml'
|
||||||
|
|||||||
Reference in New Issue
Block a user