Skip to content

Commit 49e5695

Browse files
authored
DEV-2617: Improved AtmosWorkflows (#703)
1 parent afe6cc9 commit 49e5695

File tree

6 files changed

+229
-69
lines changed

6 files changed

+229
-69
lines changed

docs/layers/gitops/setup.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ import AtmosWorkflow from '@site/src/components/AtmosWorkflow';
123123

124124
Deploy three components, `gitops/s3-bucket`, `gitops/dynamodb`, and `gitops` with the following workflow:
125125

126-
<AtmosWorkflow workflow="deploy" fileName="gitops" />
126+
<AtmosWorkflow workflow="deploy/gitops" fileName="gitops" />
127127

128128
And that's it!
129129
</Step>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// constants.ts
2+
3+
export const CLOUDPOSSE_DOCS_URL = 'https://raw.githubusercontent.com/cloudposse/docs/master/';
4+
export const WORKFLOWS_DIRECTORY_PATH = 'examples/snippets/stacks/workflows/';

src/components/AtmosWorkflow/index.tsx

+50-68
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,78 @@
1+
// index.tsx
2+
13
import React, { useEffect, useState } from 'react';
2-
import Tabs from '@theme/Tabs';
3-
import TabItem from '@theme/TabItem';
44
import CodeBlock from '@theme/CodeBlock';
5+
import Note from '@site/src/components/Note';
56
import Steps from '@site/src/components/Steps';
7+
import TabItem from '@theme/TabItem';
8+
import Tabs from '@theme/Tabs';
69

7-
import * as yaml from 'js-yaml';
8-
9-
// Define constants for the base URL and workflows directory path
10-
const CLOUDPOSSE_DOCS_URL = 'https://raw.githubusercontent.com/cloudposse/docs/master/';
11-
const WORKFLOWS_DIRECTORY_PATH = 'examples/snippets/stacks/workflows/';
12-
13-
async function GetAtmosTerraformCommands(workflow: string, fileName: string, stack?: string): Promise<string[] | undefined> {
14-
try {
15-
// Construct the full URL to the workflow YAML file
16-
const url = `${CLOUDPOSSE_DOCS_URL}${WORKFLOWS_DIRECTORY_PATH}${fileName}.yaml`;
17-
18-
// Fetch the workflow file from the constructed URL
19-
const response = await fetch(url);
20-
if (!response.ok) {
21-
console.error('Failed to fetch the file:', response.statusText);
22-
console.error('Workflow URL:', url);
23-
return undefined;
24-
}
25-
const fileContent = await response.text();
26-
27-
// Parse the YAML content
28-
const workflows = yaml.load(fileContent) as any;
29-
30-
// Find the specified workflow in the parsed YAML
31-
if (workflows && workflows.workflows && workflows.workflows[workflow]) {
32-
const workflowDetails = workflows.workflows[workflow];
33-
34-
// Extract the commands under that workflow
35-
const commands = workflowDetails.steps.map((step: any) => {
36-
let command = step.command;
37-
// TODO handle nested Atmos Workflows
38-
// For example: https://raw.githubusercontent.com/cloudposse/docs/master/examples/snippets/stacks/workflows/identity.yaml
39-
if (!step.type) {
40-
command = `atmos ${command}`;
41-
if (stack) {
42-
command += ` -s ${stack}`;
43-
}
44-
}
45-
return command;
46-
});
47-
48-
return commands;
49-
}
10+
import { GetAtmosTerraformCommands } from './utils';
11+
import { WorkflowStep, WorkflowData } from './types';
12+
import { WORKFLOWS_DIRECTORY_PATH } from './constants';
5013

51-
// Return undefined if the workflow is not found
52-
return undefined;
53-
} catch (error) {
54-
console.error('Error fetching or parsing the file:', error);
55-
return undefined;
56-
}
14+
interface AtmosWorkflowProps {
15+
workflow: string;
16+
stack?: string;
17+
fileName: string;
5718
}
5819

59-
export default function AtmosWorkflow({ workflow, stack = "", fileName }) {
60-
const [commands, setCommands] = useState<string[]>([]);
20+
export default function AtmosWorkflow({ workflow, stack = '', fileName }: AtmosWorkflowProps) {
21+
const [workflowData, setWorkflowData] = useState<WorkflowData | null>(null);
6122
const fullFilePath = `${WORKFLOWS_DIRECTORY_PATH}${fileName}.yaml`;
6223

6324
useEffect(() => {
64-
GetAtmosTerraformCommands(workflow, fileName, stack).then((cmds) => {
65-
if (Array.isArray(cmds)) {
66-
setCommands(cmds);
25+
GetAtmosTerraformCommands(workflow, fileName, stack).then((data) => {
26+
if (data) {
27+
setWorkflowData(data);
6728
} else {
68-
setCommands([]); // Default to an empty array if cmds is undefined or not an array
29+
setWorkflowData(null);
6930
}
7031
});
7132
}, [workflow, fileName, stack]);
7233

7334
return (
7435
<Tabs queryString="workflows">
7536
<TabItem value="commands" label="Commands">
76-
These are the commands included in the <code>{workflow}</code> workflow in the <code>{fullFilePath}</code> file:
37+
<Note title={workflow}>
38+
These are the commands included in the <code>{workflow}</code> workflow in the{' '}
39+
<code>{fullFilePath}</code> file:
40+
</Note>
41+
{workflowData?.description && (
42+
<p className=".workflow-title">
43+
{workflowData.description}
44+
</p>
45+
)}
7746
<Steps>
7847
<ul>
79-
{commands.length > 0 ? commands.map((cmd, index) => (
80-
<li key={index}>
81-
<CodeBlock language="bash">
82-
{cmd}
83-
</CodeBlock>
84-
</li>
85-
)) : 'No commands found'}
48+
{workflowData?.steps.length ? (
49+
workflowData.steps.map((step, index) => (
50+
<li key={index}>
51+
{step.type === 'title' ? (
52+
<>
53+
<h4 className=".workflow-title">
54+
{step.content.split('\n\n')[0]}
55+
</h4>
56+
<CodeBlock language="bash">
57+
{step.content.split('\n\n')[1]}
58+
</CodeBlock>
59+
</>
60+
) : (
61+
<CodeBlock language="bash">{step.content}</CodeBlock>
62+
)}
63+
</li>
64+
))
65+
) : (
66+
'No commands found'
67+
)}
8668
</ul>
8769
</Steps>
88-
Too many commands? Consider using the Atmos workflow! 🚀
70+
<p>Too many commands? Consider using the Atmos workflow! 🚀</p>
8971
</TabItem>
9072
<TabItem value="atmos" label="Atmos Workflow">
91-
Run the following from your Geodesic shell using the Atmos workflow:
73+
<p>Run the following from your Geodesic shell using the Atmos workflow:</p>
9274
<CodeBlock language="bash">
93-
atmos workflow {workflow} -f {fileName} {stack && `-s ${stack}`}
75+
{`atmos workflow ${workflow} -f ${fileName} ${stack ? `-s ${stack}` : ''}`}
9476
</CodeBlock>
9577
</TabItem>
9678
</Tabs>
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* styles.css */
2+
3+
.workflow-title {
4+
font-size: 1.25em;
5+
color: #2c3e50;
6+
}

src/components/AtmosWorkflow/types.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// types.ts
2+
3+
export interface WorkflowStep {
4+
type: 'command' | 'title';
5+
content: string;
6+
}
7+
8+
export interface WorkflowData {
9+
description?: string;
10+
steps: WorkflowStep[];
11+
}

src/components/AtmosWorkflow/utils.ts

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// utils.ts
2+
3+
import * as yaml from 'js-yaml';
4+
import { WorkflowStep, WorkflowData } from './types';
5+
import { CLOUDPOSSE_DOCS_URL, WORKFLOWS_DIRECTORY_PATH } from './constants';
6+
7+
export async function GetAtmosTerraformCommands(
8+
workflow: string,
9+
fileName: string,
10+
stack?: string,
11+
visitedWorkflows = new Set<string>()
12+
): Promise<WorkflowData | undefined> {
13+
try {
14+
const url = `${CLOUDPOSSE_DOCS_URL}${WORKFLOWS_DIRECTORY_PATH}${fileName}.yaml`;
15+
16+
const response = await fetch(url);
17+
if (!response.ok) {
18+
console.error('Failed to fetch the file:', response.statusText);
19+
console.error('Workflow URL:', url);
20+
return undefined;
21+
}
22+
const fileContent = await response.text();
23+
24+
const workflows = yaml.load(fileContent) as any;
25+
26+
if (workflows && workflows.workflows && workflows.workflows[workflow]) {
27+
const workflowDetails = workflows.workflows[workflow];
28+
29+
const workflowKey = `${fileName}:${workflow}`;
30+
if (visitedWorkflows.has(workflowKey)) {
31+
console.warn(
32+
`Already visited workflow ${workflow} in file ${fileName}, skipping to prevent infinite loop.`
33+
);
34+
return { description: workflowDetails.description, steps: [] };
35+
}
36+
visitedWorkflows.add(workflowKey);
37+
38+
let steps: WorkflowStep[] = [];
39+
let currentGroupCommands: string[] = [];
40+
let currentTitle: string | null = null;
41+
42+
const addGroupToSteps = () => {
43+
if (currentGroupCommands.length > 0) {
44+
if (currentTitle) {
45+
steps.push({
46+
type: 'title',
47+
content: `${currentTitle}\n\n${currentGroupCommands.join('\n')}`
48+
});
49+
} else {
50+
steps.push({
51+
type: 'command',
52+
content: currentGroupCommands.join('\n')
53+
});
54+
}
55+
currentGroupCommands = [];
56+
currentTitle = null;
57+
}
58+
};
59+
60+
// Group all vendor pull commands together
61+
const isVendorWorkflow = workflowDetails.steps.every(step =>
62+
step.command.startsWith('vendor pull')
63+
);
64+
65+
for (const step of workflowDetails.steps) {
66+
let command = step.command;
67+
68+
if (isVendorWorkflow) {
69+
// Add all vendor commands to a single group
70+
let atmosCommand = `atmos ${command}`;
71+
if (stack) {
72+
atmosCommand += ` -s ${stack}`;
73+
}
74+
currentGroupCommands.push(atmosCommand);
75+
} else if (command.trim().startsWith('echo') && step.type === 'shell') {
76+
// When we find an echo, add previous group and start new group
77+
addGroupToSteps();
78+
currentTitle = command.replace(/^echo\s+['"](.+)['"]$/, '$1');
79+
} else if (command.startsWith('workflow')) {
80+
// For nested workflows, add current group first
81+
addGroupToSteps();
82+
83+
const commandParts = command.split(' ');
84+
const nestedWorkflowIndex = commandParts.findIndex((part) => part === 'workflow') + 1;
85+
const nestedWorkflow = commandParts[nestedWorkflowIndex];
86+
87+
let nestedFileName = fileName;
88+
const fileFlagIndex = commandParts.findIndex((part) => part === '-f' || part === '--file');
89+
if (fileFlagIndex !== -1) {
90+
nestedFileName = commandParts[fileFlagIndex + 1];
91+
}
92+
93+
let nestedStack = stack;
94+
const stackFlagIndex = commandParts.findIndex((part) => part === '-s' || part === '--stack');
95+
if (stackFlagIndex !== -1) {
96+
nestedStack = commandParts[stackFlagIndex + 1];
97+
}
98+
99+
const nestedData = await GetAtmosTerraformCommands(
100+
nestedWorkflow,
101+
nestedFileName,
102+
nestedStack,
103+
visitedWorkflows
104+
);
105+
106+
if (nestedData && nestedData.steps) {
107+
steps = steps.concat(nestedData.steps);
108+
}
109+
} else {
110+
if (currentTitle) {
111+
// We're in an echo group
112+
if (step.type === 'shell') {
113+
const shebang = `#!/bin/bash\n`;
114+
const titleComment = `# Run the ${step.name || 'script'} Script\n`;
115+
currentGroupCommands.push(`${shebang}${titleComment}${command}`);
116+
} else {
117+
let atmosCommand = `atmos ${command}`;
118+
if (stack) {
119+
atmosCommand += ` -s ${stack}`;
120+
}
121+
currentGroupCommands.push(atmosCommand);
122+
}
123+
} else {
124+
// Individual step
125+
if (step.type === 'shell') {
126+
const shebang = `#!/bin/bash\n`;
127+
const titleComment = `# Run the ${step.name || 'script'} Script\n`;
128+
steps.push({
129+
type: 'command',
130+
content: `${shebang}${titleComment}${command}`,
131+
});
132+
} else {
133+
let atmosCommand = `atmos ${command}`;
134+
if (stack) {
135+
atmosCommand += ` -s ${stack}`;
136+
}
137+
steps.push({
138+
type: 'command',
139+
content: atmosCommand,
140+
});
141+
}
142+
}
143+
}
144+
}
145+
146+
// Add any remaining grouped commands
147+
addGroupToSteps();
148+
149+
return { description: workflowDetails.description, steps };
150+
}
151+
152+
return undefined;
153+
} catch (error) {
154+
console.error('Error fetching or parsing the file:', error);
155+
return undefined;
156+
}
157+
}

0 commit comments

Comments
 (0)