added repo bootstrap tool
This commit is contained in:
@@ -15,8 +15,11 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
resolve:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
ref: ${{ steps.ref.outputs.ref }}
|
||||||
|
tag: ${{ steps.ref.outputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Resolve ref
|
- name: Resolve ref
|
||||||
id: ref
|
id: ref
|
||||||
@@ -29,10 +32,35 @@ jobs:
|
|||||||
echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
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
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ steps.ref.outputs.ref }}
|
ref: ${{ needs.resolve.outputs.ref }}
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
@@ -41,12 +69,47 @@ jobs:
|
|||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Build all targets
|
- name: Build
|
||||||
run: make build-all
|
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
|
- name: Generate SHA256SUMS
|
||||||
working-directory: dist
|
working-directory: dist
|
||||||
run: |
|
run: |
|
||||||
|
ls -l
|
||||||
sha256sum bootstrap_environment-* > SHA256SUMS
|
sha256sum bootstrap_environment-* > SHA256SUMS
|
||||||
cat SHA256SUMS
|
cat SHA256SUMS
|
||||||
|
|
||||||
@@ -54,8 +117,8 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tag_name: ${{ steps.ref.outputs.tag }}
|
tag_name: ${{ needs.resolve.outputs.tag }}
|
||||||
name: ${{ steps.ref.outputs.tag }}
|
name: ${{ needs.resolve.outputs.tag }}
|
||||||
target_commitish: ${{ github.sha }}
|
target_commitish: ${{ github.sha }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ GOFLAGS := -trimpath -ldflags="-s -w"
|
|||||||
TARGETS := \
|
TARGETS := \
|
||||||
linux/amd64 \
|
linux/amd64 \
|
||||||
linux/arm64 \
|
linux/arm64 \
|
||||||
|
linux/arm/6 \
|
||||||
darwin/amd64 \
|
darwin/amd64 \
|
||||||
darwin/arm64
|
darwin/arm64
|
||||||
|
|
||||||
@@ -18,13 +19,22 @@ all: build
|
|||||||
build:
|
build:
|
||||||
go build $(GOFLAGS) -o $(BINARY) $(PKG)
|
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)
|
build-all: $(DIST)
|
||||||
@for t in $(TARGETS); do \
|
@for t in $(TARGETS); do \
|
||||||
os=$${t%/*}; arch=$${t#*/}; \
|
os=$$(echo $$t | cut -d/ -f1); \
|
||||||
out=$(DIST)/$(BINARY)-$$os-$$arch; \
|
arch=$$(echo $$t | cut -d/ -f2); \
|
||||||
echo "==> $$os/$$arch -> $$out"; \
|
goarm=$$(echo $$t | cut -s -d/ -f3); \
|
||||||
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch \
|
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; \
|
go build $(GOFLAGS) -o $$out $(PKG) || exit 1; \
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ var defaultInstallPaths = map[string]string{
|
|||||||
"pyenv": "~/.pyenv",
|
"pyenv": "~/.pyenv",
|
||||||
"neovim": "/usr/local/bin/nvim",
|
"neovim": "/usr/local/bin/nvim",
|
||||||
"oh-my-zsh": "~/.oh-my-zsh",
|
"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 {
|
func expandHome(p string) string {
|
||||||
@@ -122,6 +123,9 @@ func isCustomPkgInstalled(pkg *CustomPackage) (bool, string) {
|
|||||||
if name == "copilot" {
|
if name == "copilot" {
|
||||||
return npmInstalled("copilot")
|
return npmInstalled("copilot")
|
||||||
}
|
}
|
||||||
|
if name == "playwright" {
|
||||||
|
return npmInstalled("playwright")
|
||||||
|
}
|
||||||
raw := pkg.InstallPath
|
raw := pkg.InstallPath
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
raw = defaultInstallPath(pkg)
|
raw = defaultInstallPath(pkg)
|
||||||
@@ -519,6 +523,12 @@ func installCustomPackages(toInstall []*CustomPackage) {
|
|||||||
case "copilot":
|
case "copilot":
|
||||||
installNpmPackage("@github/copilot")
|
installNpmPackage("@github/copilot")
|
||||||
continue
|
continue
|
||||||
|
case "playwright":
|
||||||
|
installPlaywright()
|
||||||
|
continue
|
||||||
|
case "gh-repo-bootstrap":
|
||||||
|
installGHExtension("JMR-dev/gh-repo-bootstrap")
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveLatest(pkg)
|
resolveLatest(pkg)
|
||||||
|
|||||||
@@ -398,3 +398,4 @@ func TestResolveLatestEdgeCases(t *testing.T) {
|
|||||||
resolveLatest(pkgSame)
|
resolveLatest(pkgSame)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -140,5 +140,7 @@ func customPackages() []CustomPackage {
|
|||||||
{Name: "claude"},
|
{Name: "claude"},
|
||||||
{Name: "codex"},
|
{Name: "codex"},
|
||||||
{Name: "copilot"},
|
{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
|
return CmdResult{ExitCode: 0, Stdout: []byte("")}, true
|
||||||
}
|
}
|
||||||
|
runCmd = func(argv []string, opts CmdOpts) CmdResult {
|
||||||
|
return CmdResult{ExitCode: 0}
|
||||||
|
}
|
||||||
passwdPath = filepath.Join(t.TempDir(), "passwd")
|
passwdPath = filepath.Join(t.TempDir(), "passwd")
|
||||||
ensureZshDefault()
|
ensureZshDefault()
|
||||||
|
|
||||||
@@ -590,6 +593,39 @@ func (e *errReader) Read(p []byte) (n int, err error) {
|
|||||||
return 0, fmt.Errorf("simulated read 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) {
|
func TestInstallNpmPackageEdgeCases(t *testing.T) {
|
||||||
defer resetMocks()
|
defer resetMocks()
|
||||||
|
|
||||||
@@ -599,6 +635,64 @@ func TestInstallNpmPackageEdgeCases(t *testing.T) {
|
|||||||
installNpmPackage("test-pkg")
|
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) {
|
func TestEnsureNodeLTSEdgeCases(t *testing.T) {
|
||||||
defer resetMocks()
|
defer resetMocks()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user