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
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version: "1.25"
# Dogfood: gauntlet runs gauntlet against its own PR diff.
- uses: ./
with:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ jobs:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version: "1.25"
- run: go test -race ./...
- run: go test -tags=integration -race ./integration/...
- uses: goreleaser/goreleaser-action@v6
with:
version: latest
Expand Down
1 change: 1 addition & 0 deletions .structlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ file_naming_pattern:
- "*.json"
- "*.md"
- "*.txt"
- "*.sh"
- "*.diff"
- "*.jsonl"
- "Makefile"
Expand Down
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) 2026 AxeForging

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.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@ gotest ERROR internal/engine/plan_test.go:33 test-failure TestP
## Install

```bash
# From release (recommended once v1 ships):
curl -sSfL https://github.com/AxeForging/gauntlet/releases/latest/download/gauntlet_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m).tar.gz | tar -xz -C /usr/local/bin
# Latest checksum-verified release:
curl -fsSL https://raw.githubusercontent.com/AxeForging/gauntlet/main/install.sh | sh

# Pin a version or install somewhere else:
curl -fsSL https://raw.githubusercontent.com/AxeForging/gauntlet/main/install.sh | \
GAUNTLET_VERSION=v0.1.0 GAUNTLET_INSTALL_DIR="$HOME/.local/bin" sh

# From source:
go install github.com/AxeForging/gauntlet/cmd/gauntlet@main
```

Requires Go 1.24+ (for source install). Optional gate tools (skipped visibly if absent): `gofumpt`, `golangci-lint`, `prettier`, `eslint`, `tsc`, `vitest`.
Requires Go 1.25+ for source installation. Release binaries have no Go runtime
dependency. Optional gate tools (skipped visibly if absent): `gofumpt`,
`golangci-lint`, `prettier`, `eslint`, `tsc`, `vitest`.

## CI usage

Expand Down Expand Up @@ -144,4 +150,4 @@ Spec: [docs/specs/001-gauntlet-core.md](docs/specs/001-gauntlet-core.md). Plan:

## License

TBDprivate repo during development.
MITsee [LICENSE](LICENSE).
59 changes: 59 additions & 0 deletions docs/specs/008-public-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 008 — Public v0.1.0 release

## Context

Gauntlet's core, CI action, custom gates, and shell-free pipelines are implemented and
validated, but the repository is private, has no license, and documents a release URL
that does not match its GoReleaser archive name. A public first release needs a complete,
tested installation path rather than relying on source installation from `main`.

## Requirements

1. The repository MUST ship an MIT license owned by AxeForging.
2. Go source, CI, and release builds MUST use the project-pinned Go 1.25 line.
3. A POSIX installer MUST detect supported OS/architecture pairs, download a selected or
latest release, verify SHA-256 checksums, and install to a configurable directory.
4. The installer MUST default to writable `/usr/local/bin`, otherwise `$HOME/.local/bin`,
and MUST explain when the selected directory is not on PATH.
5. A real executable regression test MUST install and run a fixture archive and verify
the pinned-version release URL.
6. README and composite-action documentation MUST match actual GoReleaser asset names.
7. Pull-request validation MUST remain PR-only; tag-triggered release validation MUST
test before publishing.
8. After merge, the repository MUST become public before tagging `v0.1.0`.
9. The published release MUST contain all configured archives and checksums, and the
unauthenticated public installer MUST install a binary reporting version `0.1.0`.

## Non-goals

- Windows archives in v0.1.0.
- Package-manager distribution.
- Removing source installation as a development option.

## Design

Use the established dupehound installer contract with Gauntlet's underscore-separated
GoReleaser asset names. Keep release creation tag-driven. The installer itself is tested
against a local fake release transport so CI is deterministic, then tested once against
the real public release as a rollout verification.

## Data & API changes

- Environment variables: `GAUNTLET_VERSION`, `GAUNTLET_INSTALL_DIR`, and
`GAUNTLET_REPO`.
- No runtime data or schema migration.

## Test plan

