Fix CI: pnpm PATH, Obsidian arch matcher, Arch pkg names, Fedora lazygit COPR

- 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>
This commit is contained in:
2026-05-26 14:43:44 -05:00
co-authored by Copilot
parent 41a6f74ce1
commit e14347fd31
8 changed files with 142 additions and 15 deletions
+3 -2
View File
@@ -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"),
+9 -2
View File
@@ -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)
}
}
+12 -3
View File
@@ -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")
}
}
+19
View File
@@ -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) {
+13
View File
@@ -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},
}
}
+47 -2
View File
@@ -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))
}
}
+15 -6
View File
@@ -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
}
+24
View File
@@ -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) {