Merge pull request #34 from Photowall/add-deployment-grab-command
Add deployment grab command
This commit is contained in:
+43
-1
@@ -30,6 +30,7 @@ pwcli
|
|||||||
│ └── testservers (ts)
|
│ └── testservers (ts)
|
||||||
│ ├── create (c) ................................ d ts c
|
│ ├── create (c) ................................ d ts c
|
||||||
│ ├── delete (d) ................................ d ts d
|
│ ├── delete (d) ................................ d ts d
|
||||||
|
│ ├── grab (g) .................................. d ts g
|
||||||
│ ├── status (s) ................................ d ts s
|
│ ├── status (s) ................................ d ts s
|
||||||
│ └── update (u) ................................ d ts u
|
│ └── update (u) ................................ d ts u
|
||||||
│
|
│
|
||||||
@@ -62,6 +63,7 @@ pwcli [command] [subcommand] [action]
|
|||||||
### Command Shortcuts
|
### Command Shortcuts
|
||||||
|
|
||||||
Commands can be executed using their aliases. For example:
|
Commands can be executed using their aliases. For example:
|
||||||
|
|
||||||
- `pwcli d ts s` → deployment testservers status
|
- `pwcli d ts s` → deployment testservers status
|
||||||
- `pwcli a ps l` → aws parameterstore list
|
- `pwcli a ps l` → aws parameterstore list
|
||||||
- `pwcli wf g cb` → workflow git clean_old_branches
|
- `pwcli wf g cb` → workflow git clean_old_branches
|
||||||
@@ -71,24 +73,29 @@ Commands can be executed using their aliases. For example:
|
|||||||
## Available Commands
|
## Available Commands
|
||||||
|
|
||||||
### 🤖 **ai** (alias: `ai`)
|
### 🤖 **ai** (alias: `ai`)
|
||||||
|
|
||||||
AI-related commands
|
AI-related commands
|
||||||
|
|
||||||
#### Subcommands:
|
#### Subcommands:
|
||||||
|
|
||||||
- **schema** (alias: `sc`) - Output detailed pwcli command schema for LLM usage
|
- **schema** (alias: `sc`) - Output detailed pwcli command schema for LLM usage
|
||||||
- **promptcontextlibrary** (alias: `pcl`) - Copy context files to clipboard for AI prompts
|
- **promptcontextlibrary** (alias: `pcl`) - Copy context files to clipboard for AI prompts
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
- `pwcli ai sc`
|
- `pwcli ai sc`
|
||||||
- `pwcli ai pcl`
|
- `pwcli ai pcl`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### ☁️ **aws** (alias: `a`)
|
### ☁️ **aws** (alias: `a`)
|
||||||
|
|
||||||
AWS operations and management
|
AWS operations and management
|
||||||
|
|
||||||
#### Subcommands:
|
#### Subcommands:
|
||||||
|
|
||||||
##### **ecs** (alias: `e`) - Working with ECS on AWS
|
##### **ecs** (alias: `e`) - Working with ECS on AWS
|
||||||
|
|
||||||
- **sshtestserver** (alias: `sts`) - SSH into a test server in ECS
|
- **sshtestserver** (alias: `sts`) - SSH into a test server in ECS
|
||||||
|
|
||||||
**Example:** `pwcli a e sts`
|
**Example:** `pwcli a e sts`
|
||||||
@@ -98,40 +105,55 @@ AWS operations and management
|
|||||||
**Example:** `pwcli a l`
|
**Example:** `pwcli a l`
|
||||||
|
|
||||||
##### **parameterstore** (alias: `ps`) - Working with parameter store on AWS
|
##### **parameterstore** (alias: `ps`) - Working with parameter store on AWS
|
||||||
|
|
||||||
- **bulk-create** (alias: `bc`) - Bulk create parameters from JSON file
|
- **bulk-create** (alias: `bc`) - Bulk create parameters from JSON file
|
||||||
- **create** (alias: `c`) - Create a new item in parameter store
|
- **create** (alias: `c`) - Create a new item in parameter store
|
||||||
- **list** (alias: `l`) - List all parameters in parameter store
|
- **list** (alias: `l`) - List all parameters in parameter store
|
||||||
- **update** (alias: `u`) - Update an existing item in parameter store
|
- **update** (alias: `u`) - Update an existing item in parameter store
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
- `pwcli a ps l` - List parameters
|
- `pwcli a ps l` - List parameters
|
||||||
- `pwcli a ps c` - Create parameter
|
- `pwcli a ps c` - Create parameter
|
||||||
- `pwcli a ps bc` - Bulk create from JSON file
|
- `pwcli a ps bc` - Bulk create from JSON file
|
||||||
|
|
||||||
##### **pipeline** (alias: `pl`) - Pipeline operations
|
##### **pipeline** (alias: `pl`) - Pipeline operations
|
||||||
|
|
||||||
- **status** (alias: `s`) - View pipeline status with live tracking
|
- **status** (alias: `s`) - View pipeline status with live tracking
|
||||||
|
|
||||||
**Example:** `pwcli a pl s`
|
**Example:** `pwcli a pl s`
|
||||||
|
|
||||||
##### **s3** (alias: `s3`) - Working with S3 on AWS
|
##### **s3** (alias: `s3`) - Working with S3 on AWS
|
||||||
|
|
||||||
- **clear_prints** (alias: `cp`) - Clear prints-dev bucket on S3
|
- **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)
|
- **print_on_station** (alias: `pos`) - Print on a real station (copies from dev to production)
|
||||||
- **sync_images** (alias: `si`) - Sync images between S3 buckets
|
- **sync_images** (alias: `si`) - Sync images between S3 buckets
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
- `pwcli a s3 si` - Sync images
|
- `pwcli a s3 si` - Sync images
|
||||||
- `pwcli a s3 cp` - Clear prints
|
- `pwcli a s3 cp` - Clear prints
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🚀 **deployment** (alias: `d`)
|
### 🚀 **deployment** (alias: `d`)
|
||||||
|
|
||||||
Deployment and test server management
|
Deployment and test server management
|
||||||
|
|
||||||
#### Subcommands:
|
#### Subcommands:
|
||||||
|
|
||||||
##### **testservers** (alias: `ts`) - Deploy to testservers
|
##### **testservers** (alias: `ts`) - Deploy to testservers
|
||||||
|
|
||||||
- **create** (alias: `c`) - Create new testserver on AWS
|
- **create** (alias: `c`) - Create new testserver on AWS
|
||||||
- **delete** (alias: `d`) - Choose a testserver to delete
|
- **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. 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:
|
||||||
|
- `--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
|
- **status** (alias: `s`) - Show status for available testservers
|
||||||
- Flags:
|
- Flags:
|
||||||
- `--json` - Output status information in JSON format (non-interactive)
|
- `--json` - Output status information in JSON format (non-interactive)
|
||||||
@@ -143,9 +165,13 @@ Deployment and test server management
|
|||||||
- `--json` - Output result in JSON format with success flag and message
|
- `--json` - Output result in JSON format with success flag and message
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
- `pwcli d ts s` - View testserver status (interactive)
|
- `pwcli d ts s` - View testserver status (interactive)
|
||||||
- `pwcli d ts s --json` - View testserver status as JSON
|
- `pwcli d ts s --json` - View testserver status as JSON
|
||||||
- `pwcli d ts c` - Create new testserver
|
- `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` - 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` - 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 test-01 --skip-confirm` - Update without confirmation
|
||||||
@@ -154,9 +180,11 @@ Deployment and test server management
|
|||||||
---
|
---
|
||||||
|
|
||||||
### 📧 **mandrill** (alias: `m`)
|
### 📧 **mandrill** (alias: `m`)
|
||||||
|
|
||||||
Mandrill/Mailchimp email template operations
|
Mandrill/Mailchimp email template operations
|
||||||
|
|
||||||
#### Subcommands:
|
#### Subcommands:
|
||||||
|
|
||||||
- **fix_template** (alias: `ft`) - Remove incorrect artifacts caused by Mailchimp export to Mandrill
|
- **fix_template** (alias: `ft`) - Remove incorrect artifacts caused by Mailchimp export to Mandrill
|
||||||
|
|
||||||
**Example:** `pwcli m ft`
|
**Example:** `pwcli m ft`
|
||||||
@@ -164,9 +192,11 @@ Mandrill/Mailchimp email template operations
|
|||||||
---
|
---
|
||||||
|
|
||||||
### ⚙️ **setup** (alias: `s`)
|
### ⚙️ **setup** (alias: `s`)
|
||||||
|
|
||||||
Setup pwcli configuration
|
Setup pwcli configuration
|
||||||
|
|
||||||
#### Subcommands:
|
#### Subcommands:
|
||||||
|
|
||||||
- **settings** (alias: `se`) - Setup settings for pwcli (GitHub token, repo paths, API keys, etc.)
|
- **settings** (alias: `se`) - Setup settings for pwcli (GitHub token, repo paths, API keys, etc.)
|
||||||
|
|
||||||
**Example:** `pwcli setup settings` or `pwcli s se`
|
**Example:** `pwcli setup settings` or `pwcli s se`
|
||||||
@@ -174,9 +204,11 @@ Setup pwcli configuration
|
|||||||
---
|
---
|
||||||
|
|
||||||
### 🧪 **test** (alias: `t`)
|
### 🧪 **test** (alias: `t`)
|
||||||
|
|
||||||
Test commands
|
Test commands
|
||||||
|
|
||||||
#### Subcommands:
|
#### Subcommands:
|
||||||
|
|
||||||
- **routes** (alias: `r`) - Test pre-configured routes for 200 status on configured localhost
|
- **routes** (alias: `r`) - Test pre-configured routes for 200 status on configured localhost
|
||||||
|
|
||||||
**Example:** `pwcli t r`
|
**Example:** `pwcli t r`
|
||||||
@@ -184,6 +216,7 @@ Test commands
|
|||||||
---
|
---
|
||||||
|
|
||||||
### 🔄 **update** (alias: `up`)
|
### 🔄 **update** (alias: `up`)
|
||||||
|
|
||||||
Update pwcli to the latest version from main branch and rebuild
|
Update pwcli to the latest version from main branch and rebuild
|
||||||
|
|
||||||
**Example:** `pwcli update` or `pwcli up`
|
**Example:** `pwcli update` or `pwcli up`
|
||||||
@@ -191,19 +224,23 @@ Update pwcli to the latest version from main branch and rebuild
|
|||||||
---
|
---
|
||||||
|
|
||||||
### 🔧 **workflow** (alias: `wf`)
|
### 🔧 **workflow** (alias: `wf`)
|
||||||
|
|
||||||
Workflow tools for development
|
Workflow tools for development
|
||||||
|
|
||||||
#### Subcommands:
|
#### Subcommands:
|
||||||
|
|
||||||
##### **git** (alias: `g`) - Tools for git to make repo life easier
|
##### **git** (alias: `g`) - Tools for git to make repo life easier
|
||||||
|
|
||||||
- **clean_local_branches** (alias: `clb`) - Clean local branches without origin connection
|
- **clean_local_branches** (alias: `clb`) - Clean local branches without origin connection
|
||||||
- **clean_old_branches** (alias: `cb`) - Prune remote and clean local branches if gone
|
- **clean_old_branches** (alias: `cb`) - Prune remote and clean local branches if gone
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
- `pwcli wf g cb` - Clean old branches
|
- `pwcli wf g cb` - Clean old branches
|
||||||
- `pwcli wf g clb` - Clean local branches
|
- `pwcli wf g clb` - Clean local branches
|
||||||
|
|
||||||
##### **setupissue** (alias: `si`) - Setup everything for an issue
|
##### **setupissue** (alias: `si`) - Setup everything for an issue
|
||||||
|
|
||||||
Creates a new branch from origin/master based on a GitHub issue
|
Creates a new branch from origin/master based on a GitHub issue
|
||||||
|
|
||||||
**Example:** `pwcli wf si`
|
**Example:** `pwcli wf si`
|
||||||
@@ -213,6 +250,7 @@ Creates a new branch from origin/master based on a GitHub issue
|
|||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Run `pwcli setup settings` to configure:
|
Run `pwcli setup settings` to configure:
|
||||||
|
|
||||||
- GitHub access token
|
- GitHub access token
|
||||||
- Local photowall repository path
|
- Local photowall repository path
|
||||||
- Mandrill API key
|
- Mandrill API key
|
||||||
@@ -244,6 +282,7 @@ pwcli d ts s --json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**JSON Output Format:**
|
**JSON Output Format:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -269,6 +308,7 @@ pwcli d ts u --branch feature/123 --stack test-01 --skip-confirm --json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Success Response:**
|
**Success Response:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
@@ -280,6 +320,7 @@ pwcli d ts u --branch feature/123 --stack test-01 --skip-confirm --json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Error Response:**
|
**Error Response:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -290,6 +331,7 @@ pwcli d ts u --branch feature/123 --stack test-01 --skip-confirm --json
|
|||||||
### Automation Examples
|
### Automation Examples
|
||||||
|
|
||||||
**In a CI/CD Pipeline:**
|
**In a CI/CD Pipeline:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Deploy to testserver and check result
|
# Deploy to testserver and check result
|
||||||
@@ -306,6 +348,7 @@ fi
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Check testserver availability:**
|
**Check testserver availability:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Get all testservers and find an available one
|
# Get all testservers and find an available one
|
||||||
pwcli d ts s --json | jq '.[] | select(.status == "UPDATE_COMPLETE") | .name'
|
pwcli d ts s --json | jq '.[] | select(.status == "UPDATE_COMPLETE") | .name'
|
||||||
@@ -314,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`
|
For more information, visit the repository or run commands interactively with `pwcli`
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Run commands interactively with prompts and menus:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pwcli # Open interactive menu
|
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 u # Update testserver with interactive prompts
|
||||||
pwcli d ts s # View testserver status in table format
|
pwcli d ts s # View testserver status in table format
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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 TEST_STACK_NAME_PATTERN = /^photowall-test-\d+$/;
|
||||||
|
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<string, IssueState>();
|
||||||
|
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 (!TEST_STACK_NAME_PATTERN.test(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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -310,6 +310,10 @@ module.exports = {
|
|||||||
await system.run(runPipelineCmd);
|
await system.run(runPipelineCmd);
|
||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
|
|
||||||
|
const url = `https://test-${
|
||||||
|
stackName.split('photowall-test-')[1]
|
||||||
|
}.photowall-test.se`;
|
||||||
|
|
||||||
if (jsonOutput) {
|
if (jsonOutput) {
|
||||||
console.log(
|
console.log(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
@@ -319,6 +323,7 @@ module.exports = {
|
|||||||
stack: stackName,
|
stack: stackName,
|
||||||
branch: branchName,
|
branch: branchName,
|
||||||
pipeline: `photowall-${stacksuffix}Pipeline`,
|
pipeline: `photowall-${stacksuffix}Pipeline`,
|
||||||
|
url,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
@@ -327,6 +332,7 @@ module.exports = {
|
|||||||
} else {
|
} else {
|
||||||
spinner.stop();
|
spinner.stop();
|
||||||
print.fancy('Pipeline started, bye! 🚀');
|
print.fancy('Pipeline started, bye! 🚀');
|
||||||
|
print.info(`Will be deployed soon on ${chalk.cyan(url)}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// User cancelled
|
// User cancelled
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export const getCurrentBranch = async (
|
|||||||
cwd: config.photowall_repo,
|
cwd: config.photowall_repo,
|
||||||
trim: true,
|
trim: true,
|
||||||
});
|
});
|
||||||
console.log(`branch`, branch);
|
|
||||||
return branch;
|
return branch;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user