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
52 changes: 52 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Release

on:
release:
types: [published]

permissions:
contents: read

jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.release.tag_name }}
- uses: actions/setup-go@v7
with:
go-version: "1.26.x"
- name: Install git-annex on Linux
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y git-annex
- name: Install git-annex on macOS
if: runner.os == 'macOS'
run: brew install git-annex
- run: go test ./...

publish:
needs: test
runs-on: ubuntu-latest
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.release.tag_name }}
- uses: actions/setup-go@v7
with:
go-version: "1.26.x"
- name: Build release archives
run: |
for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64; do
./scripts/package-release.sh "$RELEASE_TAG" "${target%/*}" "${target#*/}"
done
- name: Upload release assets
run: gh release upload "$RELEASE_TAG" dist/* --clobber
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,25 @@ make build
./bin/dfs health
```

### Published releases

Maintainers publish versioned Linux and macOS archives for amd64 and arm64 by
creating a GitHub release from `main`:

```sh
gh release create v0.1.0-alpha.1 \
--repo bitbeamer/dfs \
--target main \
--prerelease \
--generate-notes
```

Publishing the release runs the full test suite on Linux and macOS, then adds
the four archives and their SHA-256 checksum files to that release. Extract an
archive and run `./bin/dfs`; keep its `scripts` directory beside `bin` so that
`dfs setup` can find the platform installer. Git, git-annex, and FUSE 3 or
macFUSE remain runtime prerequisites.

The binary uses the native Go FUSE protocol implementation and does not link
against libfuse, but FUSE 3 or macFUSE is still required at runtime. See
[INSTALL.md](INSTALL.md) for complete platform setup, macOS permissions,
Expand Down
59 changes: 59 additions & 0 deletions scripts/package-release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
printf 'Usage: %s <version> <linux|darwin> <amd64|arm64> [output-directory]\n' "$0" >&2
}

if (( $# < 3 || $# > 4 )); then
usage
exit 2
fi

version=$1
target_os=$2
target_arch=$3
output_directory=${4:-dist}

if [[ ! "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z]+)*$ ]]; then
printf 'Release version must be a v-prefixed semantic version: %s\n' "$version" >&2
exit 2
fi
case "$target_os" in
linux) installer=install-cachyos.sh ;;
darwin) installer=install-macos.sh ;;
*) usage; exit 2 ;;
esac
case "$target_arch" in
amd64|arm64) ;;
*) usage; exit 2 ;;
esac

repository=$(cd "$(dirname "$0")/.." && pwd -P)
mkdir -p "$output_directory"
output_directory=$(cd "$output_directory" && pwd -P)
archive_name="dfs-$version-$target_os-$target_arch"
temporary_directory=$(mktemp -d)
trap 'rm -rf "$temporary_directory"' EXIT
archive_root="$temporary_directory/$archive_name"
mkdir -p "$archive_root/bin" "$archive_root/scripts"

(
cd "$repository"
CGO_ENABLED=0 GOOS="$target_os" GOARCH="$target_arch" go build \
-trimpath \
-ldflags "-s -w -X github.com/bitbeamer/dfs/internal/cli.Version=$version" \
-o "$archive_root/bin/dfs" ./cmd/dfs
)
install -m 0755 "$repository/scripts/$installer" "$archive_root/scripts/$installer"
install -m 0644 "$repository/README.md" "$repository/INSTALL.md" "$repository/LICENSE" "$archive_root/"

archive="$output_directory/$archive_name.tar.gz"
tar -C "$temporary_directory" -czf "$archive" "$archive_name"
if command -v sha256sum >/dev/null 2>&1; then
(cd "$output_directory" && sha256sum "$(basename "$archive")" >"$(basename "$archive").sha256")
else
(cd "$output_directory" && shasum -a 256 "$(basename "$archive")" >"$(basename "$archive").sha256")
fi

printf 'Created %s\n' "$archive"
75 changes: 75 additions & 0 deletions scripts/package_release_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package scripts_test

import (
"archive/tar"
"compress/gzip"
"errors"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)

func TestPackageReleaseCreatesInstallableArchiveAndChecksum(t *testing.T) {
output := t.TempDir()
command := exec.Command("./package-release.sh", "v1.2.3-alpha.1", "linux", "amd64", output)
if combined, err := command.CombinedOutput(); err != nil {
t.Fatalf("package release: %v\n%s", err, combined)
}

base := "dfs-v1.2.3-alpha.1-linux-amd64"
archive := filepath.Join(output, base+".tar.gz")
want := map[string]int64{
base + "/bin/dfs": 0o755,
base + "/scripts/install-cachyos.sh": 0o755,
base + "/README.md": 0o644,
base + "/INSTALL.md": 0o644,
base + "/LICENSE": 0o644,
}
file, err := os.Open(archive)
if err != nil {
t.Fatal(err)
}
decompressed, err := gzip.NewReader(file)
if err != nil {
file.Close()
t.Fatal(err)
}
for reader := tar.NewReader(decompressed); ; {
header, err := reader.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatal(err)
}
if mode, ok := want[header.Name]; ok {
if header.Mode&0o777 != mode {
t.Errorf("%s mode = %o, want %o", header.Name, header.Mode&0o777, mode)
}
delete(want, header.Name)
}
}
decompressed.Close()
file.Close()
if len(want) != 0 {
t.Fatalf("archive is missing entries: %v", want)
}

checksum, err := os.ReadFile(archive + ".sha256")
if err != nil {
t.Fatal(err)
}
if !strings.HasSuffix(strings.TrimSpace(string(checksum)), " "+filepath.Base(archive)) {
t.Fatalf("checksum does not name archive: %q", checksum)
}
}

func TestPackageReleaseRejectsInvalidTarget(t *testing.T) {
command := exec.Command("./package-release.sh", "latest", "windows", "386", t.TempDir())
if err := command.Run(); err == nil {
t.Fatal("invalid release target unexpectedly succeeded")
}
}
Loading