Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
501164a
tests/e2e: add API testscript runner and instance reboot scenario
natalie-o-perret Feb 26, 2026
acd2a88
ci: move e2e testscript jobs to dedicated pull_request workflow
natalie-o-perret Feb 26, 2026
31031ae
tests/e2e: make PTY signal tests deterministic with @wait: token
natalie-o-perret Feb 26, 2026
13479c0
tests/e2e: eliminate hardcoded delays — annotate all PTY inputs with …
natalie-o-perret Feb 26, 2026
2f16d9f
test(testscript): make PTY inputs deterministic via event-driven @wai…
natalie-o-perret Feb 26, 2026
9ca3d94
ci: trigger build+unit-tests on pull_request events too
natalie-o-perret Feb 26, 2026
dd783eb
feat(compute): improve error message when instance not found in zone …
natalie-o-perret Feb 26, 2026
35b4b5e
fix(nlb): check UpdateLoadBalancer error before waiting on operation …
natalie-o-perret Feb 26, 2026
fbcf90b
tests/e2e: redesign exec-wait with bracket-delimited segments and ext…
natalie-o-perret Feb 27, 2026
051d81e
tests/e2e: fix exec-wait to run cmd1/cmd2 as generic processes
natalie-o-perret Feb 27, 2026
e597c1f
tests/e2e: fix exec-wait to use stdout-only capture for cmd1/cmd2
natalie-o-perret Feb 27, 2026
c0ac087
tests/e2e: rename execpty to exec-pty with bracket syntax
natalie-o-perret Feb 27, 2026
b0d3313
tests/e2e: exec-pty: use bracket group for stdin file
natalie-o-perret Feb 27, 2026
1dab64e
tests/e2e: exec-wait: named bracket groups --action=[], --polling=[],…
natalie-o-perret Feb 27, 2026
ba0ea29
tests/e2e: exec-wait: rename set= to --set= for uniform option style
natalie-o-perret Feb 27, 2026
44bb251
tests/e2e: switch group delimiters from [ ] to ( )
natalie-o-perret Feb 27, 2026
7141345
tests/e2e: exec-pty: restore --stdin= flag, one paren group for command
natalie-o-perret Feb 27, 2026
a3bd916
tests/e2e: drop config file from API test setup, use env vars only
natalie-o-perret Feb 27, 2026
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
47 changes: 47 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: CI

on:
pull_request:

jobs:
test-e2e-without-api:
name: Run E2E (Testscript) Tests / Without API
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'

- name: Build binary
run: make build

- name: Run E2E (Testscript) Tests / Without API
run: |
cd tests/e2e
go test -v

test-e2e-with-api:
name: Run E2E (Testscript) Tests / With API
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'

- name: Build binary
run: make build

- name: Run E2E (Testscript) Tests / With API
env:
EXOSCALE_API_KEY: ${{ secrets.EXOSCALE_API_KEY }}
EXOSCALE_API_SECRET: ${{ secrets.EXOSCALE_API_SECRET }}
EXOSCALE_ZONE: ch-gva-2
run: |
cd tests/e2e
go test -v -tags=api -timeout 30m -run TestScriptsAPI
23 changes: 4 additions & 19 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
- 'bucket/**'
tags-ignore:
- 'v*' # Don't run CI tests on release tags
pull_request:
paths-ignore:
- '**.md'
- 'bucket/**'

jobs:
build:
Expand All @@ -20,22 +24,3 @@ jobs:
fetch-depth: 0

- uses: ./.github/actions/build

test-e2e:
name: Run E2E (Testscript) Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'

- name: Build binary
run: make build

- name: Run E2E (Testscript) Tests / Without API
run: |
cd tests/e2e
go test -v
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
### Features

- sks: add `rotate-karpenter-credentials` command #797

### Improvements

- compute instance: enrich "not found" error with the zone searched and a hint to use -z #805
- sks: add `active-nodepool-templates` command #797

### Bug fixes

- fix(nlb): API error swallowed on load-balancer update (e.g. duplicate name conflict reported as "operation is nil") #806
- fix(config): panic when used without a default account set #798

### Documentation
Expand All @@ -19,6 +24,8 @@

- test(testscript): add PTY infrastructure for testing interactive flows and commands #800
- test(testscript): add validation handling in PTY-based interactive flows #801
- test(testscript): add API testscript runner with per-scenario resource lifecycle #804
- test(testscript): make PTY inputs deterministic via event-driven @wait: synchronisation #804

## 1.93.0

Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_console_url.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (c *instanceConsoleURLCmd) CmdRun(cmd *cobra.Command, _ []string) error {
return err
}

