From 6c4e3bfa68ebf06cdeb5caf8788054626418a92d Mon Sep 17 00:00:00 2001 From: jordanstephens Date: Thu, 12 Mar 2026 17:41:11 -0700 Subject: [PATCH 01/12] set defang-provider-handoff sample name --- samples/defang-provider-handoff/compose.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/defang-provider-handoff/compose.yaml b/samples/defang-provider-handoff/compose.yaml index a3b3bc6a..24738267 100644 --- a/samples/defang-provider-handoff/compose.yaml +++ b/samples/defang-provider-handoff/compose.yaml @@ -1,3 +1,4 @@ +name: defang-provider-handoff services: app: restart: unless-stopped From eb7d4dc4b1cdcf18925879a3ca091f8e4f6b9277 Mon Sep 17 00:00:00 2001 From: jordanstephens Date: Thu, 12 Mar 2026 17:53:13 -0700 Subject: [PATCH 02/12] add defang-provider-handoff readme --- samples/defang-provider-handoff/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 samples/defang-provider-handoff/README.md diff --git a/samples/defang-provider-handoff/README.md b/samples/defang-provider-handoff/README.md new file mode 100644 index 00000000..33e79b5a --- /dev/null +++ b/samples/defang-provider-handoff/README.md @@ -0,0 +1,5 @@ +# Defang Provider Handoff Sample + +If you are using Defang to deploy your application into your customer's cloud accounts, you may want to provide a white-labeled static site that your customers can use to configure their cloud account for your deployment. This sample demonstrates how to do that. + +The `compose.yaml` file in this directory defines a single service, `app`, which serves a static site on port 80. The static site is built from the `./app` directory, which contains an `index.html` file that provides instructions for the customer on how to configure their cloud account for your deployment. From 4d22d39cb4c25833b62a0ecf8000d730dfeb5421 Mon Sep 17 00:00:00 2001 From: jordanstephens Date: Thu, 12 Mar 2026 18:02:04 -0700 Subject: [PATCH 03/12] use --json to extract service urls --- samples/html-css-js/app/.dockerignore | 27 ++++++++++++++++++ tools/testing/deployer/deployer.go | 40 +++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 samples/html-css-js/app/.dockerignore diff --git a/samples/html-css-js/app/.dockerignore b/samples/html-css-js/app/.dockerignore new file mode 100644 index 00000000..12f6b36d --- /dev/null +++ b/samples/html-css-js/app/.dockerignore @@ -0,0 +1,27 @@ +# Default .dockerignore file for Defang +**/__pycache__ +**/.direnv +**/.DS_Store +**/.envrc +**/.git +**/.github +**/.idea +**/.next +**/.vscode +**/compose.*.yaml +**/compose.*.yml +**/compose.yaml +**/compose.yml +**/docker-compose.*.yaml +**/docker-compose.*.yml +**/docker-compose.yaml +**/docker-compose.yml +**/node_modules +**/Thumbs.db +Dockerfile +*.Dockerfile +# Ignore our own binary, but only in the root to avoid ignoring subfolders +defang +defang.exe +# Ignore our project-level state +.defang* diff --git a/tools/testing/deployer/deployer.go b/tools/testing/deployer/deployer.go index 4b10b69d..4666deac 100644 --- a/tools/testing/deployer/deployer.go +++ b/tools/testing/deployer/deployer.go @@ -191,7 +191,7 @@ func (d *CliDeployer) RunDeployTest(ctx context.Context, t test.TestInfo) (*test return cmd.Process.Signal(os.Interrupt) // Use interrupt signal to stop the command when context is cancelled } - }, "defang", "compose", "up", "--verbose", "--debug") + }, "defang", "compose", "up", "--verbose", "--debug", "--json") if err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) { @@ -207,7 +207,43 @@ func (d *CliDeployer) RunDeployTest(ctx context.Context, t test.TestInfo) (*test result.DeploySucceeded = cmd.ProcessState.Success() } - urls := findUrlsInOutput(d.Stdout.String()) + // run `defang ps --json` to get the service URLs instead of parsing the output of compose up, as the output may not be stable and may change in the future + cmd, err = d.RunCommand(ctx, nil, "defang", "ps", "--json") + if err != nil { + result.Message = fmt.Sprintf("Failed to run `defang ps --json` to get service URLs: %v", err) + log.Printf(result.Message) + return result, fmt.Errorf(result.Message) + } + + // filter out lines that start with " *" (those are the info logs + // from defang), then parse the remaining output as json. + lines := bytes.Split(d.Stdout.Bytes(), []byte{'\n'}) + var jsonOutput bytes.Buffer + for _, line := range lines { + if !bytes.HasPrefix(line, []byte(" *")) { + jsonOutput.Write(line) + jsonOutput.WriteByte('\n') + } + } + + var psOutput []struct { + Service string `json:"service"` + Endpoint string `json:"endpoint"` + } + if err := json.NewDecoder(bytes.NewReader(jsonOutput.Bytes())).Decode(&psOutput); err != nil { + result.Message = fmt.Sprintf("Failed to decode `defang ps --json` output: %v", err) + log.Printf(result.Message) + return result, fmt.Errorf(result.Message) + } + + var urls []string + for _, svc := range psOutput { + if svc.Endpoint != "" { + urls = append(urls, svc.Endpoint) + } + } + + // urls := findUrlsInOutput(d.Stdout.String()) if len(urls) == 0 { result.Message = "No service URLs found in deployment output" log.Printf(result.Message) From 26a6929b22c98525e46f06e3d9bab19f4c9a2e3c Mon Sep 17 00:00:00 2001 From: jordanstephens Date: Thu, 12 Mar 2026 18:03:27 -0700 Subject: [PATCH 04/12] add readme tags, title, description --- samples/defang-provider-handoff/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/samples/defang-provider-handoff/README.md b/samples/defang-provider-handoff/README.md index 33e79b5a..4b1ed251 100644 --- a/samples/defang-provider-handoff/README.md +++ b/samples/defang-provider-handoff/README.md @@ -3,3 +3,12 @@ If you are using Defang to deploy your application into your customer's cloud accounts, you may want to provide a white-labeled static site that your customers can use to configure their cloud account for your deployment. This sample demonstrates how to do that. The `compose.yaml` file in this directory defines a single service, `app`, which serves a static site on port 80. The static site is built from the `./app` directory, which contains an `index.html` file that provides instructions for the customer on how to configure their cloud account for your deployment. + +--- + +Title: Defang Provider Handoff Sample + +Short Description: A sample application that demonstrates how to provide a white-labeled static site for customers to configure their cloud accounts for your deployment. + +Tags: Defang, Cloud, Deployment, Static Site +Languages: Python From 72cadfb66bfb0ed069827ae3e1008edcbfcefb43 Mon Sep 17 00:00:00 2001 From: jordanstephens Date: Thu, 12 Mar 2026 18:05:58 -0700 Subject: [PATCH 05/12] update sample languages --- samples/defang-provider-handoff/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/defang-provider-handoff/README.md b/samples/defang-provider-handoff/README.md index 4b1ed251..51b936f4 100644 --- a/samples/defang-provider-handoff/README.md +++ b/samples/defang-provider-handoff/README.md @@ -11,4 +11,4 @@ Title: Defang Provider Handoff Sample Short Description: A sample application that demonstrates how to provide a white-labeled static site for customers to configure their cloud accounts for your deployment. Tags: Defang, Cloud, Deployment, Static Site -Languages: Python +Languages: HTML, CSS, JavaScript From 4d7cd8c74f04e4d1f44c2cc7fb42f6a18efe5895 Mon Sep 17 00:00:00 2001 From: jordanstephens Date: Thu, 12 Mar 2026 18:06:03 -0700 Subject: [PATCH 06/12] import json --- tools/testing/deployer/deployer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/testing/deployer/deployer.go b/tools/testing/deployer/deployer.go index 4666deac..a0bf45ff 100644 --- a/tools/testing/deployer/deployer.go +++ b/tools/testing/deployer/deployer.go @@ -14,6 +14,7 @@ import ( "regexp" "strings" "time" + "encoding/json" "defang.io/tools/testing/detector" "defang.io/tools/testing/logger" From 0ae9f8ea6ab7ec5953b5d093842d34401fbe006b Mon Sep 17 00:00:00 2001 From: jordanstephens Date: Mon, 16 Mar 2026 10:08:57 -0700 Subject: [PATCH 07/12] use npm ci in ci --- .github/workflows/check-sample.yml | 2 +- .github/workflows/publish-sample-template.yml | 2 +- .github/workflows/test-scripts.yml | 4 ++-- .github/workflows/update-template-workflows.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check-sample.yml b/.github/workflows/check-sample.yml index d56b783d..5579a2b9 100644 --- a/.github/workflows/check-sample.yml +++ b/.github/workflows/check-sample.yml @@ -54,7 +54,7 @@ jobs: } - name: Install script dependencies - run: npm install + run: npm ci working-directory: scripts - name: Create / Update Template Repo diff --git a/.github/workflows/publish-sample-template.yml b/.github/workflows/publish-sample-template.yml index 3df3d436..47f54194 100644 --- a/.github/workflows/publish-sample-template.yml +++ b/.github/workflows/publish-sample-template.yml @@ -26,7 +26,7 @@ jobs: cat modified.txt - name: Install script dependencies - run: npm install + run: npm ci working-directory: scripts - name: Create / Update Template Repo Main diff --git a/.github/workflows/test-scripts.yml b/.github/workflows/test-scripts.yml index da6a5f05..71db6873 100644 --- a/.github/workflows/test-scripts.yml +++ b/.github/workflows/test-scripts.yml @@ -30,7 +30,7 @@ jobs: node-version: "20" - name: Install dependencies - run: npm install + run: npm ci working-directory: scripts - name: Run tests @@ -52,7 +52,7 @@ jobs: node-version: "20" - name: Install dependencies - run: npm install + run: npm ci working-directory: scripts - name: Get changed samples diff --git a/.github/workflows/update-template-workflows.yml b/.github/workflows/update-template-workflows.yml index 90dee98d..b96b6264 100644 --- a/.github/workflows/update-template-workflows.yml +++ b/.github/workflows/update-template-workflows.yml @@ -23,7 +23,7 @@ jobs: node-version: "20" - name: Install dependencies - run: npm install + run: npm ci working-directory: scripts - name: Update Template Workflows From 22955170ef281b72a63db7665b76442696b5edf4 Mon Sep 17 00:00:00 2001 From: jordanstephens Date: Mon, 16 Mar 2026 10:11:21 -0700 Subject: [PATCH 08/12] rename step --- .github/workflows/deploy-changed-samples.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-changed-samples.yml b/.github/workflows/deploy-changed-samples.yml index 3b22b503..e1ab0989 100644 --- a/.github/workflows/deploy-changed-samples.yml +++ b/.github/workflows/deploy-changed-samples.yml @@ -59,8 +59,8 @@ jobs: eval "$(curl -fsSL s.defang.io/install)" if: env.should_continue == 'true' - - name: Run tests - id: run-tests + - name: Deploy changed samples to staging + id: deploy-samples shell: bash # implies set -o pipefail, so pipe below will keep the exit code from loadtest if: env.should_continue == 'true' env: @@ -125,7 +125,7 @@ jobs: ./tools/testing/loadtest -c fabric-staging.defang.dev:443 --timeout=15m --concurrency=10 -s $SAMPLES -o output --markdown=true | tee output/summary.log | grep -v '^\s*[-*]' # removes load sample log lines - name: Upload Output as Artifact uses: actions/upload-artifact@v4 - if: env.should_continue == 'true' && (success() || steps.run-tests.outcome == 'failure') # Always upload result unless cancelled + if: env.should_continue == 'true' && (success() || steps.deploy-samples.outcome == 'failure') # Always upload result unless cancelled with: name: program-output path: output/** From 5068141286bc3b027bdb26055092b5cdd0ab068b Mon Sep 17 00:00:00 2001 From: jordanstephens Date: Mon, 16 Mar 2026 10:13:46 -0700 Subject: [PATCH 09/12] remove --json from up --- tools/testing/deployer/deployer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/testing/deployer/deployer.go b/tools/testing/deployer/deployer.go index a0bf45ff..c5946f79 100644 --- a/tools/testing/deployer/deployer.go +++ b/tools/testing/deployer/deployer.go @@ -192,7 +192,7 @@ func (d *CliDeployer) RunDeployTest(ctx context.Context, t test.TestInfo) (*test return cmd.Process.Signal(os.Interrupt) // Use interrupt signal to stop the command when context is cancelled } - }, "defang", "compose", "up", "--verbose", "--debug", "--json") + }, "defang", "compose", "up", "--verbose", "--debug") if err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) { From 87da1436c8593720f5d26e3ef7483d5b2b27f021 Mon Sep 17 00:00:00 2001 From: jordanstephens Date: Mon, 16 Mar 2026 10:14:59 -0700 Subject: [PATCH 10/12] log output before parsing --- tools/testing/deployer/deployer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/testing/deployer/deployer.go b/tools/testing/deployer/deployer.go index c5946f79..ebed8c36 100644 --- a/tools/testing/deployer/deployer.go +++ b/tools/testing/deployer/deployer.go @@ -227,6 +227,7 @@ func (d *CliDeployer) RunDeployTest(ctx context.Context, t test.TestInfo) (*test } } + log.Printf("Output of `defang ps --json`:\n%v", jsonOutput.String()) var psOutput []struct { Service string `json:"service"` Endpoint string `json:"endpoint"` From 85de9a27c4a6ed2317856a6779f88de34f07fa4c Mon Sep 17 00:00:00 2001 From: jordanstephens Date: Mon, 16 Mar 2026 10:44:33 -0700 Subject: [PATCH 11/12] refactor trailing json parsing --- tools/testing/deployer/deployer.go | 49 ++++++++++++++---------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/tools/testing/deployer/deployer.go b/tools/testing/deployer/deployer.go index ebed8c36..b295d7e6 100644 --- a/tools/testing/deployer/deployer.go +++ b/tools/testing/deployer/deployer.go @@ -216,42 +216,20 @@ func (d *CliDeployer) RunDeployTest(ctx context.Context, t test.TestInfo) (*test return result, fmt.Errorf(result.Message) } - // filter out lines that start with " *" (those are the info logs - // from defang), then parse the remaining output as json. - lines := bytes.Split(d.Stdout.Bytes(), []byte{'\n'}) - var jsonOutput bytes.Buffer - for _, line := range lines { - if !bytes.HasPrefix(line, []byte(" *")) { - jsonOutput.Write(line) - jsonOutput.WriteByte('\n') - } - } - - log.Printf("Output of `defang ps --json`:\n%v", jsonOutput.String()) - var psOutput []struct { - Service string `json:"service"` - Endpoint string `json:"endpoint"` - } - if err := json.NewDecoder(bytes.NewReader(jsonOutput.Bytes())).Decode(&psOutput); err != nil { - result.Message = fmt.Sprintf("Failed to decode `defang ps --json` output: %v", err) + services, err := parseTrailingServicesJSON(d.Stdout.Bytes(), log) + if err != nil { + result.Message = fmt.Sprintf("Failed to parse service URLs: %v", err) log.Printf(result.Message) return result, fmt.Errorf(result.Message) } var urls []string - for _, svc := range psOutput { + for _, svc := range services { if svc.Endpoint != "" { urls = append(urls, svc.Endpoint) } } - // urls := findUrlsInOutput(d.Stdout.String()) - if len(urls) == 0 { - result.Message = "No service URLs found in deployment output" - log.Printf(result.Message) - return result, fmt.Errorf(result.Message) - } - result.TotalServices = len(urls) for _, url := range urls { log.Printf(" * Testing service URL %v", url) @@ -279,6 +257,25 @@ func (d *CliDeployer) RunDeployTest(ctx context.Context, t test.TestInfo) (*test return result, nil } +type ServiceEndpoint struct { + Service string `json:"service"` + Endpoint string `json:"endpoint"` +} + +func parseTrailingServicesJSON(data []byte, log logger.Logger) ([]ServiceEndpoint, error) { + idx := bytes.LastIndex(data, []byte("[")) + if idx == -1 { + return nil, fmt.Errorf("no JSON array found in output") + } + + var services []ServiceEndpoint + if err := json.NewDecoder(bytes.NewReader(data[idx:])).Decode(&services); err != nil { + return nil, fmt.Errorf("Failed to decode `defang ps --json` output: %v", err) + } + + return services, nil +} + func testURL(ctx context.Context, logger logger.Logger, url string) (int, error) { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { From d42b09753205e1133b23db73a0c9cb43cf9f156f Mon Sep 17 00:00:00 2001 From: jordanstephens Date: Mon, 16 Mar 2026 10:49:39 -0700 Subject: [PATCH 12/12] remove one loop --- tools/testing/deployer/deployer.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tools/testing/deployer/deployer.go b/tools/testing/deployer/deployer.go index b295d7e6..9463beef 100644 --- a/tools/testing/deployer/deployer.go +++ b/tools/testing/deployer/deployer.go @@ -223,15 +223,12 @@ func (d *CliDeployer) RunDeployTest(ctx context.Context, t test.TestInfo) (*test return result, fmt.Errorf(result.Message) } - var urls []string + result.TotalServices = len(services) for _, svc := range services { - if svc.Endpoint != "" { - urls = append(urls, svc.Endpoint) + if svc.Endpoint == "" { + continue } - } - - result.TotalServices = len(urls) - for _, url := range urls { + url := svc.Endpoint log.Printf(" * Testing service URL %v", url) code, err := testURL(context.Background(), log, url) // Still do a URL test if the test context is cancelledt if err == nil {