diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..caba9e9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - run: go vet ./... + - run: CGO_ENABLED=0 go test ./... + - name: Test launcher module + run: cd sandbox/launcher && go vet ./... && go test ./... + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - uses: golangci/golangci-lint-action@v8 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..77d21db --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,25 @@ +name: Release + +on: + push: + tags: ["v*"] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + # TODO: add `make embed-sync` step when embed package is implemented + - uses: goreleaser/goreleaser-action@v6 + with: + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..2841fc4 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,6 @@ +version: "2" + +linters: + default: standard + disable: + - errcheck diff --git a/cmd/deploy.go b/cmd/deploy.go index a244a54..b65ba5b 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -176,6 +176,9 @@ func deployRemote(harnessDir string, gw gateway.Gateway, kc, clusterRunner k8s.R return fmt.Errorf("determining home directory: %w", err) } mtlsDir := filepath.Join(home, ".config", "openshell", "gateways", gatewayName, "mtls") + if err := os.MkdirAll(mtlsDir, 0o700); err != nil { + return fmt.Errorf("creating mtls directory: %w", err) + } for _, field := range []string{"ca.crt", "tls.crt", "tls.key"} { data, err := kc.GetSecretField(ctx, "openshell-client-tls", field) if err != nil { diff --git a/cmd/deploy_test.go b/cmd/deploy_test.go index 390b6c9..ad5923e 100644 --- a/cmd/deploy_test.go +++ b/cmd/deploy_test.go @@ -22,6 +22,7 @@ func TestDeployRemote_Success(t *testing.T) { dir := setupDeployHarnessDir(t) t.Setenv("OPENSHELL_CHART_VERSION", "0.0.55") t.Setenv("OPENSHELL_NAMESPACE", "openshell") + t.Setenv("HOME", t.TempDir()) nsRunner := k8s.NewMockRunner() clusterRunner := k8s.NewMockRunner() diff --git a/release_plan.md b/release_plan.md new file mode 100644 index 0000000..b81af21 --- /dev/null +++ b/release_plan.md @@ -0,0 +1,150 @@ +# Release Plan: CI → Embed → GoReleaser + +## Phase 0: CI (done) + +GitHub Actions for every PR and push to main. + +**`.github/workflows/ci.yml`** — two jobs: +- **test**: `go vet` + `go test ./...` for both modules (harness + launcher) +- **lint**: `golangci-lint` via the official action + +**`.github/workflows/release.yml`** — stub, triggered on `v*` tags. Runs GoReleaser once embed is ready (Phase 2). + +Go version synced from `go.mod` via `go-version-file`. + +Integration tests (podman, OCP) are deferred — extensive unit mock coverage exists for all orchestration paths. When needed, ubuntu-latest runners have podman pre-installed; openshell CLI can be installed from GitHub releases. + +--- + +## Phase 1: Embed files + `harness init` + +**Goal:** Single binary that works without cloning the repo. + +The CLI already resolves everything relative to `harnessDir`. The plan: + +1. **Embed** all runtime files into the binary +2. **Add `harness init`** — extracts embedded files to `~/.openshell/harness/` +3. **Update `detectHarnessDir()`** — add `~/.openshell/harness/` as a fallback +4. **No other code changes** — all existing `filepath.Join(harnessDir, ...)` calls work as-is + +### Step 1: Create the embed package + +Create `internal/embed/embed.go`: + +```go +package embed + +import "embed" + +//go:embed all:files +var Files embed.FS +``` + +Create directory `internal/embed/files/` populated by `make embed-sync`. Runtime files to embed: +- `sandbox/profiles/atlassian.yaml` +- `sandbox/CLAUDE.md`, `settings.json`, `mcp.json`, `policy.yaml`, `startup.sh` +- `profiles/default.toml` +- `values-ocp.yaml` + +**Decision: copy, not symlink.** `go:embed` doesn't follow symlinks. `make embed-sync` copies originals into `internal/embed/files/`. GoReleaser hooks call this automatically. + +### Step 2: Add `harness init` command + +Create `cmd/init.go`: +- Walks embedded FS, writes to `~/.openshell/harness/` (or `--dir`) +- Preserves directory structure +- Skips existing files unless `--force` +- Makes `.sh` files executable + +### Step 3: Update `detectHarnessDir()` + +Add `~/.openshell/harness/` as fallback: + +``` +1. $HARNESS_DIR env var +2. Walk up from executable location +3. Walk up from cwd +4. ~/.openshell/harness/ (if profiles/default.toml exists there) +5. Error + exit +``` + +### Step 4: Add Makefile targets + +- `make embed-sync` — copies runtime files into `internal/embed/files/` +- Update `make cli` to depend on `embed-sync` + +--- + +## Phase 2: GoReleaser + +**Goal:** `git tag v*` → GitHub Release with binaries for all platforms. + +### `.goreleaser.yaml` + +```yaml +before: + hooks: + - make embed-sync +builds: + - env: [CGO_ENABLED=0] + goos: [linux, darwin] + goarch: [amd64, arm64] +archives: + - format: tar.gz +``` + +GitHub Releases only (no Homebrew tap — private repo at Red Hat). + +### Install experience + +**One-liner (requires `gh` auth):** +```bash +gh release download --repo robbycochran/harness-openshell -p '*darwin_arm64*' -O- | tar xz +sudo mv harness /usr/local/bin/ +harness init +``` + +**From source (unchanged):** +```bash +git clone ... && cd harness-openshell +make cli +./harness new --local +``` + +--- + +## Phase 3: Integration CI (future) + +When needed, add a GHA workflow for podman-based integration tests: +- Ubuntu runners have podman pre-installed +- Install openshell CLI from GitHub releases in workflow +- Run `test/test-flow.sh podman` (no `--full` initially) +- OCP tests stay manual (requires a live cluster) + +Not urgent — unit tests with mocked k8s.Runner and gateway.Gateway cover all orchestration paths. + +--- + +## Files summary + +| File | Phase | Action | +|------|-------|--------| +| `.github/workflows/ci.yml` | 0 | Create — unit tests + lint | +| `.github/workflows/release.yml` | 0 | Create — stub for GoReleaser | +| `internal/embed/embed.go` | 1 | Create — embed declaration | +| `internal/embed/files/` | 1 | Create — populated by `make embed-sync` | +| `cmd/init.go` | 1 | Create — `harness init` command | +| `main.go` | 1 | Modify — fallback + register init | +| `.goreleaser.yaml` | 2 | Create | +| `.gitignore` | 1 | Modify — ignore `internal/embed/files/` | +| `Makefile` | 1 | Modify — add `embed-sync` target | + +## Verification + +| Phase | Check | +|-------|-------| +| 0 | Push PR → CI runs, tests + lint pass | +| 1 | `make embed-sync && make cli` → binary builds with embedded files | +| 1 | `rm -rf ~/.openshell/harness && ./harness init` → files extracted | +| 1 | `cd /tmp && harness preflight` → detects `~/.openshell/harness/` | +| 2 | `goreleaser release --snapshot --clean` → binaries for all platforms |