From 16a56df52a3b5bf31a5cf399e4f39aba55aa032c Mon Sep 17 00:00:00 2001 From: Lucas Machado Date: Fri, 10 Jul 2026 21:00:22 +0200 Subject: [PATCH] ci: standardize PR quality gates --- .gauntlet.yml | 9 ++++ .github/workflows/pr.yml | 85 ++++++++++++++++-------------- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 59 --------------------- go.mod | 4 +- install.sh | 12 +++++ integration/install_script_test.go | 55 +++++++++++++++++++ 7 files changed, 125 insertions(+), 101 deletions(-) create mode 100644 .gauntlet.yml delete mode 100644 .github/workflows/test.yml create mode 100755 install.sh create mode 100644 integration/install_script_test.go diff --git a/.gauntlet.yml b/.gauntlet.yml new file mode 100644 index 0000000..fbb6dda --- /dev/null +++ b/.gauntlet.yml @@ -0,0 +1,9 @@ +custom_gates: + structlint: + command: ["structlint", "validate", "--format", "github"] + parser: github-annotations + line_scoped: false + dupehound: + command: ["dupehound", "scan", "--format", "github", "--quiet"] + parser: github-annotations + line_scoped: true diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index bb1f1df..c73b7cd 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -11,43 +11,13 @@ permissions: pull-requests: write jobs: - title: + pr-title: runs-on: ubuntu-latest steps: - - name: Validate PR title follows Conventional Commits - env: - TITLE: ${{ github.event.pull_request.title }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - if echo "$TITLE" | grep -qE "^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?(!)?: .+"; then - echo "PR title is valid: $TITLE" - exit 0 - fi - - BODY=$(cat <<'COMMENT' - ### ⚠️ Invalid PR Title - - PR title must follow the **Conventional Commits** format since we use squash merge: - - ``` - [optional scope][!]: - ``` - - **Allowed types:** `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `build`, `ci`, `perf`, `revert` - - **Examples:** - - `feat: add new feature` - - `fix(sandbox): resolve namespace issue` - - `feat!: breaking change` - - `chore(deps): update dependencies` - COMMENT - ) - - gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \ - -X POST -f body="$BODY" - - echo "::error::PR title must follow Conventional Commits format" - exit 1 + - uses: AxeForging/reusable-workflows/actions/pr-title-lint@291855e85ec985d1d318df82b8f9dce969b76933 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + pr-title: ${{ github.event.pull_request.title }} review: runs-on: ubuntu-latest @@ -65,12 +35,47 @@ jobs: INCREMENTAL: false REVIEW_RULES: concise - validate: + gauntlet: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: AxeForging/structlint@main with: - config: .structlint.yaml - comment-on-pr: "true" - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version: '1.25.12' + cache: true + - name: Build candidate and install external quality gates + run: | + QUALITY_BIN="$RUNNER_TEMP/quality-bin"; mkdir -p "$QUALITY_BIN" + go build -o "$QUALITY_BIN/aigate" . + 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 }} + + vuln: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.25.12' + cache: true + - run: go install golang.org/x/vuln/cmd/govulncheck@latest + - run: govulncheck ./... + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.25.12' + cache: true + - uses: golangci/golangci-lint-action@v9 + with: + version: v2.12.2 + args: --timeout=5m diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 541128a..9acd199 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.24' + go-version: '1.25.12' cache: true - name: Run tests diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 7e1929e..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Test - -on: - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.25' - cache: true - - - name: Download dependencies - run: go mod download - - - name: Run tests - run: go test ./... -v - - - name: Build binary - run: make build-local - - vuln: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.25' - cache: true - - - name: Install govulncheck - run: go install golang.org/x/vuln/cmd/govulncheck@latest - - - name: Run govulncheck - run: govulncheck ./... - - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.25' - cache: true - - - name: Install golangci-lint - run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest - - - name: Run golangci-lint - run: golangci-lint run --timeout=5m diff --git a/go.mod b/go.mod index 8c7822a..9156927 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/AxeForging/aigate -go 1.25.11 +go 1.25.0 + +toolchain go1.25.12 require ( github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..c4ca726 --- /dev/null +++ b/install.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -eu +REPO="${AIGATE_REPO:-AxeForging/aigate}"; VERSION="${AIGATE_VERSION:-latest}"; INSTALL_DIR="${AIGATE_INSTALL_DIR:-${HOME:?HOME required}/.local/bin}" +die(){ printf 'aigate installer: %s\n' "$*" >&2; exit 1; } +case "$(uname -s)" in Linux) os=linux;; Darwin) os=darwin;; *) die "unsupported OS";; esac +case "$(uname -m)" in x86_64|amd64) arch=amd64;; aarch64|arm64) arch=arm64;; *) die "unsupported architecture";; esac +if [ "$VERSION" = latest ]; then VERSION="$(curl -fsSL -o /dev/null -w '%{url_effective}' "https://github.com/$REPO/releases/latest" | awk -F/ '{print $NF}')"; fi +asset="aigate-${os}-${arch}.tar.gz"; binary="aigate-${os}-${arch}"; base="https://github.com/$REPO/releases/download/$VERSION"; tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT +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" +actual="$(sha256sum "$tmp/$asset" | awk '{print $1}')"; [ "$actual" = "$expected" ] || die "checksum verification failed" +tar -xzf "$tmp/$asset" -C "$tmp"; [ -f "$tmp/$binary" ] || die "archive missing binary"; mkdir -p "$INSTALL_DIR"; install -m 0755 "$tmp/$binary" "$INSTALL_DIR/aigate" diff --git a/integration/install_script_test.go b/integration/install_script_test.go new file mode 100644 index 0000000..7c1b607 --- /dev/null +++ b/integration/install_script_test.go @@ -0,0 +1,55 @@ +package integration_test + +import ( + "crypto/sha256" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" +) + +func TestInstallScript_InstallsVerifiedRelease(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("unsupported") + } + tmp := t.TempDir() + base := "aigate-" + runtime.GOOS + "-" + runtime.GOARCH + payload := filepath.Join(tmp, "payload") + mustWrite(t, filepath.Join(payload, base), "#!/bin/sh\nexit 0\n", 0o755) + 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") + mustWrite(t, sums, fmt.Sprintf("%x %s\n", sha256.Sum256(b), asset), 0o644) + bin := filepath.Join(tmp, "bin") + mustWrite(t, filepath.Join(bin, "curl"), `#!/bin/sh +while [ "$#" -gt 0 ]; do case "$1" in -o) out="$2"; shift 2;; -*) shift;; *) url="$1"; shift;; esac; done +case "$url" in */checksums.txt) cp "$SUMS" "$out";; *) cp "$ARCHIVE" "$out";; esac +`, 0o755) + dest := filepath.Join(tmp, "install") + cmd := exec.Command("sh", filepath.Join("..", "install.sh")) + cmd.Env = append(os.Environ(), "PATH="+bin+":"+os.Getenv("PATH"), "AIGATE_VERSION=v0.7.0", "AIGATE_INSTALL_DIR="+dest, "ARCHIVE="+archive, "SUMS="+sums) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("install: %v %s", err, out) + } + if _, err := os.Stat(filepath.Join(dest, "aigate")); err != nil { + t.Fatal(err) + } +} +func mustWrite(t *testing.T, p, s string, m os.FileMode) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(p, []byte(s), m); err != nil { + t.Fatal(err) + } +}