- Execute the installer regression with a fixture archive and checksum manifest.
- Run gofumpt, golangci-lint, `go test -race ./...`, and tagged integration tests.
- Verify GoReleaser configuration before merge.
- After publishing, run the raw GitHub installer unauthenticated into `/tmp` and execute
`gauntlet version`.

## Rollout

Merge release prep, change visibility to public, push annotated `v0.1.0`, monitor the
release workflow, inspect assets, and run the public installer. Roll back by deleting a
bad release/tag and correcting the installer before retagging; repository visibility is
not automatically rolled back.
24 changes: 24 additions & 0 deletions docs/specs/008-public-release.plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Plan — 008 Public v0.1.0 release

Spec: `docs/specs/008-public-release.md`

- [x] Add MIT `LICENSE` and update README release language.
- [x] Align Go module and workflows to Go 1.25.
- [x] Add checksum-verifying `install.sh` and executable regression coverage.
- [x] Correct README/action artifact installation examples and verify GoReleaser config.
- [x] Run formatting, lint, race, and binary integration suites.
- [x] Extend Structlint's explicit naming policy to permit the release installer's `.sh` extension.
- [ ] Commit, push, open the release-prep PR, and wait for PR-only validation.
- [ ] Merge, make the repository public, and push annotated `v0.1.0`.
- [ ] Verify release assets and the unauthenticated public installer end to end.

## Risks

- Asset naming must remain identical across GoReleaser, installer, README, and action.
- Visibility change is externally observable and explicitly authorized by the release goal.

## Done means

- Gauntlet is public under MIT.
- `v0.1.0` is published with checksums and supported archives.
- A clean machine can install it with one curl command and run the reported version.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/AxeForging/gauntlet

go 1.24.6
go 1.25

require (
github.com/goccy/go-yaml v1.19.2
Expand Down
74 changes: 74 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/bin/sh

set -eu

REPO="${GAUNTLET_REPO:-AxeForging/gauntlet}"
VERSION="${GAUNTLET_VERSION:-latest}"
if [ -n "${GAUNTLET_INSTALL_DIR:-}" ]; then
INSTALL_DIR="$GAUNTLET_INSTALL_DIR"
elif [ -d /usr/local/bin ] && [ -w /usr/local/bin ]; then
INSTALL_DIR=/usr/local/bin
else
INSTALL_DIR="${HOME:?HOME is required when /usr/local/bin is not writable}/.local/bin"
fi

die() {
printf 'gauntlet 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

version_part="${VERSION#v}"
asset="gauntlet_${version_part}_${os}_${arch}.tar.gz"
if [ "$VERSION" = latest ]; then
base_url="https://github.com/${REPO}/releases/latest/download"
# The latest URL does not expose the version needed in the asset name.
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 version"
version_part="${VERSION#v}"
asset="gauntlet_${version_part}_${os}_${arch}.tar.gz"
base_url="https://github.com/${REPO}/releases/download/${VERSION}"
else
base_url="https://github.com/${REPO}/releases/download/${VERSION}"
fi

tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT HUP INT TERM

curl -fsSL "${base_url}/${asset}" -o "${tmp_dir}/${asset}"
curl -fsSL "${base_url}/checksums.txt" -o "${tmp_dir}/checksums.txt"

expected="$(awk -v asset="$asset" '$2 == asset || $2 == "*" asset { print $1; exit }' "${tmp_dir}/checksums.txt")"
[ -n "$expected" ] || die "checksum for ${asset} not found"
if command -v sha256sum >/dev/null 2>&1; then
actual="$(sha256sum "${tmp_dir}/${asset}" | awk '{print $1}')"
elif command -v shasum >/dev/null 2>&1; then
actual="$(shasum -a 256 "${tmp_dir}/${asset}" | awk '{print $1}')"
else
die "sha256sum or shasum is required"
fi
[ "$actual" = "$expected" ] || die "checksum verification failed for ${asset}"

tar -xzf "${tmp_dir}/${asset}" -C "$tmp_dir"
[ -f "${tmp_dir}/gauntlet" ] || die "release archive does not contain gauntlet"
mkdir -p "$INSTALL_DIR"
install -m 0755 "${tmp_dir}/gauntlet" "${INSTALL_DIR}/gauntlet"
printf 'gauntlet %s installed to %s/gauntlet\n' "$VERSION" "$INSTALL_DIR"
case ":${PATH}:" in
*":${INSTALL_DIR}:"*) ;;
*) printf 'Add %s to PATH to run gauntlet from any directory.\n' "$INSTALL_DIR" ;;
esac
107 changes: 107 additions & 0 deletions integration/install_script_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package integration

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

