-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add integration tests #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
dd0bbb6
948249a
5d94726
3146591
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| 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 -tags=e2e -v -ginkgo.v -timeout 3600s --ginkgo.label-filter=integration | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| /dist/ | ||
| *.out | ||
| .DS_Store | ||
| /e2e/bin/ |
| 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 | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| //go:build e2e | ||
|
|
||
| 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") | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "name": "orbstack-e2e", | ||
| "image": "mcr.microsoft.com/devcontainers/base:alpine" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| package integration | ||
|
|
||
| import ( | ||
| "crypto/rand" | ||
| "encoding/hex" | ||
| "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" | ||
| ) | ||
|
|
||
| // uniqueWorkspaceID returns a per-run workspace ID so the vm suite never | ||
| // collides with or deletes a workspace a developer already has. | ||
| func uniqueWorkspaceID() string { | ||
| b := make([]byte, 4) | ||
| _, err := rand.Read(b) | ||
| framework.ExpectNoError(err) | ||
| return "devsy-orbstack-e2e-" + hex.EncodeToString(b) | ||
| } | ||
|
|
||
| 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)) | ||
| } | ||
|
|
||
| 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) | ||
| defer func() { _ = resp.Body.Close() }() | ||
| framework.ExpectEqual(resp.StatusCode, http.StatusOK) | ||
|
|
||
| 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) | ||
| // Close before exec: running a file still open for write triggers ETXTBSY. | ||
| framework.ExpectNoError(out.Close()) | ||
|
Comment on lines
+66
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Register resource cleanup before assertions can abort.
🤖 Prompt for AI Agents |
||
| framework.ExpectNoError(exec.Command(binPath, "--version").Run()) //nolint:gosec // fixed path | ||
|
Comment on lines
+62
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== file excerpt =="
sed -n '1,120p' e2e/tests/integration/integration.go
echo
echo "== related files/usages =="
git ls-files | rg 'e2e/tests/integration|integration\.go|Makefile|go.mod|go\.sum'
echo
rg -n "releases/latest/download|github.com/devsy-org/devsy|exec\\.Command\\(|os\\.OpenFile|Checksum|SHA|signature|Pinning|PIN" .Repository: devsy-org/devsy-provider-orbstack Length of output: 7169 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== repository references to devsy integration binary download =="
python3 - <<'PY'
from pathlib import Path
terms = [
"github.com/devsy-org/devsy/releases/latest/download/devsy-",
"releases/latest/download/devsy-",
"devsy",
]
for p in Path(".").glob("**/*.go"):
if not p.is_file():
continue
try:
text = p.read_text(errors="replace")
except Exception:
continue
lines = text.splitlines()
for term in terms:
for i,l in enumerate(lines, 1):
if term in l:
start=max(1,i-8); end=min(len(lines),i+12)
print(f"\n--- {p}:{i} ---")
for j in range(start,end+1):
print(f"{j:5}: {lines[j-1]}")
print()
PY
echo "== file excerpt =="
sed -n '1,120p' e2e/tests/integration/integration.go
echo
echo "== candidate checksum/signature/download pins =="
rg -n "releases/latest/download|checksumsum|sha256|sha1|integrity|provenance|cosign|cosign\.verify|gh verify|sigstore|gpg|gpgv|signed|Signature|checksum" e2e tests || trueRepository: devsy-org/devsy-provider-orbstack Length of output: 33668 Pin and verify the CLI binary before executing it. In 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| 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") | ||
| output, err := cmd.CombinedOutput() | ||
| framework.ExpectError(err) | ||
| gomega.Expect(string(output)).To(gomega.ContainSubstring("orbctl not found")) | ||
| }) | ||
|
Comment on lines
+107
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Ensure this test actually executes the provider binary.
🤖 Prompt for AI Agents |
||
|
|
||
| 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")) | ||
| }) | ||
| }, | ||
| ) | ||
|
|
||
| var _ = ginkgo.Describe( | ||
| "devsy provider orbstack vm lifecycle", | ||
| ginkgo.Label("vm"), | ||
| ginkgo.Ordered, | ||
| func() { | ||
| var workspaceID string | ||
|
|
||
| ginkgo.BeforeAll(func() { | ||
| workspaceID = uniqueWorkspaceID() | ||
| 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() { | ||
| //nolint:gosec // fixed devsy binary; test-controlled args | ||
| _ = exec.Command("bin/devsy", "workspace", "delete", "--force", workspaceID).Run() | ||
| }) | ||
|
Comment on lines
+140
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate and inspect the relevant e2e integration test and surrounding cleanup/workspace lifecycle.
if [ -f e2e/tests/integration/integration.go ]; then
wc -l e2e/tests/integration/integration.go
sed -n '1,220p' e2e/tests/integration/integration.go
else
echo "e2e/tests/integration/integration.go not found"
fd -a 'integration\.go$' .
fi
# Search for workspace creation/deletion and DeferCleanup usages.
rg -n "workspace|DeferCleanup|devsy|workspaceID|delete|--force|Create|Delete|idempoten|exists" e2e tests . --glob '!vendor/**' --glob '!node_modules/**' || trueRepository: devsy-org/devsy-provider-orbstack Length of output: 16093 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== e2e/README.md cleanup context =="
sed -n '1,80p' e2e/README.md
echo
echo "== pkg/orb/orbctl.go delete implementation =="
sed -n '90,125p' pkg/orb/orbctl.go
echo
echo "== cmd/lifecycle.go delete implementation =="
sed -n '30,55p' cmd/lifecycle.go
echo
echo "== Ginkgo DeferCleanup handling patterns in go modules =="
python3 - <<'PY'
# Read-only check of local ginkgo source availability and the relevant DeferCleanup implementation, if vendored/module cache is present.
import glob, os, re, pathlib
candidates = []
for root in ["/root/go/pkg/mod/github.com/onsi/ginkgo*"]:
candidates += glob.glob(root + "/core/deferpanic.go")
candidates += glob.glob(root + "/core/spec_runner.go")
print("ginkgo candidate files:", candidates[:10])
for path in candidates[:10]:
print(f"\n--- {path} ---")
text = pathlib.Path(path).read_text()
for needle in ["DeferCleanup", "AfterEach", "Cleanup", "CleanupFailed"]:
idx = text.find(needle)
if idx != -1:
start=max(0, idx-400); end=min(len(text), idx+1400)
print(text[start:end])
PYRepository: devsy-org/devsy-provider-orbstack Length of output: 2584 🌐 Web query:
💡 Result: Yes, Ginkgo runs DeferCleanup functions even if a spec fails [1]. When a failure occurs during the execution of a test (such as in a BeforeEach, JustBeforeEach, or It closure), Ginkgo halts the current spec but proceeds to execute all registered cleanup nodes, including AfterEach and DeferCleanup nodes, to ensure proper state cleanup [1]. Regarding the return value, if a function passed to DeferCleanup returns a non-nil error, Ginkgo will treat this as a failure [1]. Specifically, DeferCleanup ignores all return values from the passed function except for the last one; if that last value is a non-nil error, Ginkgo will mark the spec as having failed [1]. If the spec had already failed before the cleanup node ran, the error returned by the DeferCleanup function is typically tracked as an additional failure associated with that spec [2][3]. Citations:
🌐 Web query:
💡 Result: In the Ginkgo testing framework, DeferCleanup is a convenience function used to register cleanup callbacks that run automatically when a spec completes [1][2]. If a DeferCleanup callback function returns an error, Ginkgo handles it by failing the spec [1]. Key behaviors regarding DeferCleanup and spec failures include: Error Handling: When DeferCleanup is passed a function that returns an error as its last return value, Ginkgo checks this value [1]. If the returned error is non-nil, Ginkgo treats it as a spec failure [1]. Execution Order: DeferCleanup callbacks are executed in a first-in-last-out (FILO) order [3]. They are triggered after other standard cleanup nodes (like AfterEach) have finished [3][4]. Placement Restrictions: DeferCleanup must be called within setup or subject nodes (e.g., BeforeEach, It, BeforeSuite) [2][5]. It cannot be called at the top level or within container nodes (e.g., Describe, Context) [2][5]. Attempting to call it in an invalid location will result in an error during test execution [5][6]. Context: If a test fails in a BeforeEach, JustBeforeEach, or It closure, Ginkgo halts the current spec execution and proceeds to run registered cleanup nodes, including DeferCleanup callbacks, to ensure the environment is cleaned up properly [1]. If you are experiencing a failed cleanup spec, ensure that: 1. Your cleanup function is correctly returning an error (if intended) or nil (if successful) [1]. 2. You are not attempting to call DeferCleanup from an invalid location, such as directly inside a Describe or Context container [2][5]. 3. You are aware of the execution order; if your AfterAll block relies on state that a DeferCleanup is responsible for clearing, remember that DeferCleanups typically run after the standard After* family of nodes [4]. Citations:
Do not discard VM cleanup failures. A failed 🤖 Prompt for AI Agents |
||
| }) | ||
|
|
||
| ginkgo.It("should bring up a workspace on an OrbStack machine", func() { | ||
| //nolint:gosec // fixed devsy binary; test-controlled args | ||
| 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() { | ||
| //nolint:gosec // fixed devsy binary; test-controlled args | ||
| 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() { | ||
| //nolint:gosec // fixed devsy binary; test-controlled args | ||
| mustRun(exec.Command("bin/devsy", "workspace", "delete", "--force", workspaceID)) | ||
| }) | ||
| }, | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: devsy-org/devsy-provider-orbstack
Length of output: 4225
🌐 Web query:
actions/setup-go v6 cache input default true pull request cache poisoning💡 Result:
In
actions/setup-gov6, thecacheinput defaults totrue, which enables automatic caching of Go modules and build outputs [1][2]. Regarding cache poisoning concerns, while the defaultcache: trueconfiguration enables both restoration and saving of the cache, you can mitigate risks using the following mechanisms: 1. New Read-Only Cache Controls: Theactions/setup-goaction has introduced acache-writeinput (defaulting totrue) [3]. By settingcache-write: ${{ github.event_name != 'pull_request' }}, you can allow the action to restore the cache while preventing it from saving (and potentially overwriting) the cache during pull request workflows [3]. This is a recommended best practice for untrusted PR environments [3][4]. 2. Platform-Level Security: As of June 2026, GitHub now automatically issues read-only cache tokens to the default branch for workflow events that can be triggered without repository write permissions (such as certainpull_requestandworkflow_runcontexts) [5]. This provides a native layer of protection against cache poisoning by restricting the ability of untrusted workflows to modify the cache used by more privileged, trusted workflows [5]. If you are concerned about security in sensitive workflows, explicitly settingcache: falseor using thecache-writeinput to disable writes for pull request events are effective ways to prevent cache poisoning [3][4].Citations:
Disable setup-go cache writes for PRs.
This workflow runs untrusted
pull_requestevents, andsetup-gov6 enables caching by default. Setcache-write: ${{ github.event_name != 'pull_request' }}, or disable setup-go caching entirely withcache: false.🧰 Tools
🪛 zizmor (1.26.1)
[error] 21-21: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default
(cache-poisoning)
🤖 Prompt for AI Agents
Source: Linters/SAST tools