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
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.4.10] - 2026-07-30

### Fixed

- **Release checksum verification is fail-closed.** `make release-verify-checksums` now requires non-empty `SHA256SUMS` and `SHA512SUMS` and runs `shasum -c` without piping away the exit status, so a corrupted asset or missing/empty manifest fails the target.
- **Maintainer Make upload targets require full verification.** `make release-upload` and `make release-upload-provenance` both depend on `make release-verify` (checksums + signatures + keys). Tag CI may still publish unsigned platform archives and the install script before the manual sign/verify/upload phase; this change gates the maintainer Make upload recipes only.

### Added

- Composite `make release-verify` target and a hermetic regression harness (`scripts/test-release-verify-checksums.sh`) covering valid, corrupted, absent, and empty manifests. The harness is wired into `make precommit` (the gate CI runs).

## [0.4.9] - 2026-07-29

### Changed
Expand Down Expand Up @@ -157,7 +168,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

> **Maintenance note:** This file is pruned to the latest 10 releases. For older entries, see `docs/releases/`.

[Unreleased]: https://github.com/3leaps/sfetch/compare/v0.4.9...HEAD
[Unreleased]: https://github.com/3leaps/sfetch/compare/v0.4.10...HEAD
[0.4.10]: https://github.com/3leaps/sfetch/compare/v0.4.9...v0.4.10
[0.4.9]: https://github.com/3leaps/sfetch/compare/v0.4.8...v0.4.9
[0.4.8]: https://github.com/3leaps/sfetch/compare/v0.4.7...v0.4.8
[0.4.7]: https://github.com/3leaps/sfetch/compare/v0.4.6...v0.4.7
Expand Down
44 changes: 29 additions & 15 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ CORPUS_DEST ?= test-corpus
.PHONY: precommit prepush version corpus corpus-all corpus-dryrun corpus-validate
.PHONY: release release-download release-checksums release-verify-checksums release-sign
.PHONY: release-notes release-upload release-upload-provenance release-export-key release-export-minisign-key release-export-keys
.PHONY: release-verify-key release-verify-minisign-pubkey release-verify-keys release-verify-signatures
.PHONY: release-verify-key release-verify-minisign-pubkey release-verify-keys release-verify-signatures release-verify
.PHONY: release-clean bootstrap-script build-all gosec gosec-high update-scoop-manifest
.PHONY: version-check version-set version-patch version-minor version-major
.PHONY: print-sfetch-version
.PHONY: print-sfetch-version test-release-verify-checksums

all: build

Expand Down Expand Up @@ -205,6 +205,8 @@ precommit: ## Run pre-commit checks (goneat assess + Go tests + build)
go test -v -race ./...
$(MAKE) gosec-high
$(MAKE) build-all
# CI runs make precommit (not prepush); keep fail-closed release-verify regression on this path.
$(MAKE) test-release-verify-checksums
@echo "[ok] Pre-commit checks passed"

prepush: precommit ## Run pre-push checks (same as precommit + security)
Expand Down Expand Up @@ -250,19 +252,29 @@ bootstrap-script: ## Copy install script into release directory
release-checksums: bootstrap-script ## Generate SHA256SUMS and SHA512SUMS
go run ./scripts/cmd/generate-checksums --dir $(DIST_RELEASE)

release-verify-checksums: ## Verify checksums in dist/release
release-verify-checksums: ## Verify checksums in dist/release (fail-closed; portable /bin/sh)
@if [ ! -d "$(DIST_RELEASE)" ]; then echo "error: $(DIST_RELEASE) not found (run make release-download first)" >&2; exit 1; fi
@echo "Verifying checksums in $(DIST_RELEASE)..."
@cd $(DIST_RELEASE) && \
if [ -f SHA256SUMS ]; then \
echo "=== SHA256SUMS ===" && \
shasum -a 256 -c SHA256SUMS 2>&1 | grep -v ': OK$$' || echo "All SHA256 checksums OK"; \
fi && \
if [ -f SHA512SUMS ]; then \
echo "=== SHA512SUMS ===" && \
shasum -a 512 -c SHA512SUMS 2>&1 | grep -v ': OK$$' || echo "All SHA512 checksums OK"; \
fi
@echo "[ok] Checksum verification complete"
@set -eu; \
cd "$(DIST_RELEASE)"; \
if [ ! -s SHA256SUMS ]; then \
echo "error: SHA256SUMS missing or empty in $(DIST_RELEASE)" >&2; \
exit 1; \
fi; \
if [ ! -s SHA512SUMS ]; then \
echo "error: SHA512SUMS missing or empty in $(DIST_RELEASE)" >&2; \
exit 1; \
fi; \
echo "=== SHA256SUMS ==="; \
shasum -a 256 -c SHA256SUMS; \
echo "=== SHA512SUMS ==="; \
shasum -a 512 -c SHA512SUMS; \
echo "[ok] Checksum verification complete"

