Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
# Enable version updates for GitHub Actions
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'monthly'
open-pull-requests-limit: 3
rebase-strategy: auto
77 changes: 77 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Release (Manual)

on:
workflow_dispatch:
inputs:
bump:
description: Version bump type
required: true
type: choice
options:
- patch
- minor
- major

jobs:
ci:
uses: ./.github/workflows/test.yml

release:
needs: ci
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Harden runner
uses: step-security/harden-runner@v2
with:
egress-policy: block
allowed-endpoints: >
api.github.com:443
github.com:443
objects.githubusercontent.com:443
uploads.github.com:443

- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Compute next version
id: version
run: |
# Match tags with or without leading 'v'
latest=$(git tag --sort=-version:refname \
| grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' \
| head -1)
latest="${latest:-v0.0.0}"
# Normalise to always have a 'v' prefix for parsing
latest="v${latest#v}"

major=$(printf '%s' "$latest" | cut -d. -f1 | tr -d 'v')
minor=$(printf '%s' "$latest" | cut -d. -f2)
patch=$(printf '%s' "$latest" | cut -d. -f3)

case "${{ inputs.bump }}" in
major) major=$((major + 1)); minor=0; patch=0 ;;
minor) minor=$((minor + 1)); patch=0 ;;
patch) patch=$((patch + 1)) ;;
esac

echo "version=v${major}.${minor}.${patch}" >> "$GITHUB_OUTPUT"

- name: Tag and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "${{ steps.version.outputs.version }}"
git push origin "${{ steps.version.outputs.version }}"

- name: Create GitHub release
run: |
VERSION="${{ steps.version.outputs.version }}"
gh release create "${VERSION}" \
--title "Release ${VERSION}" \
--generate-notes \
--verify-tag
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39 changes: 39 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Tests

on:
workflow_call:
pull_request:
branches:
- main
push:
branches:
- main
paths:
- '.claude-plugin/**'
- 'plugins/slack-publish/**'

permissions:
contents: read

jobs:
shell-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Install shellcheck
run: sudo apt-get install -y shellcheck

- name: Lint
run: make lint

shell-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Install bats
run: sudo apt-get install -y bats

- name: Test
run: make test
31 changes: 31 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# OS
.DS_Store
Thumbs.db

# Editor
.idea/
.vscode/
*.swp
*.swo
*~

# Python
__pycache__/
*.py[cod]
*.pyo
.venv/
venv/
dist/
build/
*.egg-info/
.pytest_cache/
.ruff_cache/

# Shell test artifacts
/tmp/
*.bats.log

# Secrets / env
.env
.env.*
!.env.example
3 changes: 3 additions & 0 deletions .shellcheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
shell=bash
severity=warning
external-sources=true
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.PHONY: test lint sast

test:
bats tests/

