detached 6baf0bb

#5 ci(release): publish zut releases on ermite

state
merged · 6baf0bb · fast-forward
branch
ci/ermite-release → main
opened
bnema ·
updated

Moves zut releases to ermite.

  • release.yml: runs on pushed v* tags, inline GoReleaser (no reusable workflow); runs on ermite and on the GitHub mirror (copy for zut update until zut#4 ships).
  • The daily auto-tag cron is removed: ermite job tokens cannot push tags. Releases are tagged by the maintainer (CONTRIBUTING.md).
  • .goreleaser.yaml: templated github_urls (GitHub on github.com, ermite elsewhere), git changelog, forge install one-liner.
  • install.sh / install.ps1: latest + downloads from git.bnema.dev; private-repo token paths removed (repo is public).
  • go 1.27 in go.mod; CI reads go-version-file.

Checks: goreleaser check, snapshot build with the CI relay URL, shellcheck, actionlint (0 findings), go build + go test ./..., live latest-resolution against neferbar.

CommitSubjectAuthorDateSignatureStatus
6baf0bbci(lint): run golangci-lint v2.14.0, which supports go 1.27briceGPG Verified
513f4cfci: drop the auto-tag policy test and run macOS and Windows on GitHub onlybriceGPG Verified
5ac6a24ci(release): publish releases on ermite from pushed tagsbriceGPG Verified
11 files · +80 -262
2/2
M .forgejo/workflows/ci.yml +2 -5
@@ -23,7 +23,7 @@ jobs:
2323 - name: set up go
2424 uses: actions/setup-go@v7
2525 with:
26- go-version: "1.26"
26+ go-version-file: go.mod
2727 cache: true
2828
2929 - name: go vet
@@ -41,10 +41,7 @@ jobs:
4141 - name: go test
4242 run: go test -race ./...
4343
44- - name: release-version policy
45- run: bash .github/scripts/next-release-tag_test.sh
46-
4744 - name: golangci-lint
4845 uses: golangci/golangci-lint-action@v9.3.0
4946 with:
50- version: v2.12.2
47+ version: v2.14.0
@@ -23,7 +23,7 @@ jobs:
23 - name: set up go23 - name: set up go
24 uses: actions/setup-go@v724 uses: actions/setup-go@v7
25 with:25 with:
26- go-version: "1.26"26+ go-version-file: go.mod
27 cache: true27 cache: true
28 28
29 - name: go vet29 - name: go vet
@@ -41,10 +41,7 @@ jobs:
41 - name: go test41 - name: go test
42 run: go test -race ./...42 run: go test -race ./...
43 43
44- - name: release-version policy
45- run: bash .github/scripts/next-release-tag_test.sh
46-
47 - name: golangci-lint44 - name: golangci-lint
48 uses: golangci/golangci-lint-action@v9.3.045 uses: golangci/golangci-lint-action@v9.3.0
49 with:46 with:
50- version: v2.12.247+ version: v2.14.0
D .github/scripts/next-release-tag.sh +0 -60
@@ -1,60 +0,0 @@
1-#!/usr/bin/env bash
2-# Print the release tag for HEAD, or nothing when its unreleased commits do not
3-# require a release. A tag already pointing to HEAD is emitted again so a
4-# failed GoReleaser job can be retried without minting a second tag.
5-set -euo pipefail
6-
7-latest=$(git tag --list 'v*.*.*' --sort=-v:refname | head -n1)
8-head=$(git rev-parse HEAD)
9-if [ -n "$latest" ] && [ "$(git rev-parse "$latest^{}")" = "$head" ]; then
10- printf '%s\n' "$latest"
11- exit 0
12-fi
13-
14-if [ -n "$latest" ]; then
15- range="${latest}..HEAD"
16-else
17- range="HEAD"
18-fi
19-
20-minor=false
21-patch=false
22-while IFS= read -r commit; do
23- subject=$(git log -1 --format=%s "$commit")
24- body=$(git log -1 --format=%b "$commit")
25-
26- # A breaking change is a v0.x minor release.
27- if grep -Eq '^[[:alpha:]]+(\([^)]+\))?!:' <<<"$subject" || grep -Eiq '^BREAKING[ -]CHANGE:' <<<"$body"; then
28- minor=true
29- continue
30- fi
31-
32- type=${subject%%(*}
33- type=${type%%:*}
34- type=${type%%!*}
35- type=$(tr '[:upper:]' '[:lower:]' <<<"$type")
36- case "$type" in
37- feat) minor=true ;;
38- fix|perf|refactor|build|chore|ci) patch=true ;;
39- docs|test|style) ;;
40- *) ;;
41- esac
42-done < <(git rev-list "$range")
43-
44-if [ "$minor" != true ] && [ "$patch" != true ]; then
45- exit 0
46-fi
47-
48-# The renamed project starts a new public release line at v0.1.0.
49-if [ -z "$latest" ]; then
50- printf '%s\n' 'v0.1.0'
51- exit 0
52-fi
53-
54-version=${latest#v}
55-IFS=. read -r major minor_version patch_version <<<"$version"
56-if [ "$minor" = true ]; then
57- printf 'v%s.%s.0\n' "$major" "$((minor_version + 1))"
58-else
59- printf 'v%s.%s.%s\n' "$major" "$minor_version" "$((patch_version + 1))"
60-fi
@@ -1,60 +0,0 @@
1-#!/usr/bin/env bash
2-# Print the release tag for HEAD, or nothing when its unreleased commits do not
3-# require a release. A tag already pointing to HEAD is emitted again so a
4-# failed GoReleaser job can be retried without minting a second tag.
5-set -euo pipefail
6-
7-latest=$(git tag --list 'v*.*.*' --sort=-v:refname | head -n1)
8-head=$(git rev-parse HEAD)
9-if [ -n "$latest" ] && [ "$(git rev-parse "$latest^{}")" = "$head" ]; then
10- printf '%s\n' "$latest"
11- exit 0
12-fi
13-
14-if [ -n "$latest" ]; then
15- range="${latest}..HEAD"
16-else
17- range="HEAD"
18-fi
19-
20-minor=false
21-patch=false
22-while IFS= read -r commit; do
23- subject=$(git log -1 --format=%s "$commit")
24- body=$(git log -1 --format=%b "$commit")
25-
26- # A breaking change is a v0.x minor release.
27- if grep -Eq '^[[:alpha:]]+(\([^)]+\))?!:' <<<"$subject" || grep -Eiq '^BREAKING[ -]CHANGE:' <<<"$body"; then
28- minor=true
29- continue
30- fi
31-
32- type=${subject%%(*}
33- type=${type%%:*}
34- type=${type%%!*}
35- type=$(tr '[:upper:]' '[:lower:]' <<<"$type")
36- case "$type" in
37- feat) minor=true ;;
38- fix|perf|refactor|build|chore|ci) patch=true ;;
39- docs|test|style) ;;
40- *) ;;
41- esac
42-done < <(git rev-list "$range")
43-
44-if [ "$minor" != true ] && [ "$patch" != true ]; then
45- exit 0
46-fi
47-
48-# The renamed project starts a new public release line at v0.1.0.
49-if [ -z "$latest" ]; then
50- printf '%s\n' 'v0.1.0'
51- exit 0
52-fi
53-
54-version=${latest#v}
55-IFS=. read -r major minor_version patch_version <<<"$version"
56-if [ "$minor" = true ]; then
57- printf 'v%s.%s.0\n' "$major" "$((minor_version + 1))"
58-else
59- printf 'v%s.%s.%s\n' "$major" "$minor_version" "$((patch_version + 1))"
60-fi
D .github/scripts/next-release-tag_test.sh +0 -48
@@ -1,48 +0,0 @@
1-#!/usr/bin/env bash
2-set -euo pipefail
3-
4-repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
5-script="$repo_root/.github/scripts/next-release-tag.sh"
6-tmp=$(mktemp -d)
7-trap 'rm -rf "$tmp"' EXIT
8-
9-cd "$tmp"
10-git init --quiet
11-git config user.name 'Release Test'
12-git config user.email 'release-test@example.invalid'
13-
14-git commit --allow-empty --quiet -m 'feat: first release'
15-expect() {
16- local want=$1
17- local got
18- got=$($script)
19- if [ "$got" != "$want" ]; then
20- printf 'next-release-tag = %q, want %q\n' "$got" "$want" >&2
21- exit 1
22- fi
23-}
24-
25-expect 'v0.1.0'
26-git tag -a v0.1.0 -m 'release v0.1.0'
27-
28-# A rerun after tag creation must release the existing tag, not skip it.
29-expect 'v0.1.0'
30-
31-git commit --allow-empty --quiet -m 'docs: explain releases'
32-expect ''
33-
34-git commit --allow-empty --quiet -m 'fix: repair installer'
35-expect 'v0.1.1'
36-git tag -a v0.1.1 -m 'release v0.1.1'
37-
38-git commit --allow-empty --quiet -m 'refactor!: replace extension handshake'
39-expect 'v0.2.0'
40-
41-# GoReleaser passes tag_sort directly to `git tag --sort`, so keep the
42-# configured semantic-version sort valid before a release run depends on it.
43-tag_sort=$(sed -n 's/^[[:space:]]*tag_sort:[[:space:]]*//p' "$repo_root/.goreleaser.yaml")
44-if [ "$tag_sort" != '-version:refname' ]; then
45- printf 'goreleaser tag_sort = %q, want %q\n' "$tag_sort" '-version:refname' >&2
46- exit 1
47-fi
48-git tag --sort "$tag_sort" --list 'v*.*.*' >/dev/null
@@ -1,48 +0,0 @@
1-#!/usr/bin/env bash
2-set -euo pipefail
3-
4-repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
5-script="$repo_root/.github/scripts/next-release-tag.sh"
6-tmp=$(mktemp -d)
7-trap 'rm -rf "$tmp"' EXIT
8-
9-cd "$tmp"
10-git init --quiet
11-git config user.name 'Release Test'
12-git config user.email 'release-test@example.invalid'
13-
14-git commit --allow-empty --quiet -m 'feat: first release'
15-expect() {
16- local want=$1
17- local got
18- got=$($script)
19- if [ "$got" != "$want" ]; then
20- printf 'next-release-tag = %q, want %q\n' "$got" "$want" >&2
21- exit 1
22- fi
23-}
24-
25-expect 'v0.1.0'
26-git tag -a v0.1.0 -m 'release v0.1.0'
27-
28-# A rerun after tag creation must release the existing tag, not skip it.
29-expect 'v0.1.0'
30-
31-git commit --allow-empty --quiet -m 'docs: explain releases'
32-expect ''
33-
34-git commit --allow-empty --quiet -m 'fix: repair installer'
35-expect 'v0.1.1'
36-git tag -a v0.1.1 -m 'release v0.1.1'
37-
38-git commit --allow-empty --quiet -m 'refactor!: replace extension handshake'
39-expect 'v0.2.0'
40-
41-# GoReleaser passes tag_sort directly to `git tag --sort`, so keep the
42-# configured semantic-version sort valid before a release run depends on it.
43-tag_sort=$(sed -n 's/^[[:space:]]*tag_sort:[[:space:]]*//p' "$repo_root/.goreleaser.yaml")
44-if [ "$tag_sort" != '-version:refname' ]; then
45- printf 'goreleaser tag_sort = %q, want %q\n' "$tag_sort" '-version:refname' >&2
46- exit 1
47-fi
48-git tag --sort "$tag_sort" --list 'v*.*.*' >/dev/null
M .github/workflows/ci.yml +4 -7
@@ -19,14 +19,15 @@ jobs:
1919 strategy:
2020 fail-fast: false
2121 matrix:
22- os: [ubuntu-latest, macos-latest, windows-latest]
22+ # ermite has no macOS or Windows runner: those legs run on GitHub only.
23+ os: ${{ fromJSON(github.server_url == 'https://github.com' && '["ubuntu-latest","macos-latest","windows-latest"]' || '["ubuntu-latest"]') }}
2324 steps:
2425 - uses: actions/checkout@v4
2526
2627 - name: set up go
2728 uses: actions/setup-go@v7
2829 with:
29- go-version: "1.26"
30+ go-version-file: go.mod
3031 cache: true
3132
3233 - name: go vet
@@ -49,12 +50,8 @@ jobs:
4950
5051 # Keep Linux-only quality checks in the Ubuntu test job so CI needs
5152 # only the three platform runners.
52- - name: release-version policy
53- if: matrix.os == 'ubuntu-latest'
54- run: bash .github/scripts/next-release-tag_test.sh
55-
5653 - name: golangci-lint
5754 if: matrix.os == 'ubuntu-latest'
5855 uses: golangci/golangci-lint-action@v9.3.0
5956 with:
60- version: v2.12.2
57+ version: v2.14.0
@@ -19,14 +19,15 @@ jobs:
19 strategy:19 strategy:
20 fail-fast: false20 fail-fast: false
21 matrix:21 matrix:
22- os: [ubuntu-latest, macos-latest, windows-latest]22+ # ermite has no macOS or Windows runner: those legs run on GitHub only.
23+ os: ${{ fromJSON(github.server_url == 'https://github.com' && '["ubuntu-latest","macos-latest","windows-latest"]' || '["ubuntu-latest"]') }}
23 steps:24 steps:
24 - uses: actions/checkout@v425 - uses: actions/checkout@v4
25 26
26 - name: set up go27 - name: set up go
27 uses: actions/setup-go@v728 uses: actions/setup-go@v7
28 with:29 with:
29- go-version: "1.26"30+ go-version-file: go.mod
30 cache: true31 cache: true
31 32
32 - name: go vet33 - name: go vet
@@ -49,12 +50,8 @@ jobs:
49 50
50 # Keep Linux-only quality checks in the Ubuntu test job so CI needs51 # Keep Linux-only quality checks in the Ubuntu test job so CI needs
51 # only the three platform runners.52 # only the three platform runners.
52- - name: release-version policy
53- if: matrix.os == 'ubuntu-latest'
54- run: bash .github/scripts/next-release-tag_test.sh
55-
56 - name: golangci-lint53 - name: golangci-lint
57 if: matrix.os == 'ubuntu-latest'54 if: matrix.os == 'ubuntu-latest'
58 uses: golangci/golangci-lint-action@v9.3.055 uses: golangci/golangci-lint-action@v9.3.0
59 with:56 with:
60- version: v2.12.257+ version: v2.14.0
M .github/workflows/release.yml +18 -65
@@ -1,81 +1,34 @@
11 name: release
22
3-# Once each morning, create at most one tag for the unreleased conventional
4-# commits on main. GitHub schedules in UTC, so both possible UTC offsets are
5-# triggered and the first step keeps only the 07:00 Europe/Paris run.
3+# Pushing a v* tag publishes the release. This workflow intentionally runs on
4+# both ermite (publishes to git.bnema.dev) and the GitHub mirror (publishes a
5+# copy on github.com, because installed `zut update` only reads GitHub).
66 on:
7- schedule:
8- - cron: "0 5,6 * * *"
9- workflow_dispatch:
7+ push:
8+ tags: ["v*"]
109
1110 permissions:
1211 contents: write
1312
14-concurrency:
15- group: release
16- cancel-in-progress: false
17-
1813 jobs:
19- tag:
14+ release:
2015 runs-on: ubuntu-latest
21- outputs:
22- tag: ${{ steps.version.outputs.tag }}
2316 steps:
24- - name: Check the Paris release time
25- id: release-time
26- shell: bash
27- env:
28- EVENT_NAME: ${{ github.event_name }}
29- TZ: Europe/Paris
30- run: |
31- if [ "$EVENT_NAME" = workflow_dispatch ] || [ "$(date +%H)" = 07 ]; then
32- echo "run=true" >> "$GITHUB_OUTPUT"
33- fi
34-
35- - name: Check out main
36- if: steps.release-time.outputs.run == 'true'
17+ - name: Check out the tag
3718 uses: actions/checkout@v4
3819 with:
3920 fetch-depth: 0
40- fetch-tags: true
41- ref: main
4221
43- - name: Compute the next release tag
44- if: steps.release-time.outputs.run == 'true'
45- id: version
46- shell: bash
47- run: |
48- set -euo pipefail
49- if git log -1 --format=%B | grep -Fq '[release=skip]'; then
50- echo "The latest commit requested that the release be skipped."
51- exit 0
52- fi
53- tag=$(bash .github/scripts/next-release-tag.sh)
54- if [ -z "$tag" ]; then
55- echo "No releasable conventional commits for this run."
56- exit 0
57- fi
58- echo "Releasing $tag from $(git rev-parse --short HEAD)"
59- echo "tag=$tag" >> "$GITHUB_OUTPUT"
22+ - name: Set up Go
23+ uses: actions/setup-go@v7
24+ with:
25+ go-version-file: go.mod
6026
61- - name: Create and push the tag
62- if: steps.version.outputs.tag != ''
27+ - name: Run GoReleaser
28+ uses: goreleaser/goreleaser-action@v6
29+ with:
30+ distribution: goreleaser
31+ version: "~> v2"
32+ args: release --clean
6333 env:
64- TAG: ${{ steps.version.outputs.tag }}
65- run: |
66- git config user.name "github-actions[bot]"
67- git config user.email "github-actions[bot]@users.noreply.github.com"
68- git tag -a "$TAG" -m "release $TAG"
69- git push origin "$TAG"
70-
71- release:
72- needs: tag
73- if: needs.tag.outputs.tag != ''
74- permissions:
75- contents: write
76- uses: bnema/gh-actions/.github/workflows/go-release.yml@main
77- with:
78- go-version: "1.26"
79- # The reusable workflow fetches tags before this command, ensuring
80- # GoReleaser produces artifacts and notes for the newly created tag.
81- pre-command: git checkout --detach "${{ needs.tag.outputs.tag }}"
34+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,81 +1,34 @@
1 name: release1 name: release
2 2
3-# Once each morning, create at most one tag for the unreleased conventional3+# Pushing a v* tag publishes the release. This workflow intentionally runs on
4-# commits on main. GitHub schedules in UTC, so both possible UTC offsets are4+# both ermite (publishes to git.bnema.dev) and the GitHub mirror (publishes a
5-# triggered and the first step keeps only the 07:00 Europe/Paris run.5+# copy on github.com, because installed `zut update` only reads GitHub).
6 on:6 on:
7- schedule:7+ push:
8- - cron: "0 5,6 * * *"8+ tags: ["v*"]
9- workflow_dispatch:
10 9
11 permissions:10 permissions:
12 contents: write11 contents: write
13 12
14-concurrency:
15- group: release
16- cancel-in-progress: false
17-
18 jobs:13 jobs:
19- tag:14+ release:
20 runs-on: ubuntu-latest15 runs-on: ubuntu-latest
21- outputs:
22- tag: ${{ steps.version.outputs.tag }}
23 steps:16 steps:
24- - name: Check the Paris release time17+ - name: Check out the tag
25- id: release-time
26- shell: bash
27- env:
28- EVENT_NAME: ${{ github.event_name }}
29- TZ: Europe/Paris
30- run: |
31- if [ "$EVENT_NAME" = workflow_dispatch ] || [ "$(date +%H)" = 07 ]; then
32- echo "run=true" >> "$GITHUB_OUTPUT"
33- fi
34-
35- - name: Check out main
36- if: steps.release-time.outputs.run == 'true'
37 uses: actions/checkout@v418 uses: actions/checkout@v4
38 with:19 with:
39 fetch-depth: 020 fetch-depth: 0
40- fetch-tags: true
41- ref: main
42 21
43- - name: Compute the next release tag22+ - name: Set up Go
44- if: steps.release-time.outputs.run == 'true'23+ uses: actions/setup-go@v7
45- id: version24+ with:
46- shell: bash25+ go-version-file: go.mod
47- run: |
48- set -euo pipefail
49- if git log -1 --format=%B | grep -Fq '[release=skip]'; then
50- echo "The latest commit requested that the release be skipped."
51- exit 0
52- fi
53- tag=$(bash .github/scripts/next-release-tag.sh)
54- if [ -z "$tag" ]; then
55- echo "No releasable conventional commits for this run."
56- exit 0
57- fi
58- echo "Releasing $tag from $(git rev-parse --short HEAD)"
59- echo "tag=$tag" >> "$GITHUB_OUTPUT"
60 26
61- - name: Create and push the tag27+ - name: Run GoReleaser
62- if: steps.version.outputs.tag != ''28+ uses: goreleaser/goreleaser-action@v6
29+ with:
30+ distribution: goreleaser
31+ version: "~> v2"
32+ args: release --clean
63 env:33 env:
64- TAG: ${{ steps.version.outputs.tag }}34+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
65- run: |
66- git config user.name "github-actions[bot]"
67- git config user.email "github-actions[bot]@users.noreply.github.com"
68- git tag -a "$TAG" -m "release $TAG"
69- git push origin "$TAG"
70-
71- release:
72- needs: tag
73- if: needs.tag.outputs.tag != ''
74- permissions:
75- contents: write
76- uses: bnema/gh-actions/.github/workflows/go-release.yml@main
77- with:
78- go-version: "1.26"
79- # The reusable workflow fetches tags before this command, ensuring
80- # GoReleaser produces artifacts and notes for the newly created tag.
81- pre-command: git checkout --detach "${{ needs.tag.outputs.tag }}"
M .goreleaser.yaml +14 -5
@@ -1,10 +1,19 @@
1-# GoReleaser config — builds cross-platform binaries and GitHub Release
2-# artifacts for zut. The release workflow creates conventional-commit-based
3-# v0.x tags, then invokes GoReleaser for that immutable tag.
1+# GoReleaser config — builds cross-platform binaries and release artifacts
2+# for zut. The release workflow runs GoReleaser when a v* tag is pushed.
43 version: 2
54
65 project_name: zut
76
7+# Releases are published on ermite. In CI the runner sets GITHUB_API_URL and
8+# GITHUB_SERVER_URL to an address the job can reach; the public URLs are the
9+# fallback for a local run. The release workflow also runs on the GitHub
10+# mirror, so `zut update` in older builds, which only reads GitHub, still
11+# updates: there GoReleaser keeps its github.com defaults.
12+github_urls:
13+ api: '{{ if eq (index .Env "GITHUB_SERVER_URL") "https://github.com" }}https://api.github.com{{ else }}{{ or (index .Env "GITHUB_API_URL") "https://git.bnema.dev/-/github/api" }}{{ end }}/'
14+ upload: '{{ if eq (index .Env "GITHUB_SERVER_URL") "https://github.com" }}https://uploads.github.com{{ else }}{{ or (index .Env "GITHUB_API_URL") "https://git.bnema.dev/-/github/api" }}/uploads{{ end }}/'
15+ download: '{{ or (index .Env "GITHUB_SERVER_URL") "https://git.bnema.dev" }}/'
16+
817 before:
918 hooks:
1019 - go mod tidy
@@ -54,7 +63,7 @@ git:
5463
5564 changelog:
5665 sort: asc
57- use: github
66+ use: git
5867 filters:
5968 # Styling-only commits are neither releasable nor useful release notes.
6069 exclude:
@@ -93,7 +102,7 @@ release:
93102 Install with:
94103
95104 ```bash
96- curl -fsSL https://raw.githubusercontent.com/bnema/zut/main/install.sh | bash
105+ curl -fsSL https://git.bnema.dev/zut/raw/main/install.sh | bash
97106 ```
98107
99108 Or download a binary below, verify it with `checksums.txt`, and add it to your `$PATH`.
@@ -1,10 +1,19 @@
1-# GoReleaser config — builds cross-platform binaries and GitHub Release1+# GoReleaser config — builds cross-platform binaries and release artifacts
2-# artifacts for zut. The release workflow creates conventional-commit-based2+# for zut. The release workflow runs GoReleaser when a v* tag is pushed.
3-# v0.x tags, then invokes GoReleaser for that immutable tag.
4 version: 23 version: 2
5 4
6 project_name: zut5 project_name: zut
7 6
7+# Releases are published on ermite. In CI the runner sets GITHUB_API_URL and
8+# GITHUB_SERVER_URL to an address the job can reach; the public URLs are the
9+# fallback for a local run. The release workflow also runs on the GitHub
10+# mirror, so `zut update` in older builds, which only reads GitHub, still
11+# updates: there GoReleaser keeps its github.com defaults.
12+github_urls:
13+ api: '{{ if eq (index .Env "GITHUB_SERVER_URL") "https://github.com" }}https://api.github.com{{ else }}{{ or (index .Env "GITHUB_API_URL") "https://git.bnema.dev/-/github/api" }}{{ end }}/'
14+ upload: '{{ if eq (index .Env "GITHUB_SERVER_URL") "https://github.com" }}https://uploads.github.com{{ else }}{{ or (index .Env "GITHUB_API_URL") "https://git.bnema.dev/-/github/api" }}/uploads{{ end }}/'
15+ download: '{{ or (index .Env "GITHUB_SERVER_URL") "https://git.bnema.dev" }}/'
16+
8 before:17 before:
9 hooks:18 hooks:
10 - go mod tidy19 - go mod tidy
@@ -54,7 +63,7 @@ git:
54 63
55 changelog:64 changelog:
56 sort: asc65 sort: asc
57- use: github66+ use: git
58 filters:67 filters:
59 # Styling-only commits are neither releasable nor useful release notes.68 # Styling-only commits are neither releasable nor useful release notes.
60 exclude:69 exclude:
@@ -93,7 +102,7 @@ release:
93 Install with:102 Install with:
94 103
95 ```bash104 ```bash
96- curl -fsSL https://raw.githubusercontent.com/bnema/zut/main/install.sh | bash105+ curl -fsSL https://git.bnema.dev/zut/raw/main/install.sh | bash
97 ```106 ```
98 107
99 Or download a binary below, verify it with `checksums.txt`, and add it to your `$PATH`.108 Or download a binary below, verify it with `checksums.txt`, and add it to your `$PATH`.
M CONTRIBUTING.md +10 -0
@@ -145,6 +145,16 @@ Documentation-only pull requests do not need the Go suite unless they modify exe
145145
146146 If a validation command cannot pass because of an unrelated repository or environment problem, describe the exact command and failure in the pull request. Do not silently omit it.
147147
148+## Releases
149+
150+The maintainer publishes a release by pushing a signed version tag to the forge:
151+
152+```sh
153+git tag -s vX.Y.Z -m vX.Y.Z && git push origin vX.Y.Z
154+```
155+
156+The tag triggers the release workflow, which runs GoReleaser and publishes the archives and `checksums.txt` on git.bnema.dev. The GitHub mirror receives the same tag and publishes a copy, because installed `zut update` builds read releases from GitHub only.
157+
148158 ## Pull request shape
149159
150160 Open the pull request against `main` and include:
@@ -145,6 +145,16 @@ Documentation-only pull requests do not need the Go suite unless they modify exe
145 145
146 If a validation command cannot pass because of an unrelated repository or environment problem, describe the exact command and failure in the pull request. Do not silently omit it.146 If a validation command cannot pass because of an unrelated repository or environment problem, describe the exact command and failure in the pull request. Do not silently omit it.
147 147
148+## Releases
149+
150+The maintainer publishes a release by pushing a signed version tag to the forge:
151+
152+```sh
153+git tag -s vX.Y.Z -m vX.Y.Z && git push origin vX.Y.Z
154+```
155+
156+The tag triggers the release workflow, which runs GoReleaser and publishes the archives and `checksums.txt` on git.bnema.dev. The GitHub mirror receives the same tag and publishes a copy, because installed `zut update` builds read releases from GitHub only.
157+
148 ## Pull request shape158 ## Pull request shape
149 159
150 Open the pull request against `main` and include:160 Open the pull request against `main` and include:
M README.md +5 -5
@@ -25,19 +25,19 @@ Yet another coding agent harness, lightweight and written (vibe-slopped) in go.
2525 ### One-liner (macOS, Linux)
2626
2727 ```bash
28-curl -fsSL https://raw.githubusercontent.com/bnema/zut/main/install.sh | bash
28+curl -fsSL https://git.bnema.dev/zut/raw/main/install.sh | bash
2929 ```
3030
31-Detects your OS and architecture, downloads the latest release from GitHub, verifies the SHA-256 against the release's `checksums.txt`, extracts the binary, and drops it in `/usr/local/bin`, `~/.local/bin`, or `~/bin`, whichever is writable first. Pass a version or prefix to pin:
31+Detects your OS and architecture, downloads the latest release from git.bnema.dev, verifies the SHA-256 against the release's `checksums.txt`, extracts the binary, and drops it in `/usr/local/bin`, `~/.local/bin`, or `~/bin`, whichever is writable first. Pass a version or prefix to pin:
3232
3333 ```bash
34-curl -fsSL https://raw.githubusercontent.com/bnema/zut/main/install.sh | bash -s -- v0.1.0 ~/bin
34+curl -fsSL https://git.bnema.dev/zut/raw/main/install.sh | bash -s -- v0.1.0 ~/bin
3535 ```
3636
3737 ### One-liner (Windows, PowerShell)
3838
3939 ```powershell
40-iwr -useb https://raw.githubusercontent.com/bnema/zut/main/install.ps1 | iex
40+iwr -useb https://git.bnema.dev/zut/raw/main/install.ps1 | iex
4141 ```
4242
4343 Drops `zut.exe` into `$HOME\bin` and adds it to the user PATH if missing. Open a fresh terminal afterwards.
@@ -67,7 +67,7 @@ development build.
6767
6868 ### Prebuilt binaries
6969
70-Every release on the [releases page](https://github.com/bnema/zut/releases) ships archives for Linux, macOS, and Windows on amd64 and arm64 (except windows/arm64), plus a `checksums.txt` file. Download, verify, `chmod +x`, and drop on your `$PATH`.
70+Every release on the [releases page](https://git.bnema.dev/zut/releases) ships archives for Linux, macOS, and Windows on amd64 and arm64 (except windows/arm64), plus a `checksums.txt` file. Download, verify, `chmod +x`, and drop on your `$PATH`.
7171
7272 ## Authenticate
7373
@@ -25,19 +25,19 @@ Yet another coding agent harness, lightweight and written (vibe-slopped) in go.
25 ### One-liner (macOS, Linux)25 ### One-liner (macOS, Linux)
26 26
27 ```bash27 ```bash
28-curl -fsSL https://raw.githubusercontent.com/bnema/zut/main/install.sh | bash28+curl -fsSL https://git.bnema.dev/zut/raw/main/install.sh | bash
29 ```29 ```
30 30
31-Detects your OS and architecture, downloads the latest release from GitHub, verifies the SHA-256 against the release's `checksums.txt`, extracts the binary, and drops it in `/usr/local/bin`, `~/.local/bin`, or `~/bin`, whichever is writable first. Pass a version or prefix to pin:31+Detects your OS and architecture, downloads the latest release from git.bnema.dev, verifies the SHA-256 against the release's `checksums.txt`, extracts the binary, and drops it in `/usr/local/bin`, `~/.local/bin`, or `~/bin`, whichever is writable first. Pass a version or prefix to pin:
32 32
33 ```bash33 ```bash
34-curl -fsSL https://raw.githubusercontent.com/bnema/zut/main/install.sh | bash -s -- v0.1.0 ~/bin34+curl -fsSL https://git.bnema.dev/zut/raw/main/install.sh | bash -s -- v0.1.0 ~/bin
35 ```35 ```
36 36
37 ### One-liner (Windows, PowerShell)37 ### One-liner (Windows, PowerShell)
38 38
39 ```powershell39 ```powershell
40-iwr -useb https://raw.githubusercontent.com/bnema/zut/main/install.ps1 | iex40+iwr -useb https://git.bnema.dev/zut/raw/main/install.ps1 | iex
41 ```41 ```
42 42
43 Drops `zut.exe` into `$HOME\bin` and adds it to the user PATH if missing. Open a fresh terminal afterwards.43 Drops `zut.exe` into `$HOME\bin` and adds it to the user PATH if missing. Open a fresh terminal afterwards.
@@ -67,7 +67,7 @@ development build.
67 67
68 ### Prebuilt binaries68 ### Prebuilt binaries
69 69
70-Every release on the [releases page](https://github.com/bnema/zut/releases) ships archives for Linux, macOS, and Windows on amd64 and arm64 (except windows/arm64), plus a `checksums.txt` file. Download, verify, `chmod +x`, and drop on your `$PATH`.70+Every release on the [releases page](https://git.bnema.dev/zut/releases) ships archives for Linux, macOS, and Windows on amd64 and arm64 (except windows/arm64), plus a `checksums.txt` file. Download, verify, `chmod +x`, and drop on your `$PATH`.
71 71
72 ## Authenticate72 ## Authenticate
73 73
M go.mod +3 -3
@@ -1,6 +1,6 @@
11 module github.com/bnema/zut
22
3-go 1.26
3+go 1.27
44
55 require (
66 github.com/alecthomas/chroma/v2 v2.23.1
@@ -11,12 +11,12 @@ require (
1111 golang.org/x/image v0.43.0
1212 golang.org/x/mod v0.35.0
1313 golang.org/x/net v0.57.0
14+ golang.org/x/sys v0.47.0
15+ golang.org/x/term v0.45.0
1416 )
1517
1618 require (
1719 github.com/dlclark/regexp2 v1.11.5 // indirect
1820 github.com/kylelemons/godebug v1.1.0 // indirect
1921 github.com/rivo/uniseg v0.2.0 // indirect
20- golang.org/x/sys v0.47.0
21- golang.org/x/term v0.45.0
2222 )
@@ -1,6 +1,6 @@
1 module github.com/bnema/zut1 module github.com/bnema/zut
2 2
3-go 1.263+go 1.27
4 4
5 require (5 require (
6 github.com/alecthomas/chroma/v2 v2.23.16 github.com/alecthomas/chroma/v2 v2.23.1
@@ -11,12 +11,12 @@ require (
11 golang.org/x/image v0.43.011 golang.org/x/image v0.43.0
12 golang.org/x/mod v0.35.012 golang.org/x/mod v0.35.0
13 golang.org/x/net v0.57.013 golang.org/x/net v0.57.0
14+ golang.org/x/sys v0.47.0
15+ golang.org/x/term v0.45.0
14 )16 )
15 17
16 require (18 require (
17 github.com/dlclark/regexp2 v1.11.5 // indirect19 github.com/dlclark/regexp2 v1.11.5 // indirect
18 github.com/kylelemons/godebug v1.1.0 // indirect20 github.com/kylelemons/godebug v1.1.0 // indirect
19 github.com/rivo/uniseg v0.2.0 // indirect21 github.com/rivo/uniseg v0.2.0 // indirect
20- golang.org/x/sys v0.47.0
21- golang.org/x/term v0.45.0
22 )22 )
M install.ps1 +11 -25
@@ -1,23 +1,18 @@
11 # zut installer for Windows (PowerShell).
22 #
33 # Usage (in PowerShell):
4-# iwr -useb https://raw.githubusercontent.com/bnema/zut/main/install.ps1 | iex
4+# iwr -useb https://git.bnema.dev/zut/raw/main/install.ps1 | iex
55 #
66 # Or with arguments:
77 # $env:ZUT_VERSION = "v0.1.0"
88 # $env:ZUT_PREFIX = "$HOME\bin"
9-# iwr -useb https://raw.githubusercontent.com/bnema/zut/main/install.ps1 | iex
9+# iwr -useb https://git.bnema.dev/zut/raw/main/install.ps1 | iex
1010 #
11-# Detects architecture, downloads the matching .zip from the GitHub
11+# Detects architecture, downloads the matching .zip from the forge
1212 # release, verifies the sha256 against checksums.txt, extracts zut.exe,
1313 # and moves it into $ZUT_PREFIX (defaults to $HOME\bin, added to PATH
1414 # via the User environment if missing).
1515 #
16-# $env:GITHUB_TOKEN is optional for the public repo. Set it to a PAT
17-# with `contents:read` scope if you hit GitHub API rate limits (or if
18-# you are installing from a private fork); the script then uses it for
19-# the version lookup and every download.
20-
2116
2217 [CmdletBinding()]
2318 param(
@@ -31,11 +26,6 @@ $owner = "bnema"
3126 $repo = "zut"
3227 $binary = "zut"
3328
34-# Build Authorization header list once; used on every HTTP call so the
35-# script works against private repos when $env:GITHUB_TOKEN is set.
36-$headers = @{}
37-if ($env:GITHUB_TOKEN) { $headers["Authorization"] = "Bearer $($env:GITHUB_TOKEN)" }
38-
3929 if (-not $Version) { $Version = "latest" }
4030 if (-not $Prefix) { $Prefix = Join-Path $HOME "bin" }
4131
@@ -60,7 +50,7 @@ if ($arch -eq "arm64") {
6050
6151 # ---- resolve version ----
6252 #
63-# Resolve "latest" through the GitHub releases API. This works the same
53+# Resolve "latest" through the forge's GitHub-compatible releases API. This works the same
6454 # on Windows PowerShell 5.1 and PowerShell 7+, unlike scraping the
6555 # /releases/latest redirect target: on PS7 the final URL lives at
6656 # $resp.BaseResponse.RequestMessage.RequestUri while on PS5.1 it is
@@ -69,12 +59,8 @@ if ($arch -eq "arm64") {
6959 # to scrape.
7060
7161 if ($Version -eq "latest") {
72- $apiUrl = "https://api.github.com/repos/$owner/$repo/releases/latest"
73- # GitHub's API wants a User-Agent; Invoke-RestMethod sets one, but be
74- # explicit so corporate proxies that strip it don't trip a 403.
75- $apiHeaders = @{} + $headers
76- if (-not $apiHeaders.ContainsKey("User-Agent")) { $apiHeaders["User-Agent"] = "zut-installer" }
77- $apiHeaders["Accept"] = "application/vnd.github+json"
62+ $apiUrl = "https://git.bnema.dev/-/github/api/repos/$owner/$repo/releases/latest"
63+ $apiHeaders = @{ "User-Agent" = "zut-installer"; "Accept" = "application/json" }
7864
7965 try {
8066 $api = Invoke-RestMethod -UseBasicParsing -Headers $apiHeaders -Uri $apiUrl
@@ -84,7 +70,7 @@ if ($Version -eq "latest") {
8470 if ($status -eq 404) {
8571 Die "no published release found for $owner/$repo (the repo may have no releases yet)"
8672 } elseif ($status -eq 401 -or $status -eq 403) {
87- Die "GitHub API request was rejected ($status). If the repo is private, set `$env:GITHUB_TOKEN to a PAT with contents:read; otherwise you may be rate-limited (try again later or set `$env:GITHUB_TOKEN)."
73+ Die "forge API request was rejected ($status); try again later"
8874 } else {
8975 Die "could not resolve latest version: $($_.Exception.Message)"
9076 }
@@ -92,7 +78,7 @@ if ($Version -eq "latest") {
9278
9379 $Version = $api.tag_name
9480 if (-not $Version) {
95- Die "could not resolve latest version: GitHub API returned no tag_name for $owner/$repo"
81+ Die "could not resolve latest version: forge API returned no tag_name for $owner/$repo"
9682 }
9783 }
9884
@@ -102,7 +88,7 @@ $verNum = $Version.TrimStart("v")
10288 # ---- download + verify + extract ----
10389
10490 $archive = "${binary}_${verNum}_windows_${arch}.zip"
105-$baseUrl = "https://github.com/$owner/$repo/releases/download/$Version"
91+$baseUrl = "https://git.bnema.dev/$repo/releases/download/$Version"
10692 $archiveUrl = "$baseUrl/$archive"
10793 $checksumUrl = "$baseUrl/checksums.txt"
10894
@@ -110,11 +96,11 @@ $tmp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("zut-install-" +
11096
11197 try {
11298 Msg "downloading $archive"
113- Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri $archiveUrl -OutFile (Join-Path $tmp $archive)
99+ Invoke-WebRequest -UseBasicParsing -Uri $archiveUrl -OutFile (Join-Path $tmp $archive)
114100
115101 Msg "verifying checksum"
116102 $checksumFile = Join-Path $tmp "checksums.txt"
117- Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri $checksumUrl -OutFile $checksumFile
103+ Invoke-WebRequest -UseBasicParsing -Uri $checksumUrl -OutFile $checksumFile
118104 $expected = Get-Content -LiteralPath $checksumFile | ForEach-Object {
119105 $line = $_.Trim()
120106 if ($line) {
@@ -1,23 +1,18 @@
1 # zut installer for Windows (PowerShell).1 # zut installer for Windows (PowerShell).
2 #2 #
3 # Usage (in PowerShell):3 # Usage (in PowerShell):
4-# iwr -useb https://raw.githubusercontent.com/bnema/zut/main/install.ps1 | iex4+# iwr -useb https://git.bnema.dev/zut/raw/main/install.ps1 | iex
5 #5 #
6 # Or with arguments:6 # Or with arguments:
7 # $env:ZUT_VERSION = "v0.1.0"7 # $env:ZUT_VERSION = "v0.1.0"
8 # $env:ZUT_PREFIX = "$HOME\bin"8 # $env:ZUT_PREFIX = "$HOME\bin"
9-# iwr -useb https://raw.githubusercontent.com/bnema/zut/main/install.ps1 | iex9+# iwr -useb https://git.bnema.dev/zut/raw/main/install.ps1 | iex
10 #10 #
11-# Detects architecture, downloads the matching .zip from the GitHub11+# Detects architecture, downloads the matching .zip from the forge
12 # release, verifies the sha256 against checksums.txt, extracts zut.exe,12 # release, verifies the sha256 against checksums.txt, extracts zut.exe,
13 # and moves it into $ZUT_PREFIX (defaults to $HOME\bin, added to PATH13 # and moves it into $ZUT_PREFIX (defaults to $HOME\bin, added to PATH
14 # via the User environment if missing).14 # via the User environment if missing).
15 #15 #
16-# $env:GITHUB_TOKEN is optional for the public repo. Set it to a PAT
17-# with `contents:read` scope if you hit GitHub API rate limits (or if
18-# you are installing from a private fork); the script then uses it for
19-# the version lookup and every download.
20-
21 16
22 [CmdletBinding()]17 [CmdletBinding()]
23 param(18 param(
@@ -31,11 +26,6 @@ $owner = "bnema"
31 $repo = "zut"26 $repo = "zut"
32 $binary = "zut"27 $binary = "zut"
33 28
34-# Build Authorization header list once; used on every HTTP call so the
35-# script works against private repos when $env:GITHUB_TOKEN is set.
36-$headers = @{}
37-if ($env:GITHUB_TOKEN) { $headers["Authorization"] = "Bearer $($env:GITHUB_TOKEN)" }
38-
39 if (-not $Version) { $Version = "latest" }29 if (-not $Version) { $Version = "latest" }
40 if (-not $Prefix) { $Prefix = Join-Path $HOME "bin" }30 if (-not $Prefix) { $Prefix = Join-Path $HOME "bin" }
41 31
@@ -60,7 +50,7 @@ if ($arch -eq "arm64") {
60 50
61 # ---- resolve version ----51 # ---- resolve version ----
62 #52 #
63-# Resolve "latest" through the GitHub releases API. This works the same53+# Resolve "latest" through the forge's GitHub-compatible releases API. This works the same
64 # on Windows PowerShell 5.1 and PowerShell 7+, unlike scraping the54 # on Windows PowerShell 5.1 and PowerShell 7+, unlike scraping the
65 # /releases/latest redirect target: on PS7 the final URL lives at55 # /releases/latest redirect target: on PS7 the final URL lives at
66 # $resp.BaseResponse.RequestMessage.RequestUri while on PS5.1 it is56 # $resp.BaseResponse.RequestMessage.RequestUri while on PS5.1 it is
@@ -69,12 +59,8 @@ if ($arch -eq "arm64") {
69 # to scrape.59 # to scrape.
70 60
71 if ($Version -eq "latest") {61 if ($Version -eq "latest") {
72- $apiUrl = "https://api.github.com/repos/$owner/$repo/releases/latest"62+ $apiUrl = "https://git.bnema.dev/-/github/api/repos/$owner/$repo/releases/latest"
73- # GitHub's API wants a User-Agent; Invoke-RestMethod sets one, but be63+ $apiHeaders = @{ "User-Agent" = "zut-installer"; "Accept" = "application/json" }
74- # explicit so corporate proxies that strip it don't trip a 403.
75- $apiHeaders = @{} + $headers
76- if (-not $apiHeaders.ContainsKey("User-Agent")) { $apiHeaders["User-Agent"] = "zut-installer" }
77- $apiHeaders["Accept"] = "application/vnd.github+json"
78 64
79 try {65 try {
80 $api = Invoke-RestMethod -UseBasicParsing -Headers $apiHeaders -Uri $apiUrl66 $api = Invoke-RestMethod -UseBasicParsing -Headers $apiHeaders -Uri $apiUrl
@@ -84,7 +70,7 @@ if ($Version -eq "latest") {
84 if ($status -eq 404) {70 if ($status -eq 404) {
85 Die "no published release found for $owner/$repo (the repo may have no releases yet)"71 Die "no published release found for $owner/$repo (the repo may have no releases yet)"
86 } elseif ($status -eq 401 -or $status -eq 403) {72 } elseif ($status -eq 401 -or $status -eq 403) {
87- Die "GitHub API request was rejected ($status). If the repo is private, set `$env:GITHUB_TOKEN to a PAT with contents:read; otherwise you may be rate-limited (try again later or set `$env:GITHUB_TOKEN)."73+ Die "forge API request was rejected ($status); try again later"
88 } else {74 } else {
89 Die "could not resolve latest version: $($_.Exception.Message)"75 Die "could not resolve latest version: $($_.Exception.Message)"
90 }76 }
@@ -92,7 +78,7 @@ if ($Version -eq "latest") {
92 78
93 $Version = $api.tag_name79 $Version = $api.tag_name
94 if (-not $Version) {80 if (-not $Version) {
95- Die "could not resolve latest version: GitHub API returned no tag_name for $owner/$repo"81+ Die "could not resolve latest version: forge API returned no tag_name for $owner/$repo"
96 }82 }
97 }83 }
98 84
@@ -102,7 +88,7 @@ $verNum = $Version.TrimStart("v")
102 # ---- download + verify + extract ----88 # ---- download + verify + extract ----
103 89
104 $archive = "${binary}_${verNum}_windows_${arch}.zip"90 $archive = "${binary}_${verNum}_windows_${arch}.zip"
105-$baseUrl = "https://github.com/$owner/$repo/releases/download/$Version"91+$baseUrl = "https://git.bnema.dev/$repo/releases/download/$Version"
106 $archiveUrl = "$baseUrl/$archive"92 $archiveUrl = "$baseUrl/$archive"
107 $checksumUrl = "$baseUrl/checksums.txt"93 $checksumUrl = "$baseUrl/checksums.txt"
108 94
@@ -110,11 +96,11 @@ $tmp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("zut-install-" +
110 96
111 try {97 try {
112 Msg "downloading $archive"98 Msg "downloading $archive"
113- Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri $archiveUrl -OutFile (Join-Path $tmp $archive)99+ Invoke-WebRequest -UseBasicParsing -Uri $archiveUrl -OutFile (Join-Path $tmp $archive)
114 100
115 Msg "verifying checksum"101 Msg "verifying checksum"
116 $checksumFile = Join-Path $tmp "checksums.txt"102 $checksumFile = Join-Path $tmp "checksums.txt"
117- Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri $checksumUrl -OutFile $checksumFile103+ Invoke-WebRequest -UseBasicParsing -Uri $checksumUrl -OutFile $checksumFile
118 $expected = Get-Content -LiteralPath $checksumFile | ForEach-Object {104 $expected = Get-Content -LiteralPath $checksumFile | ForEach-Object {
119 $line = $_.Trim()105 $line = $_.Trim()
120 if ($line) {106 if ($line) {
M install.sh +13 -39
@@ -3,8 +3,8 @@
33 # zut installer.
44 #
55 # Usage:
6-# curl -fsSL https://raw.githubusercontent.com/bnema/zut/main/install.sh | bash
7-# curl -fsSL https://raw.githubusercontent.com/bnema/zut/main/install.sh | bash -s -- v0.1.0 ~/bin
6+# curl -fsSL https://git.bnema.dev/zut/raw/main/install.sh | bash
7+# curl -fsSL https://git.bnema.dev/zut/raw/main/install.sh | bash -s -- v0.1.0 ~/bin
88 #
99 # Positional arguments:
1010 # $1 version — release tag (e.g. v0.1.0). Defaults to "latest".
@@ -16,19 +16,15 @@
1616 # Environment overrides:
1717 # ZUT_VERSION same as $1
1818 # ZUT_PREFIX same as $2
19-# GITHUB_TOKEN personal access token — required while the repo is
20-# private, ignored once it goes public. Must have at
21-# least `contents:read` scope on the zut repository.
2219 #
2320 # The script detects your OS and architecture, downloads the matching
24-# archive from the GitHub release, verifies the sha256 against the
21+# archive from the forge release, verifies the sha256 against the
2522 # release's checksums.txt, extracts the binary, and moves it into the
2623 # prefix directory. No sudo unless you explicitly pick a prefix that
2724 # needs it.
2825
2926 set -euo pipefail
3027
31-OWNER="bnema"
3228 REPO="zut"
3329 BINARY="zut"
3430
@@ -42,22 +38,6 @@ die() { printf "\033[31merror:\033[0m %s\n" "$*" >&2; exit 1; }
4238 command -v curl >/dev/null 2>&1 || die "curl is required"
4339 command -v tar >/dev/null 2>&1 || die "tar is required"
4440
45-# CURL_AUTH is prepended to every curl invocation so private-repo
46-# downloads work while $GITHUB_TOKEN is set. Empty array when the repo
47-# is public.
48-#
49-# Note the "${CURL_AUTH[@]+"${CURL_AUTH[@]}"}" pattern at every call
50-# site: bash 3.2 (the default /bin/bash on macOS) treats an
51-# unquoted empty-array expansion as an unbound variable under
52-# `set -u` and aborts with "CURL_AUTH[@]: unbound variable". The
53-# guard expands to nothing when the array is empty and to the
54-# array's contents when it isn't. Bash 4+ doesn't need this, but
55-# the installer's primary audience is `curl | bash` on macOS.
56-CURL_AUTH=()
57-if [ -n "${GITHUB_TOKEN:-}" ]; then
58- CURL_AUTH=(-H "Authorization: Bearer $GITHUB_TOKEN")
59-fi
60-
6141 # ---- detect OS + arch ----
6242
6343 uname_s=$(uname -s)
@@ -81,18 +61,12 @@ esac
8161 # ---- resolve version ----
8262
8363 if [ "$VERSION" = "latest" ]; then
84- # Private-repo friendly: hit the api, grab tag_name. Falls back to
85- # following the /releases/latest redirect on public repos.
86- if [ ${#CURL_AUTH[@]} -gt 0 ]; then
87- VERSION=$(curl -fsSL "${CURL_AUTH[@]+"${CURL_AUTH[@]}"}" \
88- "https://api.github.com/repos/${OWNER}/${REPO}/releases/latest" \
89- | sed -nE 's/.*"tag_name": *"([^"]+)".*/\1/p' | head -n1)
90- else
91- VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
92- "https://github.com/${OWNER}/${REPO}/releases/latest" \
93- | sed -E 's|.*/tag/([^/]+).*|\1|')
94- fi
95- [ -n "$VERSION" ] || die "could not resolve latest version (set GITHUB_TOKEN if the repo is private)"
64+ # The forge answers /releases/latest with a redirect to
65+ # /releases/tag/<tag>; read the tag from the final URL.
66+ VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
67+ "https://git.bnema.dev/${REPO}/releases/latest" \
68+ | sed -E 's|.*/tag/([^/]+).*|\1|')
69+ [ -n "$VERSION" ] || die "could not resolve latest version"
9670 fi
9771
9872 case "$VERSION" in v*) ;; *) VERSION="v$VERSION" ;; esac
@@ -126,7 +100,7 @@ mkdir -p "$PREFIX"
126100 # ---- download + verify + extract ----
127101
128102 ARCHIVE="${BINARY}_${VER_NUM}_${OS}_${ARCH}.tar.gz"
129-BASE_URL="https://github.com/${OWNER}/${REPO}/releases/download/${VERSION}"
103+BASE_URL="https://git.bnema.dev/${REPO}/releases/download/${VERSION}"
130104 ARCHIVE_URL="${BASE_URL}/${ARCHIVE}"
131105 CHECKSUMS_URL="${BASE_URL}/checksums.txt"
132106
@@ -134,11 +108,11 @@ TMP=$(mktemp -d)
134108 trap 'rm -rf "$TMP"' EXIT
135109
136110 msg "downloading ${ARCHIVE}"
137-curl -fsSL "${CURL_AUTH[@]+"${CURL_AUTH[@]}"}" -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \
138- || die "download failed: $ARCHIVE_URL (set GITHUB_TOKEN if the repo is private)"
111+curl -fsSL -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \
112+ || die "download failed: $ARCHIVE_URL"
139113
140114 msg "verifying checksum"
141-curl -fsSL "${CURL_AUTH[@]+"${CURL_AUTH[@]}"}" -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \
115+curl -fsSL -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \
142116 || die "download failed: $CHECKSUMS_URL"
143117
144118 expected=$(grep " ${ARCHIVE}\$" "$TMP/checksums.txt" | awk '{print $1}' || true)
@@ -3,8 +3,8 @@
3 # zut installer.3 # zut installer.
4 #4 #
5 # Usage:5 # Usage:
6-# curl -fsSL https://raw.githubusercontent.com/bnema/zut/main/install.sh | bash6+# curl -fsSL https://git.bnema.dev/zut/raw/main/install.sh | bash
7-# curl -fsSL https://raw.githubusercontent.com/bnema/zut/main/install.sh | bash -s -- v0.1.0 ~/bin7+# curl -fsSL https://git.bnema.dev/zut/raw/main/install.sh | bash -s -- v0.1.0 ~/bin
8 #8 #
9 # Positional arguments:9 # Positional arguments:
10 # $1 version — release tag (e.g. v0.1.0). Defaults to "latest".10 # $1 version — release tag (e.g. v0.1.0). Defaults to "latest".
@@ -16,19 +16,15 @@
16 # Environment overrides:16 # Environment overrides:
17 # ZUT_VERSION same as $117 # ZUT_VERSION same as $1
18 # ZUT_PREFIX same as $218 # ZUT_PREFIX same as $2
19-# GITHUB_TOKEN personal access token — required while the repo is
20-# private, ignored once it goes public. Must have at
21-# least `contents:read` scope on the zut repository.
22 #19 #
23 # The script detects your OS and architecture, downloads the matching20 # The script detects your OS and architecture, downloads the matching
24-# archive from the GitHub release, verifies the sha256 against the21+# archive from the forge release, verifies the sha256 against the
25 # release's checksums.txt, extracts the binary, and moves it into the22 # release's checksums.txt, extracts the binary, and moves it into the
26 # prefix directory. No sudo unless you explicitly pick a prefix that23 # prefix directory. No sudo unless you explicitly pick a prefix that
27 # needs it.24 # needs it.
28 25
29 set -euo pipefail26 set -euo pipefail
30 27
31-OWNER="bnema"
32 REPO="zut"28 REPO="zut"
33 BINARY="zut"29 BINARY="zut"
34 30
@@ -42,22 +38,6 @@ die() { printf "\033[31merror:\033[0m %s\n" "$*" >&2; exit 1; }
42 command -v curl >/dev/null 2>&1 || die "curl is required"38 command -v curl >/dev/null 2>&1 || die "curl is required"
43 command -v tar >/dev/null 2>&1 || die "tar is required"39 command -v tar >/dev/null 2>&1 || die "tar is required"
44 40
45-# CURL_AUTH is prepended to every curl invocation so private-repo
46-# downloads work while $GITHUB_TOKEN is set. Empty array when the repo
47-# is public.
48-#
49-# Note the "${CURL_AUTH[@]+"${CURL_AUTH[@]}"}" pattern at every call
50-# site: bash 3.2 (the default /bin/bash on macOS) treats an
51-# unquoted empty-array expansion as an unbound variable under
52-# `set -u` and aborts with "CURL_AUTH[@]: unbound variable". The
53-# guard expands to nothing when the array is empty and to the
54-# array's contents when it isn't. Bash 4+ doesn't need this, but
55-# the installer's primary audience is `curl | bash` on macOS.
56-CURL_AUTH=()
57-if [ -n "${GITHUB_TOKEN:-}" ]; then
58- CURL_AUTH=(-H "Authorization: Bearer $GITHUB_TOKEN")
59-fi
60-
61 # ---- detect OS + arch ----41 # ---- detect OS + arch ----
62 42
63 uname_s=$(uname -s)43 uname_s=$(uname -s)
@@ -81,18 +61,12 @@ esac
81 # ---- resolve version ----61 # ---- resolve version ----
82 62
83 if [ "$VERSION" = "latest" ]; then63 if [ "$VERSION" = "latest" ]; then
84- # Private-repo friendly: hit the api, grab tag_name. Falls back to64+ # The forge answers /releases/latest with a redirect to
85- # following the /releases/latest redirect on public repos.65+ # /releases/tag/<tag>; read the tag from the final URL.
86- if [ ${#CURL_AUTH[@]} -gt 0 ]; then66+ VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
87- VERSION=$(curl -fsSL "${CURL_AUTH[@]+"${CURL_AUTH[@]}"}" \67+ "https://git.bnema.dev/${REPO}/releases/latest" \
88- "https://api.github.com/repos/${OWNER}/${REPO}/releases/latest" \68+ | sed -E 's|.*/tag/([^/]+).*|\1|')
89- | sed -nE 's/.*"tag_name": *"([^"]+)".*/\1/p' | head -n1)69+ [ -n "$VERSION" ] || die "could not resolve latest version"
90- else
91- VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
92- "https://github.com/${OWNER}/${REPO}/releases/latest" \
93- | sed -E 's|.*/tag/([^/]+).*|\1|')
94- fi
95- [ -n "$VERSION" ] || die "could not resolve latest version (set GITHUB_TOKEN if the repo is private)"
96 fi70 fi
97 71
98 case "$VERSION" in v*) ;; *) VERSION="v$VERSION" ;; esac72 case "$VERSION" in v*) ;; *) VERSION="v$VERSION" ;; esac
@@ -126,7 +100,7 @@ mkdir -p "$PREFIX"
126 # ---- download + verify + extract ----100 # ---- download + verify + extract ----
127 101
128 ARCHIVE="${BINARY}_${VER_NUM}_${OS}_${ARCH}.tar.gz"102 ARCHIVE="${BINARY}_${VER_NUM}_${OS}_${ARCH}.tar.gz"
129-BASE_URL="https://github.com/${OWNER}/${REPO}/releases/download/${VERSION}"103+BASE_URL="https://git.bnema.dev/${REPO}/releases/download/${VERSION}"
130 ARCHIVE_URL="${BASE_URL}/${ARCHIVE}"104 ARCHIVE_URL="${BASE_URL}/${ARCHIVE}"
131 CHECKSUMS_URL="${BASE_URL}/checksums.txt"105 CHECKSUMS_URL="${BASE_URL}/checksums.txt"
132 106
@@ -134,11 +108,11 @@ TMP=$(mktemp -d)
134 trap 'rm -rf "$TMP"' EXIT108 trap 'rm -rf "$TMP"' EXIT
135 109
136 msg "downloading ${ARCHIVE}"110 msg "downloading ${ARCHIVE}"
137-curl -fsSL "${CURL_AUTH[@]+"${CURL_AUTH[@]}"}" -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \111+curl -fsSL -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \
138- || die "download failed: $ARCHIVE_URL (set GITHUB_TOKEN if the repo is private)"112+ || die "download failed: $ARCHIVE_URL"
139 113
140 msg "verifying checksum"114 msg "verifying checksum"
141-curl -fsSL "${CURL_AUTH[@]+"${CURL_AUTH[@]}"}" -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \115+curl -fsSL -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \
142 || die "download failed: $CHECKSUMS_URL"116 || die "download failed: $CHECKSUMS_URL"
143 117
144 expected=$(grep " ${ARCHIVE}\$" "$TMP/checksums.txt" | awk '{print $1}' || true)118 expected=$(grep " ${ARCHIVE}\$" "$TMP/checksums.txt" | awk '{print $1}' || true)

Comments0 open / 0

no comments yet.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
l
merge request: the list of files (j k move, enter opens, esc back)
M
merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
R
merge request: mark the head read
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close