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
54 changes: 45 additions & 9 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,10 @@ jobs:
runs-on: ubuntu-latest
environment: release
needs: [darwin-build, windows-app, docker-build-push]
concurrency:
group: release-publish
queue: max
cancel-in-progress: false
permissions:
contents: write
env:
Expand Down Expand Up @@ -784,28 +788,59 @@ jobs:
- name: Generate checksum file
run: find . -type f -not -name 'sha256sum.txt' | xargs sha256sum | tee sha256sum.txt
working-directory: dist
- name: Create or update Release for tag
- name: Publish release artifacts
shell: bash
run: |
RELEASE_VERSION="$(echo ${GITHUB_REF_NAME} | cut -f1 -d-)"
retry() {
local attempt
for attempt in 1 2 3; do
if "$@"; then
return 0
fi
if [ "$attempt" -lt 3 ]; then
echo "::warning::Attempt $attempt failed; retrying in $((attempt * 15))s: $*"
sleep "$((attempt * 15))"
fi
done
echo "::error::Failed after 3 attempts: $*"
return 1
}

delete_assets() {
local assets asset
# Re-list on each attempt: a failed DELETE may still have removed the asset.
assets=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/releases/$1/assets" --jq '.[].url') || return 1
while IFS= read -r asset; do
[ -z "$asset" ] && continue
gh api --method DELETE "$asset" || return 1
done <<< "$assets"
}

RELEASE_VERSION="${GITHUB_REF_NAME%%-*}"
echo "Looking for existing release for ${RELEASE_VERSION}"
OLD_TAG=$(gh release ls --json name,tagName | jq -r ".[] | select(.name == \"${RELEASE_VERSION}\") | .tagName")
OLD_TAG=$(gh release ls --json name,tagName | jq -r --arg version "$RELEASE_VERSION" '.[] | select(.name == $version) | .tagName')
if [ -n "$OLD_TAG" ]; then
RELEASE_ID=$(gh release view "$OLD_TAG" --json databaseId --jq .databaseId)
echo "Deleting old assets from ${OLD_TAG}"
retry delete_assets "$RELEASE_ID"
echo "Updating release ${RELEASE_VERSION} to point to new tag ${GITHUB_REF_NAME}"
gh release edit ${OLD_TAG} --tag ${GITHUB_REF_NAME}
gh release edit "$OLD_TAG" --tag "$GITHUB_REF_NAME"
else
echo "Creating new release ${RELEASE_VERSION} pointing to tag ${GITHUB_REF_NAME}"
gh release create ${GITHUB_REF_NAME} \
--title ${RELEASE_VERSION} \
gh release create "$GITHUB_REF_NAME" \
--title "$RELEASE_VERSION" \
--draft \
--generate-notes \
--prerelease
fi
- name: Upload release artifacts
run: |

