From e14347fd3122af66db04692b5016f467413d7d0c Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 26 May 2026 14:43:44 -0500 Subject: [PATCH] Fix CI: pnpm PATH, Obsidian arch matcher, Arch pkg names, Fedora lazygit COPR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - post.go: export PNPM_HOME+PATH before pnpm add -g so global bins install correctly in the non-interactive shell. Applied to installNpmPackage and installPlaywright (incl. browser install). - system.go: treat token-less Obsidian AppImage as x86_64 (upstream publishes "Obsidian-X.Y.Z.AppImage" with no arch suffix). - pkgmgr.go: docker-ce-rootless-extras and vagrant are AUR-only on Arch → skipOverride; pipx → python-pipx on pacman. - repos.go: new setupLazygitCoprRepo enables copr.fedorainfracloud.org/ dejan/lazygit on Fedora; wired into repoGroups so lazygit installs. - Tests: PNPM_HOME export (Linux+macOS), token-less Obsidian asset, pacman pkg overrides, COPR repo setup, updated repoGroups count. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkgmgr.go | 5 +++-- pkgmgr_test.go | 11 +++++++++-- post.go | 15 ++++++++++++--- post_test.go | 19 +++++++++++++++++++ repos.go | 13 +++++++++++++ repos_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- system.go | 21 +++++++++++++++------ system_test.go | 24 ++++++++++++++++++++++++ 8 files changed, 142 insertions(+), 15 deletions(-) diff --git a/pkgmgr.go b/pkgmgr.go index 6a5a579..3b840a3 100644 --- a/pkgmgr.go +++ b/pkgmgr.go @@ -105,7 +105,8 @@ var packageOverrides = map[string]map[string]overrideEntry{ "containerd.io": replace("containerd"), "docker-ce": replace("docker"), "docker-ce-cli": skipOverride(), // covered by docker - "docker-ce-rootless-extras": replace("docker-rootless-extras"), + "docker-ce-rootless-extras": skipOverride(), // AUR-only + "pipx": replace("python-pipx"), "docker-buildx-plugin": replace("docker-buildx"), "docker-compose-plugin": replace("docker-compose"), "dotnet-sdk-10.0": replace("dotnet-sdk"), @@ -121,7 +122,7 @@ var packageOverrides = map[string]map[string]overrideEntry{ "rg": replace("ripgrep"), "shutter": skipOverride(), // AUR-only "temurin-25-jdk": replace("jdk-openjdk"), - "vagrant": replace("vagrant"), + "vagrant": skipOverride(), // AUR-only "vivaldi-stable": replace("vivaldi"), "webcamoid": skipOverride(), // AUR-only; provided via Flatpak when --gui "wireshark": replace("wireshark-qt"), diff --git a/pkgmgr_test.go b/pkgmgr_test.go index 47968f7..aaf8d58 100644 --- a/pkgmgr_test.go +++ b/pkgmgr_test.go @@ -91,8 +91,15 @@ func TestResolveSystemPkgs(t *testing.T) { if len(resolved) != 2 || resolved[0] != "btop" || resolved[1] != "ripgrep" { t.Errorf("expected [btop ripgrep], got %v", resolved) } - if len(skipped) != 1 || skipped[0] != "buildah" { - t.Errorf("expected skipped to be [buildah], got %v", skipped) + // pacman: docker-ce-rootless-extras and vagrant are AUR-only -> skipped; + // pipx is replaced with python-pipx. + pkgMgr = "pacman" + resolved, skipped = resolveSystemPkgs([]string{"docker-ce-rootless-extras", "vagrant", "pipx", "rg"}) + if len(resolved) != 2 || resolved[0] != "python-pipx" || resolved[1] != "ripgrep" { + t.Errorf("expected [python-pipx ripgrep], got %v", resolved) + } + if len(skipped) != 2 || skipped[0] != "docker-ce-rootless-extras" || skipped[1] != "vagrant" { + t.Errorf("expected skipped [docker-ce-rootless-extras vagrant], got %v", skipped) } } diff --git a/post.go b/post.go index c68c73a..b1facea 100644 --- a/post.go +++ b/post.go @@ -478,6 +478,13 @@ func installAgy() { } } +func pnpmEnvPrefix() string { + if isMacOS { + return `export PNPM_HOME="$HOME/Library/pnpm" && export PATH="$PNPM_HOME:$PATH" && ` + } + return `export PNPM_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/pnpm" && export PATH="$PNPM_HOME:$PATH" && ` +} + func installNpmPackage(pkgName string) { home, _ := os.UserHomeDir() if _, err := osStat(filepath.Join(home, ".nvm")); err != nil { @@ -486,7 +493,8 @@ func installNpmPackage(pkgName string) { } ensureNodeLTS() fmt.Printf(" Installing %s via pnpm ...\n", pkgName) - if !runShell(fmt.Sprintf(`bash -c "source ~/.nvm/nvm.sh && pnpm add -g %s"`, pkgName), CmdOpts{}).OK() { + cmd := fmt.Sprintf(`bash -c "%ssource ~/.nvm/nvm.sh && pnpm add -g %s"`, pnpmEnvPrefix(), pkgName) + if !runShell(cmd, CmdOpts{}).OK() { errLog(fmt.Sprintf("%s installation failed", pkgName)) } } @@ -499,7 +507,8 @@ func installPlaywright() { } ensureNodeLTS() fmt.Println(" Installing playwright via pnpm ...") - if !runShell(`bash -c "source ~/.nvm/nvm.sh && source ~/.bashrc 2>/dev/null; pnpm add -g playwright"`, CmdOpts{}).OK() { + addCmd := fmt.Sprintf(`bash -c "%ssource ~/.nvm/nvm.sh && pnpm add -g playwright"`, pnpmEnvPrefix()) + if !runShell(addCmd, CmdOpts{}).OK() { errLog("playwright installation failed") return } @@ -510,7 +519,7 @@ func installPlaywright() { } else { fmt.Println(" Installing Playwright browsers ...") } - if !runShell(fmt.Sprintf(`bash -c "source ~/.nvm/nvm.sh && source ~/.bashrc 2>/dev/null; %s"`, installCmd), CmdOpts{}).OK() { + if !runShell(fmt.Sprintf(`bash -c "%ssource ~/.nvm/nvm.sh && %s"`, pnpmEnvPrefix(), installCmd), CmdOpts{}).OK() { errLog("playwright browser installation failed") } } diff --git a/post_test.go b/post_test.go index 7ad2e32..e5279dc 100644 --- a/post_test.go +++ b/post_test.go @@ -361,6 +361,25 @@ func TestInstallPipAgyNpm(t *testing.T) { if len(runShellCmds) < 1 || !strings.Contains(runShellCmds[len(runShellCmds)-1], "pnpm add -g my-package") { t.Errorf("unexpected npm install command: %v", runShellCmds) } + if !strings.Contains(runShellCmds[len(runShellCmds)-1], `PNPM_HOME=`) { + t.Errorf("expected PNPM_HOME export, got: %v", runShellCmds) + } + + // macOS uses ~/Library/pnpm + resetMocks() + isMacOS = true + runShellCmds = nil + runShell = func(cmd string, opts CmdOpts) CmdResult { + runShellCmds = append(runShellCmds, cmd) + return CmdResult{ExitCode: 0} + } + osStat = func(name string) (os.FileInfo, error) { + return nil, nil + } + installNpmPackage("my-package") + if len(runShellCmds) < 1 || !strings.Contains(runShellCmds[len(runShellCmds)-1], `Library/pnpm`) { + t.Errorf("expected macOS PNPM_HOME path, got: %v", runShellCmds) + } } func TestEnsurePythonAndNode(t *testing.T) { diff --git a/repos.go b/repos.go index 1f8265a..ad3fb9d 100644 --- a/repos.go +++ b/repos.go @@ -208,6 +208,18 @@ func setupDotnetRepo() { ) } +func setupLazygitCoprRepo() { + if pkgMgr != "dnf" { + return + } + if repoFileExists("/etc/yum.repos.d/_copr:copr.fedorainfracloud.org:dejan:lazygit.repo") { + return + } + // dnf-plugins-core provides the `copr` subcommand on dnf4; on dnf5 it's a no-op. + runCmd([]string{"dnf", "install", "-y", "dnf-plugins-core"}, CmdOpts{AsSudo: true}) + runCmd([]string{"dnf", "copr", "enable", "-y", "dejan/lazygit"}, CmdOpts{AsSudo: true}) +} + type repoGroup struct { members map[string]bool setup func() @@ -229,5 +241,6 @@ func repoGroups() []repoGroup { {mk("vivaldi-stable"), setupVivaldiRepo}, {mk("temurin-25-jdk"), setupTemurinRepo}, {mk("dotnet-sdk-10.0"), setupDotnetRepo}, + {mk("lazygit"), setupLazygitCoprRepo}, } } diff --git a/repos_test.go b/repos_test.go index 012abfa..d3f0eff 100644 --- a/repos_test.go +++ b/repos_test.go @@ -193,10 +193,55 @@ func TestSetupDotnetRepo(t *testing.T) { } } +func TestSetupLazygitCoprRepo(t *testing.T) { + defer resetMocks() + + // On non-dnf hosts, it's a no-op. + pkgMgr = "apt-get" + called := false + runCmd = func(argv []string, opts CmdOpts) CmdResult { + called = true + return CmdResult{ExitCode: 0} + } + setupLazygitCoprRepo() + if called { + t.Errorf("expected no-op on apt-get, but runCmd was called") + } + + // On dnf with repo absent, copr enable is invoked. + pkgMgr = "dnf" + osStat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + var calls [][]string + runCmd = func(argv []string, opts CmdOpts) CmdResult { + calls = append(calls, argv) + return CmdResult{ExitCode: 0} + } + setupLazygitCoprRepo() + joined := "" + for _, c := range calls { + joined += strings.Join(c, " ") + "\n" + } + if !strings.Contains(joined, "copr enable -y dejan/lazygit") { + t.Errorf("expected copr enable call, got: %s", joined) + } + + // If the COPR repo file already exists, no commands are run. + osStat = func(name string) (os.FileInfo, error) { + return nil, nil + } + calls = nil + setupLazygitCoprRepo() + if len(calls) != 0 { + t.Errorf("expected no commands when repo file exists, got %v", calls) + } +} + func TestRepoGroups(t *testing.T) { groups := repoGroups() - if len(groups) != 6 { - t.Errorf("expected 6 repo groups, got %d", len(groups)) + if len(groups) != 7 { + t.Errorf("expected 7 repo groups, got %d", len(groups)) } } diff --git a/system.go b/system.go index 02dc52b..57443ee 100644 --- a/system.go +++ b/system.go @@ -170,18 +170,27 @@ func installObsidian(tmp string) { hostTokens := archTokens[archName] otherTokens := archTokens[otherArch()] + hasAnyToken := func(n string, tokens []string) bool { + for _, t := range tokens { + if strings.Contains(n, t) { + return true + } + } + return false + } + matches := func(name string) bool { n := strings.ToLower(name) if !strings.HasSuffix(n, ".appimage") { return false } - matched := false - for _, t := range hostTokens { - if strings.Contains(n, t) { - matched = true - break - } + // Obsidian publishes the x86_64 AppImage without an arch suffix + // (e.g. "Obsidian-1.12.7.AppImage") and the arm64 build as + // "Obsidian-1.12.7-arm64.AppImage". Treat a token-less AppImage as x86_64. + if !hasAnyToken(n, hostTokens) && !hasAnyToken(n, otherTokens) { + return archName == "x86_64" } + matched := hasAnyToken(n, hostTokens) if !matched { return false } diff --git a/system_test.go b/system_test.go index bf9a502..7bc76c1 100644 --- a/system_test.go +++ b/system_test.go @@ -191,6 +191,30 @@ func TestInstallObsidian(t *testing.T) { if len(runCmdCalls) != 2 || runCmdCalls[0][0] != "cp" || runCmdCalls[1][0] != "chmod" { t.Errorf("expected cp and chmod calls, got %v", runCmdCalls) } + + // Obsidian publishes the x86_64 AppImage without an arch token + // (e.g. "Obsidian-1.12.7.AppImage"). It should still match on x86_64. + fetchJSON = func(url string, v any) bool { + rel := v.(*ghRelease) + rel.Assets = []ghAsset{ + {Name: "Obsidian-1.12.7-arm64.AppImage", BrowserDownloadURL: "http://obs-arm64"}, + {Name: "Obsidian-1.12.7.AppImage", BrowserDownloadURL: "http://obs-default"}, + } + return true + } + downloadedURL = "" + archName = "x86_64" + installObsidian("/tmp") + if downloadedURL != "http://obs-default" { + t.Errorf("expected token-less AppImage to be selected for x86_64, got %q", downloadedURL) + } + + downloadedURL = "" + archName = "aarch64" + installObsidian("/tmp") + if downloadedURL != "http://obs-arm64" { + t.Errorf("expected arm64 AppImage for aarch64, got %q", downloadedURL) + } } func TestInstallMinikube(t *testing.T) {