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/taskgate-allow.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
# would be in the module graph of every repository that imports it.
.github/workflows/ci.yml/gate/Task's pin # read before Task is installed
.github/workflows/pages.yml/build/Task's pin # read before Task is installed
.github/workflows/ci.yml/release/Task's pin # read before Task is installed
.github/workflows/release.yml/release/Task's pin # read before Task is installed
41 changes: 0 additions & 41 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,44 +111,3 @@ jobs:
archive: false
retention-days: 7
if-no-files-found: ignore

# Main's commit, once the gate has passed on it, as the binaries a repository's `go tool task`
# runs (cmd/task): every command for each platform, attested as built here from this commit, and
# published as the release bin-<commit time> that a versions.yaml pins by sha256.
release:
name: Release
needs: gate
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod

- name: Task's pin
run: go run ./cmd/versions env task >> "$GITHUB_ENV"

- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: ${{ env.TASK_VERSION }}
checksum: ${{ env.TASK_SHA256 }}

- name: The binaries
run: task release:build

- name: Their provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: _output/release/*.tar.gz

- name: The release
env:
GH_TOKEN: ${{ github.token }}
run: task release:publish
78 changes: 78 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# go-tools' release, named as spin-stack names releases: v<YYYYMMDD>.<NN>, the day in UTC and which
# release of that day (internal/release). Made of main each time CI passes on it - the commit that
# passed, not whatever main is when this starts - and published last: a release is its files, built
# and attested here, then the tag. What a repository's `go tool task` (cmd/task) runs is the latest.
name: Release

on:
workflow_run:
workflows: [CI]
types: [completed]
branches: [main]

# One at a time: a release is named at the start and tagged at the end, so two at once would name
# the same one.
concurrency:
group: release
cancel-in-progress: false

permissions:
contents: read

env:
GITHUB_TOKEN: ${{ github.token }}

jobs:
release:
name: Release
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.workflow_run.head_sha }}
# Every tag, which the next release's name is counted from.
fetch-depth: 0

- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod

- name: Task's pin
run: go run ./cmd/versions env task >> "$GITHUB_ENV"

- uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
with:
version: ${{ env.TASK_VERSION }}
checksum: ${{ env.TASK_SHA256 }}

# The release this commit is: the one already tagged at it when CI was run again, the next
# name today otherwise.
- name: Its name
id: name
env:
SHA: ${{ github.event.workflow_run.head_sha }}
run: task release:name

- name: The binaries
if: steps.name.outputs.new == 'true'
run: task release:build

- name: Their provenance
if: steps.name.outputs.new == 'true'
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: _output/release/*.tar.gz

- name: The release
if: steps.name.outputs.new == 'true'
env:
GH_TOKEN: ${{ github.token }}
SHA: ${{ github.event.workflow_run.head_sha }}
VERSION: ${{ steps.name.outputs.version }}
run: task release:publish
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ their `go.mod`; each fails CI with a specific finding and says how to resolve it
| [`linkweight`](https://spin-stack.github.io/go-tools/linkweight/) | what each binary links and why: the bytes every module puts in it, the function that stops the linker dropping unused methods, the import whose cut drops the most, and a capability - exec, listen, mount, setuid - no line of the allowlist explains |
| [`proven`](https://spin-stack.github.io/go-tools/proven/) | what CI already proved about a commit, by its tree: the run that answers for it, what a run on main is of, whether it may be released, each test's time, which failed tests are flaky |
| [`exposure`](https://spin-stack.github.io/go-tools/exposure/) | what a repository would show if made public: secrets and private values in every ref's commits, pull request heads included, and in its issues, comments and releases |
| [`task`](https://spin-stack.github.io/go-tools/start/) | not a check: Task at the version `versions.yaml` pins, with every other go-tools command on its `PATH`, both downloaded as released binaries - the one command a repository's `go.mod` takes, so neither Task's modules nor the tools' are compiled or cached |
| [`task`](https://spin-stack.github.io/go-tools/start/) | not a check: Task at the version `versions.yaml` pins, with every other go-tools command on its `PATH`, the tools at their latest release - both downloaded as binaries; the one command a repository's `go.mod` takes, so neither Task's modules nor the tools' are compiled or cached |
| [`gencache`](https://spin-stack.github.io/go-tools/gencache/) | not a check: `go generate` with each mockgen directive answered from a cache when nothing its mock is made from has changed |
| [`ghatrace`](https://spin-stack.github.io/go-tools/ghatrace/) | not a check: where a GitHub Actions run's time went - the critical path, each job's wait for a runner, its steps and the spans inside them - as the run's summary, an HTML page and a Perfetto trace, with nothing hosted |
| [`provenance`, `sbom`, `attestgate`, `trustroot`](https://spin-stack.github.io/go-tools/supply-chain/) | releases published without SLSA provenance or an SBOM |

```sh
go get -tool github.com/spin-stack/go-tools/cmd/task # and task + go-tools in versions.yaml
go get -tool github.com/spin-stack/go-tools/cmd/task # and task in versions.yaml
go tool task mutate # a task that runs `mutate -base origin/main`
```

[Getting started](https://spin-stack.github.io/go-tools/start/) has the two `versions.yaml` entries.
[Getting started](https://spin-stack.github.io/go-tools/start/) has the `versions.yaml` entry.

`versions`, `versions/upstream`, `versions/gate`, `provenance`, `attestgate`, `sbom`, `ghatrace` and
`trustroot` are also packages another repository imports; the rest is `internal/`.
Expand Down
32 changes: 18 additions & 14 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,23 +137,27 @@ tasks:
cmd: >-
tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="@$(git show -s --format=%ct HEAD)"
-C {{.OUT}}/release/{{replace "/" "_" .ITEM}} -czf {{.OUT}}/release/go-tools_{{replace "/" "_" .ITEM}}.tar.gz .
# What cmd/task holds a download of the latest release to, no versions.yaml pinning it.
- cd {{.OUT}}/release && sha256sum go-tools_*.tar.gz > checksums.txt

release:name:
desc: The release SHA is - released already, or the next v<YYYYMMDD>.<NN> today - as version= and new= for $GITHUB_OUTPUT.
requires:
vars: [SHA]
cmds:
- go run ./internal/release/cmd/releasename {{.SHA}} >> "${GITHUB_OUTPUT:-/dev/stdout}"

release:publish:
desc: Publish _output/release's archives as the GitHub release bin-<commit time>, the version a versions.yaml pins (GH_TOKEN, GITHUB_SHA).
# Named by the commit's UTC time, which is also the time in the pseudo-version a go.mod requires
# this commit at: versions/gate holds a repository's go-tools entry to its go.mod's. A tag of
# that shape is no module version, so `go get` never takes it for one. A run that is retried
# publishes the release it made the first time, and fails rather than replace another's files.
desc: Publish _output/release as the GitHub release VERSION of SHA, which tags it (GH_TOKEN).
# The tag is made by the release, last: a release that failed to publish is no tag that names a
# commit with no files.
requires:
vars: [SHA, VERSION]
cmds:
- |
set -euo pipefail
tag="bin-$(TZ=UTC git show -s --format=%cd --date=format-local:%Y%m%d.%H%M%S "${GITHUB_SHA}")"
if gh release view "$tag" >/dev/null 2>&1; then
echo "$tag is published already"
exit 0
fi
gh release create "$tag" {{.OUT}}/release/*.tar.gz --target "${GITHUB_SHA}" --title "$tag" \
--notes "Every go-tools command at ${GITHUB_SHA}, for a repository's versions.yaml: \`go tool versions bump go-tools\`."
- >-
gh release create {{.VERSION}} {{.OUT}}/release/*.tar.gz {{.OUT}}/release/checksums.txt
--target {{.SHA}} --title {{.VERSION}}
--notes "Every go-tools command at {{.SHA}}, attested as built by release.yml. A repository's \`go tool task\` runs the latest."

site:
desc: Build the documentation site (site/, Astro Starlight) into site/dist; task site:dev serves it with live reload.
Expand Down
37 changes: 20 additions & 17 deletions cmd/task/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
//
// A repository takes this one command in its go.mod instead of Task's and go-tools' own: compiling
// those is about 450 modules and a minute or more on a runner, for programs their authors already
// release. This one links versions.yaml's reader and a download, and it puts versions.yaml's `task`
// entry - and its `go-tools` entry, when it has one - in the user's cache, each held to its sha256
// and unpacked once by it (internal/toolcache), then becomes Task: the same process, so a signal,
// the terminal and the exit status are Task's.
// release. This one links versions.yaml's reader and a download. It puts versions.yaml's `task`
// in the user's cache, held to its sha256, and go-tools' latest release, held to the sha256 its
// checksums give - every repository runs the newest commands main has passed its gate with, and
// none bumps them - each unpacked once (internal/toolcache). Then it becomes Task: the same
// process, so a signal, the terminal and the exit status are Task's.
package main

import (
Expand All @@ -25,18 +26,23 @@ import (
"github.com/spin-stack/go-tools/versions"
)

// goTools is where go-tools' releases are, made by its release.yml of main each time its gate
// passes.
const goTools = "https://github.com/spin-stack/go-tools"

func main() {
err := run(context.Background(), os.Args[1:], syscall.Exec) // mutate-exempt: the process's own arguments; run is what the test drives
fmt.Fprintln(os.Stderr, "task:", err) //nolint:errcheck // the error is the exit status's reason; stderr failing changes no status
os.Exit(2) // mutate-exempt: main is reached only when the exec failed, which no test makes it do; run is what the tests drive
err := run(context.Background(), os.Args[1:], goTools, syscall.Exec) // mutate-exempt: the process's own arguments; run is what the test drives
fmt.Fprintln(os.Stderr, "task:", err) //nolint:errcheck // the error is the exit status's reason; stderr failing changes no status
os.Exit(2) // mutate-exempt: main is reached only when the exec failed, which no test makes it do; run is what the tests drive
}

// execFunc replaces the process with argv[0]'s program: syscall.Exec, and a recorder in a test.
type execFunc func(path string, argv []string, env []string) error

// run finds the repository's versions.yaml, installs what it pins, and execs Task with args. It
// returns only when that fails: on success the process is Task's.
func run(ctx context.Context, args []string, exec execFunc) error {
// run finds the repository's versions.yaml, installs the Task it pins and the latest release of
// the tools at base, and execs Task with args. It returns only when that fails: on success the
// process is Task's.
func run(ctx context.Context, args []string, base string, exec execFunc) error {
wd, err := os.Getwd()
if err != nil {
return err
Expand All @@ -62,15 +68,12 @@ func run(ctx context.Context, args []string, exec execFunc) error {
if err != nil {
return err
}
path := os.Getenv("PATH")
if tools, err := v.Get("go-tools"); err == nil {
dir, err := toolcache.Install(ctx, tools, root)
if err != nil {
return err
}
path = dir + string(os.PathListSeparator) + path
tools, err := toolcache.Latest(ctx, base, "go-tools", root)
if err != nil {
return err
}
bin := filepath.Join(taskDir, "task")
path := tools + string(os.PathListSeparator) + os.Getenv("PATH")
return exec(bin, append([]string{bin}, args...), withPath(os.Environ(), path))
}

Expand Down
Loading
Loading