func TestInstallScript_InstallsVerifiedPinnedRelease(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("install.sh targets POSIX systems")
}
osName, archName := installerPlatform(t)
tmp := t.TempDir()
payload := filepath.Join(tmp, "payload")
if err := os.Mkdir(payload, 0o755); err != nil {
t.Fatal(err)
}
binary := filepath.Join(payload, "gauntlet")
if err := os.WriteFile(binary, []byte("#!/bin/sh\nprintf 'installed gauntlet\\n'\n"), 0o755); err != nil {
t.Fatal(err)
}
asset := "gauntlet_0.1.0_" + osName + "_" + archName + ".tar.gz"
archive := filepath.Join(tmp, asset)
cmd := exec.Command("tar", "-czf", archive, "-C", payload, "gauntlet")
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("create fixture archive: %v\n%s", err, output)
}
archiveBytes, err := os.ReadFile(archive)
if err != nil {
t.Fatal(err)
}
checksums := filepath.Join(tmp, "checksums.txt")
line := fmt.Sprintf("%x %s\n", sha256.Sum256(archiveBytes), asset)
if err := os.WriteFile(checksums, []byte(line), 0o644); err != nil {
t.Fatal(err)
}

fakeBin := filepath.Join(tmp, "fake-bin")
if err := os.Mkdir(fakeBin, 0o755); err != nil {
t.Fatal(err)
}
logPath := filepath.Join(tmp, "curl.log")
fakeCurl := `#!/bin/sh
set -eu
url=""
out=""
while [ "$#" -gt 0 ]; do
case "$1" in
-o) out="$2"; shift 2 ;;
-*) shift ;;
*) url="$1"; shift ;;
esac
done
printf '%s\n' "$url" >> "$FAKE_CURL_LOG"
case "$url" in
*/checksums.txt) cp "$FIXTURE_CHECKSUMS" "$out" ;;
*) cp "$FIXTURE_ARCHIVE" "$out" ;;
esac
`
if err := os.WriteFile(filepath.Join(fakeBin, "curl"), []byte(fakeCurl), 0o755); err != nil {
t.Fatal(err)
}

installDir := filepath.Join(tmp, "install")
cmd = exec.Command("sh", filepath.Join("..", "install.sh"))
cmd.Env = append(os.Environ(),
"PATH="+fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"),
"GAUNTLET_VERSION=v0.1.0",
"GAUNTLET_INSTALL_DIR="+installDir,
"FIXTURE_ARCHIVE="+archive,
"FIXTURE_CHECKSUMS="+checksums,
"FAKE_CURL_LOG="+logPath,
)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("install script failed: %v\n%s", err, output)
}
runOutput, err := exec.Command(filepath.Join(installDir, "gauntlet")).CombinedOutput()
if err != nil || strings.TrimSpace(string(runOutput)) != "installed gauntlet" {
t.Fatalf("installed binary: err=%v output=%q", err, runOutput)
}
logBytes, err := os.ReadFile(logPath)
if err != nil {
t.Fatal(err)
}
wantURL := "/releases/download/v0.1.0/" + asset
if !strings.Contains(string(logBytes), wantURL) {
t.Fatalf("versioned asset URL missing %q:\n%s", wantURL, logBytes)
}
}

func installerPlatform(t *testing.T) (string, string) {
t.Helper()
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
t.Skipf("unsupported test OS %s", runtime.GOOS)
}
if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
t.Skipf("unsupported test architecture %s", runtime.GOARCH)
}
return runtime.GOOS, runtime.GOARCH
}
Loading