# Negative + positive regression for release-verify-checksums (exit status only).
# Wired into precommit because sfetch CI runs make precommit, not prepush.
test-release-verify-checksums: ## Regression: fail-closed checksum verify (corrupt/absent/empty)
@./scripts/test-release-verify-checksums.sh

release-notes: ## Copy release notes into dist/release
@if [ -z "$(RELEASE_TAG)" ]; then echo "error: RELEASE_TAG not set" >&2; exit 1; fi
Expand Down Expand Up @@ -319,13 +331,15 @@ release-verify-keys: release-verify-key ## Verify all exported public keys
release-verify-signatures: ## Verify minisign and PGP signatures on checksum manifests
./scripts/verify-signatures.sh $(DIST_RELEASE)

release-upload: release-notes release-verify-key ## Upload all assets and update release notes
release-verify: release-verify-checksums release-verify-signatures release-verify-keys ## Full post-signing release verification

release-upload: release-notes release-verify ## Upload all assets (requires full verification chain)
./scripts/upload-release-assets.sh $(RELEASE_TAG) $(DIST_RELEASE)
@echo ""
@echo "📝 Updating Scoop manifest..."
@$(MAKE) update-scoop-manifest

release-upload-provenance: release-notes ## Upload provenance only (manifests, signatures, keys, notes)
release-upload-provenance: release-notes release-verify ## Upload provenance only (requires full verification chain)
./scripts/release-upload-provenance.sh $(RELEASE_TAG) $(DIST_RELEASE)

update-scoop-manifest: ## Update Scoop bucket manifest with the current release version (requires sibling ../scoop-bucket)
Expand Down
13 changes: 10 additions & 3 deletions RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export SFETCH_GPG_HOMEDIR=/path/to/custom/gpg/homedir # optional, defaults to
make release-checksums
```

4. **Verify checksums**
4. **Verify checksums** (fail-closed: missing/empty manifests and bad hashes fail)
```bash
make release-verify-checksums
```
Expand All @@ -96,6 +96,12 @@ export SFETCH_GPG_HOMEDIR=/path/to/custom/gpg/homedir # optional, defaults to
make release-verify-keys
```

Or run the full post-signing chain in one step:

```bash
make release-verify
```