lint:
shellcheck bin/* lib/*.sh
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# cli-tools

Personal shell utilities for git branch hygiene and GPG cache warming.
Personal shell utilities for git branch hygiene, GPG cache warming, and Claude plugin static analysis.

## Installation

Expand Down Expand Up @@ -43,13 +43,30 @@ Warms the GPG agent cache by performing a throwaway clearsign. Useful to pre-unl
cache-gpg
```

### `sast`

Static analysis for Claude plugin markdown files. Scans `plugins/` for risky `allowed-tools` declarations in YAML frontmatter.

| Severity | Check |
|----------|-------|
| ERROR | Bare `Bash` or `Bash(*)` — unrestricted shell access |
| ERROR | Wildcard `[*]`, `Agent(*)`, or `Skill(*)` — all tools granted |
| WARN | Bare `WebFetch` — any domain fetchable |

```sh
sast
```

Exits non-zero if any ERROR findings are found.

## Structure

```
bin/
cache-gpg # warms GPG agent cache
gitprune # wrapper for gitprune()
gitrefresh # wrapper for gitrefresh()
sast # static analysis for claude plugin frontmatter
lib/
gitcmds.sh # shared function definitions
```
Expand Down
6 changes: 3 additions & 3 deletions bin/gitprune
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/bash
# shellcheck source=lib/gitcmds.sh
source "$(dirname "${BASH_SOURCE[0]}")/../lib/gitcmds.sh"

source ../lib/gitcmds.sh

gitprune $1
gitprune "$1"
6 changes: 3 additions & 3 deletions bin/gitrefresh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/bash
# shellcheck source=lib/gitcmds.sh
source "$(dirname "${BASH_SOURCE[0]}")/../lib/gitcmds.sh"

source ../lib/gitcmds.sh

gitrefresh $1
gitrefresh "$1"
6 changes: 3 additions & 3 deletions lib/gitcmds.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ gitprune () {

if [[ "$1" == "--force" ]]; then
echo "Force deleting unmerged branches"
git branch -r | awk '{print $1}' | egrep -v -f /dev/fd/0 <(git branch -vv | grep origin) | awk '{print $1}' | xargs -r git branch -D
git branch -r | awk '{print $1}' | grep -Ev -f /dev/fd/0 <(git branch -vv | grep origin) | awk '{print $1}' | xargs -r git branch -D
else
git branch -r | awk '{print $1}' | egrep -v -f /dev/fd/0 <(git branch -vv | grep origin) | awk '{print $1}' | xargs -r git branch -d
git branch -r | awk '{print $1}' | grep -Ev -f /dev/fd/0 <(git branch -vv | grep origin) | awk '{print $1}' | xargs -r git branch -d
fi

git gc --prune=now
Expand All @@ -20,7 +20,7 @@ gitrefresh () {
echo "Refreshing local clone."
if [[ "$1" != "" ]]
then
git checkout $1
git checkout "$1"
else
git checkout main
fi
Expand Down
41 changes: 41 additions & 0 deletions tests/test_cache_gpg.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bats

SCRIPT="$BATS_TEST_DIRNAME/../bin/cache-gpg"

setup() {
MOCK_BIN="$(mktemp -d)"
}

teardown() {
rm -rf "$MOCK_BIN"
}

mock_gpg() {
local exit_code="${1:-0}"
cat > "$MOCK_BIN/gpg" <<EOF
#!/bin/bash
cat > /dev/null
exit $exit_code
EOF
chmod +x "$MOCK_BIN/gpg"
}

@test "cache-gpg: script is executable" {
[ -x "$SCRIPT" ]
}

@test "cache-gpg: has bash shebang" {
head -1 "$SCRIPT" | grep -q 'bash'
}

@test "cache-gpg: exits 0 when gpg succeeds" {
mock_gpg 0
PATH="$MOCK_BIN:$PATH" run "$SCRIPT"
[ "$status" -eq 0 ]
}

@test "cache-gpg: exits non-zero when gpg fails" {
mock_gpg 2
PATH="$MOCK_BIN:$PATH" run "$SCRIPT"
[ "$status" -ne 0 ]
}
70 changes: 70 additions & 0 deletions tests/test_gitcmds.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env bats

LIB="$BATS_TEST_DIRNAME/../lib/gitcmds.sh"

setup() {
MOCK_BIN="$(mktemp -d)"
GIT_LOG="$MOCK_BIN/git.log"

cat > "$MOCK_BIN/git" <<'EOF'
#!/usr/bin/env bash
echo "git $*" >> "$GIT_LOG"
case "$1" in
branch)
if [[ "$2" == "-r" ]]; then echo " origin/main"; fi
if [[ "$2" == "-vv" ]]; then echo " main abc123 [origin/main] msg"; fi
;;
*) ;;
esac
exit 0
EOF
chmod +x "$MOCK_BIN/git"

export PATH="$MOCK_BIN:$PATH"
export GIT_LOG

# shellcheck source=../lib/gitcmds.sh
source "$LIB"
}

teardown() {
rm -rf "$MOCK_BIN"
}

@test "lib sources without error" {
# sourced in setup; if we got here it worked
true
}

@test "gitprune: function is defined" {
declare -f gitprune > /dev/null
}

@test "gitrefresh: function is defined" {
declare -f gitrefresh > /dev/null
}

@test "gitprune: calls git gc" {
gitprune
grep -q "git gc" "$GIT_LOG"
}

@test "gitprune --force: calls git fetch" {
gitprune --force
grep -q "git fetch" "$GIT_LOG"
}

@test "gitrefresh: defaults to main branch" {
gitrefresh
grep -q "git checkout main" "$GIT_LOG"
}

@test "gitrefresh: uses provided branch name" {
gitrefresh develop
grep -q "git checkout develop" "$GIT_LOG"
}

@test "gitrefresh: calls git pull" {
gitrefresh
grep -q "git pull" "$GIT_LOG"
}
Loading