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
7 changes: 6 additions & 1 deletion .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jobs:
go-version: '1.25.x'

- name: Run Dagger Integration Pipeline
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
go run ci/main.go --os ${{ matrix.os }}

Expand All @@ -53,8 +55,11 @@ jobs:
mkdir -p ~/.pyenv

- name: Run bootstrap tool
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo y | ./bootstrap_environment --only custom --no-vm --no-ai
echo y | ./bootstrap_environment --gui

conclusion:
name: Final Status Check
Expand Down
75 changes: 69 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -41,21 +69,56 @@ 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

- name: Create release
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
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Unit Tests

on:
pull_request:
branches:
- main
- dev

jobs:
unit-test:
name: Unit Tests
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.25.x'

- name: Run Unit Tests
run: go test -v ./...

- name: Vet
run: go vet ./...
20 changes: 15 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ GOFLAGS := -trimpath -ldflags="-s -w"
TARGETS := \
linux/amd64 \
linux/arm64 \
linux/arm/6 \
darwin/amd64 \
darwin/arm64

Expand All @@ -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

Expand Down
Binary file added ci/ci
Binary file not shown.
12 changes: 10 additions & 2 deletions ci/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,18 @@ func main() {
WithFile("/usr/local/bin/bootstrap_environment", binaryFile).
WithWorkdir("/tmp")

// 4. Run bootstrap binary with safe args: --only custom --no-vm --no-ai
// Forward GitHub token so API calls are authenticated (avoids 403 rate-limits)
if tok := os.Getenv("GITHUB_TOKEN"); tok != "" {
secret := client.SetSecret("github-token", tok)
testContainer = testContainer.
WithSecretVariable("GITHUB_TOKEN", secret).
WithSecretVariable("GH_TOKEN", secret)
}

// 4. Run bootstrap binary against the full package set (no scope flags).
// We pipe 'y' to satisfy the "Proceed? [y/N]" prompt.
fmt.Printf("[%s] Executing bootstrap_environment...\n", target)
testContainer = testContainer.WithExec([]string{"sh", "-c", "echo y | bootstrap_environment --only custom --no-vm --no-ai"})
testContainer = testContainer.WithExec([]string{"sh", "-c", "echo y | bootstrap_environment --gui"})

// 5. Verify all installed custom packages return a path and zero exit code from version command
fmt.Printf("[%s] Verifying package installations on PATH and running version checks...\n", target)
Expand Down
12 changes: 11 additions & 1 deletion custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions custom_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,4 @@ func TestResolveLatestEdgeCases(t *testing.T) {
resolveLatest(pkgSame)
}


1 change: 1 addition & 0 deletions helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ func resetMocks() {
issuesMu.Lock()
issues = nil
notices = nil
errorCount = 0
issuesMu.Unlock()
}
22 changes: 18 additions & 4 deletions issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
Expand All @@ -13,21 +14,34 @@ import (
// when there's something to report.

var (
issuesMu sync.Mutex
issues []string
notices []string
issuesMu sync.Mutex
issues []string
notices []string
errorCount int
// issueLogWriter is the destination for human-facing issue log lines.
// Overridden during tests to suppress intentional error-path output.
issueLogWriter io.Writer = os.Stdout
)

func logIssue(level, msg string) {
issuesMu.Lock()
defer issuesMu.Unlock()
fmt.Printf(" [%s] %s\n", level, msg)
fmt.Fprintf(issueLogWriter, " [%s] %s\n", level, msg)
issues = append(issues, fmt.Sprintf("[%s] %s", level, msg))
if level == "ERROR" {
errorCount++
}
}

func warn(msg string) { logIssue("WARN", msg) }
func errLog(msg string) { logIssue("ERROR", msg) }

func hasErrors() bool {
issuesMu.Lock()
defer issuesMu.Unlock()
return errorCount > 0
}

func notice(msg string) {
issuesMu.Lock()
defer issuesMu.Unlock()
Expand Down
43 changes: 32 additions & 11 deletions issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,29 @@ func TestIssuesLogging(t *testing.T) {
defer resetMocks()
resetMocks()

// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Capture issue log output via the injectable writer.
var buf bytes.Buffer
oldWriter := issueLogWriter
issueLogWriter = &buf
defer func() { issueLogWriter = oldWriter }()

warn("something is deprecated")
errLog("something failed")
notice("please restart shell")

w.Close()
os.Stdout = oldStdout

var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()

if !strings.Contains(output, "[WARN] something is deprecated") {
t.Errorf("stdout missing warning: %q", output)
t.Errorf("output missing warning: %q", output)
}
if !strings.Contains(output, "[ERROR] something failed") {
t.Errorf("stdout missing error: %q", output)
t.Errorf("output missing error: %q", output)
}

issuesMu.Lock()
issueLen := len(issues)
noticeLen := len(notices)
errCount := errorCount
issuesMu.Unlock()

if issueLen != 2 {
Expand All @@ -46,6 +43,12 @@ func TestIssuesLogging(t *testing.T) {
if noticeLen != 1 {
t.Errorf("expected 1 notice, got %d", noticeLen)
}
if errCount != 1 {
t.Errorf("expected 1 error count, got %d", errCount)
}
if !hasErrors() {
t.Error("expected hasErrors() to return true after errLog call")
}
}

func TestWriteRunLog(t *testing.T) {
Expand Down Expand Up @@ -83,6 +86,24 @@ func TestWriteRunLog(t *testing.T) {
}
}

func TestHasErrors(t *testing.T) {
defer resetMocks()

if hasErrors() {
t.Error("expected hasErrors() false with no errors logged")
}

warn("just a warning")
if hasErrors() {
t.Error("expected hasErrors() false after only a warning")
}

errLog("a real error")
if !hasErrors() {
t.Error("expected hasErrors() true after errLog call")
}
}

func TestPrintNotices(t *testing.T) {
defer resetMocks()

Expand Down
Loading
Loading