From 011f6fa1025c660ff8370bd0ecb98098b1dec5b6 Mon Sep 17 00:00:00 2001 From: Hop Date: Sat, 11 Jul 2026 12:57:45 -0700 Subject: [PATCH] Reconcile no-Actions local release workflow with accepted Hop implementation Hop-State: A_06FN5GEZTNDD5V7A7PXHBR0 Hop-Proposal: R_06FN5GDDA8WVH1V6JW0FBY0 Hop-Task: T_06FN3MBF98GWD4NA5PA1RWG Hop-Attempt: AT_06FN5FPYNDP5C49S0G52NQG --- .gitea/workflows/ci.yml | 67 ---------------------- .gitea/workflows/release.yml | 59 -------------------- .gitignore | 1 + README.md | 2 + internal/hop/docs_test.go | 18 ++++++ scripts/release-local.sh | 105 +++++++++++++++++++++++++++++++++++ wiki/Release-Checklist.md | 53 +++++++++--------- wiki/Security-and-Privacy.md | 9 +-- 8 files changed, 156 insertions(+), 158 deletions(-) delete mode 100644 .gitea/workflows/ci.yml delete mode 100644 .gitea/workflows/release.yml create mode 100755 scripts/release-local.sh diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml deleted file mode 100644 index 0f0ca43..0000000 --- a/.gitea/workflows/ci.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: CI - -on: - push: - branches: - - main - pull_request: - -permissions: - code: read - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Check out repository - uses: https://gitea.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Set up Go - uses: https://gitea.com/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 - with: - go-version-file: go.mod - cache: true - - name: Test with race detection - run: go test -race ./... - - name: Vet - run: go vet ./... - - name: Cross-compile supported targets - shell: bash - run: | - set -euo pipefail - for target in darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64 windows/arm64; do - os=${target%/*} - arch=${target#*/} - output="dist-smoke/hop_${os}_${arch}" - if [[ "$os" == windows ]]; then output="${output}.exe"; fi - GOOS="$os" GOARCH="$arch" CGO_ENABLED=0 go build -trimpath -o "$output" ./cmd/hop - done - - name: Validate POSIX installer syntax - run: sh -n scripts/install.sh - - name: Smoke-test POSIX installer - run: sh scripts/test-install.sh - - powershell-installer: - runs-on: windows-latest - steps: - - name: Check out repository - uses: https://gitea.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Set up Go - uses: https://gitea.com/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 - with: - go-version-file: go.mod - cache: true - - name: Validate PowerShell installer syntax - shell: pwsh - run: | - $tokens = $null - $errors = $null - [void][System.Management.Automation.Language.Parser]::ParseFile( - (Resolve-Path scripts/install.ps1), [ref]$tokens, [ref]$errors - ) - if ($errors.Count -gt 0) { - $errors | Format-List | Out-String | Write-Error - exit 1 - } - - name: Smoke-test PowerShell installer - shell: pwsh - run: ./scripts/test-install.ps1 diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml deleted file mode 100644 index 83a0f99..0000000 --- a/.gitea/workflows/release.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Release - -on: - push: - tags: - - "v*" - -permissions: - code: read - releases: write - -jobs: - release: - runs-on: ubuntu-latest - steps: - - name: Check out full history - uses: https://gitea.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: 0 - - name: Set up Go - uses: https://gitea.com/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 - with: - go-version-file: go.mod - cache: true - - name: Require a public-release license - run: test -f LICENSE - - name: Test with race detection - run: go test -race ./... - - name: Vet - run: go vet ./... - - name: Build and upload a draft Gitea Release - env: - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} - run: | - set -euo pipefail - version=2.17.0 - case "$(uname -m)" in - x86_64|amd64) - arch=x86_64 - expected=dde10e2d5a13cef969c0eec00c74f359c0ac306d702b1bd291ad9337b4e54c1d - ;; - aarch64|arm64) - arch=arm64 - expected=75f93fc0e25d10d8535ffd0e4abcf39d6784a2467ba453d479ae513729a9ebbf - ;; - *) - echo "Unsupported release runner architecture: $(uname -m)" >&2 - exit 1 - ;; - esac - archive="goreleaser_Linux_${arch}.tar.gz" - tmp_dir=$(mktemp -d) - trap 'rm -rf "$tmp_dir"' EXIT - curl -fsSL --proto '=https' --tlsv1.2 \ - -o "$tmp_dir/$archive" \ - "https://github.com/goreleaser/goreleaser/releases/download/v${version}/${archive}" - printf '%s %s\n' "$expected" "$tmp_dir/$archive" | sha256sum -c - - tar -xzf "$tmp_dir/$archive" -C "$tmp_dir" goreleaser - "$tmp_dir/goreleaser" release --clean diff --git a/.gitignore b/.gitignore index 411ac70..ddd1771 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # Local build and verification artifacts. /hop +/dist/ /SHA256SUMS /coverage.out *.test diff --git a/README.md b/README.md index db82cd3..c501a17 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,8 @@ No project is modified during installation. A project receives its local Release installer URLs become available after the corresponding Gitea Release is published. Until the first public release exists, use the source build. +Hop does not require Gitea Actions: maintainers build and upload draft releases +from a trusted local machine with `scripts/release-local.sh`. ## Getting started diff --git a/internal/hop/docs_test.go b/internal/hop/docs_test.go index e432834..1efb256 100644 --- a/internal/hop/docs_test.go +++ b/internal/hop/docs_test.go @@ -65,3 +65,21 @@ func TestProductDocumentationUsesCanonicalGiteaHost(t *testing.T) { } } } + +func TestDistributionDoesNotRequireGiteaActions(t *testing.T) { + root := filepath.Clean(filepath.Join("..", "..")) + workflows, err := filepath.Glob(filepath.Join(root, ".gitea", "workflows", "*")) + if err != nil { + t.Fatal(err) + } + if len(workflows) != 0 { + t.Fatalf("Gitea Actions workflows remain enabled in source: %v", workflows) + } + contents, err := os.ReadFile(filepath.Join(root, "wiki", "Release-Checklist.md")) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(contents), ".gitea/workflows/") { + t.Fatal("release checklist still depends on a Gitea Actions workflow") + } +} diff --git a/scripts/release-local.sh b/scripts/release-local.sh new file mode 100755 index 0000000..baf5b87 --- /dev/null +++ b/scripts/release-local.sh @@ -0,0 +1,105 @@ +#!/bin/sh +set -eu + +fail() { + printf 'hop release: %s\n' "$*" >&2 + exit 1 +} + +usage() { + cat <<'EOF' +Usage: scripts/release-local.sh --snapshot + scripts/release-local.sh --publish + +--snapshot Test and build all release archives locally without uploading. +--publish Test, build, and upload a draft release to githop.xyz. + Requires a clean signed tag, LICENSE, and GITEA_TOKEN. +EOF +} + +[ "$#" -eq 1 ] || { usage >&2; exit 2; } +mode=$1 +case "$mode" in + --snapshot | --publish) ;; + *) usage >&2; exit 2 ;; +esac + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +cd "$root" + +for command in curl git go tar; do + command -v "$command" >/dev/null 2>&1 || fail "$command is required" +done + +if [ "$mode" = --publish ]; then + [ -f LICENSE ] || fail "LICENSE is required before publishing" + [ -n "${GITEA_TOKEN:-}" ] || fail "set a scoped GITEA_TOKEN in the local environment" + [ -z "$(git status --porcelain)" ] || fail "the Git worktree must be clean" + tag=$(git describe --tags --exact-match HEAD 2>/dev/null) || + fail "HEAD must have an exact release tag" + case "$tag" in + v[0-9]*) ;; + *) fail "release tag must start with v followed by a number" ;; + esac + [ "$(git cat-file -t "$tag")" = tag ] || fail "$tag must be an annotated tag" + git verify-tag "$tag" >/dev/null 2>&1 || fail "$tag must have a valid signature" + git ls-remote --exit-code origin "refs/tags/$tag" >/dev/null 2>&1 || + fail "$tag must be pushed to origin before publishing" +fi + +printf 'Running local release validation...\n' +go test -race ./... +go vet ./... +sh -n scripts/install.sh +sh scripts/test-install.sh +git diff --check + +goreleaser_version=2.17.0 +case "$(uname -s)/$(uname -m)" in + Darwin/arm64) + archive=goreleaser_Darwin_arm64.tar.gz + expected=58912a80159199c0fd5c8484e4c868bf87414129655d6d87cd1cd84ee645736c + ;; + Darwin/x86_64) + archive=goreleaser_Darwin_x86_64.tar.gz + expected=f37e89fb844ddfd23cffb97e30d91f972c42da68232a676bfba2beacea300543 + ;; + Linux/arm64 | Linux/aarch64) + archive=goreleaser_Linux_arm64.tar.gz + expected=75f93fc0e25d10d8535ffd0e4abcf39d6784a2467ba453d479ae513729a9ebbf + ;; + Linux/x86_64 | Linux/amd64) + archive=goreleaser_Linux_x86_64.tar.gz + expected=dde10e2d5a13cef969c0eec00c74f359c0ac306d702b1bd291ad9337b4e54c1d + ;; + *) fail "unsupported release host: $(uname -s)/$(uname -m)" ;; +esac + +tmp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t hop-release) +cleanup() { + rm -rf "$tmp_dir" +} +trap cleanup EXIT HUP INT TERM + +printf 'Downloading verified GoReleaser %s...\n' "$goreleaser_version" +curl -fsSL --retry 3 --proto '=https' --tlsv1.2 \ + -o "$tmp_dir/$archive" \ + "https://github.com/goreleaser/goreleaser/releases/download/v${goreleaser_version}/${archive}" +if command -v sha256sum >/dev/null 2>&1; then + actual=$(sha256sum "$tmp_dir/$archive" | awk '{print $1}') +elif command -v shasum >/dev/null 2>&1; then + actual=$(shasum -a 256 "$tmp_dir/$archive" | awk '{print $1}') +else + fail "sha256sum or shasum is required" +fi +[ "$actual" = "$expected" ] || fail "GoReleaser checksum verification failed" +tar -xzf "$tmp_dir/$archive" -C "$tmp_dir" goreleaser + +"$tmp_dir/goreleaser" check +if [ "$mode" = --snapshot ]; then + "$tmp_dir/goreleaser" release --snapshot --clean + printf 'Local snapshot complete: %s/dist\n' "$root" +else + "$tmp_dir/goreleaser" release --clean + printf 'Draft release uploaded to https://githop.xyz/GnosysLabs/Hop/releases\n' +fi diff --git a/wiki/Release-Checklist.md b/wiki/Release-Checklist.md index 2af58af..cda5c9d 100644 --- a/wiki/Release-Checklist.md +++ b/wiki/Release-Checklist.md @@ -1,7 +1,8 @@ # Release checklist -Hop releases are built on `githop.xyz` by Gitea Actions and uploaded by -GoReleaser as draft Gitea Releases. +Hop releases are built on a trusted maintainer machine and uploaded by +GoReleaser as draft Gitea Releases. Gitea Actions and act runners are not +required; the server only stores source, tags, release metadata, and assets. The canonical repository is `githop.xyz/GnosysLabs/Hop`. @@ -9,15 +10,12 @@ The canonical repository is `githop.xyz/GnosysLabs/Hop`. - Create the `GnosysLabs/Hop` repository and set its default branch to `main`. - Configure `origin` as `https://githop.xyz/GnosysLabs/Hop.git`. -- Run a currently patched Gitea 1.26.x or newer, enable Actions, and enable - Actions for the repository. Hop's least-privilege workflow uses the 1.26 job - token permission model. -- Register trusted, isolated `ubuntu-latest` and `windows-latest` act runners. -- Allow the repository job token `code: read` and `releases: write`. -- Ensure `${{ secrets.GITEA_TOKEN }}` can create releases in this repository. -- Keep third-party Actions pinned to reviewed commit SHAs. When upgrading - GoReleaser, update its version and both hard-coded Linux archive checksums in - `.gitea/workflows/release.yml` from the official release checksum file. +- Keep Gitea Actions disabled when the instance does not have dedicated runner + capacity; Hop's release process does not depend on it. +- Create a narrowly scoped maintainer access token that can write releases. + Export it as `GITEA_TOKEN` only for the local publish command, then unset it. +- When upgrading GoReleaser, update its pinned version and four archive + checksums in `scripts/release-local.sh` from the official checksum file. - Permit release attachment MIME types for `.tar.gz`, `.zip`, and `.txt` in Gitea's `[attachment] ALLOWED_TYPES` configuration. - Enable the repository wiki, then push the files in `wiki/` to its wiki Git @@ -25,24 +23,19 @@ The canonical repository is `githop.xyz/GnosysLabs/Hop`. ## Public-launch gates -- Choose and add a `LICENSE`. The release workflow intentionally fails without - one; this is a product/legal decision, not a build default. +- Choose and add a `LICENSE`. The local publishing script intentionally fails + without one; this is a product/legal decision, not a build default. - Add `SECURITY.md` with a monitored private disclosure address. - Create an offline-controlled release-signing key, publish its public key, and add detached signing for `checksums.txt` before general availability. - Confirm the `githop.xyz/GnosysLabs/Hop` Go import path serves valid `go-import` metadata. -- Back up the Gitea database, repositories, release attachments, and Actions - secrets. +- Back up the Gitea database, repositories, and release attachments. ## Validate before tagging ```bash -go test -race ./... -go vet ./... -sh -n scripts/install.sh -goreleaser check -goreleaser release --snapshot --clean +scripts/release-local.sh --snapshot ``` Inspect `dist/` and test at least one archive on each operating system family. @@ -51,15 +44,19 @@ Confirm `hop version` reports the snapshot/tag-injected version and ## Create a release -1. Update release notes and the expected version. -2. Create a signed semantic-version tag such as `v0.1.0-alpha.1`. -3. Push the tag to `githop.xyz`. -4. The `.gitea/workflows/release.yml` workflow runs race tests and vet, builds - six platform archives, generates `checksums.txt`, and uploads a draft. -5. Download the draft assets and independently verify checksums, version output, +1. Update release notes. The signed Git tag is the version source and is + injected into the binaries automatically. +2. Run `scripts/release-local.sh --snapshot` and inspect the artifacts. +3. Create a signed semantic-version tag such as `v0.1.0-alpha.1` and push it. +4. Export a locally stored, scoped token: `export GITEA_TOKEN=...`. +5. Run `scripts/release-local.sh --publish`. It reruns race tests, vet, installer + checks, builds six platform archives, generates `checksums.txt`, and uploads + a draft without executing build work on the Gitea server. +6. Immediately run `unset GITEA_TOKEN`. +7. Download the draft assets and independently verify checksums, version output, skill installation, and a disposable Hop project. -6. Publish the Gitea draft only after those checks pass. -7. Test both one-command installers against the now-published release. +8. Publish the Gitea draft only after those checks pass. +9. Test both one-command installers against the now-published release. ## Expected assets diff --git a/wiki/Security-and-Privacy.md b/wiki/Security-and-Privacy.md index da7c77c..bf6b918 100644 --- a/wiki/Security-and-Privacy.md +++ b/wiki/Security-and-Privacy.md @@ -33,11 +33,12 @@ sign `checksums.txt` with an offline-controlled release key and publish the public key independently. Checksum signing is listed as a launch gate in the [release checklist](Release-Checklist). -## Runner trust +## Release-machine trust -Release jobs execute on Gitea act runners. Only trusted, isolated runners should -receive release tags or release tokens. Do not run secret-bearing release jobs -from unreviewed fork pull requests. +Release builds execute on a maintainer machine, not on the Gitea server. Use a +trusted, patched machine; keep the release token out of shell history and source +files; scope it to the Hop repository; export it only for the publish command; +and unset it immediately afterward. Releases upload as drafts for review. ## Filesystem safety