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
150 changes: 150 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
name: CI

on:
pull_request:
branches: [main]

permissions:
contents: read
pull-requests: write

jobs:
test:
name: Test & Coverage
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Run tests with coverage
id: test
run: |
go vet ./...

set +e
go test ./... -coverprofile=coverage.out -covermode=atomic > test-output.txt 2>&1
exit_code=$?
set -e

total=$(go tool cover -func=coverage.out | grep total | awk '{print $NF}')
echo "coverage=$total" >> $GITHUB_OUTPUT
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT

go tool cover -func=coverage.out > coverage_detail.txt

EXIT_CODE=$exit_code TOTAL=$total python3 << 'PYEOF'
import os, re

exit_code = int(os.environ.get("EXIT_CODE", "0"))
total = os.environ.get("TOTAL", "0.0%")

# Parse go tool cover -func output
with open("coverage_detail.txt") as f:
lines = f.readlines()

pkg_data = {}
for line in lines:
if not line.strip() or "total" in line.lower():
continue
# Format: path/file.go:line: func pct%
parts = line.split()
if len(parts) < 2:
continue
file_func = parts[0]
pct_str = parts[-1].rstrip('%')
try:
pct = float(pct_str)
except ValueError:
continue

# Extract package path
if ':' in file_func:
file_path = file_func.split(':')[0]
else:
continue

# Normalize package
if file_path.startswith("github.com/"):
segments = file_path.split("/")
if "internal" in segments:
idx = segments.index("internal")
# Keep internal/subpackage structure
pkg = "/".join(segments[:idx+2]) if idx+1 < len(segments) else file_path
else:
pkg = "/".join(segments[:-1])
if not pkg:
pkg = "github.com/PyratLabs/ugo"
else:
pkg = file_path

if pkg not in pkg_data:
pkg_data[pkg] = []
pkg_data[pkg].append(pct)

# Write report
with open("report.md", "w") as f:
f.write("## Test Results\n\n")

if exit_code == 0:
f.write("✅ **All tests passed**\n\n")
else:
f.write("❌ **Some tests failed**\n\n")
f.write("<details><summary>Test output</summary>\n\n")
f.write("```\n")
with open("test-output.txt") as tf:
f.write(tf.read())
f.write("```\n\n")
f.write("</details>\n\n")

f.write(f"📊 **Total Coverage**: `{total}`\n\n")
f.write("### Coverage by Package\n\n")
f.write("| Package | Coverage |\n")
f.write("|---------|----------|\n")

for pkg in sorted(pkg_data.keys()):
pcts = pkg_data[pkg]
avg = sum(pcts) / len(pcts)
f.write(f"| `{pkg}` | `{avg:.1f}%` |\n")
PYEOF

- name: Post/Update Coverage Comment
if: github.event_name == 'pull_request'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
REPO="${{ github.repository }}"
PR_NUM="${{ github.event.pull_request.number }}"

COMMENT_ID=$(gh api repos/$REPO/issues/$PR_NUM/comments \
--jq '.[] | select(.body | contains("<!-- ugo-ci-report -->")) | .id' | head -1)

python3 << 'PYEOF'
import json

with open("report.md") as f:
report = f.read()

body = "<!-- ugo-ci-report -->\n" + report
payload = json.dumps({"body": body})

with open("payload.json", "w") as f:
f.write(payload)
PYEOF

if [ -n "$COMMENT_ID" ]; then
gh api repos/$REPO/issues/comments/$COMMENT_ID \
-X PATCH --input payload.json
else
gh api repos/$REPO/issues/$PR_NUM/comments \
-X POST --input payload.json
fi

- name: Fail if tests failed
if: steps.test.outputs.exit_code != '0'
run: exit 1
42 changes: 42 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Release

on:
push:
tags:
- 'v*'

permissions:
contents: write

jobs:
release:
name: Release
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Build binaries
run: |
PLATFORMS="linux/amd64 linux/arm64 darwin/amd64 darwin/arm64"
for platform in $PLATFORMS; do
os="${platform%/*}"
arch="${platform#*/}"
bin="ugo-${os}-${arch}"
[ "$os" = "windows" ] && bin="${bin}.exe"
GOOS=$os GOARCH=$arch go build -o "$bin" .
tar czf "${bin}.tar.gz" "$bin"
done
ls -la ugo-*.tar.gz

- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: ugo-*.tar.gz
generate_release_notes: true
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Built binary
ugo

# Local project config (global config lives in ~/.config/<binary>/)
ugo.yaml
*.yaml

# IDE / editor
.idea/
.vscode/
*.swp
*~

# OS
.DS_Store
42 changes: 42 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# AGENTS.md

## What is this repo?

- uGo — a Go CLI using Cobra that executes project-specific commands defined in YAML config.
- Binary is renameable; config file name and help text follow the binary name automatically.
- Config loading: global (`~/.config/<binary>/config.yaml`) merged with local (`./<binary>.yaml`), local overrides.

## Commands

```bash
go build -o ugo . # build
go test ./... # run all tests
go test ./... -cover # run tests with coverage
go vet ./... # vet
```

## Structure

- `main.go` — entry point, calls `cmd.RootCmd().Execute()`
- `cmd/root.go` — Cobra root command; dynamically creates subcommands from YAML config
- `internal/config/config.go` — loads global + local config, merges them
- `internal/checker/checker.go` — pre-flight tool dependency validation
- `internal/version/version.go` — semver extraction and comparison
- `internal/output/output.go` — colored/emoji output with `--no-color` flag support
- `internal/output/output_test.go` — ANSI stripping verification
- `internal/args/args.go` — argument validation (enum, glob, regex)

Tests: `cmd/root_test.go`, `internal/{config,checker,version,output,args}/*_test.go`

## Working conventions

- Verbs are defined in YAML, not hardcoded. Adding a new verb means editing config, not code.
- Config schema:
- `commands.<verb>.{cmd, description, arguments[]}` — arguments are objects with `name`, optional `values` (enum), optional `match` (glob or regex)
- `tools.<binary>.{min_version, max_version, version_cmd, download_url}` — pre-flight checks run before every verb
- Arguments after the verb are mapped positionally to `arguments` entries and expanded into `${name}` placeholders in `cmd`
- `match` is auto-detected: contains `*` or `?` → glob (checks files on disk); otherwise → regex (full string match, auto-anchored)
- Glob matching accepts full path, basename, or basename without extension
- `ugo check` runs tool checks and prints status for each tool
- Version comparison uses `golang.org/x/mod/semver`; `version_cmd` output is scanned for a semver pattern
- Running a verb without required arguments (or with invalid args) prints the error then the help, then exits
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2020 Xan Manning

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading
Loading