shopt -s nullglob
pids=()
for payload in dist/*.txt dist/*.zip dist/*.tgz dist/*.tar.zst dist/*.exe dist/*.dmg dist/*.ps1 dist/*.sh ; do
# Publish the checksum manifest only after every payload has succeeded.
[ "$payload" = dist/sha256sum.txt ] && continue
echo "Uploading $payload"
gh release upload ${GITHUB_REF_NAME} $payload --clobber &
retry gh release upload "$GITHUB_REF_NAME" "$payload" --clobber &
pids+=($!)
sleep 1
done
Expand All @@ -821,4 +856,5 @@ jobs:
echo "One or more uploads failed"
exit 1
fi
retry gh release upload "$GITHUB_REF_NAME" dist/sha256sum.txt --clobber
echo "done"
7 changes: 6 additions & 1 deletion app/store/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -1268,14 +1268,19 @@ func (db *database) setSettings(s Settings) error {

_, err := db.conn.Exec(`
UPDATE settings
SET expose = ?, survey = ?, browser = ?, models = ?, agent = ?, tools = ?, working_dir = ?, context_length = ?, turbo_enabled = ?, websearch_enabled = ?, selected_model = ?, sidebar_open = ?, last_home_view = ?, onboarding_version = ?, think_enabled = ?, think_level = ?, auto_update_enabled = ?, claude_desktop_used = ?
SET expose = ?, survey = ?, browser = ?, models = ?, agent = ?, tools = ?, working_dir = ?, context_length = ?, turbo_enabled = ?, websearch_enabled = ?, selected_model = ?, sidebar_open = ?, last_home_view = ?, onboarding_version = MAX(onboarding_version, ?), think_enabled = ?, think_level = ?, auto_update_enabled = ?, claude_desktop_used = ?
`, s.Expose, s.Survey, s.Browser, s.Models, s.Agent, s.Tools, s.WorkingDir, s.ContextLength, s.TurboEnabled, s.WebSearchEnabled, s.SelectedModel, s.SidebarOpen, lastHomeView, s.OnboardingVersion, s.ThinkEnabled, s.ThinkLevel, s.AutoUpdateEnabled, s.ClaudeDesktopUsed)
if err != nil {
return fmt.Errorf("set settings: %w", err)
}
return nil
}

func (db *database) markOnboardingCompleted() error {
_, err := db.conn.Exec("UPDATE settings SET onboarding_version = MAX(onboarding_version, ?)", CurrentOnboardingVersion)
return err
}

func (db *database) markCodexDesktopUsed() error {
_, err := db.conn.Exec(`UPDATE settings SET codex_desktop_used = 1`)
return err
Expand Down
60 changes: 47 additions & 13 deletions app/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/google/uuid"
"github.com/ollama/ollama/app/types/not"
"github.com/ollama/ollama/internal/onboarding"
)

type File struct {
Expand Down Expand Up @@ -185,27 +186,19 @@ type Settings struct {
}

// Keep in sync with CURRENT_ONBOARDING_VERSION in app/ui/app/src/lib/onboarding.ts.
const CurrentOnboardingVersion = 1
const CurrentOnboardingVersion = onboarding.CurrentVersion

type Store struct {
// DBPath allows overriding the default database path (mainly for testing)
// DBPath overrides the database path. Custom stores keep their shared
// onboarding record alongside the database, isolated from the user's state.
DBPath string

// dbMu protects database initialization only
dbMu sync.Mutex
db *database
}

var defaultDBPath = func() string {
switch runtime.GOOS {
case "windows":
return filepath.Join(os.Getenv("LOCALAPPDATA"), "Ollama", "db.sqlite")
case "darwin":
return filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Ollama", "db.sqlite")
default:
return filepath.Join(os.Getenv("HOME"), ".ollama", "db.sqlite")
}
}()
var defaultDBPath = onboarding.AppDatabasePath()

// legacyConfigPath is the path to the old config.json file
var legacyConfigPath = func() string {
Expand Down Expand Up @@ -401,6 +394,9 @@ func (s *Store) Settings() (Settings, error) {
if err != nil {
return Settings{}, err
}
if err := s.syncOnboarding(&settings); err != nil {
return Settings{}, fmt.Errorf("load shared onboarding state: %w", err)
}

// Set default models directory if not set
if settings.Models == "" {
Expand All @@ -427,7 +423,45 @@ func (s *Store) SetSettings(settings Settings) error {
return err
}

return s.db.setSettings(settings)
if err := s.db.setSettings(settings); err != nil {
return err
}
if settings.OnboardingVersion >= CurrentOnboardingVersion {
return s.syncOnboarding(&settings)
}
return nil
}

func (s *Store) onboardingState() onboarding.State {
if s.DBPath != "" {
return onboarding.State{Dir: filepath.Dir(s.DBPath)}
}
return onboarding.State{}
}

func (s *Store) syncOnboarding(settings *Settings) error {
state := s.onboardingState()
if settings.OnboardingVersion >= CurrentOnboardingVersion {
// Migrate existing app completion so the CLI recognizes it too.
// SQLite is already saved; publishing the shared record is best-effort.
if err := state.Complete(); err != nil {
slog.Warn("could not share onboarding completion", "error", err)
}
return nil
}
completed, err := state.Completed()
if err != nil {
slog.Warn("could not read shared onboarding completion", "error", err)
return nil
}
if !completed {
return nil
}
if err := s.db.markOnboardingCompleted(); err != nil {
return err
}
settings.OnboardingVersion = CurrentOnboardingVersion
return nil
}

func (s *Store) MarkCodexDesktopUsed() error {
Expand Down
126 changes: 126 additions & 0 deletions app/store/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
package store

import (
"fmt"
"os"
"path/filepath"
"testing"

"github.com/ollama/ollama/cmd/config"
"github.com/ollama/ollama/internal/onboarding"
)

func TestStore(t *testing.T) {
Expand Down Expand Up @@ -253,6 +258,127 @@ func TestOnboardingVersionRoundTrip(t *testing.T) {
}
}

func setupPairedOnboarding(t *testing.T) *Store {
t.Helper()
s, cleanup := setupTestStore(t)
t.Cleanup(cleanup)
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("LOCALAPPDATA", filepath.Join(home, "AppData", "Local"))
previous := defaultDBPath
defaultDBPath = onboarding.AppDatabasePath()
t.Cleanup(func() { defaultDBPath = previous })
s.DBPath = ""
return s
}

func TestSettingsSurviveInvalidOnboardingMarker(t *testing.T) {
s, cleanup := setupTestStore(t)
t.Cleanup(cleanup)
marker := filepath.Join(filepath.Dir(s.DBPath), "onboarding-v1.completed")
if err := os.MkdirAll(marker, 0o755); err != nil {
t.Fatal(err)
}
if err := s.SetSettings(Settings{SelectedModel: "saved-model"}); err != nil {
t.Fatal(err)
}
settings, err := s.Settings()
if err != nil || settings.SelectedModel != "saved-model" || settings.OnboardingVersion != 0 {
t.Fatalf("invalid marker must preserve readable database settings: %+v, %v", settings, err)
}
}

func TestAppCompletionReachesCLI(t *testing.T) {
for _, source := range []string{"app", "existing database"} {
t.Run(source, func(t *testing.T) {
s := setupPairedOnboarding(t)
if err := s.ensureDB(); err != nil {
t.Fatal(err)
}
if needed, err := config.NeedsWelcome(); err != nil || !needed {
t.Fatalf("unfinished app skipped CLI onboarding: %v, %v", needed, err)
}
settings := Settings{OnboardingVersion: CurrentOnboardingVersion}
save := s.SetSettings
if source == "existing database" {
save = s.db.setSettings // Older app completed without publishing a marker.
}
if err := save(settings); err != nil {
t.Fatal(err)
}
if err := s.Close(); err != nil {
t.Fatal(err)
}
if needed, err := config.NeedsWelcome(); err != nil || needed {
t.Fatalf("app completion did not reach CLI: %v, %v", needed, err)
}
})
}
}

func TestLegacyAppCompletionReachesCLI(t *testing.T) {
for _, schema := range []int{1, 16, 17, 0} {
t.Run(fmt.Sprint(schema), func(t *testing.T) {
s := setupPairedOnboarding(t)
if err := s.ensureDB(); err != nil {
t.Fatal(err)
}
if _, err := s.db.conn.Exec("ALTER TABLE settings DROP COLUMN onboarding_version"); err != nil {
t.Fatal(err)
}
if _, err := s.db.conn.Exec("UPDATE settings SET schema_version = ?", schema); err != nil {
t.Fatal(err)
}
wantWelcome := schema < 1 || schema > 16
if needed, err := config.NeedsWelcome(); err != nil || needed != wantWelcome {
t.Fatalf("welcome needed=%v, err=%v; want %v", needed, err, wantWelcome)
}
var unchanged int
if err := s.db.conn.QueryRow("SELECT schema_version FROM settings WHERE id = 1").Scan(&unchanged); err != nil || unchanged != schema {
t.Fatalf("CLI changed the app schema: %d, %v", unchanged, err)
}
var columns int
if err := s.db.conn.QueryRow("SELECT count(*) FROM pragma_table_info('settings') WHERE name = 'onboarding_version'").Scan(&columns); err != nil || columns != 0 {
t.Fatalf("CLI migrated the app database: columns=%d, err=%v", columns, err)
}
})
}
}

func TestCLICompletionReachesApp(t *testing.T) {
for _, installed := range []bool{false, true} {
s := setupPairedOnboarding(t)
stale := Settings{SelectedModel: "saved-model"}
if installed {
if err := s.SetSettings(stale); err != nil {
t.Fatal(err)
}
}
if err := config.CompleteWelcome(); err != nil {
t.Fatal(err)
}
if !installed {
if _, err := os.Stat(defaultDBPath); !os.IsNotExist(err) {
t.Fatal("CLI completion must not create the app database")
}
}
settings, err := s.Settings()
if err != nil || settings.OnboardingVersion != CurrentOnboardingVersion {
t.Fatalf("app did not import CLI completion: %+v, %v", settings, err)
}
if installed && settings.SelectedModel != stale.SelectedModel {
t.Fatal("completion changed app settings")
}
if err := s.SetSettings(stale); err != nil {
t.Fatal(err)
}
if saved, err := s.Settings(); err != nil || saved.OnboardingVersion != CurrentOnboardingVersion {
t.Fatalf("stale settings reset completion: %+v, %v", saved, err)
}
}
}

func TestClaudeDesktopUsedRoundTrip(t *testing.T) {
s, cleanup := setupTestStore(t)
defer cleanup()
Expand Down
Loading
Loading