Skip to content

Verify Taskfile

FieldValue
TypeSkill Resource
Source~/.copilot/skills/go-task/scripts/verify-taskfile.sh
DescriptionNot specified

Source Content

#!/usr/bin/env bash
#
# verify-taskfile.sh — prove a Taskfile is parse-clean, dry-runnable, and that
# every shell script it calls is syntax-clean. Used by the go-task skill before
# any Taskfile is handed off.
#
# Usage:
# verify-taskfile.sh [path/to/Taskfile.yml]
#
# Exit code is non-zero if any hard check fails. Missing optional tools
# (shellcheck, yamllint) degrade to built-in fallbacks rather than being skipped.
set -uo pipefail
TASKFILE="${1:-Taskfile.yml}"
if [ ! -f "$TASKFILE" ]; then
echo "FAIL no Taskfile at: $TASKFILE" >&2
exit 2
fi
DIR="$(cd "$(dirname "$TASKFILE")" && pwd)"
FILE="$(basename "$TASKFILE")"
TF="$DIR/$FILE" # absolute taskfile path — `task -t` resolves against CWD, not -d
FAILURES=0
WARNINGS=0
pass() { printf 'PASS %s\n' "$1"; }
warn() { printf 'WARN %s\n' "$1"; WARNINGS=$((WARNINGS + 1)); }
fail() { printf 'FAIL %s\n' "$1" >&2; FAILURES=$((FAILURES + 1)); }
echo "== Verifying $TASKFILE =="
# 0) Skill pin vs installed binary (non-fatal — a mismatch means the docs may
# be stale for the installed Task, not that this Taskfile is wrong).
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "$SELF_DIR/check-task-version.sh" ]; then
if ! bash "$SELF_DIR/check-task-version.sh"; then
WARNINGS=$((WARNINGS + 1))
fi
fi
# 1) YAML is well-formed (catches an unquoted colon inside a desc/cmd value).
if command -v yamllint >/dev/null 2>&1; then
if yamllint -d "{rules: {line-length: disable, document-start: disable, truthy: disable, comments: disable, indentation: disable}}" "$TASKFILE" >/tmp/gt_yamllint.$$ 2>&1; then
pass "yaml well-formed (yamllint)"
else
fail "yaml invalid (yamllint)"; sed 's/^/ /' /tmp/gt_yamllint.$$ >&2
fi
rm -f /tmp/gt_yamllint.$$
elif command -v python3 >/dev/null 2>&1; then
if python3 -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))" "$TASKFILE" 2>/tmp/gt_pyyaml.$$; then
pass "yaml well-formed (python yaml)"
else
fail "yaml invalid (python yaml)"; sed 's/^/ /' /tmp/gt_pyyaml.$$ >&2
fi
rm -f /tmp/gt_pyyaml.$$
else
warn "no yamllint or python3 — skipped YAML lint"
fi
# 2) Task itself must be installed to do anything further.
if ! command -v task >/dev/null 2>&1; then
warn "go-task not installed — skipped parse and dry-run; install from taskfile.dev"
echo
echo "== Summary: $FAILURES failure(s), $WARNINGS warning(s) =="
[ "$FAILURES" -eq 0 ] || exit 1
exit 0
fi
# 3) Task can parse the file and resolve every task.
if task -t "$TF" -d "$DIR" --list-all >/tmp/gt_list.$$ 2>&1; then
pass "task parses and lists all tasks ($(grep -c '^\* ' /tmp/gt_list.$$ 2>/dev/null || echo '?') task(s))"
else
fail "task could not parse the file"; sed 's/^/ /' /tmp/gt_list.$$ >&2
rm -f /tmp/gt_list.$$
echo
echo "== Summary: $FAILURES failure(s), $WARNINGS warning(s) =="
exit 1
fi
rm -f /tmp/gt_list.$$
# 4) Collect task names (JSON + python preferred; text fallback) and dry-run each.
TASK_NAMES=""
if command -v python3 >/dev/null 2>&1 && task -t "$TF" -d "$DIR" --list-all --json >/tmp/gt_json.$$ 2>/dev/null; then
TASK_NAMES="$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print('\n'.join(t['name'] for t in d.get('tasks',[])))" /tmp/gt_json.$$ 2>/dev/null)"
rm -f /tmp/gt_json.$$
fi
if [ -z "$TASK_NAMES" ]; then
# Fallback: scrape "* name:" lines from --list-all text output.
TASK_NAMES="$(task -t "$TF" -d "$DIR" --list-all 2>/dev/null | sed -n 's/^\* \([^:]*\):.*/\1/p')"
fi
if [ -z "$TASK_NAMES" ]; then
warn "could not enumerate task names — skipped per-task dry-run"
else
while IFS= read -r name; do
[ -z "$name" ] && continue
out="$(task -t "$TF" -d "$DIR" --yes "$name" --dry 2>&1)"
rc=$?
if [ "$rc" -eq 0 ]; then
pass "dry-run ok: $name"
elif printf '%s' "$out" | grep -qiE 'required variable|not a valid value|enum|precondition'; then
# Compiled fine; it just wants runtime input. Not a syntax problem.
warn "dry-run needs input (ok): $name"
else
fail "dry-run error: $name"; printf '%s\n' "$out" | sed 's/^/ /' >&2
fi
done <<EOF
$TASK_NAMES
EOF
fi
# 5) Syntax-check every shell script the Taskfile references.
SCRIPTS="$(grep -oE '[A-Za-z0-9_./-]+\.sh' "$TASKFILE" | sort -u)"
if [ -z "$SCRIPTS" ]; then
pass "no external .sh scripts referenced"
else
while IFS= read -r rel; do
[ -z "$rel" ] && continue
script="$rel"
[ -f "$script" ] || script="$DIR/$rel"
if [ ! -f "$script" ]; then
warn "referenced script not found (skipped): $rel"
continue
fi
if command -v shellcheck >/dev/null 2>&1; then
if shellcheck "$script" >/tmp/gt_sc.$$ 2>&1; then
pass "shellcheck clean: $rel"
else
fail "shellcheck issues: $rel"; sed 's/^/ /' /tmp/gt_sc.$$ >&2
fi
rm -f /tmp/gt_sc.$$
else
# No shellcheck — fall back to a syntax-only parse.
checker="bash"; command -v bash >/dev/null 2>&1 || checker="sh"
if "$checker" -n "$script" 2>/tmp/gt_syn.$$; then
pass "syntax ok ($checker -n, no shellcheck): $rel"
else
fail "syntax error: $rel"; sed 's/^/ /' /tmp/gt_syn.$$ >&2
fi
rm -f /tmp/gt_syn.$$
fi
done <<EOF
$SCRIPTS
EOF
fi
echo
echo "== Summary: $FAILURES failure(s), $WARNINGS warning(s) =="
[ "$FAILURES" -eq 0 ] || exit 1
exit 0