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
72 changes: 72 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,78 @@ jobs:
files: |
./Devsy.flatpak

publish-homebrew:
name: Publish Homebrew Formula
needs: [build-desktop]
if: ${{ !github.event.release.prerelease }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod

- name: download darwin CLI artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: devsy-darwin
path: ${{ runner.temp }}/devsy-bin/

- name: download linux CLI artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: devsy-linux
path: ${{ runner.temp }}/devsy-bin/

- name: flatten CLI artifacts
run: go run ./hack/release_artifacts inventory --dir "${{ runner.temp }}/devsy-bin" --flatten

- name: generate GitHub App token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ secrets.DEVSY_GITHUB_APP_ID }}
private-key: ${{ secrets.DEVSY_GITHUB_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: homebrew-tap

- name: generate and push formula
env:
TAP_TOKEN: ${{ steps.app-token.outputs.token }}
TAP_REPO: ${{ github.repository_owner }}/homebrew-tap
REPO: ${{ github.repository }}
TAG: ${{ inputs.tag || github.ref_name }}
BIN_DIR: ${{ runner.temp }}/devsy-bin
run: |
set -euo pipefail

if [ -z "${TAP_TOKEN:-}" ]; then
echo "::error::GitHub App token was not generated"
exit 1
fi

git clone --depth 1 "https://x-access-token:${TAP_TOKEN}@github.com/${TAP_REPO}.git" tap
mkdir -p tap/Formula
go run ./hack/homebrew_formula \
--bin-dir "$BIN_DIR" \
--repo "$REPO" \
--tag "$TAG" \
--out tap/Formula/devsy.rb

cd tap
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git diff --quiet; then
echo "Formula unchanged; nothing to publish"
exit 0
fi
git add Formula/devsy.rb
git commit -m "devsy ${TAG}"
git push

prerelease:
name: Publish Prerelease
needs: [build-desktop, build-flatpak, deploy-update-metadata]
Expand Down
139 changes: 139 additions & 0 deletions hack/homebrew_formula/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Generates the Homebrew formula for the Devsy CLI from the released
// per-platform binaries, embedding each asset's release URL and sha256.
package main

import (
"crypto/sha256"
"encoding/hex"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
)

type platform struct {
OS string // Homebrew block: macos, linux
Arch string // Homebrew block: arm, intel
Binary string // release asset filename
URL string
SHA256 string
}

var platforms = []platform{
{OS: "macos", Arch: "arm", Binary: "devsy-darwin-arm64"},
{OS: "macos", Arch: "intel", Binary: "devsy-darwin-amd64"},
{OS: "linux", Arch: "arm", Binary: "devsy-linux-arm64"},
{OS: "linux", Arch: "intel", Binary: "devsy-linux-amd64"},
}

const formulaTmpl = `class Devsy < Formula
desc "Standardized dev workspaces across Docker, Kubernetes, cloud, and SSH"
homepage "https://www.devsy.sh"
version "{{ .Version }}"
license "MPL-2.0"

on_macos do
on_arm do
url "{{ (index .Platforms "macos/arm").URL }}"
sha256 "{{ (index .Platforms "macos/arm").SHA256 }}"
end
on_intel do
url "{{ (index .Platforms "macos/intel").URL }}"
sha256 "{{ (index .Platforms "macos/intel").SHA256 }}"
end
end

on_linux do
on_arm do
url "{{ (index .Platforms "linux/arm").URL }}"
sha256 "{{ (index .Platforms "linux/arm").SHA256 }}"
end
on_intel do
url "{{ (index .Platforms "linux/intel").URL }}"
sha256 "{{ (index .Platforms "linux/intel").SHA256 }}"
end
end

def install
bin.install Dir["devsy-*"].first => "devsy"
end

test do
system "#{bin}/devsy", "--version"
end
end
`

func fileSHA256(path string) (string, error) {
data, err := os.ReadFile(path) // #nosec G304 -- release tooling reads local build artifacts.
if err != nil {
return "", err
}
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:]), nil
}

func assetURL(repo, tag, filename string) string {
return fmt.Sprintf("https://github.com/%s/releases/download/%s/%s", repo, tag, filename)
}