9. **Copy release notes** (requires `docs/releases/$RELEASE_TAG.md`)
```bash
make release-notes
Expand All @@ -105,12 +111,13 @@ export SFETCH_GPG_HOMEDIR=/path/to/custom/gpg/homedir # optional, defaults to
```bash
make release-upload
```
> **Note:** This uploads ALL assets with `--clobber`, including binaries CI already uploaded.
> **Note:** This target depends on `release-verify` (checksums + signatures + keys).
> It uploads ALL assets with `--clobber`, including binaries CI already uploaded.
> This is intentional for idempotency - rerun safely to fix any mistakes.
>
> If `../scoop-bucket` is present, this target also runs `make update-scoop-manifest` at the end so the bucket is ready for commit/push immediately after release upload.
>
> To upload provenance only (manifests, signatures, keys, notes):
> To upload provenance only (manifests, signatures, keys, notes) — also requires `release-verify`:
> ```bash
> make release-upload-provenance
> ```
Expand Down
27 changes: 27 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
## v0.4.10

### Summary
Release-path automation hardening: checksum verification fails closed, and both maintainer Make upload targets require the full verification chain. Tag CI may still publish unsigned archives before the manual sign/verify phase. No user-facing feature or API changes.

### Highlights

**Release verification**
- `make release-verify-checksums` requires non-empty `SHA256SUMS` and `SHA512SUMS` and fails when `shasum -c` fails (no masked exit status).
- New composite `make release-verify` (checksums + signatures + keys).
- `make release-upload` and `make release-upload-provenance` both depend on `release-verify` (maintainer Make paths only; not initial CI asset publish).
- Hermetic regression harness for corrupt/absent/empty manifests runs in `make precommit` (and therefore CI).

### Install

```bash
curl -sSfL https://github.com/3leaps/sfetch/releases/latest/download/install-sfetch.sh | bash
```

Or self-update:

```bash
sfetch --self-update --yes
```

---

## v0.4.9

### Summary
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.4.9
0.4.10
27 changes: 27 additions & 0 deletions docs/releases/v0.4.10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Summary
Release-path automation hardening: checksum verification fails closed, and both maintainer Make upload targets require the full verification chain. Tag CI may still publish unsigned archives before the manual sign/verify phase. No user-facing feature or API changes.

## Highlights

### Release verification
- `make release-verify-checksums` requires non-empty `SHA256SUMS` and `SHA512SUMS` and fails when `shasum -c` fails (no masked exit status).
- New composite `make release-verify` runs checksums, signatures, and key verification together.
- `make release-upload` and `make release-upload-provenance` both depend on `release-verify`. These are the maintainer Make upload recipes; they do not change initial tag CI publication of unsigned platform archives and the install script.
- Hermetic regression harness (`scripts/test-release-verify-checksums.sh`) covers valid, corrupted, absent, and empty manifests for both SHA256SUMS and SHA512SUMS. The harness is part of `make precommit`, which is the gate CI runs.

## Validation
- `make precommit` green (includes fail-closed checksum regression under `/bin/sh`)
- `make prepush` green
- `actionlint` green on workflow files

## Install

```bash
curl -sSfL https://github.com/3leaps/sfetch/releases/latest/download/install-sfetch.sh | bash
```

Or self-update:

```bash
sfetch --self-update --yes
```
109 changes: 109 additions & 0 deletions scripts/test-release-verify-checksums.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# Regression harness for make release-verify-checksums (fail-closed).
# Asserts exit status only (zero vs non-zero) — not make-specific codes or
# presence/absence of success strings. Cover both SHA256SUMS and SHA512SUMS.
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"

fail() {
echo "FAIL: $*" >&2
exit 1
}

pass() {
echo "PASS: $*"
}

run_verify() {
local dir="$1"
# Force /bin/sh so Linux CI (dash) is exercised even on bash hosts.
make -s release-verify-checksums DIST_RELEASE="$dir" SHELL="${MAKE_SHELL:-/bin/sh}"
}

# Initialize before trap so early exit never hits unbound variables under set -u.
valid=""
corrupt=""
absent256=""
absent512=""
empty256=""
empty512=""
trap 'rm -rf "${valid:-}" "${corrupt:-}" "${absent256:-}" "${absent512:-}" "${empty256:-}" "${empty512:-}" 2>/dev/null || true' EXIT

# --- Case 1: valid fixture exits 0 ---
valid="$(mktemp -d "${TMPDIR:-/tmp}/sft-rv-valid.XXXXXX")"

printf 'payload-a\n' >"$valid/a.bin"
printf 'payload-b\n' >"$valid/b.bin"
(
cd "$valid"
shasum -a 256 a.bin b.bin >SHA256SUMS
shasum -a 512 a.bin b.bin >SHA512SUMS
)
if run_verify "$valid"; then
pass "valid fixture exits 0"
else
fail "valid fixture should exit 0"
fi

# --- Case 2: corrupted checksum exits non-zero ---
corrupt="$(mktemp -d "${TMPDIR:-/tmp}/sft-rv-corrupt.XXXXXX")"
cp -R "$valid/." "$corrupt/"
python3 - "$corrupt/SHA256SUMS" <<'PY'
import pathlib, sys
p = pathlib.Path(sys.argv[1])
lines = p.read_text().splitlines(True)
c = lines[0][0]
lines[0] = ('0' if c != '0' else '1') + lines[0][1:]
p.write_text(''.join(lines))
PY
if run_verify "$corrupt"; then
fail "corrupted checksum should exit non-zero"
else
pass "corrupted checksum exits non-zero"
fi

# --- Case 3: absent SHA256SUMS exits non-zero ---
absent256="$(mktemp -d "${TMPDIR:-/tmp}/sft-rv-absent256.XXXXXX")"
cp "$valid/a.bin" "$absent256/"
cp "$valid/b.bin" "$absent256/"
cp "$valid/SHA512SUMS" "$absent256/"
if run_verify "$absent256"; then
fail "absent SHA256SUMS should exit non-zero"
else
pass "absent SHA256SUMS exits non-zero"
fi

# --- Case 4: absent SHA512SUMS exits non-zero ---
absent512="$(mktemp -d "${TMPDIR:-/tmp}/sft-rv-absent512.XXXXXX")"
cp "$valid/a.bin" "$absent512/"
cp "$valid/b.bin" "$absent512/"
cp "$valid/SHA256SUMS" "$absent512/"
if run_verify "$absent512"; then
fail "absent SHA512SUMS should exit non-zero"
else
pass "absent SHA512SUMS exits non-zero"
fi

# --- Case 5: empty SHA256SUMS exits non-zero ---
empty256="$(mktemp -d "${TMPDIR:-/tmp}/sft-rv-empty256.XXXXXX")"
cp -R "$valid/." "$empty256/"
: >"$empty256/SHA256SUMS"
if run_verify "$empty256"; then
fail "empty SHA256SUMS should exit non-zero"
else
pass "empty SHA256SUMS exits non-zero"
fi

# --- Case 6: empty SHA512SUMS exits non-zero ---
empty512="$(mktemp -d "${TMPDIR:-/tmp}/sft-rv-empty512.XXXXXX")"
cp -R "$valid/." "$empty512/"
: >"$empty512/SHA512SUMS"
if run_verify "$empty512"; then
fail "empty SHA512SUMS should exit non-zero"
else
pass "empty SHA512SUMS exits non-zero"
fi

echo "[ok] release-verify-checksums regression harness complete"