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
34 changes: 27 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,13 @@ jobs:
desktop/release/*.deb
desktop/release/*.rpm

- name: upload macOS dmg for homebrew cask
if: runner.os == 'macOS'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: desktop-dmg-${{ matrix.name }}
path: desktop/release/*.dmg

- name: rewrite manifest URLs to GitHub Release assets
if: hashFiles('desktop/release/latest*.yml', 'desktop/release/beta*.yml') != ''
# Windows defaults to pwsh; `$REPO`/`$TAG` would be empty PowerShell vars.
Expand Down Expand Up @@ -372,7 +379,7 @@ jobs:
./Devsy.flatpak

publish-homebrew:
name: Publish Homebrew Formula
name: Publish Homebrew Formula & Cask
needs: [build-desktop]
if: ${{ !github.event.release.prerelease }}
runs-on: ubuntu-latest
Expand All @@ -397,6 +404,13 @@ jobs:
name: devsy-linux
path: ${{ runner.temp }}/devsy-bin/

- name: download macOS dmg artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: desktop-dmg-*
path: ${{ runner.temp }}/devsy-dmg/
merge-multiple: true

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

Expand All @@ -409,13 +423,14 @@ jobs:
owner: ${{ github.repository_owner }}
repositories: homebrew-tap

- name: generate and push formula
- name: generate and push formula and cask
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
DMG_DIR: ${{ runner.temp }}/devsy-dmg
run: |
set -euo pipefail

Expand All @@ -425,23 +440,28 @@ jobs:
fi

git clone --depth 1 "https://x-access-token:${TAP_TOKEN}@github.com/${TAP_REPO}.git" tap
mkdir -p tap/Formula
mkdir -p tap/Formula tap/Casks
go run ./hack/homebrew_formula \
--bin-dir "$BIN_DIR" \
--repo "$REPO" \
--tag "$TAG" \
--out tap/Formula/devsy.rb
go run ./hack/homebrew_cask \
--dmg-dir "$DMG_DIR" \
--repo "$REPO" \
--tag "$TAG" \
--out tap/Casks/devsy.rb
Comment on lines +449 to +453

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -a '^electron-builder\.(yml|yaml)$' desktop \
  -x sh -c 'echo "--- $1"; rg -n -C4 "artifactName|dmg|Devsy_mac_(arm64|x64)" "$1"' sh

Repository: devsy-org/devsy

Length of output: 1139


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- workflow relevant snippets"
sed -n '1,90p' .github/workflows/release.yml
sed -n '400,470p' .github/workflows/release.yml
echo "--- all references to homebrew_cask and dmg file filters"
rg -n "homebrew_cask|Devsy_mac_(arm64|x64)|\\.dmg|artifactName|os: mac|arch:" .github/workflows/release.yml desktop/electron-builder.yml

echo "--- deterministic artifactName expansion for the two mac arch values used by electron-builder env vars"
python3 - <<'PY'
for os in ("Darwin",):
  for arch in ("arm64", "x64"):
    ext = "dmg"
    print("mac", os, arch, f"Devsy_{os}_{arch}.{ext}")
PY

echo "--- check available electron-builder package docs/docs snippets in repo"
fd -a '^README|CHANGELOG|HISTORY|package.json|pnpm-lock.yaml|yarn.lock|package-lock.json$' desktop -x sh -c 'echo "--- $1"; sed -n "1,220p" "$1"' sh

Repository: devsy-org/devsy

Length of output: 16906


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- homebrew cask implementation"
fd -a 'homebrew_cask\.(go|ts|js)$' . -x sh -c 'echo "--- $1"; cat -n "$1"' sh

echo "--- release dmg upload/download and filename handling"
sed -n '190,245p' .github/workflows/release.yml
rg -n -C3 "dmgDir|Devsy_mac|*.dmg|listDir|files|artifacts|Cask|dmg" . -g '!node_modules' -g '!*.lock'

Repository: devsy-org/devsy

Length of output: 2775


🌐 Web query:

electron-builder 26.15.3 mac artifactName arch variables mac-arm64 release directory

💡 Result:

To configure the artifact file name for macOS builds in electron-builder, you can use the artifactName property within your configuration [1][2]. To include the architecture (such as arm64 or x64) in the file name, use the ${arch} variable in your template [3][4][5]. Configuration Example: { "mac": { "artifactName": "${productName}-${version}-${arch}.${ext}" } } Key Details: 1. Supported Variables: You can use several macros in the artifactName template, including ${arch}, ${productName}, ${version}, ${ext}, and environment variables like ${env.VAR_NAME} [4][5]. 2. Arch Variable Behavior: When using ${arch}, if no architecture is specified for the build, electron-builder may remove the macro and any leading separators (like hyphens or underscores) to avoid double punctuation [4]. If you are building both x64 and arm64, including ${arch} is necessary to prevent files from overwriting each other [6][3]. 3. Target Considerations: While standard for DMG and ZIP targets, some specific targets like pkg have historically required explicit macro handling to ensure the arch correctly reflects in the filename [6]. 4. Release Directory: By default, artifacts are placed in the dist/ directory of your project root, unless otherwise configured via the outDir property. If you encounter issues where the architecture is not being reflected in the filename, ensure you are using a recent version of electron-builder, as earlier versions had limitations regarding dynamic expansion for specific targets like pkg [6][7].

Citations:


Make the generated DMG names match the Cask contract.

The macOS matrix uses electron-builder’s arch values, so artifactName: Devsy_${os}_${arch}.${ext} can emit Devsy_Darwin_arm64.dmg / Devsy_Darwin_x64.dmg, but the cask consumer expects Devsy_mac_arm64.dmg / Devsy_mac_x64.dmg. Align the filename used by electron-builder or update hack/homebrew_cask so publishing does not fail after builds complete.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 449 - 453, Align the macOS DMG
naming between the electron-builder artifactName configuration and the
homebrew_cask consumer: ensure generated files use the Cask contract names
Devsy_mac_arm64.dmg and Devsy_mac_x64.dmg instead of Darwin-prefixed names.
Update the relevant naming logic in the release workflow or the
hack/homebrew_cask lookup while preserving Linux and other artifact naming.


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"
git add Formula/devsy.rb Casks/devsy.rb
if git diff --cached --quiet; then
echo "Formula and cask unchanged; nothing to publish"
exit 0
fi
git add Formula/devsy.rb
git commit -m "devsy ${TAG}"
git push
git push -u origin HEAD

prerelease:
name: Publish Prerelease
Expand Down
116 changes: 116 additions & 0 deletions hack/homebrew_cask/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Generates the Homebrew cask for the Devsy desktop app from the released
// per-arch macOS disk images, embedding each image's sha256.
package main

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

type arch struct {
Cask string // cask arch token: arm, intel
DMG string // release asset filename
SHA256 string
}

var arches = []arch{
{Cask: "arm", DMG: "Devsy_mac_arm64.dmg"},
{Cask: "intel", DMG: "Devsy_mac_x64.dmg"},
}

const caskTmpl = `cask "devsy" do
arch arm: "arm64", intel: "x64"

version "{{ .Version }}"
sha256 arm: "{{ (index .Arches "arm").SHA256 }}",
intel: "{{ (index .Arches "intel").SHA256 }}"

url "https://github.com/{{ .Repo }}/releases/download/v#{version}/Devsy_mac_#{arch}.dmg"
name "Devsy"
desc "Standardized dev workspaces across Docker, Kubernetes, cloud, and SSH"
homepage "https://www.devsy.sh"

app "Devsy.app"

zap trash: [
"~/Library/Application Support/Devsy",
"~/Library/Logs/Devsy",
"~/Library/Preferences/sh.devsy.app.plist",
"~/Library/Saved Application State/sh.devsy.app.savedState",
]
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 render(dmgDir, repo, tag string) (string, error) {
byKey := make(map[string]arch, len(arches))
for _, a := range arches {
sum, err := fileSHA256(filepath.Join(dmgDir, a.DMG))
if err != nil {
return "", fmt.Errorf("checksum %s: %w", a.DMG, err)
}
a.SHA256 = sum
byKey[a.Cask] = a
}

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

func main() {
dmgDir := flag.String(
"dmg-dir",
"",
"directory containing the Devsy_mac_<arch>.dmg release images",
)
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 cask")
flag.Parse()

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

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

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

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

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

// sha256("content-Devsy_mac_arm64.dmg")
const wantARM = "f65094affc51b44bb65e948773afbdb527e5073f82569bcd9361deb17581807b"
// sha256("content-Devsy_mac_x64.dmg")
const wantIntel = "f1045e0e16797365c7f23e330cbab90e74218dc48b55a030fe23a93f4e8c6b9e"

for _, want := range []string{
`version "1.2.3"`, // leading v stripped
`cask "devsy" do`,
`arch arm: "arm64", intel: "x64"`,
"https://github.com/devsy-org/devsy/releases/download/v#{version}/Devsy_mac_#{arch}.dmg",
`app "Devsy.app"`,
`homepage "https://www.devsy.sh"`,
`sha256 arm: "` + wantARM + `"`,
`intel: "` + wantIntel + `"`,
} {
if !strings.Contains(out, want) {
t.Errorf("cask missing %q\n---\n%s", want, out)
}
}
}

func TestRenderMissingDMG(t *testing.T) {
dir := writeDMGs(t)
if err := os.Remove(filepath.Join(dir, "Devsy_mac_x64.dmg")); err != nil {
t.Fatal(err)
}
if _, err := render(dir, "devsy-org/devsy", "v1.2.3"); err == nil {
t.Fatal("expected error for missing dmg, got nil")
}
}
Loading