diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ec7282..1ad9039 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,186 +1,32 @@ -name: Build and Attach Release Binaries -permissions: - contents: read +name: Release on: - workflow_dispatch: - inputs: - release_tag: - description: "Release tag to attach binaries to (format: vX.Y.Z)" - required: true - type: string + push: + tags: + - "v*" + +permissions: + contents: write jobs: - validate-and-test: + release: runs-on: ubuntu-latest - outputs: - major: ${{ steps.extract-version.outputs.major }} - minor: ${{ steps.extract-version.outputs.minor }} - patch: ${{ steps.extract-version.outputs.patch }} steps: - - name: Checkout code + - name: Checkout uses: actions/checkout@v6 - - - name: Validate version format and extract components - id: extract-version - env: - RELEASE_TAG: ${{ github.event.inputs.release_tag }} - run: | - # Validate format (must be vX.Y.Z) - if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::error::Invalid version format. Must be vX.Y.Z (e.g., v0.1.0)" - exit 1 - fi - - # Extract components - VERSION_NUM="${RELEASE_TAG#v}" - IFS=. read -r MAJOR MINOR PATCH <<< "$VERSION_NUM" - - echo "major=$MAJOR" >> "$GITHUB_OUTPUT" - echo "minor=$MINOR" >> "$GITHUB_OUTPUT" - echo "patch=$PATCH" >> "$GITHUB_OUTPUT" - - - name: Verify draft release exists - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ github.event.inputs.release_tag }} - run: | - # Check if release exists - if ! gh release view "$RELEASE_TAG" > /dev/null 2>&1; then - echo "::error::Release with tag $RELEASE_TAG does not exist" - exit 1 - fi - - # Check if it's a draft - IS_DRAFT=$(gh release view "$RELEASE_TAG" --json isDraft -q .isDraft) - if [ "$IS_DRAFT" != "true" ]; then - echo "::error::Release $RELEASE_TAG is not a draft release" - exit 1 - fi - - echo "✓ Draft release $RELEASE_TAG found" - - - name: Extract and validate source code version - env: - INPUT_MAJOR: ${{ steps.extract-version.outputs.major }} - INPUT_MINOR: ${{ steps.extract-version.outputs.minor }} - INPUT_PATCH: ${{ steps.extract-version.outputs.patch }} - run: | - # Parse version from pkg/config/env.go - MAJOR=$(grep -E '^\s*VersionMajor\s*=\s*"[0-9]+"' pkg/config/env.go | sed -E 's/.*"([0-9]+)".*/\1/') - MINOR=$(grep -E '^\s*VersionMinor\s*=\s*"[0-9]+"' pkg/config/env.go | sed -E 's/.*"([0-9]+)".*/\1/') - PATCH=$(grep -E '^\s*VersionPatch\s*=\s*"[0-9]+"' pkg/config/env.go | sed -E 's/.*"([0-9]+)".*/\1/') - - if [ -z "$MAJOR" ] || [ -z "$MINOR" ] || [ -z "$PATCH" ]; then - echo "::error::Failed to extract version from pkg/config/env.go" - exit 1 - fi - - SOURCE_VERSION="$MAJOR.$MINOR.$PATCH" - INPUT_VERSION="$INPUT_MAJOR.$INPUT_MINOR.$INPUT_PATCH" - - echo "Workflow input version: $INPUT_VERSION" - echo "Source code version: $SOURCE_VERSION" - - if [ "$INPUT_VERSION" != "$SOURCE_VERSION" ]; then - echo "::error::Version mismatch!" - echo "::error::Workflow input: v$INPUT_VERSION" - echo "::error::Source code (pkg/config/env.go): $SOURCE_VERSION" - echo "::error::Please update VersionMajor, VersionMinor, VersionPatch in pkg/config/env.go before releasing." - exit 1 - fi - - echo "✓ Version validation passed: $SOURCE_VERSION" + with: + fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v6 with: go-version: "1.24" - - name: Install Task - uses: go-task/setup-task@v1 - - - name: Run unit tests - run: | - echo "Running unit tests..." - task test-unit - echo "✓ Unit tests passed" - - - name: Build release binary - run: | - echo "Building release binary..." - task build-release - - if [ ! -f mbvpn ]; then - echo "::error::Build failed - mbvpn binary not found" - exit 1 - fi - - echo "✓ Build successful" - - - name: Validate binary version output - env: - EXPECTED_VERSION: ${{ github.event.inputs.release_tag }} - run: | - echo "Testing binary version output..." - - # Execute version command and capture output - BINARY_OUTPUT=$(./mbvpn version) - - # Trim any whitespace - BINARY_OUTPUT=$(echo "$BINARY_OUTPUT" | tr -d '[:space:]') - EXPECTED_VERSION=$(echo "$EXPECTED_VERSION" | tr -d '[:space:]') - - echo "Binary output: $BINARY_OUTPUT" - echo "Expected version: $EXPECTED_VERSION" - - if [ "$BINARY_OUTPUT" != "$EXPECTED_VERSION" ]; then - echo "::error::Binary version mismatch!" - echo "::error::Expected: $EXPECTED_VERSION" - echo "::error::Got: $BINARY_OUTPUT" - exit 1 - fi - - echo "✓ Binary version validation passed" - - build-binaries: - needs: validate-and-test - runs-on: ubuntu-latest - permissions: - contents: write - strategy: - matrix: - arch: [amd64, arm64, 386] - os: [linux] - env: - BINARY_NAME: mbvpn-${{ matrix.os }}-${{ matrix.arch }} - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup Go - uses: actions/setup-go@v6 + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 #v7 with: - go-version: "1.24" - - - name: Install Task - uses: go-task/setup-task@v1 - - - name: Build for ${{ matrix.os }}-${{ matrix.arch }} - env: - VERSION_MAJOR: ${{ needs.validate-and-test.outputs.major }} - VERSION_MINOR: ${{ needs.validate-and-test.outputs.minor }} - VERSION_PATCH: ${{ needs.validate-and-test.outputs.patch }} - VERSION_BUILD: ${{ github.run_number }} - GOOS: ${{ matrix.os }} - GOARCH: ${{ matrix.arch }} - run: | - task build-release - mv mbvpn "$BINARY_NAME" - - - name: Upload Release Asset + version: "~> v2" + args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ github.event.inputs.release_tag }} - run: | - gh release upload "$RELEASE_TAG" "$BINARY_NAME" + MWB_HOMEBREW_TAP: ${{ secrets.MWB_HOMEBREW_TAP }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..98cd9ba --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,53 @@ +version: 2 + +builds: + - main: ./cmd/mbvpn + binary: mbvpn + env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + - arm64 + - "386" + flags: + - -trimpath + ldflags: + - -s -w -X github.com/malwarebytes/mbvpn-linux/pkg/config.Version={{.Version}} + +archives: + - formats: + - tar.gz + name_template: >- + {{ .ProjectName }}_ + {{- .Version }}_ + {{- .Os }}_ + {{- .Arch }} + +checksum: + name_template: "checksums.txt" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^chore:" + - "^style:" + - "^ci:" + - "^test:" + +brews: + - name: mbvpn + repository: + owner: malwarebytes + name: homebrew-tap + token: "{{ .Env.MWB_HOMEBREW_TAP }}" + homepage: "https://github.com/malwarebytes/mbvpn-linux" + description: "Malwarebytes VPN client for Linux" + license: "Apache-2.0" + install: | + bin.install "mbvpn" + test: | + system "#{bin}/mbvpn", "version" diff --git a/README.md b/README.md index f75a853..96a0c23 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,17 @@ The client is in experimental mode, so it is important to know which parts of th ## Installation -### Installation via Go Package Manager (Recommended) +### Homebrew (Linux) + +```bash +brew install malwarebytes/tap/mbvpn +``` + +Then provide network capabilities (see below). + +### Installation via Go Package Manager + + 1. Install Go version 1.23.4 or above. diff --git a/Taskfile.yml b/Taskfile.yml index 6a77289..6ba8500 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -59,6 +59,11 @@ tasks: env: MBVPN_TEST_LICENSE_KEY: "{{.MBVPN_TEST_LICENSE_KEY}}" + release-snapshot: + desc: Build a local snapshot release with goreleaser (no publish) + cmds: + - goreleaser release --snapshot --clean + clean: desc: Clean build artifacts cmds: diff --git a/cmd/version.go b/cmd/version.go index 01bd65e..9f6519b 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -14,7 +14,7 @@ func NewVersionCommand() *cobra.Command { Short: "Display version information", Long: `Display the application version.`, Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("v%s\n", config.Version()) + fmt.Printf("v%s\n", config.Version) }, } } diff --git a/cmd/version_test.go b/cmd/version_test.go index 032f092..1ed90bb 100644 --- a/cmd/version_test.go +++ b/cmd/version_test.go @@ -32,16 +32,17 @@ func TestNewVersionCommand(t *testing.T) { } func TestVersionCommandOutput(t *testing.T) { - // Capture stdout + origVersion := config.Version + config.Version = "1.2.3" + defer func() { config.Version = origVersion }() + oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w - // Run the command cmd := NewVersionCommand() cmd.Run(cmd, []string{}) - // Restore stdout and get output w.Close() os.Stdout = oldStdout @@ -49,23 +50,19 @@ func TestVersionCommandOutput(t *testing.T) { io.Copy(&buf, r) output := strings.TrimSpace(buf.String()) - // Verify output is single line if strings.Contains(output, "\n") { t.Errorf("Expected single line output, got multiple lines: %s", output) } - // Verify output starts with "v" if !strings.HasPrefix(output, "v") { t.Errorf("Expected output to start with 'v', got: %s", output) } - // Verify version format matches "vX.Y.Z" - expectedVersion := "v" + config.Version() + expectedVersion := "v" + config.Version if output != expectedVersion { t.Errorf("Expected version '%s', got '%s'", expectedVersion, output) } - // Verify it contains dots (semantic version format) if !strings.Contains(output, ".") { t.Errorf("Expected version format 'vX.Y.Z', got: %s", output) } diff --git a/pkg/config/env.go b/pkg/config/env.go index c3d72b6..d505664 100644 --- a/pkg/config/env.go +++ b/pkg/config/env.go @@ -1,7 +1,6 @@ package config import ( - "fmt" "os" "testing" ) @@ -11,10 +10,8 @@ var ( BuildEnv = "production" HolocronUrl = "https://holocron.mwbsys.com/graphql" - // Version information - VersionMajor = "0" - VersionMinor = "1" - VersionPatch = "0" + // Version is injected at build time via -ldflags by GoReleaser. + Version = "dev" ) func Debug() bool { @@ -25,11 +22,6 @@ func Verbose() bool { return testing.Testing() && testing.Verbose() } -// Version returns the full version string in the format "major.minor.patch" -func Version() string { - return fmt.Sprintf("%s.%s.%s", VersionMajor, VersionMinor, VersionPatch) -} - // GetHolocronUrl returns the Holocron URL, checking environment variable first func GetHolocronUrl() string { if url := os.Getenv("MBVPN_HOLOCRON_URL"); url != "" { diff --git a/pkg/config/env_test.go b/pkg/config/env_test.go index 684d7c6..d7774ca 100644 --- a/pkg/config/env_test.go +++ b/pkg/config/env_test.go @@ -33,30 +33,16 @@ func TestVerbose(t *testing.T) { } func TestVersion(t *testing.T) { - // Save original version values and restore them after test - origMajor, origMinor, origPatch := VersionMajor, VersionMinor, VersionPatch - defer func() { - VersionMajor, VersionMinor, VersionPatch = origMajor, origMinor, origPatch - }() - - // Test case 1: Default version values - VersionMajor, VersionMinor, VersionPatch = "1", "2", "3" - expected := "1.2.3" - if Version() != expected { - t.Errorf("Version() = %s, want %s", Version(), expected) - } + orig := Version + defer func() { Version = orig }() - // Test case 2: Different version values - VersionMajor, VersionMinor, VersionPatch = "5", "6", "7" - expected = "5.6.7" - if Version() != expected { - t.Errorf("Version() = %s, want %s", Version(), expected) + Version = "1.2.3" + if Version != "1.2.3" { + t.Errorf("Version = %s, want %s", Version, "1.2.3") } - // Test case 3: Zero values - VersionMajor, VersionMinor, VersionPatch = "0", "0", "0" - expected = "0.0.0" - if Version() != expected { - t.Errorf("Version() = %s, want %s", Version(), expected) + Version = "dev" + if Version != "dev" { + t.Errorf("Version = %s, want %s", Version, "dev") } } \ No newline at end of file