From b1f17669e6150836b0e61d3ca5e025a99ee8635a Mon Sep 17 00:00:00 2001 From: balthazarbk Date: Wed, 20 May 2026 10:36:30 +0200 Subject: [PATCH 1/6] Remove console log of current branch --- src/globals.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/globals.ts b/src/globals.ts index a5a93e5..408b6a7 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -50,7 +50,6 @@ export const getCurrentBranch = async ( cwd: config.photowall_repo, trim: true, }); - console.log(`branch`, branch); return branch; } return null; From d2bd1a6a8ff9efe7a4e943cf1c5e744474b023d1 Mon Sep 17 00:00:00 2001 From: balthazarbk Date: Wed, 20 May 2026 10:37:18 +0200 Subject: [PATCH 2/6] Print preview URL (se) in deployment update --- src/commands/deployment/testservers/update/update.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/commands/deployment/testservers/update/update.ts b/src/commands/deployment/testservers/update/update.ts index 0fea97c..5ef4084 100644 --- a/src/commands/deployment/testservers/update/update.ts +++ b/src/commands/deployment/testservers/update/update.ts @@ -310,6 +310,10 @@ module.exports = { await system.run(runPipelineCmd); await sleep(2000); + const url = `https://test-${ + stackName.split('photowall-test-')[1] + }.photowall-test.se`; + if (jsonOutput) { console.log( JSON.stringify( @@ -319,6 +323,7 @@ module.exports = { stack: stackName, branch: branchName, pipeline: `photowall-${stacksuffix}Pipeline`, + url, }, null, 2, @@ -327,6 +332,7 @@ module.exports = { } else { spinner.stop(); print.fancy('Pipeline started, bye! ๐Ÿš€'); + print.info(`Will be deployed soon on ${chalk.cyan(url)}`); } } else { // User cancelled From 770bb4f930393b349e5f896098f465efae20bc82 Mon Sep 17 00:00:00 2001 From: balthazarbk Date: Wed, 20 May 2026 10:37:33 +0200 Subject: [PATCH 3/6] Add deployment grab command --- .../deployment/testservers/grab/grab.ts | 414 ++++++++++++++++++ 1 file changed, 414 insertions(+) create mode 100644 src/commands/deployment/testservers/grab/grab.ts diff --git a/src/commands/deployment/testservers/grab/grab.ts b/src/commands/deployment/testservers/grab/grab.ts new file mode 100644 index 0000000..016546c --- /dev/null +++ b/src/commands/deployment/testservers/grab/grab.ts @@ -0,0 +1,414 @@ +import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu'; +import chalk = require('chalk'); +const { Confirm } = require('enquirer'); +import { Octokit } from '@octokit/core'; +import { exit } from 'process'; +import { + defaultMenuSettings, + getCurrentBranch, + getSettings, + sleep, +} from '../../../../globals'; +import { getTestStacksInfo, Stack } from '../../../../common/testservers'; +import { getIssueFromRepo } from '../../../../services/github_rest'; +const { spawn } = require('child_process'); + +const EXCLUDED_NAMED_STACKS = ['photowall-test-ingrid', 'photowall-test-usa']; +const EXCLUDED_STATUSES = [ + 'UPDATE_FAILED', + 'CREATE_FAILED', + 'ROLLBACK_COMPLETE', + 'UPDATE_ROLLBACK_FAILED', + 'UPDATE_ROLLBACK_COMPLETE', + 'UPDATE_IN_PROGRESS', + 'CREATE_IN_PROGRESS', +]; +const ONE_MONTH_SECONDS = 2592000; + +type IssueState = 'open' | 'closed' | 'unknown'; +type Candidate = { + stack: Stack; + tier: number; + reason: string; + ageSeconds: number; +}; + +const previewUrl = (stackName: string): string => { + const suffix = stackName.split('photowall-test-')[1]; + return `https://test-${suffix}.photowall-test.se`; +}; + +module.exports = { + name: 'grab', + alias: ['g'], + description: + 'Auto-pick a free testserver and update it with current branch (g)', + hidden: false, + run: async (toolbox: GluegunMenuToolbox) => { + const { system, print } = toolbox; + + const jsonOutput = toolbox.parameters.options.json; + const skipConfirm = + toolbox.parameters.options['skip-confirm'] || + toolbox.parameters.options.y || + !!jsonOutput; + const dryRun = toolbox.parameters.options['dry-run']; + + const fail = (message: string) => { + if (jsonOutput) { + console.log(JSON.stringify({ success: false, message }, null, 2)); + } else { + print.error(message); + } + }; + + const config = await getSettings(toolbox); + if (!config || !config.photowall_repo || config.photowall_repo === '') { + fail( + 'The grab command requires that you set an absolute path to your local photowall repository.', + ); + if (!jsonOutput) { + await toolbox.menu.showMenu('setup', defaultMenuSettings); + } + return; + } + + const currentBranch = await getCurrentBranch( + config.photowall_repo, + toolbox, + ); + if (!currentBranch) { + fail('Could not determine current git branch.'); + return; + } + + const octokit = new Octokit({ auth: config.gh_token }); + try { + await octokit.request('GET /repos/{owner}/{repo}/branches/{branch}', { + owner: 'photowall', + repo: 'photowall', + branch: currentBranch, + }); + } catch (e) { + fail( + `Branch ${currentBranch} is not on the photowall remote โ€” push it first.`, + ); + return; + } + + const testStacks = await getTestStacksInfo(toolbox); + + const alreadyDeployed = testStacks.find((s) => s.branch === currentBranch); + if (alreadyDeployed) { + const msg = `Your branch ${currentBranch} is already on ${alreadyDeployed.name} โ€” nothing to do.`; + if (jsonOutput) { + console.log( + JSON.stringify( + { + success: false, + message: msg, + stack: alreadyDeployed.name, + branch: currentBranch, + url: previewUrl(alreadyDeployed.name), + }, + null, + 2, + ), + ); + } else { + print.info( + `Your branch ${chalk.green(currentBranch)} is already on ${chalk.yellow( + alreadyDeployed.name, + )} โ€” nothing to do.`, + ); + print.info(`Live at ${chalk.cyan(previewUrl(alreadyDeployed.name))}`); + } + return; + } + + const issueStateByNumber = new Map(); + await Promise.all( + testStacks.map(async (stack) => { + const match = stack.branch.match(/^(\d+)/); + const issueNumber = match ? match[1] : null; + if (!issueNumber || issueStateByNumber.has(issueNumber)) return; + const issueData = await getIssueFromRepo( + toolbox, + config.gh_token, + 'photowall', + issueNumber, + ); + issueStateByNumber.set( + issueNumber, + issueData ? (issueData.state as IssueState) : 'unknown', + ); + }), + ); + + const now = Date.now(); + const candidates: Candidate[] = []; + for (const stack of testStacks) { + if (EXCLUDED_NAMED_STACKS.includes(stack.name)) continue; + if (EXCLUDED_STATUSES.includes(stack.status)) continue; + + const updatedTs = new Date(stack.updated).getTime(); + const ageSeconds = isNaN(updatedTs) + ? Number.POSITIVE_INFINITY + : (now - updatedTs) / 1000; + const match = stack.branch.match(/^(\d+)/); + const issueNumber = match ? match[1] : null; + const issueState: IssueState = issueNumber + ? (issueStateByNumber.get(issueNumber) ?? 'unknown') + : 'unknown'; + + if (stack.branch === 'master') { + candidates.push({ + stack, + tier: 1, + reason: 'branch is master', + ageSeconds, + }); + } else if (issueState === 'closed') { + candidates.push({ + stack, + tier: 2, + reason: `issue #${issueNumber} closed`, + ageSeconds, + }); + } else if (ageSeconds > ONE_MONTH_SECONDS) { + candidates.push({ + stack, + tier: 3, + reason: 'last commit >1 month ago', + ageSeconds, + }); + } else if (issueState === 'unknown') { + candidates.push({ + stack, + tier: 4, + reason: 'no linked issue in branch name', + ageSeconds, + }); + } + } + + if (candidates.length === 0) { + if (jsonOutput) { + console.log( + JSON.stringify( + { + success: false, + message: 'No free testservers โ€” all are claimed.', + }, + null, + 2, + ), + ); + return; + } + print.info('No free testservers โ€” all are claimed. Current state:'); + await require('../status/status').run(toolbox); + return; + } + + candidates.sort((a, b) => { + if (a.tier !== b.tier) return a.tier - b.tier; + return b.ageSeconds - a.ageSeconds; + }); + + const pick = candidates[0]; + const stackSuffix = pick.stack.name.replace('photowall-', ''); + const url = previewUrl(pick.stack.name); + + if (dryRun) { + if (jsonOutput) { + console.log( + JSON.stringify( + { + success: true, + dryRun: true, + stack: pick.stack.name, + branch: currentBranch, + reason: pick.reason, + url, + }, + null, + 2, + ), + ); + } else { + print.info( + `Would deploy ${chalk.green(currentBranch)} โ†’ ${chalk.yellow( + pick.stack.name, + )}. Reason: ${pick.reason}.`, + ); + } + return; + } + + let shouldRun = skipConfirm; + if (!skipConfirm) { + const confirm = new Confirm({ + name: 'confirm', + message: `Will deploy ${chalk.green(currentBranch)} โ†’ ${chalk.yellow( + pick.stack.name, + )}. Reason: ${pick.reason}.`, + }); + try { + shouldRun = await confirm.run(); + } catch (e) { + shouldRun = false; + } + } + + if (!shouldRun) { + if (!jsonOutput) print.info('Grab cancelled'); + if (toolbox.fromMenu()) { + await toolbox.menu.showMenu( + 'deployment testservers', + defaultMenuSettings, + ); + } + return; + } + + try { + const cmd = `make update-stack STACK_ENVIRONMENT_NAME=${stackSuffix} GITHUB_BRANCH=${currentBranch}`; + if (!jsonOutput) { + print.fancy( + `Running [${chalk.yellow( + cmd, + )}] in folder [${`${config.photowall_repo}/cloudformation/`}]`, + ); + } + const child = spawn( + 'make', + [ + 'update-stack', + `STACK_ENVIRONMENT_NAME=${stackSuffix}`, + `GITHUB_BRANCH=${currentBranch}`, + ], + { + cwd: `${config.photowall_repo}/cloudformation/`, + }, + ); + child.stdout.setEncoding('utf8'); + child.stdout.on('data', (data) => { + if (!jsonOutput) { + process.stdout.write(data); + } + }); + child.stderr.on('data', (data) => { + if (!jsonOutput) { + process.stderr.write(data); + } + }); + await new Promise((resolve, reject) => { + child.on('close', (code) => { + if (code === 0) { + resolve(0); + } else { + reject(new Error(`make exited with code ${code}`)); + } + }); + }); + + const spinner = jsonOutput + ? null + : print.spin(`Waiting for stack to update`); + let currentStackStatus = ''; + while (currentStackStatus !== 'UPDATE_COMPLETE') { + const stackCmd = `aws cloudformation describe-stacks --stack-name ${pick.stack.name}`; + const response = await system.run(stackCmd, { trim: true }); + const stackData = JSON.parse(response); + currentStackStatus = stackData.Stacks[0].StackStatus; + + if (!jsonOutput) { + let statusOutput = ''; + switch (currentStackStatus) { + case 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS': + statusOutput = `[${chalk.yellow( + currentStackStatus, + )}]: Almost done, cleanup in progress`; + break; + case 'UPDATE_COMPLETE': + statusOutput = `[${chalk.green( + currentStackStatus, + )}]: Update complete`; + break; + default: + statusOutput = `[${chalk.red( + currentStackStatus, + )}]: Updating the stack`; + break; + } + spinner.text = statusOutput; + } + + if (currentStackStatus !== 'UPDATE_COMPLETE') { + await sleep(1000); + } + } + + if (!jsonOutput) { + spinner.text = `Will start the pipeline now [${chalk.yellow( + `${pick.stack.name}Pipeline`, + )}]`; + } + + await system.run( + `aws codepipeline start-pipeline-execution --name ${pick.stack.name}Pipeline`, + ); + await sleep(2000); + + if (jsonOutput) { + console.log( + JSON.stringify( + { + success: true, + message: `Successfully updated ${pick.stack.name} with branch ${currentBranch} and started pipeline`, + stack: pick.stack.name, + branch: currentBranch, + reason: pick.reason, + pipeline: `${pick.stack.name}Pipeline`, + url, + }, + null, + 2, + ), + ); + } else { + spinner.stop(); + print.fancy('Pipeline started, bye! ๐Ÿš€'); + print.info(`Will be deployed soon on ${chalk.cyan(url)}`); + } + } catch (e) { + if (jsonOutput) { + console.log( + JSON.stringify( + { + success: false, + message: `Error during grab: ${e.message || e}`, + error: e.message || String(e), + }, + null, + 2, + ), + ); + } else { + console.log(`e`, e); + } + } + + if (jsonOutput) return; + + if (toolbox.fromMenu()) { + await toolbox.menu.showMenu( + 'deployment testservers', + defaultMenuSettings, + ); + } else { + exit(); + } + }, +}; From 82cc1bd68f7e62a1fc20455b494ee57793cfe448 Mon Sep 17 00:00:00 2001 From: balthazarbk Date: Wed, 20 May 2026 10:45:51 +0200 Subject: [PATCH 4/6] Add help for deployment grab command --- docs/help.md | 12 ++++++++++++ readme.md | 1 + 2 files changed, 13 insertions(+) diff --git a/docs/help.md b/docs/help.md index 90ff7f4..fa4bbec 100644 --- a/docs/help.md +++ b/docs/help.md @@ -30,6 +30,7 @@ pwcli โ”‚ โ””โ”€โ”€ testservers (ts) โ”‚ โ”œโ”€โ”€ create (c) ................................ d ts c โ”‚ โ”œโ”€โ”€ delete (d) ................................ d ts d +โ”‚ โ”œโ”€โ”€ grab (g) .................................. d ts g โ”‚ โ”œโ”€โ”€ status (s) ................................ d ts s โ”‚ โ””โ”€โ”€ update (u) ................................ d ts u โ”‚ @@ -132,6 +133,14 @@ Deployment and test server management ##### **testservers** (alias: `ts`) - Deploy to testservers - **create** (alias: `c`) - Create new testserver on AWS - **delete** (alias: `d`) - Choose a testserver to delete +- **grab** (alias: `g`) - Auto-pick a free testserver and update it with your current branch + - Picks the "most free" server using this priority: branch is `master` โ†’ issue closed โ†’ last commit >1 month โ†’ no linked issue. Excludes named servers (`-ingrid`, `-usa`) and failed/in-progress stacks. + - If your branch is already deployed on a server, exits early with the URL. + - If no free servers are found, prints the full status table. + - Flags: + - `--dry-run` - Show which server would be picked and why, without deploying + - `--skip-confirm` / `-y` - Skip the confirmation prompt + - `--json` - Output result in JSON format (implies `--skip-confirm`) - **status** (alias: `s`) - Show status for available testservers - Flags: - `--json` - Output status information in JSON format (non-interactive) @@ -146,6 +155,9 @@ Deployment and test server management - `pwcli d ts s` - View testserver status (interactive) - `pwcli d ts s --json` - View testserver status as JSON - `pwcli d ts c` - Create new testserver +- `pwcli d ts g` - Auto-grab a free testserver with your current branch +- `pwcli d ts g --dry-run` - Preview which server would be grabbed and why +- `pwcli d ts g --skip-confirm` - Grab without confirmation prompt - `pwcli d ts u` - Update existing testserver (interactive) - `pwcli d ts u --branch feature/123 --stack test-01` - Update test-01 with feature/123 branch (with confirmation) - `pwcli d ts u --branch feature/123 --stack test-01 --skip-confirm` - Update without confirmation diff --git a/readme.md b/readme.md index 980d0aa..e659120 100644 --- a/readme.md +++ b/readme.md @@ -21,6 +21,7 @@ Run commands interactively with prompts and menus: ```bash pwcli # Open interactive menu +pwcli d ts g # Auto-grab a free testserver with your current branch pwcli d ts u # Update testserver with interactive prompts pwcli d ts s # View testserver status in table format ``` From a978b8fdf9aa1c8d0219bb24ea4234b1624834a9 Mon Sep 17 00:00:00 2001 From: balthazarbk Date: Wed, 20 May 2026 13:19:14 +0200 Subject: [PATCH 5/6] Refine grab command to only use numbered test servers and update help documentation --- docs/help.md | 2 +- src/commands/deployment/testservers/grab/grab.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/help.md b/docs/help.md index fa4bbec..5887e1d 100644 --- a/docs/help.md +++ b/docs/help.md @@ -134,7 +134,7 @@ Deployment and test server management - **create** (alias: `c`) - Create new testserver on AWS - **delete** (alias: `d`) - Choose a testserver to delete - **grab** (alias: `g`) - Auto-pick a free testserver and update it with your current branch - - Picks the "most free" server using this priority: branch is `master` โ†’ issue closed โ†’ last commit >1 month โ†’ no linked issue. Excludes named servers (`-ingrid`, `-usa`) and failed/in-progress stacks. + - Picks the "most free" server using this priority: branch is `master` โ†’ issue closed โ†’ last commit >1 month โ†’ no linked issue. Only uses servers with numbers (ex: `-3`, `-12`) and excludes failed/in-progress stacks. - If your branch is already deployed on a server, exits early with the URL. - If no free servers are found, prints the full status table. - Flags: diff --git a/src/commands/deployment/testservers/grab/grab.ts b/src/commands/deployment/testservers/grab/grab.ts index 016546c..e16849c 100644 --- a/src/commands/deployment/testservers/grab/grab.ts +++ b/src/commands/deployment/testservers/grab/grab.ts @@ -13,7 +13,7 @@ import { getTestStacksInfo, Stack } from '../../../../common/testservers'; import { getIssueFromRepo } from '../../../../services/github_rest'; const { spawn } = require('child_process'); -const EXCLUDED_NAMED_STACKS = ['photowall-test-ingrid', 'photowall-test-usa']; +const TEST_STACK_NAME_PATTERN = /^photowall-test-\d+$/; const EXCLUDED_STATUSES = [ 'UPDATE_FAILED', 'CREATE_FAILED', @@ -148,7 +148,7 @@ module.exports = { const now = Date.now(); const candidates: Candidate[] = []; for (const stack of testStacks) { - if (EXCLUDED_NAMED_STACKS.includes(stack.name)) continue; + if (!TEST_STACK_NAME_PATTERN.test(stack.name)) continue; if (EXCLUDED_STATUSES.includes(stack.status)) continue; const updatedTs = new Date(stack.updated).getTime(); From 05e9ada55bb2216677a3a10252ab2e044c83498e Mon Sep 17 00:00:00 2001 From: balthazarbk Date: Wed, 20 May 2026 13:19:29 +0200 Subject: [PATCH 6/6] Lint --- docs/help.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/help.md b/docs/help.md index 5887e1d..d1498b2 100644 --- a/docs/help.md +++ b/docs/help.md @@ -63,6 +63,7 @@ pwcli [command] [subcommand] [action] ### Command Shortcuts Commands can be executed using their aliases. For example: + - `pwcli d ts s` โ†’ deployment testservers status - `pwcli a ps l` โ†’ aws parameterstore list - `pwcli wf g cb` โ†’ workflow git clean_old_branches @@ -72,24 +73,29 @@ Commands can be executed using their aliases. For example: ## Available Commands ### ๐Ÿค– **ai** (alias: `ai`) + AI-related commands #### Subcommands: + - **schema** (alias: `sc`) - Output detailed pwcli command schema for LLM usage - **promptcontextlibrary** (alias: `pcl`) - Copy context files to clipboard for AI prompts **Examples:** + - `pwcli ai sc` - `pwcli ai pcl` --- ### โ˜๏ธ **aws** (alias: `a`) + AWS operations and management #### Subcommands: ##### **ecs** (alias: `e`) - Working with ECS on AWS + - **sshtestserver** (alias: `sts`) - SSH into a test server in ECS **Example:** `pwcli a e sts` @@ -99,38 +105,45 @@ AWS operations and management **Example:** `pwcli a l` ##### **parameterstore** (alias: `ps`) - Working with parameter store on AWS + - **bulk-create** (alias: `bc`) - Bulk create parameters from JSON file - **create** (alias: `c`) - Create a new item in parameter store - **list** (alias: `l`) - List all parameters in parameter store - **update** (alias: `u`) - Update an existing item in parameter store **Examples:** + - `pwcli a ps l` - List parameters - `pwcli a ps c` - Create parameter - `pwcli a ps bc` - Bulk create from JSON file ##### **pipeline** (alias: `pl`) - Pipeline operations + - **status** (alias: `s`) - View pipeline status with live tracking **Example:** `pwcli a pl s` ##### **s3** (alias: `s3`) - Working with S3 on AWS + - **clear_prints** (alias: `cp`) - Clear prints-dev bucket on S3 - **print_on_station** (alias: `pos`) - Print on a real station (copies from dev to production) - **sync_images** (alias: `si`) - Sync images between S3 buckets **Examples:** + - `pwcli a s3 si` - Sync images - `pwcli a s3 cp` - Clear prints --- ### ๐Ÿš€ **deployment** (alias: `d`) + Deployment and test server management #### Subcommands: ##### **testservers** (alias: `ts`) - Deploy to testservers + - **create** (alias: `c`) - Create new testserver on AWS - **delete** (alias: `d`) - Choose a testserver to delete - **grab** (alias: `g`) - Auto-pick a free testserver and update it with your current branch @@ -152,6 +165,7 @@ Deployment and test server management - `--json` - Output result in JSON format with success flag and message **Examples:** + - `pwcli d ts s` - View testserver status (interactive) - `pwcli d ts s --json` - View testserver status as JSON - `pwcli d ts c` - Create new testserver @@ -166,9 +180,11 @@ Deployment and test server management --- ### ๐Ÿ“ง **mandrill** (alias: `m`) + Mandrill/Mailchimp email template operations #### Subcommands: + - **fix_template** (alias: `ft`) - Remove incorrect artifacts caused by Mailchimp export to Mandrill **Example:** `pwcli m ft` @@ -176,9 +192,11 @@ Mandrill/Mailchimp email template operations --- ### โš™๏ธ **setup** (alias: `s`) + Setup pwcli configuration #### Subcommands: + - **settings** (alias: `se`) - Setup settings for pwcli (GitHub token, repo paths, API keys, etc.) **Example:** `pwcli setup settings` or `pwcli s se` @@ -186,9 +204,11 @@ Setup pwcli configuration --- ### ๐Ÿงช **test** (alias: `t`) + Test commands #### Subcommands: + - **routes** (alias: `r`) - Test pre-configured routes for 200 status on configured localhost **Example:** `pwcli t r` @@ -196,6 +216,7 @@ Test commands --- ### ๐Ÿ”„ **update** (alias: `up`) + Update pwcli to the latest version from main branch and rebuild **Example:** `pwcli update` or `pwcli up` @@ -203,19 +224,23 @@ Update pwcli to the latest version from main branch and rebuild --- ### ๐Ÿ”ง **workflow** (alias: `wf`) + Workflow tools for development #### Subcommands: ##### **git** (alias: `g`) - Tools for git to make repo life easier + - **clean_local_branches** (alias: `clb`) - Clean local branches without origin connection - **clean_old_branches** (alias: `cb`) - Prune remote and clean local branches if gone **Examples:** + - `pwcli wf g cb` - Clean old branches - `pwcli wf g clb` - Clean local branches ##### **setupissue** (alias: `si`) - Setup everything for an issue + Creates a new branch from origin/master based on a GitHub issue **Example:** `pwcli wf si` @@ -225,6 +250,7 @@ Creates a new branch from origin/master based on a GitHub issue ## Configuration Run `pwcli setup settings` to configure: + - GitHub access token - Local photowall repository path - Mandrill API key @@ -256,6 +282,7 @@ pwcli d ts s --json ``` **JSON Output Format:** + ```json [ { @@ -281,6 +308,7 @@ pwcli d ts u --branch feature/123 --stack test-01 --skip-confirm --json ``` **Success Response:** + ```json { "success": true, @@ -292,6 +320,7 @@ pwcli d ts u --branch feature/123 --stack test-01 --skip-confirm --json ``` **Error Response:** + ```json { "success": false, @@ -302,6 +331,7 @@ pwcli d ts u --branch feature/123 --stack test-01 --skip-confirm --json ### Automation Examples **In a CI/CD Pipeline:** + ```bash #!/bin/bash # Deploy to testserver and check result @@ -318,6 +348,7 @@ fi ``` **Check testserver availability:** + ```bash # Get all testservers and find an available one pwcli d ts s --json | jq '.[] | select(.status == "UPDATE_COMPLETE") | .name' @@ -326,4 +357,3 @@ pwcli d ts s --json | jq '.[] | select(.status == "UPDATE_COMPLETE") | .name' --- For more information, visit the repository or run commands interactively with `pwcli` -