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
171 changes: 29 additions & 142 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,155 +1,42 @@
name: Release

# Every release is tag-driven and publishes BOTH the source code and the
# binaries: GoReleaser cross-compiles the full matrix, builds the archives,
# packages, and a reproducible source tarball, generates SBOMs and checksums,
# creates the GitHub Release (body from the tagged CHANGELOG section), and
# updates the Homebrew/Scoop/Winget 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/osaat --ref vX.Y.Z -f confirm=release-go
# Release: build osaat's binaries and publish them on its GitHub release, and nowhere else (no
# `go install` path, Homebrew, Scoop, Winget, image or any other channel; owner, 2026-11-13).
# The pipeline is simtabi/.github's shared go-release.yml; this file only says when.
#
# 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.
# src/cmd/osaat's TestReleaseRunsOnlyOnAConfirmedDispatch and
# src/internal/release's TestReleaseRefusesFirst fail if an automatic trigger
# comes back or the refusal stops being the first step.
# git push origin vX.Y.Z builds and publishes vX.Y.Z
# gh workflow run release.yml --repo simtabi/osaat -f tag=vX.Y.Z the same, for a tag already pushed
#
# The module lives in src/: GoReleaser runs from the repository root with
# --config src/.goreleaser.yaml and writes into .cache/goreleaser/, never into
# build/.
# Push a version tag only for final code: tags are final, and a pushed tag publishes. The shared
# workflow reads src/.goreleaser.yaml (dist: .cache/goreleaser, never build/), takes the notes from
# the tag's CHANGELOG.md section and pushes the src/vX.Y.Z module tag, which matches no trigger here.
# src/cmd/osaat's TestReleaseRunsOnAVersionTagOrOnRequest fails if a trigger is added or the job
# stops calling the shared workflow.
name: Release

on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-*'
workflow_dispatch:
inputs:
confirm:
description: Type release-go to publish binaries for the tag this run is dispatched on
tag:
description: 'An existing vX.Y.Z tag to build and publish'
required: true
type: string

# Releases must never be cancelled mid-publish by a newer tag.
permissions: {}

# One run per tag, never cancelled: two runs must not publish the same release.
concurrency:
group: release-${{ github.ref }}
group: release-${{ inputs.tag || github.ref_name }}
cancel-in-progress: false

permissions:
contents: read

jobs:
goreleaser:
# Linux cross-compiles every target (pure Go) and builds the native
# deb/rpm/apk packages, so one ubuntu runner produces the full matrix.
runs-on: ubuntu-latest
environment: release
timeout-minutes: 30
env:
# Whether both deploy keys are set, never the keys: secrets cannot be read in a step's `if`.
USE_DEPLOY_KEYS: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY != '' && secrets.SCOOP_BUCKET_DEPLOY_KEY != '' }}
release:
permissions:
contents: write # create the release and upload assets
id-token: write # keyless signing for the provenance attestation
attestations: write # write the build-provenance attestation
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 # full history + tags for GoReleaser

- uses: actions/setup-go@v7
with:
go-version-file: src/go.mod
cache: true
cache-dependency-path: src/go.sum

# syft powers GoReleaser's SBOM generation.
- uses: anchore/sbom-action/download-syft@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3

# A Go build of this module needs a tag named after its subdirectory: the
# module is github.com/simtabi/osaat/src, so Go resolves src/vX.Y.Z and
# ignores vX.Y.Z. OSS GoReleaser rejects the prefixed tag as non-semver,
# so the release runs off vX.Y.Z and mirrors src/vX.Y.Z onto the same
# commit here, where it cannot be forgotten. This workflow starts on no
# tag push, so pushing src/... starts no second run. A re-run finds the
# tag already there and leaves it alone: a public tag is never moved.
- name: Mirror the tag Go resolves the module through
run: |
alias_tag="src/${GITHUB_REF_NAME}"
if git rev-parse -q --verify "refs/tags/${alias_tag}" >/dev/null; then
echo "${alias_tag} already exists"
else
git tag "${alias_tag}" "${GITHUB_REF_NAME}^{commit}"
git push origin "refs/tags/${alias_tag}"
echo "pushed ${alias_tag}"
fi

- name: Extract CHANGELOG section for this tag
# Written outside the checkout: GoReleaser refuses to release from a dirty tree,
# and an untracked release_notes.md in the repo root is exactly that (the
# v0.1.0 tag failed on it). A snapshot build skips that check, so it never showed.
run: ./src/scripts/extract-changelog.sh "${GITHUB_REF_NAME}" > "${RUNNER_TEMP}/release_notes.md"

