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
11 changes: 11 additions & 0 deletions .gauntlet.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
custom_gates:
# Structural policy is whole-run because findings may identify paths only.
structlint:
command: ["structlint", "validate", "--format", "github"]
parser: github-annotations
line_scoped: false
# Duplication findings carry locations and are scoped to changed lines.
dupehound:
command: ["dupehound", "scan", "--format", "github", "--quiet"]
parser: github-annotations
line_scoped: true
43 changes: 29 additions & 14 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
name: CI
name: PR

on:
pull_request:
push:
types: [opened, edited, synchronize, reopened]
branches:
- main
workflow_dispatch:

permissions:
contents: read
pull-requests: write

jobs:
test:
pr-title:
runs-on: ubuntu-latest
steps:
- uses: AxeForging/reusable-workflows/actions/pr-title-lint@291855e85ec985d1d318df82b8f9dce969b76933
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
pr-title: ${{ github.event.pull_request.title }}

gauntlet:
runs-on: ubuntu-latest

steps:
Expand All @@ -23,23 +31,30 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
go-version: '1.25.12'

- name: Build candidate and install external quality gates
run: |
make build
QUALITY_BIN="$RUNNER_TEMP/quality-bin"
mkdir -p "$QUALITY_BIN"
cp ./dist/pipekit "$QUALITY_BIN/pipekit"
curl -fsSL https://raw.githubusercontent.com/AxeForging/structlint/main/install.sh | \
STRUCTLINT_VERSION=v0.6.0 STRUCTLINT_INSTALL_DIR="$QUALITY_BIN" sh
curl -fsSL https://raw.githubusercontent.com/AxeForging/dupehound/main/install.sh | \
DUPEHOUND_VERSION=v0.1.0 DUPEHOUND_INSTALL_DIR="$QUALITY_BIN" sh
echo "$QUALITY_BIN" >> "$GITHUB_PATH"

- uses: AxeForging/gauntlet@v0.1.0
with:
since: origin/${{ github.base_ref }}

- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest

- name: Unit tests
run: go test ./...

- name: Vulnerability scan
run: make vuln

- name: Build pipekit
run: make build

- name: Integration tests
run: go test ./integration/... -v

- name: Export env from JSON
run: |
./dist/pipekit env from-json --flatten --uppercase-keys --to-github <<'JSON'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
go-version: '1.25.12'
cache: true

- name: Install govulncheck
Expand Down
55 changes: 55 additions & 0 deletions .structlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
dir_structure:
allowedPaths:
- "."
- "actions/**"
- "domain/**"
- "helpers/**"
- "integration/**"
- "services/**"
- "docs/**"
- ".github/**"
requiredPaths:
- "actions"
- "domain"
- "services"
- "integration"
- "go.mod"
- "README.md"

file_naming_pattern:
allowed:
- "*.go"
- "*.mod"
- "*.sum"
- "*.yaml"
- "*.yml"
- "*.md"
- "*.json"
- "*.sh"
- "*.svg"
- "*.png"
- "*.gif"
- "README*"
- "LICENSE*"
- "Makefile"
- ".gitignore"
- ".goreleaser.yml"
- ".structlint.yaml"
disallowed:
- "*.env*"
- ".env*"
- "*.key"
- "*.pem"
- "*.log"
- "*.tmp"
required:
- "go.mod"
- "README.md"
- ".gitignore"
- "*.go"

ignore:
- ".git"
- "vendor"
- "bin"
- "dist"
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,18 @@ One static binary, no runtime deps, works the same on Linux, macOS, and Windows.
## Quick install

```sh
# Linux x86_64
curl -L https://github.com/AxeForging/pipekit/releases/latest/download/pipekit-linux-amd64.tar.gz | tar xz
sudo mv pipekit-linux-amd64 /usr/local/bin/pipekit
curl -fsSL https://raw.githubusercontent.com/AxeForging/pipekit/main/install.sh | sh
```

The installer verifies the archive checksum. Set `PIPEKIT_VERSION=<tag>` to pin
a release or `PIPEKIT_INSTALL_DIR` to choose the destination.

ARM64, macOS, Windows, source builds → **[docs/INSTALL.md](docs/INSTALL.md)**

PR validation runs Structlint and Dupehound through Gauntlet. Structural
findings apply to the whole run; duplication findings are scoped to changed
lines.

---

## 30-second tour
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/AxeForging/pipekit

go 1.25.0

toolchain go1.25.11
toolchain go1.25.12

