added repo bootstrap tool
This commit is contained in:
@@ -15,8 +15,11 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
resolve:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ref: ${{ steps.ref.outputs.ref }}
|
||||
tag: ${{ steps.ref.outputs.tag }}
|
||||
steps:
|
||||
- name: Resolve ref
|
||||
id: ref
|
||||
@@ -29,10 +32,35 @@ jobs:
|
||||
echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.suffix }}
|
||||
needs: resolve
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
suffix: linux-amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
suffix: linux-arm64
|
||||
- goos: linux
|
||||
goarch: arm
|
||||
goarm: '6'
|
||||
suffix: linux-armv6
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
suffix: darwin-amd64
|
||||
- goos: darwin
|
||||
goarch: arm64
|
||||
suffix: darwin-arm64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.ref.outputs.ref }}
|
||||
ref: ${{ needs.resolve.outputs.ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Go
|
||||
@@ -41,12 +69,47 @@ jobs:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
- name: Build all targets
|
||||
run: make build-all
|
||||
- name: Build
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
CGO_ENABLED: '0'
|
||||
run: |
|
||||
mkdir -p dist
|
||||
out="dist/bootstrap_environment-${{ matrix.suffix }}"
|
||||
echo "==> Building $out (GOOS=$GOOS GOARCH=$GOARCH GOARM=${GOARM:-n/a})"
|
||||
go build -trimpath -ldflags="-s -w" -o "$out" .
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bootstrap_environment-${{ matrix.suffix }}
|
||||
path: dist/bootstrap_environment-${{ matrix.suffix }}
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
release:
|
||||
needs: [resolve, build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.resolve.outputs.ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
pattern: bootstrap_environment-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Generate SHA256SUMS
|
||||
working-directory: dist
|
||||
run: |
|
||||
ls -l
|
||||
sha256sum bootstrap_environment-* > SHA256SUMS
|
||||
cat SHA256SUMS
|
||||
|
||||
@@ -54,8 +117,8 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag_name: ${{ steps.ref.outputs.tag }}
|
||||
name: ${{ steps.ref.outputs.tag }}
|
||||
tag_name: ${{ needs.resolve.outputs.tag }}
|
||||
name: ${{ needs.resolve.outputs.tag }}
|
||||
target_commitish: ${{ github.sha }}
|
||||
generate_release_notes: true
|
||||
fail_on_unmatched_files: true
|
||||
|
||||
@@ -8,6 +8,7 @@ GOFLAGS := -trimpath -ldflags="-s -w"
|
||||
TARGETS := \
|
||||
linux/amd64 \
|
||||
linux/arm64 \
|
||||
linux/arm/6 \
|
||||
darwin/amd64 \
|
||||
darwin/arm64
|
||||
|
||||
@@ -18,13 +19,22 @@ all: build
|
||||
build:
|
||||
go build $(GOFLAGS) -o $(BINARY) $(PKG)
|
||||
|
||||
# Cross-compile native binaries for each supported (OS, arch) pair into dist/.
|
||||
# Cross-compile native binaries for each supported (OS, arch[, GOARM]) triple
|
||||
# into dist/. Targets formatted as "os/arch" produce "$(BINARY)-os-arch";
|
||||
# "os/arm/N" produces "$(BINARY)-os-armvN" with GOARM=N.
|
||||
build-all: $(DIST)
|
||||
@for t in $(TARGETS); do \
|
||||
os=$${t%/*}; arch=$${t#*/}; \
|
||||
out=$(DIST)/$(BINARY)-$$os-$$arch; \
|
||||
echo "==> $$os/$$arch -> $$out"; \
|
||||
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch \
|
||||
os=$$(echo $$t | cut -d/ -f1); \
|
||||
arch=$$(echo $$t | cut -d/ -f2); \
|
||||
goarm=$$(echo $$t | cut -s -d/ -f3); \
|
||||
if [ -n "$$goarm" ]; then \
|
||||
suffix=$$arch"v"$$goarm; \
|
||||
else \
|
||||
suffix=$$arch; \
|
||||
fi; \
|
||||
out=$(DIST)/$(BINARY)-$$os-$$suffix; \
|
||||
echo "==> $$os/$$arch$${goarm:+ GOARM=$$goarm} -> $$out"; \
|
||||
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch GOARM=$$goarm \
|
||||
go build $(GOFLAGS) -o $$out $(PKG) || exit 1; \
|
||||
done
|
||||
|
||||
|
||||
@@ -55,7 +55,8 @@ var defaultInstallPaths = map[string]string{
|
||||
"pyenv": "~/.pyenv",
|
||||
"neovim": "/usr/local/bin/nvim",
|
||||
"oh-my-zsh": "~/.oh-my-zsh",
|
||||
"agy": "~/.local/bin/agy",
|
||||
"agy": "~/.local/bin/agy",
|
||||
"gh-repo-bootstrap": "~/.local/share/gh/extensions/gh-repo-bootstrap",
|
||||
}
|
||||
|
||||
func expandHome(p string) string {
|
||||
@@ -122,6 +123,9 @@ func isCustomPkgInstalled(pkg *CustomPackage) (bool, string) {
|
||||
if name == "copilot" {
|
||||
return npmInstalled("copilot")
|
||||
}
|
||||
if name == "playwright" {
|
||||
return npmInstalled("playwright")
|
||||
}
|
||||
raw := pkg.InstallPath
|
||||
if raw == "" {
|
||||
raw = defaultInstallPath(pkg)
|
||||
@@ -519,6 +523,12 @@ func installCustomPackages(toInstall []*CustomPackage) {
|
||||
case "copilot":
|
||||
installNpmPackage("@github/copilot")
|
||||
continue
|
||||
case "playwright":
|
||||
installPlaywright()
|
||||
continue
|
||||
case "gh-repo-bootstrap":
|
||||
installGHExtension("JMR-dev/gh-repo-bootstrap")
|
||||
continue
|
||||
}
|
||||
|
||||
resolveLatest(pkg)
|
||||
|
||||
@@ -398,3 +398,4 @@ func TestResolveLatestEdgeCases(t *testing.T) {
|
||||
resolveLatest(pkgSame)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -140,5 +140,7 @@ func customPackages() []CustomPackage {
|
||||
{Name: "claude"},
|
||||
{Name: "codex"},
|
||||
{Name: "copilot"},
|
||||
{Name: "playwright"},
|
||||
{Name: "gh-repo-bootstrap"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,3 +481,36 @@ func installNpmPackage(pkgName string) {
|
||||
}
|
||||
}
|
||||
|
||||
func installPlaywright() {
|
||||
home, _ := os.UserHomeDir()
|
||||
if _, err := osStat(filepath.Join(home, ".nvm")); err != nil {
|
||||
errLog("NVM is not installed — cannot install playwright")
|
||||
return
|
||||
}
|
||||
ensureNodeLTS()
|
||||
fmt.Println(" Installing playwright via pnpm ...")
|
||||
if !runShell(`bash -c "source ~/.nvm/nvm.sh && pnpm add -g playwright"`, CmdOpts{}).OK() {
|
||||
errLog("playwright installation failed")
|
||||
return
|
||||
}
|
||||
pnpmBin := filepath.Join(home, ".local/share/pnpm/bin")
|
||||
if !strings.Contains(os.Getenv("PATH"), pnpmBin) {
|
||||
appendProfileLine("pnpm", `export PATH="$HOME/.local/share/pnpm/bin:$PATH"`)
|
||||
}
|
||||
fmt.Println(" Installing Playwright browsers with dependencies ...")
|
||||
if !runShell(fmt.Sprintf(`bash -c "source ~/.nvm/nvm.sh && PATH=%s:$PATH pnpx playwright install --with-deps"`, pnpmBin), CmdOpts{}).OK() {
|
||||
errLog("playwright browser installation failed")
|
||||
}
|
||||
}
|
||||
|
||||
func installGHExtension(repo string) {
|
||||
if !hasCmd("gh") {
|
||||
errLog("gh CLI is not installed — cannot install extension " + repo)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Installing gh extension %s ...\n", repo)
|
||||
if !runCmd([]string{"gh", "extension", "install", repo}, CmdOpts{}).OK() {
|
||||
errLog(fmt.Sprintf("gh extension install %s failed", repo))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -465,6 +465,9 @@ func TestEnsureZshDefaultEdgeCases(t *testing.T) {
|
||||
}
|
||||
return CmdResult{ExitCode: 0, Stdout: []byte("")}, true
|
||||
}
|
||||
runCmd = func(argv []string, opts CmdOpts) CmdResult {
|
||||
return CmdResult{ExitCode: 0}
|
||||
}
|
||||
passwdPath = filepath.Join(t.TempDir(), "passwd")
|
||||
ensureZshDefault()
|
||||
|
||||
@@ -590,6 +593,39 @@ func (e *errReader) Read(p []byte) (n int, err error) {
|
||||
return 0, fmt.Errorf("simulated read error")
|
||||
}
|
||||
|
||||
func TestInstallGHExtension(t *testing.T) {
|
||||
defer resetMocks()
|
||||
|
||||
// gh not installed
|
||||
hasCmd = func(name string) bool { return false }
|
||||
installGHExtension("JMR-dev/gh-repo-bootstrap") // should log error and return
|
||||
|
||||
// gh installed, install succeeds
|
||||
resetMocks()
|
||||
hasCmd = func(name string) bool { return true }
|
||||
var runCmdCalls [][]string
|
||||
runCmd = func(argv []string, opts CmdOpts) CmdResult {
|
||||
runCmdCalls = append(runCmdCalls, argv)
|
||||
return CmdResult{ExitCode: 0}
|
||||
}
|
||||
installGHExtension("JMR-dev/gh-repo-bootstrap")
|
||||
if len(runCmdCalls) != 1 ||
|
||||
runCmdCalls[0][0] != "gh" ||
|
||||
runCmdCalls[0][1] != "extension" ||
|
||||
runCmdCalls[0][2] != "install" ||
|
||||
runCmdCalls[0][3] != "JMR-dev/gh-repo-bootstrap" {
|
||||
t.Errorf("expected gh extension install command, got: %v", runCmdCalls)
|
||||
}
|
||||
|
||||
// gh installed, install fails
|
||||
resetMocks()
|
||||
hasCmd = func(name string) bool { return true }
|
||||
runCmd = func(argv []string, opts CmdOpts) CmdResult {
|
||||
return CmdResult{ExitCode: 1}
|
||||
}
|
||||
installGHExtension("JMR-dev/gh-repo-bootstrap") // should log error
|
||||
}
|
||||
|
||||
func TestInstallNpmPackageEdgeCases(t *testing.T) {
|
||||
defer resetMocks()
|
||||
|
||||
@@ -599,6 +635,64 @@ func TestInstallNpmPackageEdgeCases(t *testing.T) {
|
||||
installNpmPackage("test-pkg")
|
||||
}
|
||||
|
||||
func TestInstallPlaywright(t *testing.T) {
|
||||
defer resetMocks()
|
||||
|
||||
// Edge case: NVM not installed
|
||||
osStat = func(name string) (os.FileInfo, error) {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
installPlaywright() // should log error and return
|
||||
|
||||
// Happy path
|
||||
resetMocks()
|
||||
osStat = func(name string) (os.FileInfo, error) {
|
||||
return nil, nil // nvm exists
|
||||
}
|
||||
var runShellCmds []string
|
||||
runShell = func(cmd string, opts CmdOpts) CmdResult {
|
||||
runShellCmds = append(runShellCmds, cmd)
|
||||
return CmdResult{ExitCode: 0}
|
||||
}
|
||||
installPlaywright()
|
||||
hasPnpmInstall := false
|
||||
hasPlaywrightBrowsers := false
|
||||
for _, cmd := range runShellCmds {
|
||||
if strings.Contains(cmd, "pnpm add -g playwright") {
|
||||
hasPnpmInstall = true
|
||||
}
|
||||
if strings.Contains(cmd, "pnpx playwright install --with-deps") {
|
||||
hasPlaywrightBrowsers = true
|
||||
}
|
||||
}
|
||||
if !hasPnpmInstall {
|
||||
t.Errorf("expected pnpm add -g playwright, got: %v", runShellCmds)
|
||||
}
|
||||
if !hasPlaywrightBrowsers {
|
||||
t.Errorf("expected pnpx playwright install --with-deps, got: %v", runShellCmds)
|
||||
}
|
||||
|
||||
// Failure path: pnpm install fails, browser install should be skipped
|
||||
resetMocks()
|
||||
osStat = func(name string) (os.FileInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
var failShellCmds []string
|
||||
runShell = func(cmd string, opts CmdOpts) CmdResult {
|
||||
failShellCmds = append(failShellCmds, cmd)
|
||||
if strings.Contains(cmd, "pnpm add -g playwright") {
|
||||
return CmdResult{ExitCode: 1}
|
||||
}
|
||||
return CmdResult{ExitCode: 0}
|
||||
}
|
||||
installPlaywright()
|
||||
for _, cmd := range failShellCmds {
|
||||
if strings.Contains(cmd, "pnpx playwright install") {
|
||||
t.Errorf("browser install should be skipped when pnpm install fails, got: %v", failShellCmds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureNodeLTSEdgeCases(t *testing.T) {
|
||||
defer resetMocks()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user