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
24 changes: 20 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ jobs:
goos: linux
goarch: amd64
name: linux_amd64
- os: windows-latest
goos: windows
goarch: amd64
name: windows_amd64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
Expand All @@ -30,18 +34,27 @@ jobs:
go-version-file: go.mod

- name: Build
shell: bash
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
VERSION=${GITHUB_REF_NAME#v}
go build -ldflags "-s -w -X main.Version=${VERSION}" -o mnemonic ./cmd/mnemonic
BINARY=mnemonic
if [ "${{ matrix.goos }}" = "windows" ]; then BINARY=mnemonic.exe; fi
go build -ldflags "-s -w -X main.Version=${VERSION}" -o "${BINARY}" ./cmd/mnemonic

- name: Package
shell: bash
run: |
VERSION=${GITHUB_REF_NAME#v}
ARCHIVE="mnemonic_${VERSION}_${{ matrix.name }}.tar.gz"
tar czf "${ARCHIVE}" mnemonic README.md LICENSE config.example.yaml docs/
if [ "${{ matrix.goos }}" = "windows" ]; then
ARCHIVE="mnemonic_${VERSION}_${{ matrix.name }}.zip"
7z a "${ARCHIVE}" mnemonic.exe README.md LICENSE config.example.yaml docs/
else
ARCHIVE="mnemonic_${VERSION}_${{ matrix.name }}.tar.gz"
tar czf "${ARCHIVE}" mnemonic README.md LICENSE config.example.yaml docs/
fi
echo "ARCHIVE=${ARCHIVE}" >> $GITHUB_ENV

- name: Upload artifact
Expand All @@ -64,13 +77,16 @@ jobs:

- name: Generate checksums
working-directory: dist
run: sha256sum *.tar.gz > checksums.txt
run: |
shopt -s nullglob
sha256sum *.tar.gz *.zip > checksums.txt

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
dist/*.tar.gz
dist/*.zip
dist/checksums.txt
generate_release_notes: true
draft: false
7 changes: 7 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,24 @@ builds:
goos:
- darwin
- linux
- windows
goarch:
- amd64
- arm64
ignore:
- goos: linux
goarch: arm64
- goos: windows
goarch: arm64

archives:
- id: default
formats:
- tar.gz
format_overrides:
- goos: windows
formats:
- zip
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
files:
- README.md
Expand Down
64 changes: 60 additions & 4 deletions internal/updater/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package updater

import (
"archive/tar"
"archive/zip"
"compress/gzip"
"context"
"crypto/sha256"
Expand Down Expand Up @@ -85,7 +86,7 @@ func CheckForUpdate(ctx context.Context, currentVersion string) (*UpdateInfo, er
latestVersion := strings.TrimPrefix(release.TagName, "v")

// Find the asset for this platform
assetName := fmt.Sprintf("mnemonic_%s_%s_%s.tar.gz", latestVersion, runtime.GOOS, runtime.GOARCH)
assetName := fmt.Sprintf("mnemonic_%s_%s_%s%s", latestVersion, runtime.GOOS, runtime.GOARCH, archiveExt())
var assetURL, checksumsURL string
for _, a := range release.Assets {
switch a.Name {
Expand Down Expand Up @@ -130,7 +131,7 @@ func PerformUpdate(ctx context.Context, info *UpdateInfo) (*UpdateResult, error)
}

execDir := filepath.Dir(execPath)
archivePath := filepath.Join(execDir, ".mnemonic.update.tar.gz")
archivePath := filepath.Join(execDir, ".mnemonic.update"+archiveExt())
newBinaryPath := filepath.Join(execDir, ".mnemonic.update.tmp")

// Clean up temp files on failure
Expand All @@ -146,7 +147,7 @@ func PerformUpdate(ctx context.Context, info *UpdateInfo) (*UpdateResult, error)

// Verify checksum if available
if info.ChecksumsURL != "" {
assetName := fmt.Sprintf("mnemonic_%s_%s_%s.tar.gz", info.LatestVersion, runtime.GOOS, runtime.GOARCH)
assetName := fmt.Sprintf("mnemonic_%s_%s_%s%s", info.LatestVersion, runtime.GOOS, runtime.GOARCH, archiveExt())
if err := verifyChecksum(ctx, archivePath, info.ChecksumsURL, assetName); err != nil {
return nil, fmt.Errorf("checksum verification failed: %w", err)
}
Expand Down Expand Up @@ -296,8 +297,63 @@ func verifyChecksum(ctx context.Context, archivePath, checksumsURL, expectedName
return nil
}

// extractBinary extracts the "mnemonic" binary from a tar.gz archive.
// archiveExt returns the archive file extension for the current platform.
func archiveExt() string {
if runtime.GOOS == "windows" {
return ".zip"
}
return ".tar.gz"
}

// extractBinary extracts the "mnemonic" binary from an archive.
// Supports tar.gz (macOS/Linux) and zip (Windows).
func extractBinary(archivePath, destPath string) error {
if strings.HasSuffix(archivePath, ".zip") {
return extractBinaryFromZip(archivePath, destPath)
}
return extractBinaryFromTarGz(archivePath, destPath)
}

// extractBinaryFromZip extracts the binary from a zip archive.
func extractBinaryFromZip(archivePath, destPath string) error {
r, err := zip.OpenReader(archivePath)
if err != nil {
return fmt.Errorf("opening zip archive: %w", err)
}
defer func() { _ = r.Close() }()

binaryName := "mnemonic"
if runtime.GOOS == "windows" {
binaryName = "mnemonic.exe"
}

for _, f := range r.File {
name := filepath.Base(f.Name)
if name == binaryName && !f.FileInfo().IsDir() {
rc, err := f.Open()
if err != nil {
return fmt.Errorf("opening zip entry: %w", err)
}
defer func() { _ = rc.Close() }()

out, err := os.Create(destPath)
if err != nil {
return fmt.Errorf("creating output file: %w", err)
}
// Limit copy to 500MB to prevent zip bomb attacks
if _, err := io.Copy(out, io.LimitReader(rc, 500*1024*1024)); err != nil {
_ = out.Close()
return fmt.Errorf("extracting binary: %w", err)
}
return out.Close()
}
}

return fmt.Errorf("binary %q not found in archive", binaryName)
}

// extractBinaryFromTarGz extracts the binary from a tar.gz archive.
func extractBinaryFromTarGz(archivePath, destPath string) error {
f, err := os.Open(archivePath)
if err != nil {
return fmt.Errorf("opening archive: %w", err)
Expand Down
90 changes: 90 additions & 0 deletions internal/updater/updater_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package updater

import (
"archive/tar"
"archive/zip"
"compress/gzip"
"context"
"crypto/sha256"
Expand Down Expand Up @@ -247,6 +248,95 @@ func TestExtractBinaryNotFound(t *testing.T) {
}
}

func TestExtractBinaryFromZip(t *testing.T) {
tmpDir := t.TempDir()
archivePath := filepath.Join(tmpDir, "test.zip")
destPath := filepath.Join(tmpDir, "mnemonic_extracted")
binaryContent := []byte("#!/bin/sh\necho hello\n")

binaryName := "mnemonic"
if runtime.GOOS == "windows" {
binaryName = "mnemonic.exe"
}

// Build the zip archive
f, err := os.Create(archivePath)
if err != nil {
t.Fatal(err)
}
zw := zip.NewWriter(f)

// Add a non-binary file first
w, err := zw.Create("README.md")
if err != nil {
t.Fatal(err)
}
if _, err := w.Write([]byte("hello")); err != nil {
t.Fatal(err)
}

// Add the binary
w, err = zw.Create(binaryName)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write(binaryContent); err != nil {
t.Fatal(err)
}

if err := zw.Close(); err != nil {
t.Fatal(err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}

// Extract
if err := extractBinary(archivePath, destPath); err != nil {
t.Fatalf("extractBinary (zip) failed: %v", err)
}

// Verify content
got, err := os.ReadFile(destPath)
if err != nil {
t.Fatal(err)
}
if string(got) != string(binaryContent) {
t.Errorf("extracted content = %q, want %q", got, binaryContent)
}
}

func TestExtractBinaryFromZipNotFound(t *testing.T) {
tmpDir := t.TempDir()
archivePath := filepath.Join(tmpDir, "test.zip")

f, err := os.Create(archivePath)
if err != nil {
t.Fatal(err)
}
zw := zip.NewWriter(f)

w, err := zw.Create("README.md")
if err != nil {
t.Fatal(err)
}
if _, err := w.Write([]byte("hello")); err != nil {
t.Fatal(err)
}

if err := zw.Close(); err != nil {
t.Fatal(err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}

err = extractBinary(archivePath, filepath.Join(tmpDir, "out"))
if err == nil {
t.Fatal("expected error when binary not in zip archive")
}
}

func TestVerifyChecksum(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.tar.gz")
Expand Down