diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5876d76..d685dd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,10 @@ jobs: # user-named files (G304/G306) with conventional perms (G301/G302) by # design, and G104 duplicates errcheck. The high-severity taint rules # (G702/G703) stay on, with #nosec on the few benign sites. - - run: go install github.com/securego/gosec/v2/cmd/gosec@latest + # Pinned, not @latest: gosec v2.30.0 requires go >= 1.26 and fails to + # install on this job's Go 1.25 (GOTOOLCHAIN=local). v2.29.0 is the + # newest release whose go.mod accepts 1.25; bump with the go-version. + - run: go install github.com/securego/gosec/v2/cmd/gosec@v2.29.0 - run: | "$(go env GOPATH)/bin/gosec" -exclude=G104,G204,G301,G302,G304,G306 ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e51f7e..394d1fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,9 +6,23 @@ name: release # GitHub Release (body from the tagged CHANGELOG section), and updates the # Homebrew/Scoop manifests. A build-provenance attestation is then signed for # every artifact via the checksums file. +# +# Go releases are git only until the owner says otherwise for this project, so +# nothing here starts on a tag push: pushing a vX.Y.Z tag records a version and +# publishes nothing. A release is dispatched by hand against the tag, with an +# explicit confirmation, and refuses to run without both: +# +# gh workflow run release.yml --repo simtabi/probaci --ref vX.Y.Z -f confirm=release-go +# +# It stays tag-driven: GoReleaser reads the version from the ref it is run +# against, and the first step refuses a ref that is not a v[0-9]* tag. on: - push: - tags: ["v*"] + workflow_dispatch: + inputs: + confirm: + description: Type release-go to publish binaries for the tag this run is dispatched on + required: true + type: string # A newer tag must never abort an in-flight publish. concurrency: @@ -29,6 +43,19 @@ jobs: attestations: write # write the build-provenance attestation packages: write # push the container image steps: + - name: Refuse without confirmation or a release tag + env: + CONFIRM: ${{ inputs.confirm }} + run: | + if [ "${CONFIRM}" != "release-go" ]; then + echo "::error::confirm must be release-go; nothing was published" + exit 1 + fi + case "${GITHUB_REF}" in + refs/tags/v[0-9]*) ;; + *) echo "::error::dispatch this on a vX.Y.Z tag (--ref vX.Y.Z), not ${GITHUB_REF}"; exit 1 ;; + esac + - uses: actions/checkout@v7 with: fetch-depth: 0 diff --git a/CHANGELOG.md b/CHANGELOG.md index b37745c..6568911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed + +- The release workflow no longer starts on a tag push. Binary releases are on hold, and a release + runs only when dispatched by hand on a `v[0-9]*` tag with `-f confirm=release-go`; a test fails + if an automatic trigger returns. +- The README and installation guide build probaci from source only: no download, install-script, + package-manager or `go install` path is offered while binary releases are on hold. + ## [0.1.0] - 2026-06-18 ### Added diff --git a/README.md b/README.md index 688b01f..89782a4 100644 --- a/README.md +++ b/README.md @@ -38,25 +38,19 @@ probaci is built to catch the common causes automatically: ## Install -Most users install the binary (you only need the source to change probaci): +probaci is built from source: binary releases are on hold, so there is no +download, install-script, package-manager or `go install` channel to use. You need +Go (the version `go.mod` names), `git` and `make`. ```sh -# macOS / Linux — Homebrew -brew install simtabi/tap/probaci - -# macOS / Linux — install script (downloads + checksum-verifies the right binary) -curl -fsSL https://raw.githubusercontent.com/simtabi/probaci/main/scripts/install.sh | bash - -# Windows — Scoop, or the install script -scoop bucket add simtabi https://github.com/simtabi/scoop-bucket; scoop install probaci -irm https://raw.githubusercontent.com/simtabi/probaci/main/scripts/install.ps1 | iex - -# Go toolchain -go install github.com/simtabi/probaci/cmd/probaci@latest +git clone https://github.com/simtabi/probaci ~/src/probaci +make -C ~/src/probaci build # -> bin/probaci +~/src/probaci/bin/probaci install # copies itself onto PATH (~/.local/bin) +probaci doctor ``` -deb/rpm packages and a container image are published per release. Full details, -PATH guidance, and building from source: [docs/installation.md](docs/installation.md). +Pinning a version, updating, Windows, and PATH guidance are in +[docs/installation.md](docs/installation.md). ## Quick start guide and usage diff --git a/docs/installation.md b/docs/installation.md index b212292..80d282f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,46 +1,63 @@ # Installation -probaci ships as a single static binary. Most users should install the binary — -you only need the source to make changes. The only hard runtime dependencies are -a container runtime (Docker, rootless Docker, or Podman) and your VCS. +probaci is a single static binary, built from source. Binary releases are on +hold until the maintainer decides otherwise, so this page offers no download, +install-script, package-manager or `go install` path. The only hard runtime +dependencies are a container runtime (Docker, rootless Docker, or Podman) and +your VCS. -## Quick install (recommended) +## Build from source -**macOS / Linux — Homebrew:** +You need Go (the version `go.mod` names), `git`, and `make` (or +[go-task](https://taskfile.dev)). ```sh -brew install simtabi/tap/probaci +git clone https://github.com/simtabi/probaci ~/src/probaci +make -C ~/src/probaci build # -> bin/probaci +~/src/probaci/bin/probaci install # copies itself to ~/.local/bin (Windows: %LOCALAPPDATA%\Programs\probaci) +probaci doctor ``` -**Windows — Scoop:** +`probaci install --system` installs machine-wide (`/usr/local/bin`, +`%ProgramFiles%`, needs elevation), `--dir DIR` picks an explicit directory, and +`probaci uninstall [--purge]` removes it again. Copying `bin/probaci` onto your +`PATH` by hand works just as well. -```powershell -scoop bucket add simtabi https://github.com/simtabi/scoop-bucket -scoop install probaci -``` +Without `make`, the dependency-free primitive is +`go -C ~/src/probaci build -o bin/probaci ./cmd/probaci`. On Windows, run the same +`git clone` and that `go build` (with `-o bin\probaci.exe`) from PowerShell, then +run `bin\probaci.exe install`. + +### Pinning a version -**macOS / Linux — install script** (downloads the right binary for your OS/arch, -verifies its checksum, installs it on your PATH): +A version is a tag: check one out to build exactly that release (`git -C ~/src/probaci tag` +lists them). ```sh -curl -fsSL https://raw.githubusercontent.com/simtabi/probaci/main/scripts/install.sh | bash +git -C ~/src/probaci fetch --tags +git -C ~/src/probaci checkout vX.Y.Z +make -C ~/src/probaci build ``` -**Windows — install script:** +### Updating -```powershell -irm https://raw.githubusercontent.com/simtabi/probaci/main/scripts/install.ps1 | iex +```sh +git -C ~/src/probaci pull --ff-only +make -C ~/src/probaci build +~/src/probaci/bin/probaci install ``` -The script installs to `/usr/local/bin` by default, requesting `sudo` only if -that directory isn't writable (Windows: `%LOCALAPPDATA%\Programs\probaci`). -Override the directory with `PROBACI_INSTALL_DIR`, and pin a version by passing -it as an argument: +`probaci version` reports the version the build was stamped with (`git describe`), +so a build from a branch is still identifiable. -```sh -PROBACI_INSTALL_DIR="$HOME/bin" \ - curl -fsSL https://raw.githubusercontent.com/simtabi/probaci/main/scripts/install.sh | bash -s v0.1.0 -``` +### Binary releases + +No binary release has been published. The release tooling stays in the tree: +the GoReleaser config, and `scripts/install.sh` / `scripts/install.ps1`, which +download release archives. The release workflow would also publish Homebrew and +Scoop manifests, deb/rpm packages and a `ghcr.io/simtabi/probaci` container image. +None of those channels is offered while binary releases are on hold; see +[release.md](release.md). ## Run it globally @@ -48,7 +65,7 @@ Once the install directory is on your `PATH`, run `probaci` from anywhere — it auto-discovers the repository root by walking up from the current directory (ddev-style), so `probaci run` works from any subdirectory. -If the installer warns that the directory isn't on your `PATH`, add it: +If `probaci install` warns that the directory isn't on your `PATH`, add it: | Shell / OS | Add to PATH | |---|---| @@ -58,34 +75,7 @@ If the installer warns that the directory isn't on your `PATH`, add it: | Windows (PowerShell) | `[Environment]::SetEnvironmentVariable('Path', "$dir;$env:Path", 'User')` | `/usr/local/bin` is already on `PATH` for most systems — prefer it for a -machine-wide install (it may require `sudo` or `PROBACI_INSTALL_DIR=/usr/local/bin` -with elevation). - -## go install (Go toolchain) - -```sh -go install github.com/simtabi/probaci/cmd/probaci@latest -``` - -## Manual download - -Download the archive for your OS/arch from -[GitHub Releases](https://github.com/simtabi/probaci/releases), verify it against -`checksums.txt` (and the cosign signature), extract, and either place `probaci` -on your `PATH` or let probaci place itself: - -```sh -probaci install # per-user (~/.local/bin, %LOCALAPPDATA%\Programs\probaci) -probaci install --system # machine-wide (/usr/local/bin, %ProgramFiles%) — needs elevation -probaci install --dir DIR # explicit directory -probaci uninstall [--purge] -``` - -## Container image - -```sh -docker run --rm -v "$PWD:/workspace" ghcr.io/simtabi/probaci:latest run -``` +machine-wide install (`probaci install --system`, which needs elevation). ## Supported platforms and architectures @@ -99,9 +89,9 @@ docker run --rm -v "$PWD:/workspace" ghcr.io/simtabi/probaci:latest run > Windows configurations, runtime availability varies — probaci's `--no-docker` > mode runs the non-container checks and skips the rest. -## Build from source (contributors only) +## Development builds -You only need this to modify probaci. One task definition, three entrypoints: +One task definition, three entrypoints: ```sh task build # go-task (Taskfile.yml) diff --git a/docs/release.md b/docs/release.md index 30b5a5e..35be87f 100644 --- a/docs/release.md +++ b/docs/release.md @@ -1,7 +1,12 @@ # Release -Releases are tag-driven and built by GoReleaser. The CI GoReleaser version is -pinned (`~> v2.4`) so the config schema and tool stay in sync. +Releases are tag-driven and built by GoReleaser, and binary releases are on +hold. A `vX.Y.Z` tag records a version and publishes nothing; a binary release is +built from a tag only when dispatched by hand with a confirmation (see +[Cutting a release](#cutting-a-release)). Until binary releases resume, users +build from source ([installation](installation.md#build-from-source)). The CI +GoReleaser version is pinned (`~> v2.4`) so the config schema and tool stay in +sync. ## Local deliverables (no publishing) @@ -52,7 +57,18 @@ regenerated `digests.json`. git tag v0.1.0 git push origin v0.1.0 ``` -3. The `release` workflow runs GoReleaser, which builds the matrix, signs with + Pushing a tag publishes nothing: binary releases are on hold, and probaci is + built from source until the owner decides otherwise. +3. Only when a binary release has been approved for this version, dispatch the + `release` workflow on the tag: + ```sh + gh workflow run release.yml --repo simtabi/probaci --ref vX.Y.Z -f confirm=release-go + ``` + `vX.Y.Z` must be a tag that is already on GitHub. This publishes for real, so + run it only for a version the owner approved. The workflow's first step + refuses to run without `confirm=release-go`, or on anything but a `v[0-9]*` + tag, before anything is built. +4. The `release` workflow runs GoReleaser, which builds the matrix, signs with cosign (keyless via OIDC), attaches an SBOM and SLSA provenance, and publishes the GitHub Release, container image, deb/rpm, and Homebrew/Scoop manifests. @@ -68,7 +84,8 @@ Pure Go, `CGO_ENABLED=0`. long-lived registry credentials). 3. Add the write deploy keys `HOMEBREW_TAP_DEPLOY_KEY` (on `simtabi/homebrew-tap`) and `SCOOP_BUCKET_DEPLOY_KEY` (on `simtabi/scoop-bucket`), each a repository secret that reaches its one repository and nothing else (`TAP_GITHUB_TOKEN` is only a fallback, and the Winget pull request's token), plus any registry tokens as needed. -4. Cut `v0.1.0`. +4. Tag `v0.1.0`, then dispatch the release on it as in + [Cutting a release](#cutting-a-release). The full org-wide gate is `/opensource/shipping-checklist.md` (local; not in any repo). Run it before every release. diff --git a/release_workflow_test.go b/release_workflow_test.go new file mode 100644 index 0000000..0dbcd0d --- /dev/null +++ b/release_workflow_test.go @@ -0,0 +1,28 @@ +package probaci + +import ( + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// Go releases are git only until the owner says otherwise, so release.yml must +// never start on its own: no push trigger of any kind, and a dispatch that +// refuses to publish without confirm=release-go. A tag push that started +// GoReleaser is how an unwanted release began elsewhere on 2026-11-02. +func TestReleaseRunsOnlyOnAConfirmedDispatch(t *testing.T) { + wf, err := os.ReadFile(filepath.Join(".github", "workflows", "release.yml")) + if err != nil { + t.Fatal(err) + } + if regexp.MustCompile(`(?m)^[ \t]*(push|schedule|pull_request|release)\s*:`).Match(wf) { + t.Error("release.yml has an automatic trigger; it must run on workflow_dispatch only") + } + for _, want := range []string{"workflow_dispatch:", "confirm:", `"${CONFIRM}" != "release-go"`, "refs/tags/v[0-9]*"} { + if !strings.Contains(string(wf), want) { + t.Errorf("release.yml lacks %q: a release needs an explicit, confirmed dispatch on a tag", want) + } + } +}