require (
github.com/Masterminds/semver/v3 v3.4.0
Expand Down
17 changes: 17 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/sh
set -eu
REPO="${PIPEKIT_REPO:-AxeForging/pipekit}"; VERSION="${PIPEKIT_VERSION:-latest}"
if [ -n "${PIPEKIT_INSTALL_DIR:-}" ]; then INSTALL_DIR="$PIPEKIT_INSTALL_DIR"; elif [ -d /usr/local/bin ] && [ -w /usr/local/bin ]; then INSTALL_DIR=/usr/local/bin; else INSTALL_DIR="${HOME:?HOME is required}/.local/bin"; fi
die() { printf 'pipekit installer: %s\n' "$*" >&2; exit 1; }
command -v curl >/dev/null 2>&1 || die "curl is required"; command -v tar >/dev/null 2>&1 || die "tar is required"
case "$(uname -s)" in Linux) os=linux ;; Darwin) os=darwin ;; *) die "unsupported operating system: $(uname -s)" ;; esac
case "$(uname -m)" in x86_64|amd64) arch=amd64 ;; aarch64|arm64) arch=arm64 ;; *) die "unsupported architecture: $(uname -m)" ;; esac
if [ "$VERSION" = latest ]; then VERSION="$(curl -fsSL -o /dev/null -w '%{url_effective}' "https://github.com/${REPO}/releases/latest" | awk -F/ '{print $NF}')"; [ -n "$VERSION" ] || die "could not resolve latest release"; fi
asset="pipekit-${os}-${arch}.tar.gz"; binary="pipekit-${os}-${arch}"; base="https://github.com/${REPO}/releases/download/${VERSION}"
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT HUP INT TERM
curl -fsSL "$base/$asset" -o "$tmp/$asset"; curl -fsSL "$base/checksums.txt" -o "$tmp/checksums.txt"
expected="$(awk -v a="$asset" '$2 == a || $2 == "*" a {print $1; exit}' "$tmp/checksums.txt")"; [ -n "$expected" ] || die "checksum not found"
if command -v sha256sum >/dev/null 2>&1; then actual="$(sha256sum "$tmp/$asset" | awk '{print $1}')"; elif command -v shasum >/dev/null 2>&1; then actual="$(shasum -a 256 "$tmp/$asset" | awk '{print $1}')"; else die "sha256sum or shasum is required"; fi
[ "$actual" = "$expected" ] || die "checksum verification failed"; tar -xzf "$tmp/$asset" -C "$tmp"; [ -f "$tmp/$binary" ] || die "archive does not contain $binary"
mkdir -p "$INSTALL_DIR"; install -m 0755 "$tmp/$binary" "$INSTALL_DIR/pipekit"
printf 'pipekit %s installed to %s/pipekit\n' "$VERSION" "$INSTALL_DIR"
69 changes: 69 additions & 0 deletions integration/install_script_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package integration_test

import (
"crypto/sha256"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
)

func TestInstallScript_InstallsVerifiedRelease(t *testing.T) {
if runtime.GOOS == "windows" || (runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64") {
t.Skip("unsupported installer platform")
}
tmp := t.TempDir()
base := "pipekit-" + runtime.GOOS + "-" + runtime.GOARCH
payload := filepath.Join(tmp, "payload")
writeInstallerExecutable(t, filepath.Join(payload, base), "#!/bin/sh\nprintf 'installed pipekit\\n'\n")
asset := base + ".tar.gz"
archive := filepath.Join(tmp, asset)
if out, err := exec.Command("tar", "-czf", archive, "-C", payload, base).CombinedOutput(); err != nil {
t.Fatalf("archive: %v %s", err, out)
}
b, err := os.ReadFile(archive)
if err != nil {
t.Fatal(err)
}
sums := filepath.Join(tmp, "checksums.txt")
if err := os.WriteFile(sums, []byte(fmt.Sprintf("%x %s\n", sha256.Sum256(b), asset)), 0o644); err != nil {
t.Fatal(err)
}
bin := filepath.Join(tmp, "bin")
fake := `#!/bin/sh
set -eu
while [ "$#" -gt 0 ]; do case "$1" in -o) out="$2"; shift 2;; -*) shift;; *) url="$1"; shift;; esac; done
printf '%s\n' "$url" >> "$CURL_LOG"; case "$url" in */checksums.txt) cp "$SUMS" "$out";; *) cp "$ARCHIVE" "$out";; esac
`
writeInstallerExecutable(t, filepath.Join(bin, "curl"), fake)
dest, log := filepath.Join(tmp, "install"), filepath.Join(tmp, "curl.log")
cmd := exec.Command("sh", filepath.Join("..", "install.sh"))
cmd.Env = append(os.Environ(), "PATH="+bin+string(os.PathListSeparator)+os.Getenv("PATH"), "PIPEKIT_VERSION=v0.1.0", "PIPEKIT_INSTALL_DIR="+dest, "ARCHIVE="+archive, "SUMS="+sums, "CURL_LOG="+log)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("install: %v %s", err, out)
}
out, err := exec.Command(filepath.Join(dest, "pipekit")).CombinedOutput()
if err != nil || strings.TrimSpace(string(out)) != "installed pipekit" {
t.Fatalf("binary: %v %q", err, out)
}
logged, err := os.ReadFile(log)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(logged), "/releases/download/v0.1.0/"+asset) {
t.Fatalf("wrong URL: %s", logged)
}
}

func writeInstallerExecutable(t *testing.T, path, contents string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(contents), 0o755); err != nil {
t.Fatal(err)
}
}
Loading