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
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 }}

- 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
65 changes: 43 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,34 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v6
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false

- 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
29 changes: 28 additions & 1 deletion cmd/self_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ import (
"github.com/spf13/cobra"
)

const (
channelStable = "stable"
channelBeta = "beta"
)

// SelfUpdateCmd is a struct that defines a command call for "self-update".
type SelfUpdateCmd struct {
Version string
Channel string
DryRun bool
}

Expand All @@ -20,9 +26,27 @@ func NewSelfUpdateCmd() *cobra.Command {
Use: "self-update",
Short: "Update the Devsy CLI to the newest version",
Args: cobra.NoArgs,
PreRunE: func(_ *cobra.Command, _ []string) error {
switch cmd.Channel {
case channelStable, channelBeta:
return nil
default:
return fmt.Errorf(
"invalid channel %q: must be %q or %q",
cmd.Channel,
channelStable,
channelBeta,
)
}
},
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 == channelBeta,
}
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 +56,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", channelStable,
"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
25 changes: 25 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, channelStable, f.DefValue)
}

func TestNewSelfUpdateCmd_AcceptsNoPositionalArgs(t *testing.T) {
cmd := NewSelfUpdateCmd()
err := cmd.Args(cmd, []string{"unexpected"})
Expand All @@ -34,3 +41,21 @@ func TestNewSelfUpdateCmd_AcceptsNoPositionalArgs(t *testing.T) {
err = cmd.Args(cmd, []string{})
assert.NoError(t, err, "should accept zero arguments")
}

func TestNewSelfUpdateCmd_RejectsInvalidChannel(t *testing.T) {
cmd := NewSelfUpdateCmd()
cmd.SetArgs([]string{"--channel", "nightly"})
err := cmd.Execute()
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid channel")
}

func TestNewSelfUpdateCmd_AcceptsValidChannels(t *testing.T) {
for _, ch := range []string{channelStable, channelBeta} {
cmd := NewSelfUpdateCmd()
require.NoError(t, cmd.Flags().Set("channel", ch))
preRunE := cmd.PreRunE
require.NotNil(t, preRunE)
assert.NoError(t, preRunE(cmd, nil))
}
}
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
32 changes: 32 additions & 0 deletions desktop/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ 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,
checkForUpdates,
checkForUpdatesWithChannel,
getReleaseChannel,
installUpdate,
setReleaseChannel,
} from "./updater.js"
import { type ProviderEntry, parseProviderEntries } from "./watcher.js"

const execFileAsync = promisify(execFile)
Expand Down Expand Up @@ -656,6 +664,30 @@ 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 }) => {
if (args.channel !== "stable" && args.channel !== "beta") {
throw new Error(`Invalid release channel: ${args.channel}`)
}
setReleaseChannel(args.channel)
await checkForUpdatesWithChannel(args.channel)
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)

ipcMain.handle("check_for_updates", async () => {
await checkForUpdates()
})

ipcMain.handle("install_update", async () => {
installUpdate()
})

// ── Analytics ──
ipcMain.handle(
"analytics_track",
Expand Down
Loading
Loading