# The Homebrew tap and Scoop bucket only accept changes through a pull
# request, so GoReleaser pushes a branch there and opens one. GITHUB_TOKEN
# is scoped to this repository and cannot do that, and a PAT would be a
# long-lived credential; Refresh Bot mints a token for this run only,
# limited to the two package repositories. Both values are org-level.
# Not minted once both deploy keys are set: GoReleaser then pushes with the keys, and the
# step's output is simply empty.
- name: Mint a package-repository token
id: tap
if: env.USE_DEPLOY_KEYS != 'true'
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.REFRESH_APP_CLIENT_ID }}
private-key: ${{ secrets.REFRESH_APP_PRIVATE_KEY }}
owner: simtabi
repositories: |
homebrew-tap
scoop-bucket
permission-contents: write
permission-pull-requests: write

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@a4f02ab5355a589de60d917a659ce617ed19b3de # v7.3.0
with:
version: latest
args: release --clean --config src/.goreleaser.yaml --release-notes=${{ runner.temp }}/release_notes.md
env:
# Two tags now sit on this commit (vX.Y.Z and src/vX.Y.Z); name the one
# GoReleaser releases so it never reads the version from the alias.
GORELEASER_CURRENT_TAG: ${{ github.ref_name }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAP_GITHUB_TOKEN: ${{ steps.tap.outputs.token }}
# Write deploy keys on simtabi/homebrew-tap and simtabi/scoop-bucket. When set, GoReleaser
# pushes with them and the token is not used for those two.
HOMEBREW_TAP_DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
SCOOP_BUCKET_DEPLOY_KEY: ${{ secrets.SCOOP_BUCKET_DEPLOY_KEY }}
# Scoop/Winget publishing is skipped until the bucket/fork exist. Set
# repo variable SKIP_WINDOWS_PKGS to "false" to enable it.
SKIP_WINDOWS_PKGS: ${{ vars.SKIP_WINDOWS_PKGS || 'true' }}
# Scoop's own switch; empty falls back to SKIP_WINDOWS_PKGS (see the GoReleaser config).
SKIP_SCOOP: ${{ vars.SKIP_SCOOP }}

# Sign a build-provenance attestation for every released artifact. The
# checksums file lists them all, so `gh attestation verify <file>
# --repo <owner>/<repo>` works for any downloaded binary or archive.
- name: Attest build provenance
uses: actions/attest-build-provenance@v4
with:
subject-checksums: .cache/goreleaser/checksums.txt
contents: write
id-token: write
attestations: write
uses: simtabi/.github/.github/workflows/go-release.yml@745880bdb9b35a21eb16824cfc995f16e1e82fd5 # simtabi/.github#27
with:
tag: ${{ inputs.tag || github.ref_name }}
27 changes: 20 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `src/scripts/build-all.sh` no longer deletes `build/` whole or rewrites the tracked
`build/targets.txt`; it removes generated output only.

- 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 osaat from source only: no download, package-manager
or `go install` path is offered while binary releases are on hold. The 0.1.1 binaries and
Homebrew cask are described as frozen, not removed.
- Binaries ship on the GitHub release only. A release builds on a pushed `vX.Y.Z` tag (or a
`vX.Y.Z-rc.N` pre-release), or on request for a tag already pushed
(`gh workflow run release.yml --repo simtabi/osaat -f tag=vX.Y.Z`). `release.yml` is now a thin
caller of simtabi/.github's shared `go-release.yml`, which checks the tag, takes the notes from
this file, mirrors `src/vX.Y.Z`, runs GoReleaser and attests provenance. This replaces the
dispatch-only, `-f confirm=release-go` workflow of this cycle, which never shipped.
- The README and installation guide offer two paths, in order: download the binary from the GitHub
release and check it against `checksums.txt`, or build from source. No package-manager or
`go install` path is offered.

### Removed

- The Homebrew cask (`simtabi/homebrew-tap`), Scoop and Winget publishing from
`src/.goreleaser.yaml`, and the deploy keys, app token and `SKIP_SCOOP` / `SKIP_WINDOWS_PKGS`
switches that fed them. The 0.1.1 cask is frozen: it stays in the tap and keeps working, and no
release updates it.

### Added

- Tests that fail if `build/` holds anything but OS folders, its inputs, `checksums.txt` and
`archives/`; if a Dockerfile, compose file or image-publishing step appears; or if the docs offer
a `go install` path while releases are on hold.
a `go install` path.
- Tests that fail if `release.yml` gains a branch push, schedule, pull-request or release trigger,
stops calling the shared release workflow, or switches off the `src/vX.Y.Z` alias, and if
`src/.goreleaser.yaml` gains any publishing block besides the GitHub release.

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# as macos and .exe on Windows. `make check-build` (TestBuildLayout) fails on
# anything else.
#
# Nothing here publishes. Releases are dispatched by hand (docs/release.md).
# Nothing here publishes. A release builds on a pushed vX.Y.Z tag or on request (docs/release.md).

SHELL := /usr/bin/env bash
.DEFAULT_GOAL := help
Expand Down
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,19 @@ The binary is `osaat`.

## Install

osaat is built from source: binary releases are on hold, so there is no
download, package-manager or `go install` channel to use. You need Go (the
version `src/go.mod` names), `git` and `make`.
Download the binary for your platform from the
[latest GitHub release](https://github.com/simtabi/osaat/releases/latest)
(`osaat_<os>_<arch>`, e.g. `osaat_macos_universal`, `osaat_linux_amd64`), check
it against the release's `checksums.txt`, and put it on your `PATH`:

```sh
shasum -a 256 -c checksums.txt --ignore-missing
install -m 0755 osaat_macos_universal ~/.local/bin/osaat
```

The GitHub release is the only channel: there is no package-manager or
`go install` path. Or build from source, with Go (the version `src/go.mod`
names), `git` and `make`:

```sh
git clone https://github.com/simtabi/osaat ~/src/osaat
Expand All @@ -20,8 +30,8 @@ make -C ~/src/osaat install # builds build/<os>/osaat, copies it to ~/.local/
`make build` alone leaves the binary at `build/macos/osaat` (or `build/linux/`,
`build/freebsd/`, `build/windows/osaat.exe`) for you to put on your `PATH`.

Pinning a version, updating, building without `make`, and the binaries and
Homebrew cask published for 0.1.1 (frozen, not updated) are in
Verifying the attestation, pinning a version, updating, building without
`make`, and the Homebrew cask published for 0.1.1 (frozen, not updated) are in
[docs/installation.md](docs/installation.md).

**Zero-dep Bash fallback (macOS only):** for the cold-start case
Expand Down Expand Up @@ -116,7 +126,7 @@ End-user documentation for `osaat`. Project-internal design notes live in
| [Installation](docs/installation.md) | How to build and install from source on macOS, Linux, BSD |
| [Configuration](docs/configuration.md) | Config file format, environment variables, profiles |
| [Architecture](docs/architecture.md) | What the tool does and how it does it, at a level useful for contributors |
| [Release process](docs/release.md) | How a release is cut, and the confirmed dispatch that publishes binaries |
| [Release process](docs/release.md) | How a release is cut: a pushed tag, or a request, publishes the binaries |
| [Migration / shipping checklist](docs/shipping-checklist.md) | Step-by-step migration runbook |

Per-command reference:
Expand Down
3 changes: 2 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ This policy covers the source code in this repository and the binaries
distributed via:

- GitHub Releases under `simtabi/osaat`
- The `simtabi/homebrew-tap` Homebrew tap formula `osaat`
- The `simtabi/homebrew-tap` Homebrew cask `osaat`, for the versions it
already holds (frozen since 2026-11-13: no release updates it)

It does not cover:

Expand Down
39 changes: 29 additions & 10 deletions docs/installation.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
# Installation

Two ways, in this order: download the binary from the GitHub release, or build
from source. The GitHub release is the only channel: this page offers no
package-manager or `go install` path.

## Download a release

Each [GitHub release](https://github.com/simtabi/osaat/releases) carries bare
binaries `osaat_<os>_<arch>` (`.exe` on Windows; `macos`, not `darwin`), archives,
Linux `.deb`/`.rpm`/`.apk` packages, and `checksums.txt`. Download the one for
your platform and the checksums, verify, and put it on your `PATH`:

```sh
gh release download --repo simtabi/osaat --pattern 'osaat_macos_universal' --pattern checksums.txt
shasum -a 256 -c checksums.txt --ignore-missing
install -m 0755 osaat_macos_universal ~/.local/bin/osaat
```

Every asset in `checksums.txt` carries a build-provenance attestation:

```sh
gh attestation verify osaat_macos_universal --repo simtabi/osaat
```

## Build from source

osaat is built from source. Binary releases are on hold until the maintainer
decides otherwise, so this page offers no download, package-manager or
`go install` path. You need Go 1.25 or newer (the version `src/go.mod` names),
`git` and `make`.
You need Go 1.25 or newer (the version `src/go.mod` names), `git` and `make`.

```sh
git clone https://github.com/simtabi/osaat ~/src/osaat
Expand Down Expand Up @@ -48,13 +68,12 @@ make -C ~/src/osaat install
the tag or commit the binary was built from, and a build from a branch is still
identifiable.

### Earlier binary releases
### Frozen channels

0.1.1 was published with binaries and archives for macOS, Linux, Windows and
FreeBSD, Linux `.deb`/`.rpm`/`.apk` packages, checksums with a build-provenance
attestation, and a Homebrew cask in `simtabi/homebrew-tap`. Those stay where they
are, and an existing install keeps working, but none of those channels is updated
while releases are on hold: build from source to get anything newer.
0.1.1 was also published as a Homebrew cask in `simtabi/homebrew-tap`. That cask
stays where it is and an existing install keeps working, but it is frozen: no
release updates it. Download from the GitHub release, or build from source, to
get anything newer.

## Zero-dependency Bash fallback (macOS only)

Expand Down
Loading
Loading