Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
8 changes: 7 additions & 1 deletion .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ jobs:
name: Create GitHub Release
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: ${{ secrets.DEVSY_GITHUB_APP_ID }}
private-key: ${{ secrets.DEVSY_GITHUB_APP_PRIVATE_KEY }}
Comment on lines +16 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify non-SHA action refs in create-release workflow
rg -n '^\s*-\s+uses:\s+[^@]+@v[0-9]+' .github/workflows/create-release.yml

Repository: devsy-org/devsy

Length of output: 148


Pin actions/create-github-app-token to a full commit SHA.

@v3 is mutable and weakens provenance guarantees for release-authentication logic. Use the commit SHA from the release tag instead (e.g., actions/create-github-app-token@<full-sha>) to prevent unexpected action updates during release workflows.

🧰 Tools
🪛 zizmor (1.25.2)

[error] 16-16: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 16-16: dangerous use of GitHub App tokens (github-app): app token inherits blanket installation permissions

(github-app)

🤖 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/create-release.yml around lines 16 - 20, Replace the
mutable tag actions/create-github-app-token@v3 used in the step with a full
commit SHA to pin the exact action version; update the step with id "app-token"
(the block that supplies app-id and private-key inputs) to reference
actions/create-github-app-token@<full-commit-sha> (use the commit SHA from the
action's release tag) so the release workflow's authentication step is immutable
and provenance is preserved.


- uses: actions/checkout@v6
with:
fetch-depth: 0
Expand All @@ -29,7 +35,7 @@ jobs:

- name: Create release
env:
GH_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
gh release create "$GITHUB_REF_NAME" \
--generate-notes \
Expand Down
63 changes: 41 additions & 22 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,31 @@ jobs:
path: dist/devsy-${{ steps.os.outputs.runner_os }}_*/devsy-${{ steps.os.outputs.runner_os }}-*

build-desktop:
name: Build Desktop App on ${{ matrix.os }}
name: Build Desktop App (${{ matrix.name }})
needs: build-cli
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
- name: linux-x64
os: ubuntu-22.04
go-os: linux
go-arch: amd64
builder-args: --linux
- os: macos-latest
- name: macos-arm64
os: macos-latest
go-os: darwin
go-arch: universal
builder-args: --mac
- os: windows-2022
go-arch: arm64
builder-args: --mac --arm64
- name: macos-x64
os: macos-latest
go-os: darwin
go-arch: amd64
builder-args: --mac --x64
- name: windows-x64
os: windows-2022
go-os: windows
go-arch: amd64
builder-args: --win
Expand Down Expand Up @@ -114,15 +122,8 @@ jobs:
run: |
TEMP_DIR="${{ runner.temp }}"
mkdir -p desktop/resources/bin

if [ "${{ matrix.go-arch }}" = "universal" ]; then
# macOS: use both arch binaries for universal build
cp "$TEMP_DIR/devsy-bin/devsy-darwin-amd64" desktop/resources/bin/devsy
chmod +x desktop/resources/bin/devsy
else
cp "$TEMP_DIR/devsy-bin/devsy-${{ matrix.go-os }}-${{ matrix.go-arch }}" desktop/resources/bin/devsy
chmod +x desktop/resources/bin/devsy
fi
cp "$TEMP_DIR/devsy-bin/devsy-${{ matrix.go-os }}-${{ matrix.go-arch }}" desktop/resources/bin/devsy
chmod +x desktop/resources/bin/devsy

# Also stage binaries for dist/ upload and provider generation
mkdir -p dist/
Expand All @@ -136,7 +137,7 @@ jobs:
Copy-Item "${{ runner.temp }}/devsy-bin/devsy-windows-amd64.exe" desktop/resources/bin/devsy.exe

- name: generate pro provider
if: runner.os == 'Linux'
if: matrix.name == 'linux-x64'
working-directory: ./hack/pro
env:
PARTIAL: "true"
Expand Down Expand Up @@ -199,21 +200,23 @@ jobs:
- name: upload update metadata artifact
uses: actions/upload-artifact@v7
with:
name: update-metadata-${{ matrix.os }}
path: desktop/release/latest*.yml
name: update-metadata-${{ matrix.name }}
path: |
desktop/release/latest*.yml
desktop/release/beta*.yml
if-no-files-found: ignore

- name: upload cli assets
uses: softprops/action-gh-release@v3
if: runner.os == 'Linux'
if: matrix.name == 'linux-x64'
with:
tag_name: ${{ inputs.tag || github.ref_name }}
draft: true
files: |
./dist/*

- name: upload desktop linux artifact for flatpak
if: runner.os == 'Linux'
if: matrix.name == 'linux-x64'
uses: actions/upload-artifact@v7
with:
name: devsy-flatpak
Expand All @@ -226,16 +229,32 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v6
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: download update metadata artifacts
uses: actions/download-artifact@v8
Comment on lines +232 to 237

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify non-SHA action refs in this workflow
rg -n '^\s*-\s+uses:\s+[^@]+@v[0-9]+' .github/workflows/release.yml

Repository: devsy-org/devsy

Length of output: 506


🏁 Script executed:

sed -n '230,240p' .github/workflows/release.yml

Repository: devsy-org/devsy

Length of output: 349


Pin GitHub Actions to full commit SHAs.

Using floating version tags (@v6, @v8) for actions/checkout and actions/download-artifact weakens supply-chain security guarantees and conflicts with strict pinning policy. Pin these actions to their full commit SHAs instead.

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 232-232: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 232-232: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 235-235: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 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 232 - 235, Replace the floating
action tags with full commit SHAs for supply-chain pinning: locate the two steps
using "uses: actions/checkout@v6" and "uses: actions/download-artifact@v8" (the
checkout step and the "download update metadata artifacts" step) and change each
to the corresponding repository@<full-commit-sha> form (e.g.,
actions/checkout@<sha> and actions/download-artifact@<sha>), fetching the latest
trusted commit SHAs from the official action repos and updating the workflow
YAML accordingly so the actions are pinned to immutable commit SHAs.

with:
pattern: update-metadata-*
path: metadata/

- name: arrange files into publish directory
- name: fetch existing metadata from live site
run: |
mkdir -p publish-dir/desktop
find metadata/ -name 'latest*.yml' -exec cp {} publish-dir/desktop/ \;
for file in latest-mac.yml latest-linux.yml latest.yml beta-mac.yml beta-linux.yml beta.yml; do
curl -sSf "https://dl.devsy.sh/desktop/$file" -o "publish-dir/desktop/$file" 2>/dev/null || true
done

- name: merge macOS metadata from separate arch builds
run: |
pip install pyyaml
python3 hack/merge-mac-metadata.py metadata/ publish-dir/desktop/

- name: overlay non-mac metadata files
run: |
find metadata/ -name 'latest-linux.yml' -exec cp {} publish-dir/desktop/ \;
find metadata/ -name 'latest.yml' -exec cp {} publish-dir/desktop/ \;
find metadata/ -name 'beta-linux.yml' -exec cp {} publish-dir/desktop/ \;
find metadata/ -name 'beta.yml' -exec cp {} publish-dir/desktop/ \;

- name: deploy to Netlify (dl.devsy.sh)
uses: nwtgck/actions-netlify@v3
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,13 @@ Take a look at the <a href="https://devsy.sh/docs/getting-started/install">Devsy
</tr>
<tr>
<td><b>macOS</b></td>
<td>Universal (Apple Silicon + Intel)</td>
<td><a href="https://github.com/devsy-org/devsy/releases/latest/download/Devsy_mac_universal.dmg"><img src="https://img.shields.io/badge/Download-DMG-blue?style=flat-square&logo=apple" alt="macOS Universal"></a></td>
<td>Apple Silicon (ARM64)</td>
<td><a href="https://github.com/devsy-org/devsy/releases/latest/download/Devsy_mac_arm64.dmg"><img src="https://img.shields.io/badge/Download-DMG-blue?style=flat-square&logo=apple" alt="macOS ARM64"></a></td>
</tr>
<tr>
<td><b>macOS</b></td>
<td>Intel (x64)</td>
<td><a href="https://github.com/devsy-org/devsy/releases/latest/download/Devsy_mac_x64.dmg"><img src="https://img.shields.io/badge/Download-DMG-blue?style=flat-square&logo=apple" alt="macOS x64"></a></td>
</tr>
<tr>
<td><b>Windows</b></td>
Expand Down
11 changes: 10 additions & 1 deletion cmd/self_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
// SelfUpdateCmd is a struct that defines a command call for "self-update".
type SelfUpdateCmd struct {
Version string
Channel string
DryRun bool
}

Expand All @@ -22,7 +23,12 @@ func NewSelfUpdateCmd() *cobra.Command {
Args: cobra.NoArgs,
RunE: func(cobraCmd *cobra.Command, args []string) error {
ctx := cobraCmd.Context()
if err := selfupdate.Upgrade(ctx, cmd.Version, cmd.DryRun); err != nil {
opts := selfupdate.Options{
Version: cmd.Version,
DryRun: cmd.DryRun,
IncludePrerelease: cmd.Channel == "beta",
}
if err := selfupdate.Upgrade(ctx, opts); err != nil {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return fmt.Errorf("unable to update: %w", err)
}
return nil
Expand All @@ -32,6 +38,9 @@ func NewSelfUpdateCmd() *cobra.Command {
selfUpdateCmd.Flags().
StringVar(&cmd.Version, "version", "",
"The version to update to. Defaults to the latest stable version available")
selfUpdateCmd.Flags().
StringVar(&cmd.Channel, "channel", "stable",
"Release channel: 'stable' for production releases, 'beta' for pre-release versions")
selfUpdateCmd.Flags().
BoolVar(&cmd.DryRun, "dry-run", false, "Show which version would be downloaded without actually updating")
return selfUpdateCmd
Expand Down
7 changes: 7 additions & 0 deletions cmd/self_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ func TestNewSelfUpdateCmd_HasDryRunFlag(t *testing.T) {
assert.Equal(t, "false", f.DefValue)
}

func TestNewSelfUpdateCmd_HasChannelFlag(t *testing.T) {
cmd := NewSelfUpdateCmd()
f := cmd.Flags().Lookup("channel")
require.NotNil(t, f, "--channel flag must exist")
assert.Equal(t, "stable", f.DefValue)
}

func TestNewSelfUpdateCmd_AcceptsNoPositionalArgs(t *testing.T) {
cmd := NewSelfUpdateCmd()
err := cmd.Args(cmd, []string{"unexpected"})
Expand Down
7 changes: 4 additions & 3 deletions desktop/electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ extraResources:
mac:
category: public.app-category.developer-tools
icon: resources/icon.icns
x64ArchFiles: "Contents/Resources/bin/devsy"
artifactName: Devsy_${os}_${arch}.${ext}
target:
- target: dmg
arch:
- universal
- x64
- arm64
- target: zip
arch:
- universal
- x64
- arm64
hardenedRuntime: true
gatekeeperAssess: false
entitlements: build/entitlements.mac.plist
Expand Down
20 changes: 20 additions & 0 deletions desktop/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import type { CliRunner } from "./cli.js"
import type { LogStore } from "./log-store.js"
import type { PtyManager } from "./pty.js"
import type { DaemonState } from "./state.js"
import {
type ReleaseChannel,
checkForUpdatesWithChannel,
getReleaseChannel,
setReleaseChannel,
} from "./updater.js"
import { type ProviderEntry, parseProviderEntries } from "./watcher.js"

const execFileAsync = promisify(execFile)
Expand Down Expand Up @@ -656,6 +662,20 @@ export function registerIpcHandlers(deps: IpcDependencies): void {
},
)

// ── Release Channel ──
ipcMain.handle("get_release_channel", () => {
return getReleaseChannel()
})

ipcMain.handle(
"set_release_channel",
async (_event, args: { channel: string }) => {
const channel = args.channel as ReleaseChannel
setReleaseChannel(channel)
await checkForUpdatesWithChannel(channel)
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)

// ── Analytics ──
ipcMain.handle(
"analytics_track",
Expand Down
50 changes: 48 additions & 2 deletions desktop/src/main/updater.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,49 @@
import { dialog, type BrowserWindow } from "electron"
import { readFileSync, writeFileSync } from "node:fs"
import { join } from "node:path"
import { app, dialog, type BrowserWindow } from "electron"
import { trackEvent } from "./analytics.js"

export type ReleaseChannel = "stable" | "beta"

function settingsPath(): string {
return join(app.getPath("userData"), "update-settings.json")
}

function loadChannel(): ReleaseChannel {
try {
const data = JSON.parse(readFileSync(settingsPath(), "utf-8"))
if (data.channel === "beta") return "beta"
} catch {
// File doesn't exist or is corrupt
}
return "stable"
}

function saveChannel(channel: ReleaseChannel): void {
writeFileSync(settingsPath(), JSON.stringify({ channel }))
}

let currentChannel: ReleaseChannel = "stable"

export function setReleaseChannel(channel: ReleaseChannel): void {
currentChannel = channel
saveChannel(channel)
}

export function getReleaseChannel(): ReleaseChannel {
return currentChannel
}

export async function initAutoUpdater(
getMainWindow: () => BrowserWindow | null,
): Promise<void> {
currentChannel = loadChannel()
const { autoUpdater } = await import("electron-updater")

autoUpdater.autoDownload = true
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.allowPrerelease = currentChannel === "beta"
autoUpdater.channel = currentChannel === "beta" ? "beta" : "latest"

autoUpdater.on("checking-for-update", () => {
trackEvent("update_check")
Expand Down Expand Up @@ -45,10 +81,20 @@ export async function initAutoUpdater(
console.error("Auto-update error:", err.message)
})

// Check for updates after a short delay to avoid slowing down app launch
setTimeout(() => {
autoUpdater.checkForUpdates().catch((err: Error) => {
console.error("Update check failed:", err.message)
})
}, 10_000)
}

export async function checkForUpdatesWithChannel(
channel: ReleaseChannel,
): Promise<void> {
const { autoUpdater } = await import("electron-updater")
currentChannel = channel
saveChannel(channel)
autoUpdater.allowPrerelease = channel === "beta"
autoUpdater.channel = channel === "beta" ? "beta" : "latest"
await autoUpdater.checkForUpdates()
}
11 changes: 11 additions & 0 deletions desktop/src/renderer/src/lib/ipc/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,17 @@ export async function sshKeyGenerate(params: {
return invoke<SshKeyInfo>("ssh_key_generate", params)
}

// Release channel
export type ReleaseChannel = "stable" | "beta"

export async function getReleaseChannel(): Promise<ReleaseChannel> {
return invoke<ReleaseChannel>("get_release_channel")
}

export async function setReleaseChannel(channel: ReleaseChannel): Promise<void> {
return invoke("set_release_channel", { channel })
}

// Analytics
export function analyticsTrack(
name: string,
Expand Down
Loading
Loading