func render(binDir, repo, tag string) (string, error) {
byKey := make(map[string]platform, len(platforms))
for _, p := range platforms {
sum, err := fileSHA256(filepath.Join(binDir, p.Binary))
if err != nil {
return "", fmt.Errorf("checksum %s: %w", p.Binary, err)
}
p.URL = assetURL(repo, tag, p.Binary)
p.SHA256 = sum
byKey[p.OS+"/"+p.Arch] = p
}

tmpl, err := template.New("formula").Parse(formulaTmpl)
if err != nil {
return "", fmt.Errorf("parse template: %w", err)
}
var sb strings.Builder
data := struct {
Version string
Platforms map[string]platform
}{Version: strings.TrimPrefix(tag, "v"), Platforms: byKey}
if err := tmpl.Execute(&sb, data); err != nil {
return "", fmt.Errorf("render template: %w", err)
}
return sb.String(), nil
}

func main() {
binDir := flag.String(
"bin-dir",
"",
"directory containing the devsy-<os>-<arch> release binaries",
)
repo := flag.String("repo", "", "owner/repo the release assets are published under")
tag := flag.String("tag", "", "release tag (e.g. v1.2.3)")
out := flag.String("out", "", "path to write the generated formula")
flag.Parse()

if *binDir == "" || *repo == "" || *tag == "" || *out == "" {
fmt.Fprintln(
os.Stderr,
"usage: homebrew_formula --bin-dir DIR --repo OWNER/REPO --tag TAG --out FILE",
)
os.Exit(2)
}

formula, err := render(*binDir, *repo, *tag)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
// #nosec G306 -- formula is public.
if err := os.WriteFile(*out, []byte(formula), 0o644); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("wrote %s\n", *out)
}
53 changes: 53 additions & 0 deletions hack/homebrew_formula/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package main

import (
"os"
"path/filepath"
"strings"
"testing"
)

func writeBinaries(t *testing.T) string {
t.Helper()
dir := t.TempDir()
for _, p := range platforms {
path := filepath.Join(dir, p.Binary)
if err := os.WriteFile(path, []byte("content-"+p.Binary), 0o644); err != nil {
t.Fatalf("write %s: %v", p.Binary, err)
}
}
return dir
}

func TestRender(t *testing.T) {
out, err := render(writeBinaries(t), "devsy-org/devsy", "v1.2.3")
if err != nil {
t.Fatalf("render: %v", err)
}

// sha256("content-devsy-darwin-arm64")
const wantARM = "9bc996e636ac5321e2aae6bd3fb421c5ea82b34a5b20e2c9fd65cc81b2f3753e"

for _, want := range []string{
`version "1.2.3"`, // leading v stripped
`license "MPL-2.0"`,
"https://github.com/devsy-org/devsy/releases/download/v1.2.3/devsy-darwin-arm64",
"https://github.com/devsy-org/devsy/releases/download/v1.2.3/devsy-linux-amd64",
`sha256 "` + wantARM + `"`,
`bin.install Dir["devsy-*"].first => "devsy"`,
} {
if !strings.Contains(out, want) {
t.Errorf("formula missing %q\n---\n%s", want, out)
}
}
}

func TestRenderMissingBinary(t *testing.T) {
dir := writeBinaries(t)
if err := os.Remove(filepath.Join(dir, "devsy-linux-arm64")); err != nil {
t.Fatal(err)
}
if _, err := render(dir, "devsy-org/devsy", "v1.2.3"); err == nil {
t.Fatal("expected error for missing binary, got nil")
}
}
10 changes: 9 additions & 1 deletion sites/docs-devsy-sh/pages/getting-started/install.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,15 @@ Devsy Desktop needs [WebView 2](https://developer.microsoft.com/en-us/microsoft-

## Install Devsy CLI

Run the command for your platform. You can also install the CLI later from Devsy Desktop.
On macOS or Linux, install with [Homebrew](https://brew.sh):

```bash
brew install devsy-org/homebrew-tap/devsy
```

Upgrade later with `brew upgrade devsy`.

Otherwise, run the command for your platform below. You can also install the CLI later from Devsy Desktop.

<Tabs values={[
{label: 'MacOS Silicon/ARM', value: 'macarm64'},
Expand Down
8 changes: 7 additions & 1 deletion sites/docs-devsy-sh/pages/getting-started/update.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@ If you installed Devsy from a `.deb` or `.rpm` package, update through your pack

## Devsy CLI

Re-run the command from [Install Devsy CLI](./install.mdx#install-devsy-cli) to download the latest version.
If you installed with Homebrew, upgrade with:

```bash
brew upgrade devsy
```

Otherwise, re-run the command from [Install Devsy CLI](./install.mdx#install-devsy-cli) to download the latest version.
Loading