foundInstance, err := resp.FindListInstancesResponseInstances(c.Instance)
foundInstance, err := findInstance(resp, c.Instance, string(c.Zone))
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func (c *instanceDeleteCmd) CmdRun(_ *cobra.Command, _ []string) error {

instanceToDelete := []v3.UUID{}
for _, i := range c.Instances {
instance, err := instances.FindListInstancesResponseInstances(i)
instance, err := findInstance(instances, i, c.Zone)
if err != nil {
if !c.Force {
return err
Expand Down
4 changes: 2 additions & 2 deletions cmd/compute/instance/instance_elastic_ip_attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ func (c *instanceEIPAttachCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instancesList.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instancesList, c.Instance, c.Zone)
if err != nil {
return fmt.Errorf("error retrieving Instance: %w", err)
return err
}

elasticIPs, err := client.ListElasticIPS(ctx)
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_elastic_ip_detach.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func (c *instanceEIPDetachCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instancesList.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instancesList, c.Instance, c.Zone)
if err != nil {
return fmt.Errorf("error retrieving Instance: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_enable_tpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (c *instanceEnableTPMCmd) CmdRun(_ *cobra.Command, _ []string) error {
return err
}

instance, err := resp.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(resp, c.Instance, string(c.Zone))
if err != nil {
return err
}
Expand Down
26 changes: 26 additions & 0 deletions cmd/compute/instance/instance_find.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package instance

import (
"errors"
"fmt"

v3 "github.com/exoscale/egoscale/v3"
)

// findInstance looks up an instance by name or ID from a ListInstancesResponse
// and enriches the "not found" error with the zone that was searched,
// reminding the user to use -z to target a different zone.
func findInstance(resp *v3.ListInstancesResponse, nameOrID, zone string) (v3.ListInstancesResponseInstances, error) {
instance, err := resp.FindListInstancesResponseInstances(nameOrID)
if err != nil {
if errors.Is(err, v3.ErrNotFound) {
return v3.ListInstancesResponseInstances{}, fmt.Errorf(
"instance %q not found in zone %s\nHint: use -z <zone> to specify a different zone, or run 'exo compute instance list' to see instances across all zones",
nameOrID,
zone,
)
}
return v3.ListInstancesResponseInstances{}, err
}
return instance, nil
}
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_private_network_attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (c *instancePrivnetAttachCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_private_network_detach.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func (c *instancePrivnetDetachCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_private_network_updateip.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (c *instancePrivnetUpdateIPCmd) CmdRun(_ *cobra.Command, _ []string) error
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_reboot.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (c *instanceRebootCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (c *instanceResetCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_reset_password.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (c *instanceResetPasswordCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_resizedisk.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (c *instanceResizeDiskCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_reveal_password.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (c *instanceRevealCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_scale.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func (c *instanceScaleCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_scp.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func (c *instanceSCPCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_security_group_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (c *instanceSGAddCmd) CmdRun(cmd *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_security_group_remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (c *instanceSGRemoveCmd) CmdRun(cmd *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_show.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (c *instanceShowCmd) CmdRun(cmd *cobra.Command, _ []string) error {
return err
}

foundInstance, err := resp.FindListInstancesResponseInstances(c.Instance)
foundInstance, err := findInstance(resp, c.Instance, string(c.Zone))
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_snapshot_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (c *instanceSnapshotCreateCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_snapshot_revert.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (c *instanceSnapshotRevertCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func (c *instanceSSHCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (c *instanceStartCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (c *instanceStopCmd) CmdRun(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compute/instance/instance_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (c *instanceUpdateCmd) CmdRun(cmd *cobra.Command, _ []string) error {
if err != nil {
return err
}
instance, err := instances.FindListInstancesResponseInstances(c.Instance)
instance, err := findInstance(instances, c.Instance, c.Zone)
if err != nil {
return err
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/compute/load_balancer/nlb_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ func (c *nlbUpdateCmd) CmdRun(cmd *cobra.Command, _ []string) error {
if updated {

op, err := client.UpdateLoadBalancer(ctx, n.ID, nlbRequest)
if err != nil {
return err
}

utils.DecorateAsyncOperation(
fmt.Sprintf("Updating Network Load Balancer %q...", c.NetworkLoadBalancer),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Test: exo compute instance <action> returns enriched error when instance not found
# Verifies that when an instance lookup fails, the error message includes the zone
# that was searched and a hint to use -z.
# No resources are created so no teardown is required.
# TEST_ZONE is injected by the API test runner.

# show: instance not found should mention zone and hint
! exec exo --output-format json compute instance show nonexistent-e2e-instance
stderr 'not found in zone'
stderr 'Hint: use -z'

# reboot: same helper, same enriched error
! exec exo --output-format json compute instance reboot --force nonexistent-e2e-instance
stderr 'not found in zone'
stderr 'Hint: use -z'

# stop: same helper, same enriched error
! exec exo --output-format json compute instance stop --force nonexistent-e2e-instance
stderr 'not found in zone'
stderr 'Hint: use -z'
12 changes: 12 additions & 0 deletions tests/e2e/scenarios/with-api/compute/instance_reboot.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Test: exo compute instance reboot
# Full lifecycle: create a dedicated instance, reboot it, then delete it.
# TEST_ZONE and TEST_RUN_ID are injected by the API test runner.

# Create the instance (sets INSTANCE_ID) and wait until running.
exec-wait --set=INSTANCE_ID:id --action=( exo --zone $TEST_ZONE --output-format json compute instance create cli-e2e-reboot-$TEST_RUN_ID --instance-type standard.tiny --template 'Linux Ubuntu 22.04 LTS 64-bit' --disk-size 10 ) --polling=( exo --zone $TEST_ZONE --output-format json compute instance show {INSTANCE_ID} ) --predicate=( jq -e '.state == "running"' )

# Reboot the instance and wait until it returns to running state.
exec-wait --action=( exo --zone $TEST_ZONE compute instance reboot --force $INSTANCE_ID ) --polling=( exo --zone $TEST_ZONE --output-format json compute instance show $INSTANCE_ID ) --predicate=( jq -e '.state == "running"' )

# Teardown: delete the instance.
exec exo --zone $TEST_ZONE compute instance delete --force $INSTANCE_ID
Loading