Skip to content
Closed
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
39 changes: 39 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Tests

on:
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
integration:
name: Integration (no machine)
# The provider binary is darwin-only, so the suite runs on macOS.
runs-on: macos-15
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false

- name: setup Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: go.mod

- name: install goreleaser
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7
with:
version: "~> v2"
args: release --snapshot --clean

- name: integration tests
working-directory: ./e2e
run: go test -v -ginkgo.v -timeout 3600s --ginkgo.label-filter=integration

# The `vm` label (full workspace lifecycle on a real OrbStack machine) is not
# run in CI: OrbStack cannot start on GitHub-hosted macOS runners, which do not
# expose the Apple Virtualization capability it needs (orbctl start panics with
# "vm config get max ipa size: unsupported"). Run it locally or on a
# self-hosted macOS runner: `task test:e2e:vm`.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/dist/
*.out
.DS_Store
/e2e/bin/
12 changes: 12 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ tasks:
desc: Run unit tests
cmd: go test ./...

test:e2e:
desc: Run no-machine e2e integration tests (macOS; no OrbStack required)
deps: ["build:cli"]
dir: e2e
cmd: go test -v -ginkgo.v -timeout 3600s --ginkgo.label-filter=integration

test:e2e:vm:
desc: Run full machine-lifecycle e2e tests (requires OrbStack)
deps: ["build:cli"]
dir: e2e
cmd: go test -v -ginkgo.v -timeout 3600s --ginkgo.label-filter=vm

lint:
desc: Run go vet
cmd: go vet ./...
23 changes: 23 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## E2E tests

The suite has two label groups (both require macOS, since the provider binary is
darwin-only):

- `integration` builds the provider and exercises the no-machine paths (manifest
generation, option validation, provider-binary error handling). It does not
require OrbStack to be installed or running.
- `vm` provisions a real OrbStack machine and runs a full workspace lifecycle.
It requires OrbStack (`orbctl` on `PATH`, OrbStack running). This label does
not run in CI: OrbStack cannot start on GitHub-hosted macOS runners (they lack
the Apple Virtualization capability it needs). Run it on a real Mac or a
self-hosted macOS runner.

### Run

Build the provider binaries first, then run a label group from this directory:

```sh
task build:cli
go test -v -ginkgo.v -timeout 3600s --ginkgo.label-filter=integration
go test -v -ginkgo.v -timeout 3600s --ginkgo.label-filter=vm
```
14 changes: 14 additions & 0 deletions e2e/e2e_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package e2e

import (
"testing"

_ "github.com/devsy-org/devsy-provider-orbstack/e2e/tests/integration"
"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
)

func TestRunE2ETests(t *testing.T) {
gomega.RegisterFailHandler(ginkgo.Fail)
ginkgo.RunSpecs(t, "Devsy OrbStack Provider e2e suite")
}
4 changes: 4 additions & 0 deletions e2e/fixtures/workspace/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "orbstack-e2e",
"image": "mcr.microsoft.com/devcontainers/base:alpine"
}
161 changes: 161 additions & 0 deletions e2e/tests/integration/integration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package integration

import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
goruntime "runtime"
"strings"
"time"

"github.com/devsy-org/devsy/e2e/framework"
"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
)

// providerBinary returns the goreleaser-built provider binary for this host.
// The provider is darwin-only, so the e2e suite runs on macOS.
func providerBinary() string {
archDir := map[string]string{"amd64": "amd64_v1", "arm64": "arm64_v8.0"}[goruntime.GOARCH]
dir := fmt.Sprintf("build_%s_%s", goruntime.GOOS, archDir)
bin := fmt.Sprintf("devsy-provider-orbstack-%s-%s", goruntime.GOOS, goruntime.GOARCH)
return filepath.Join("..", "dist", dir, bin)
}

func mustRun(cmd *exec.Cmd) {
out, err := cmd.CombinedOutput()
gomega.Expect(err).NotTo(gomega.HaveOccurred(), string(out))
}

// isolateDevsyHome points devsy at a throwaway home so tests never touch the
// developer's real config and each run starts clean.
func isolateDevsyHome() {
dir, err := os.MkdirTemp("", "devsy-orbstack-e2e-")
framework.ExpectNoError(err)
framework.ExpectNoError(os.Setenv("DEVSY_HOME", dir))
}

