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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ their `go.mod`; each fails CI with a specific finding and says how to resolve it
| [`versions`](https://spin-stack.github.io/go-tools/versions/) | pins written outside `versions.yaml`, and pins behind their upstream |
| [`sfw`](https://spin-stack.github.io/go-tools/sfw/) | npm, npx or yarn, and a pnpm install that does not go through Socket Firewall |
| [`taskgate`](https://spin-stack.github.io/go-tools/taskgate/) | a workflow step that is not one `task`, or names one the Taskfile does not have, a task that holds a program rather than a few commands, and a script a task runs that decides anything - so CI and a developer run the same thing, and what decides is Go with a test |
| [`testidentity`](https://spin-stack.github.io/go-tools/testidentity/) | a commit of the branch whose author or committer is a test's - an address at a domain reserved for testing, or one the tree's tests spell - which a test that wrote git's author into the checkout leaves on every commit after it |
| [`deadgate`](https://spin-stack.github.io/go-tools/deadgate/) | a function the binaries link and never call, or a package none imports, that neither the allowlist nor the pending list explains - and an entry in either that explains nothing |
| [`linkweight`](https://spin-stack.github.io/go-tools/linkweight/) | what each binary links and why: the bytes every module puts in it, the function that stops the linker dropping unused methods, the import whose cut drops the most, and a capability - exec, listen, mount, setuid - no line of the allowlist explains |
| [`proven`](https://spin-stack.github.io/go-tools/proven/) | what CI already proved about a commit, by its tree: the run that answers for it, what a run on main is of, whether it may be released, each test's time, which failed tests are flaky |
Expand Down
6 changes: 5 additions & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
#
# task test the Go tests, under the race detector, held to a coverage floor
# task lint golangci-lint with spin's rules, at spin's version, this repository's own
# gates run on itself: ctxlife, testquality, refs, sfw, taskgate, and actionlint
# gates run on itself: ctxlife, testquality, refs, sfw, taskgate, testidentity,
# and actionlint
# task mutate break what the change against BASE (origin/main) touched, and ask the tests
# task coverage coverage.out's numbers, to the terminal or the workflow run's summary
# task trace where a workflow run's time went (ghatrace), to its summary and _output/ghatrace
Expand Down Expand Up @@ -62,6 +63,9 @@ tasks:
- go run ./cmd/mutate -stale-exempts
- go run ./cmd/sfw check
- go run ./cmd/taskgate -allow .github/taskgate-allow.txt -task {{.TASK_EXE}}
# A commit since main whose author is a test's: what a test's git wrote into the checkout's
# config. On main, the range is empty.
- go run ./cmd/testidentity -base origin/main
# A vulnerability the code calls and that has a fix fails, the standard library's too: the
# answer is the upgrade. It asks vuln.go.dev, so lint needs the network.
- go run ./cmd/vulngate ./...
Expand Down
8 changes: 3 additions & 5 deletions cmd/mutate/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/spin-stack/go-tools/internal/gittest"
)

// The example the site shows (site/src/content/docs/mutate.mdx) is this command run over
Expand All @@ -31,12 +33,8 @@ func TestTheExampleTheSiteShows(t *testing.T) {

repo := t.TempDir()
git := func(args ...string) {
cmd := exec.Command("git", append([]string{"-c", "user.name=example", "-c", "user.email=example@example.com", "-c", "commit.gpgsign=false"}, args...)...)
cmd.Dir = repo
// The machine's own git configuration has no say in what the example prints.
cmd.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL="+os.DevNull, "GIT_CONFIG_SYSTEM="+os.DevNull)
out, err := cmd.CombinedOutput()
require.NoError(t, err, "git %v: %s", args, out)
gittest.Run(t, repo, args...)
}
put := func(from, to string) {
raw, err := os.ReadFile(filepath.Join(example, from))
Expand Down
15 changes: 15 additions & 0 deletions cmd/mutate/fence_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

import (
"os"
"testing"

"github.com/spin-stack/go-tools/internal/gittest"
)

// The command runs git, and a git of it that misses its repository - a mutant - finds the
// checkout's: fenced first, it finds none (internal/gittest).
func TestMain(m *testing.M) {
gittest.Fence()
os.Exit(m.Run())
}
15 changes: 15 additions & 0 deletions cmd/proven/fence_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

import (
"os"
"testing"

"github.com/spin-stack/go-tools/internal/gittest"
)

// The command runs git, and a git of it that misses its repository - a mutant - finds the
// checkout's: fenced first, it finds none (internal/gittest).
func TestMain(m *testing.M) {
gittest.Fence()
os.Exit(m.Run())
}
6 changes: 2 additions & 4 deletions cmd/proven/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import (
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/spin-stack/go-tools/internal/gittest"
"github.com/spin-stack/go-tools/internal/proven"
)

Expand Down Expand Up @@ -62,9 +62,7 @@ func setup(t *testing.T) *fixture {
root := t.TempDir()
git := func(args ...string) string {
t.Helper()
out, err := exec.Command("git", append([]string{"-C", root, "-c", "user.name=t", "-c", "user.email=t@t", "-c", "commit.gpgsign=false"}, args...)...).CombinedOutput()
require.NoError(t, err, "git %v: %s", args, out)
return strings.TrimSpace(string(out))
return gittest.Run(t, root, args...)
}
git("init", "-q", "-b", "main")
for _, name := range []string{"one", "two", "three"} {
Expand Down
40 changes: 40 additions & 0 deletions cmd/testidentity/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Command testidentity fails on a commit of the branch - every one in BASE..HEAD - whose author
// or committer is one of the identities a test uses: an address at a domain reserved for testing,
// one with no dot, or one the tree's tests and fixtures spell. A test that wrote git's author into
// a checkout's config makes every commit from it, every worktree's, that test's; this is what
// stops those commits before they are pushed.
//
// go tool testidentity [-base origin/main] [-allow file] [dir]
//
// -allow names an address that reads as a test's and is somebody's real author, one
// `<address> # why` a line; a line whose address is not a test identity any more fails too.
// GitHub's own addresses - its Apps' and users' noreply, and noreply@github.com - need no line.
package main

import (
"context"
"errors"
"flag"
"fmt"
"os"

"github.com/spin-stack/go-tools/internal/testidentity"
)

func main() {
base := flag.String("base", "origin/main", "the commit the branch's own commits start after")
allowPath := flag.String("allow", "", "addresses that are somebody's and read as a test's, `<address> # why` a line (none by default)")
flag.Parse()
root := "."
if flag.NArg() > 0 {
root = flag.Arg(0)
}
err := testidentity.Run(context.Background(), root, *base, *allowPath, os.Stdout)
switch {
case errors.Is(err, testidentity.ErrFound):
os.Exit(1)
case err != nil:
fmt.Fprintf(os.Stderr, "testidentity: %v\n", err) //nolint:errcheck // the exit status is the answer
os.Exit(2)
}
}
8 changes: 4 additions & 4 deletions internal/exposure/exposure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
Expand All @@ -14,6 +13,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/spin-stack/go-tools/internal/exposure"
"github.com/spin-stack/go-tools/internal/gittest"
)

// Each kind finds what is someone's and passes over what is no one's: AWS's documented example
Expand Down Expand Up @@ -67,9 +67,9 @@ func TestARepositorysOwnKindsAreRead(t *testing.T) {
func TestEveryRefIsReadAndAPullRequestsHeadToo(t *testing.T) {
dir := t.TempDir()
git := func(args ...string) string {
cmd := exec.Command("git", append([]string{"-c", "user.name=Ana", "-c", "user.email=ana@acme.io", "-c", "commit.gpgsign=false", "-c", "init.defaultBranch=main"}, args...)...)
cmd.Dir = dir
cmd.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL="+os.DevNull, "GIT_CONFIG_SYSTEM="+os.DevNull)
cmd := gittest.Command(t, dir, append([]string{"-c", "init.defaultBranch=main"}, args...)...)
// The author whose address the scan reads, over gittest's own.
cmd.Env = append(cmd.Env, "GIT_AUTHOR_NAME=Ana", "GIT_AUTHOR_EMAIL=ana@acme.io", "GIT_COMMITTER_NAME=Ana", "GIT_COMMITTER_EMAIL=ana@acme.io")
out, err := cmd.CombinedOutput()
require.NoError(t, err, "git %v: %s", args, out)
return strings.TrimSpace(string(out))
Expand Down
15 changes: 15 additions & 0 deletions internal/exposure/fence_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package exposure_test

import (
"os"
"testing"

"github.com/spin-stack/go-tools/internal/gittest"
)

// The package runs git, and a git of it that misses its repository - a mutant - finds the
// checkout's: fenced first, it finds none (internal/gittest).
func TestMain(m *testing.M) {
gittest.Fence()
os.Exit(m.Run())
}
195 changes: 195 additions & 0 deletions internal/gitexec/gitexec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Package gitexec holds this repository's tests to running git through internal/gittest.
//
// git looks for its repository upward from where it runs, and a test runs inside the checkout,
// so a git a test meant for a repository it made can land on the checkout's own config - which
// every worktree and every agent commits from. In spin that put a test's author on commits
// pushed to GitHub, twice. internal/gittest is the fence; this is what makes it the only way,
// decided from the syntax:
//
// - a test file that execs git itself, rather than through gittest.Command;
// - a package whose own code execs git, with tests and no gittest.Fence() in them: its tests
// run that git, and one that loses its -C - a mutant mutate makes - finds the checkout.
//
// What it sees is os/exec's Command or CommandContext with the program spelled "git". A wrapper
// that takes the program as an argument is not seen.
package gitexec

import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/fs"
"path/filepath"
"strconv"
"strings"
)

// Finding is one git a test can run unfenced, as a line to go to. Key is what an allow line
// names: the test file, or the package's directory.
type Finding struct {
File string
Line int
Key string
Why string
}

func (f Finding) String() string { return fmt.Sprintf("%s:%d: %s", f.File, f.Line, f.Why) }

// fence is the package whose Command and Fence are the way a test runs git.
const fence = "internal/gittest"

type pkg struct {
runs []Finding // the package's own code's execs of git
tested []Finding // its test files' own execs of git
tests bool
fenced bool
}

// Check walks root as go walks a module - no directory whose name starts with . or _, no
// testdata - and reports every git a test can run unfenced, in the order of the tree.
func Check(root string) ([]Finding, error) {
pkgs := &tree{byDir: map[string]*pkg{}}
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
name := d.Name()
if d.IsDir() {
if path != root && (strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") || name == "testdata" || name == "node_modules") {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(name, ".go") {
return nil
}
return read(root, path, pkgs)
})
if err != nil {
return nil, err
}
var found []Finding
for _, p := range pkgs.inOrder {
found = append(found, p.tested...)
if p.tests && !p.fenced {
found = append(found, p.runs...)
}
}
return found, nil
}

// tree is the packages read, by directory and in the order the walk met them - which is the
// order their findings are said in, the same on every run.
type tree struct {
byDir map[string]*pkg
inOrder []*pkg
}

func (t *tree) of(dir string) *pkg {
p := t.byDir[dir]
if p == nil {
p = &pkg{}
t.byDir[dir] = p
t.inOrder = append(t.inOrder, p)
}
return p
}

// read adds the Go file at path to its package in pkgs: its execs of git, and whether it is a
// test and calls gittest.Fence.
func read(root, path string, pkgs *tree) error {
rel, err := filepath.Rel(root, path)
if err != nil {
return err
}
dir := filepath.ToSlash(filepath.Dir(rel))
if dir == fence {
return nil
}
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, path, nil, parser.SkipObjectResolution)
if err != nil {
return err
}
p := pkgs.of(dir)
test := strings.HasSuffix(path, "_test.go")
for _, line := range gitExecs(fset, file) {
f := Finding{File: filepath.ToSlash(rel), Line: line}
if test {
f.Key, f.Why = f.File, "a test runs git itself: run it through gittest.Command (internal/gittest), "+
"which keeps git off any repository the test did not make"
p.tested = append(p.tested, f)
} else {
f.Key, f.Why = dir, "this package's code runs git and its tests do not fence it: call gittest.Fence() "+
"first in its TestMain (internal/gittest), or a git of it that misses its -C writes the checkout's config"
p.runs = append(p.runs, f)
}
}
if test {
p.tests = true
p.fenced = p.fenced || callsFence(file)
}
return nil
}

// programArg is which argument of os/exec's constructors names the program.
var programArg = map[string]int{"Command": 0, "CommandContext": 1}

// gitExecs is the line of every os/exec Command or CommandContext in file whose program is "git".
func gitExecs(fset *token.FileSet, file *ast.File) []int {
name := ""
for _, imp := range file.Imports {
if imp.Path.Value == `"os/exec"` {
name = "exec"
if imp.Name != nil {
name = imp.Name.Name
}
}
}
if name == "" {
return nil
}
var lines []int
ast.Inspect(file, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
if x, ok := sel.X.(*ast.Ident); !ok || x.Name != name {
return true
}
// Both take the program as a parameter of their own, so code that builds has the argument.
program, ok := programArg[sel.Sel.Name]
if !ok {
return true
}
if lit, ok := call.Args[program].(*ast.BasicLit); ok && lit.Kind == token.STRING {
if s, err := strconv.Unquote(lit.Value); err == nil && s == "git" {
lines = append(lines, fset.Position(call.Pos()).Line)
}
}
return true
})
return lines
}

// callsFence is whether file calls gittest.Fence.
func callsFence(file *ast.File) bool {
found := false
ast.Inspect(file, func(n ast.Node) bool {
if call, ok := n.(*ast.CallExpr); ok {
if sel, ok := call.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "Fence" {
if x, ok := sel.X.(*ast.Ident); ok && x.Name == "gittest" {
found = true
}
}
}
return !found
})
return found
}
Loading
Loading