diff --git a/docs/help.md b/docs/help.md index ecc0cf3..1eea320 100644 --- a/docs/help.md +++ b/docs/help.md @@ -2,6 +2,54 @@ **pwcli** is a CLI for Photowall platform that provides commands for deployment, AWS operations, workflow automation, and more. +## Quick Command Reference + +``` +pwcli +├── ai (ai) +│ └── promptcontextlibrary (pcl) ..................... ai pcl +│ +├── aws (a) +│ ├── ecs (e) +│ │ └── sshtestserver (sts) ....................... a e sts +│ ├── logs (l) ....................................... a l +│ ├── parameterstore (ps) +│ │ ├── bulk-create (bc) .......................... a ps bc +│ │ ├── create (c) ................................ a ps c +│ │ ├── list (l) .................................. a ps l +│ │ └── update (u) ................................ a ps u +│ ├── pipeline (pl) +│ │ └── status (s) ................................ a pl s +│ └── s3 (s3) +│ ├── clear_prints (cp) ......................... a s3 cp +│ ├── print_on_station (pos) .................... a s3 pos +│ └── sync_images (si) .......................... a s3 si +│ +├── deployment (d) +│ └── testservers (ts) +│ ├── create (c) ................................ d ts c +│ ├── delete (d) ................................ d ts d +│ ├── status (s) ................................ d ts s +│ └── update (u) ................................ d ts u +│ +├── mandrill (m) +│ └── fix_template (ft) .............................. m ft +│ +├── setup (s) +│ └── settings (se) .................................. s se +│ +├── test (t) +│ └── routes (r) ..................................... t r +│ +├── update (up) ........................................ up +│ +└── workflow (wf) + ├── git (g) + │ ├── clean_local_branches (clb) ................ wf g clb + │ └── clean_old_branches (cb) ................... wf g cb + └── setupissue (si) ................................ wf si +``` + ## Usage Run `pwcli` to start the interactive menu, or use command shortcuts directly: @@ -81,12 +129,23 @@ Deployment and test server management - **create** (alias: `c`) - Create new testserver on AWS - **delete** (alias: `d`) - Choose a testserver to delete - **status** (alias: `s`) - Show status for available testservers + - Flags: + - `--json` - Output status information in JSON format (non-interactive) - **update** (alias: `u`) - Update stack with different branch + - Flags: + - `--branch ` - Specify GitHub branch to deploy + - `--stack ` - Specify test stack (formats: `photowall-test-01`, `test-01`, or `01`) + - `--skip-confirm` - Skip confirmation prompt when using parameters + - `--json` - Output result in JSON format with success flag and message **Examples:** -- `pwcli d ts s` - View testserver status +- `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 u` - Update existing testserver +- `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 +- `pwcli d ts u --branch feature/123 --stack 01 --skip-confirm --json` - Update and output JSON result --- @@ -168,5 +227,87 @@ Settings are stored in `~/.pwcli_settings` --- +## Non-Interactive Mode & JSON Output + +Several commands support non-interactive mode with JSON output, making them perfect for CI/CD pipelines and automation scripts. + +### Testserver Status with JSON Output + +Get testserver status information in JSON format: + +```bash +pwcli d ts s --json +``` + +**JSON Output Format:** +```json +[ + { + "name": "photowall-test-01", + "status": "UPDATE_COMPLETE", + "branch": "feature/123-my-feature", + "issueNumber": "123", + "issueStatus": "✅ open", + "issueUpdated": "2025-11-06T10:30:00Z", + "created": "2025-10-15T08:00:00Z", + "updated": "2025-11-05T14:20:00Z", + "url": "https://test-01.photowall-test.com/us" + } +] +``` + +### Testserver Update - Non-Interactive + +Update a testserver without interactive prompts: + +```bash +pwcli d ts u --branch feature/123 --stack test-01 --skip-confirm --json +``` + +**Success Response:** +```json +{ + "success": true, + "message": "Successfully updated photowall-test-01 with branch feature/123 and started pipeline", + "stack": "photowall-test-01", + "branch": "feature/123", + "pipeline": "photowall-test-01Pipeline" +} +``` + +**Error Response:** +```json +{ + "success": false, + "message": "Not a valid branch on photowall repo: invalid-branch" +} +``` + +### Automation Examples + +**In a CI/CD Pipeline:** +```bash +#!/bin/bash +# Deploy to testserver and check result +result=$(pwcli d ts u --branch $CI_BRANCH --stack test-01 --skip-confirm --json) +success=$(echo $result | jq -r '.success') + +if [ "$success" == "true" ]; then + echo "Deployment successful!" + exit 0 +else + echo "Deployment failed!" + exit 1 +fi +``` + +**Check testserver availability:** +```bash +# Get all testservers and find an available one +pwcli d ts s --json | jq '.[] | select(.status == "UPDATE_COMPLETE") | .name' +``` + +--- + For more information, visit the repository or run commands interactively with `pwcli` diff --git a/readme.md b/readme.md index 1a285d7..980d0aa 100644 --- a/readme.md +++ b/readme.md @@ -14,3 +14,27 @@ needed for the photowall organisation. # Usage When running `pwcli` command in the terminal is will open a interactive view but all commands are accessable through shortcuts. So if you want to run status on development testservers its possible to run `pwcli d ts s` for d = development, ts = test servers, s = status. Shortcuts are listed in the interactive view. + +## Interactive Mode + +Run commands interactively with prompts and menus: + +```bash +pwcli # Open interactive menu +pwcli d ts u # Update testserver with interactive prompts +pwcli d ts s # View testserver status in table format +``` + +## Non-Interactive Mode + +Many commands support non-interactive mode with parameters and JSON output, perfect for CI/CD and automation: + +```bash +# View testserver status as JSON +pwcli d ts s --json + +# Update testserver without interactive prompts +pwcli d ts u --branch feature/123 --stack test-01 --skip-confirm --json +``` + +For detailed documentation including all flags, parameters, and JSON output formats, see [docs/help.md](docs/help.md). diff --git a/src/commands/deployment/testservers/status/status.ts b/src/commands/deployment/testservers/status/status.ts index a255615..788f2c4 100644 --- a/src/commands/deployment/testservers/status/status.ts +++ b/src/commands/deployment/testservers/status/status.ts @@ -18,6 +18,9 @@ module.exports = { return; } + // Check for --json flag + const jsonOutput = toolbox.parameters.options.json; + const testStacks = await getTestStacksInfo(toolbox); const status = (status: string) => { switch (status) { @@ -189,6 +192,33 @@ module.exports = { return aNumber - bNumber; }); + // If JSON output is requested, output and return early + if (jsonOutput) { + const jsonData = testStacks.map((stack) => { + const regex = /^(\d+)/; + const match = stack.branch.match(regex); + const branchIssueNumber = match ? match[1] : null; + const issueStatus = issueStatuses.find( + (issueStatus) => issueStatus.issue === branchIssueNumber, + ); + + return { + name: stack.name, + status: stack.status, + branch: stack.branch, + issueNumber: branchIssueNumber, + issueStatus: issueStatus ? issueStatus.status : getStatusIcon('null'), + issueUpdated: issueStatus ? issueStatus.issueUpdated : null, + created: stack.created, + updated: stack.updated, + url: stackUrl(stack.name), + }; + }); + + console.log(JSON.stringify(jsonData, null, 2)); + return; + } + for (const stack of testStacks) { const regex = /^(\d+)/; const match = stack.branch.match(regex); diff --git a/src/commands/deployment/testservers/update/update.ts b/src/commands/deployment/testservers/update/update.ts index 218b352..0fea97c 100644 --- a/src/commands/deployment/testservers/update/update.ts +++ b/src/commands/deployment/testservers/update/update.ts @@ -80,9 +80,30 @@ module.exports = { run: async (toolbox: GluegunMenuToolbox) => { const { system, strings, print } = toolbox; + // Check for parameters + const jsonOutput = toolbox.parameters.options.json; + const branchParam = toolbox.parameters.options.branch; + const stackParam = + toolbox.parameters.options.stack || toolbox.parameters.options.server; + const skipConfirm = toolbox.parameters.options['skip-confirm']; + // Load settings const config = await getSettings(toolbox); if (!config || !config.photowall_repo || config.photowall_repo === '') { + if (jsonOutput) { + console.log( + JSON.stringify( + { + success: false, + message: + 'The update command requires that you set an absolute path to your local photowall repository.', + }, + null, + 2, + ), + ); + return; + } print.error(` The create command requires that you set an absolute path to your local photowall repository. @@ -93,40 +114,122 @@ module.exports = { } let branchName = null; - while (!branchName) { - branchName = await getBranchName( - toolbox, - config.gh_token, - config.photowall_repo, - ); + + // If branch is provided as parameter, validate it + if (branchParam) { + const octokit = new Octokit({ auth: config.gh_token }); + try { + const response = await octokit.request( + 'GET /repos/{owner}/{repo}/branches/{branch}', + { + owner: 'photowall', + repo: 'photowall', + branch: branchParam, + }, + ); + if (response.status === 200) { + branchName = branchParam; + } + } catch (e) { + if (jsonOutput) { + console.log( + JSON.stringify( + { + success: false, + message: `Not a valid branch on photowall repo: ${branchParam}`, + }, + null, + 2, + ), + ); + return; + } + print.error(`Not a valid branch on photowall repo: ${branchParam}`); + return; + } + } else { + // Interactive mode - ask for branch + while (!branchName) { + branchName = await getBranchName( + toolbox, + config.gh_token, + config.photowall_repo, + ); + } } const testStacks = await getTestStacksInfo(toolbox); - const options = testStacks.map((item) => { - return item.name; - }); + let stackName = null; - const prompt = new AutoComplete({ - name: 'testserver', - message: 'Choose a server', - limit: options.length - 1, - initial: options.length - 1, - choices: options, - }); + // If stack is provided as parameter, validate it + if (stackParam) { + const foundStack = testStacks.find((stack) => { + return ( + stack.name === stackParam || + stack.name === `photowall-${stackParam}` || + stack.name.replace('photowall-', '') === stackParam + ); + }); + + if (foundStack) { + stackName = foundStack.name; + } else { + if (jsonOutput) { + console.log( + JSON.stringify( + { + success: false, + message: `Stack not found: ${stackParam}`, + availableStacks: testStacks.map((s) => s.name), + }, + null, + 2, + ), + ); + return; + } + print.error(`Stack not found: ${stackParam}`); + print.info('Available stacks:'); + testStacks.forEach((s) => print.info(` - ${s.name}`)); + return; + } + } else { + // Interactive mode - ask for stack + const options = testStacks.map((item) => { + return item.name; + }); + + const prompt = new AutoComplete({ + name: 'testserver', + message: 'Choose a server', + limit: options.length - 1, + initial: options.length - 1, + choices: options, + }); + + stackName = await prompt.run(); + } try { - const stackName = await prompt.run(); const stacksuffix = stackName.replace('photowall-', ''); - const shouldRun = await getConfirmation(branchName, stackName); + // Skip confirmation if parameters are provided or skip-confirm flag is set + const shouldRun = + branchParam && stackParam + ? skipConfirm + ? true + : await getConfirmation(branchName, stackName) + : await getConfirmation(branchName, stackName); if (shouldRun) { const cmd = `make update-stack STACK_ENVIRONMENT_NAME=${stacksuffix} GITHUB_BRANCH=${branchName}`; - print.fancy( - `Running [${chalk.yellow( - cmd, - )}] in folder [${`${config.photowall_repo}/cloudformation/`}]`, - ); + if (!jsonOutput) { + print.fancy( + `Running [${chalk.yellow( + cmd, + )}] in folder [${`${config.photowall_repo}/cloudformation/`}]`, + ); + } const child = spawn( 'make', [ @@ -140,10 +243,14 @@ module.exports = { ); child.stdout.setEncoding('utf8'); child.stdout.on('data', (data) => { - process.stdout.write(data); + if (!jsonOutput) { + process.stdout.write(data); + } }); child.stderr.on('data', (data) => { - process.stderr.write(data); + if (!jsonOutput) { + process.stderr.write(data); + } }); await new Promise((resolve, reject) => { child.on('close', (code) => { @@ -156,48 +263,109 @@ module.exports = { }); // Check the status - const spinner = print.spin(`Waiting for stack to update`); + 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 photowall-${stacksuffix}`; const response = await system.run(stackCmd, { trim: true }); const stackData = JSON.parse(response); currentStackStatus = stackData.Stacks[0].StackStatus; - 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; + + 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; } - spinner.text = statusOutput; + if (currentStackStatus !== 'UPDATE_COMPLETE') { await sleep(1000); } } - spinner.text = `Will start the pipeline now [${chalk.yellow( - `photowall-${stacksuffix}Pipeline`, - )}]`; + if (!jsonOutput) { + spinner.text = `Will start the pipeline now [${chalk.yellow( + `photowall-${stacksuffix}Pipeline`, + )}]`; + } + const runPipelineCmd = `aws codepipeline start-pipeline-execution --name photowall-${stacksuffix}Pipeline`; await system.run(runPipelineCmd); await sleep(2000); - spinner.stop(); - print.fancy('Pipeline started, bye! 🚀'); + + if (jsonOutput) { + console.log( + JSON.stringify( + { + success: true, + message: `Successfully updated ${stackName} with branch ${branchName} and started pipeline`, + stack: stackName, + branch: branchName, + pipeline: `photowall-${stacksuffix}Pipeline`, + }, + null, + 2, + ), + ); + } else { + spinner.stop(); + print.fancy('Pipeline started, bye! 🚀'); + } + } else { + // User cancelled + if (jsonOutput) { + console.log( + JSON.stringify( + { + success: false, + message: 'Update cancelled by user', + }, + null, + 2, + ), + ); + } else { + print.info('Update cancelled'); + } } } catch (e) { - console.log(`e`, e); + if (jsonOutput) { + console.log( + JSON.stringify( + { + success: false, + message: `Error during update: ${e.message || e}`, + error: e.message || String(e), + }, + null, + 2, + ), + ); + } else { + console.log(`e`, e); + } + } + + // Skip menu if using parameters or JSON output + if (jsonOutput || (branchParam && stackParam)) { + return; } if (toolbox.fromMenu()) {