func setupProvider() {
cmd := exec.Command("go", "run", "hack/provider/main.go", "0.0.0")
cmd.Dir = "../"
projectRoot, err := filepath.Abs("../")
framework.ExpectNoError(err)
cmd.Env = append(os.Environ(), "PROJECT_ROOT="+filepath.Join(projectRoot, "dist"))

output, err := cmd.Output()
framework.ExpectNoError(err)
framework.ExpectNoError(os.WriteFile("../dist/provider.yaml", output, 0o600))
}

func setupDevsyCLI() {
client := &http.Client{Timeout: 30 * time.Second}
url := fmt.Sprintf(
"https://github.com/devsy-org/devsy/releases/latest/download/devsy-%s-%s",
goruntime.GOOS, goruntime.GOARCH,
)
resp, err := client.Get(url) //nolint:gosec // fixed release URL
framework.ExpectNoError(err)
framework.ExpectEqual(resp.StatusCode, http.StatusOK)
defer func() { _ = resp.Body.Close() }()

framework.ExpectNoError(os.MkdirAll("bin", 0o750))
binPath := filepath.Join("bin", "devsy")
//nolint:gosec // fixed path; the devsy binary needs the executable bit
out, err := os.OpenFile(binPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755)
framework.ExpectNoError(err)
_, err = io.Copy(out, resp.Body)
framework.ExpectNoError(err)
framework.ExpectNoError(out.Close())
framework.ExpectNoError(exec.Command(binPath, "--version").Run()) //nolint:gosec // fixed path
}

// The integration specs exercise manifest generation and provider-binary error
// handling. They do not require OrbStack to be installed or running.
var _ = ginkgo.Describe(
"devsy provider orbstack integration",
ginkgo.Label("integration"),
ginkgo.Ordered,
func() {
ginkgo.BeforeAll(func() {
setupProvider()
})

ginkgo.It("should generate a non-empty provider.yaml", func() {
data, err := os.ReadFile("../dist/provider.yaml")
framework.ExpectNoError(err)
framework.ExpectNotEqual(len(data), 0)
})

ginkgo.It("should have required provider options", func() {
data, err := os.ReadFile("../dist/provider.yaml")
framework.ExpectNoError(err)
content := string(data)
for _, opt := range []string{"ORBSTACK_DISTRO", "ORBSTACK_ISOLATED", "AGENT_PATH"} {
gomega.Expect(content).To(gomega.ContainSubstring(opt))
}
})

ginkgo.It("should fail init when orbctl is missing", func() {
cmd := exec.Command(providerBinary(), "init") //nolint:gosec // built provider path
cmd.Env = append(cmd.Environ(), "ORBSTACK_PATH=/nonexistent/orbctl")
framework.ExpectError(cmd.Run())
})

ginkgo.It("should fail create without a machine id", func() {
cmd := exec.Command(providerBinary(), "create") //nolint:gosec // built provider path
cmd.Env = append(cmd.Environ(), "MACHINE_ID=")
output, err := cmd.CombinedOutput()
framework.ExpectError(err)
gomega.Expect(string(output)).To(gomega.ContainSubstring("MACHINE_ID"))
})
},
)

// The vm suite provisions a real OrbStack machine and requires OrbStack, so it
// is labelled separately from the no-machine integration specs.
var _ = ginkgo.Describe(
"devsy provider orbstack vm lifecycle",
ginkgo.Label("vm"),
ginkgo.Ordered,
func() {
const workspaceID = "devsy-provider-orbstack"

ginkgo.BeforeAll(func() {
isolateDevsyHome()
setupProvider()
setupDevsyCLI()
mustRun(exec.Command("bin/devsy", "provider", "add", "../dist/provider.yaml",
"--name", "orbstack", "-o", "ORBSTACK_CPUS=2", "-o", "ORBSTACK_MEMORY=4G"))

ginkgo.DeferCleanup(func() {
_ = exec.Command("bin/devsy", "workspace", "delete", "--force", workspaceID).Run()
})
})

ginkgo.It("should bring up a workspace on an OrbStack machine", func() {
mustRun(exec.Command("bin/devsy", "workspace", "up", "--ide=none",
"--id", workspaceID, "--provider", "orbstack", "fixtures/workspace"))
})

ginkgo.It("should run a command in the workspace", func() {
cmd := exec.Command(
"bin/devsy",
"workspace",
"ssh",
workspaceID,
"--command",
"echo test",
)
output, err := cmd.Output()
framework.ExpectNoError(err)
gomega.Expect(strings.TrimSpace(string(output))).To(gomega.Equal("test"))
})

ginkgo.It("should delete the workspace", func() {
mustRun(exec.Command("bin/devsy", "workspace", "delete", "--force", workspaceID))
})
},
)
Loading
Loading