Skip to content

Commit de2d732

Browse files
authored
fix(action): use aws sdk to remove dependence on aws cli (#701)
Co-authored-by: danadajian <danadajian@users.noreply.github.com>
1 parent fbbfe3c commit de2d732

File tree

10 files changed

+46830
-18308
lines changed

10 files changed

+46830
-18308
lines changed

action/dist/main.js

Lines changed: 46492 additions & 17399 deletions
Large diffs are not rendered by default.

action/dist/main.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

action/dist/post.js

Lines changed: 105 additions & 806 deletions
Large diffs are not rendered by default.

action/dist/post.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

action/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"dependencies": {
66
"@actions/core": "3.0.0",
77
"@actions/github": "9.0.0",
8+
"@aws-sdk/client-s3": "^3.705.0",
89
"bluebird": "3.7.2",
910
"glob": "13.0.0"
1011
},

action/src/post.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { info, getInput } from '@actions/core';
2-
import { exec } from '@actions/exec';
2+
import { rm } from 'fs/promises';
33

44
const post = async () => {
55
info('Cleaning up screenshots directory...');
66

77
const screenshotsDirectory = getInput('screenshots-directory');
88

9-
await exec(`rm -rf ${screenshotsDirectory}`);
9+
try {
10+
await rm(screenshotsDirectory, { recursive: true, force: true });
11+
} catch (error) {
12+
// Directory might not exist, which is fine
13+
info(`Could not remove directory: ${error}`);
14+
}
1015

1116
info('Cleanup complete!');
1217
};

action/src/run.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { octokit } from './octokit';
1616
import { context } from '@actions/github';
1717
import * as path from 'path';
1818
import { sync } from 'glob';
19+
import { unlinkSync } from 'fs';
1920
import { createGithubComment } from './comment';
2021
import { getLatestVisualRegressionStatus } from './get-latest-visual-regression-status';
2122
import {
@@ -62,6 +63,7 @@ export const run = async () => {
6263
const newFilePaths = filesInScreenshotDirectory.filter(file =>
6364
file.endsWith('new.png')
6465
);
66+
6567
const diffFileCount = diffFilePaths.reduce((count, diffPath) => {
6668
if (
6769
newFilePaths.some(
@@ -70,9 +72,12 @@ export const run = async () => {
7072
) {
7173
return count + 1;
7274
}
73-
exec(`rm ${diffPath}`);
75+
// Delete orphaned diff files (no corresponding new file)
76+
unlinkSync(diffPath);
77+
7478
return count;
7579
}, 0);
80+
7681
const newFileCount = newFilePaths.length;
7782

7883
if (numVisualTestFailures > diffFileCount) {

action/src/s3-operations.ts

Lines changed: 133 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import { exec } from '@actions/exec';
21
import { getInput, info } from '@actions/core';
2+
import {
3+
S3Client,
4+
ListObjectsV2Command,
5+
GetObjectCommand,
6+
PutObjectCommand
7+
} from '@aws-sdk/client-s3';
38
import {
49
BASE_IMAGE_NAME,
510
BASE_IMAGES_DIRECTORY,
@@ -8,62 +13,172 @@ import {
813
} from 'shared';
914
import { map } from 'bluebird';
1015
import * as path from 'path';
16+
import * as fs from 'fs';
17+
import { promises as fsPromises } from 'fs';
18+
import { glob } from 'glob';
19+
import { Readable } from 'stream';
20+
21+
const s3Client = new S3Client();
22+
23+
async function checkS3PrefixExists(
24+
bucketName: string,
25+
prefix: string
26+
): Promise<boolean> {
27+
try {
28+
const command = new ListObjectsV2Command({
29+
Bucket: bucketName,
30+
Prefix: prefix,
31+
MaxKeys: 1
32+
});
33+
const response = await s3Client.send(command);
34+
return (response.Contents?.length ?? 0) > 0;
35+
} catch {
36+
return false;
37+
}
38+
}
39+
40+
async function downloadS3Directory(
41+
bucketName: string,
42+
s3Prefix: string,
43+
localDir: string
44+
): Promise<void> {
45+
const command = new ListObjectsV2Command({
46+
Bucket: bucketName,
47+
Prefix: s3Prefix
48+
});
49+
50+
const response = await s3Client.send(command);
51+
const objects = response.Contents ?? [];
52+
53+
await map(objects, async object => {
54+
if (!object.Key) return;
55+
56+
const relativePath = object.Key.substring(s3Prefix.length);
57+
const localFilePath = path.join(localDir, relativePath);
58+
59+
await fsPromises.mkdir(path.dirname(localFilePath), { recursive: true });
60+
61+
const getCommand = new GetObjectCommand({
62+
Bucket: bucketName,
63+
Key: object.Key
64+
});
65+
66+
const { Body } = await s3Client.send(getCommand);
67+
if (Body instanceof Readable) {
68+
const writeStream = fs.createWriteStream(localFilePath);
69+
await new Promise((resolve, reject) => {
70+
Body.pipe(writeStream).on('finish', resolve).on('error', reject);
71+
});
72+
}
73+
});
74+
}
75+
76+
async function uploadLocalDirectory(
77+
localDir: string,
78+
bucketName: string,
79+
s3Prefix: string
80+
): Promise<void> {
81+
const files = await glob('**/*', {
82+
cwd: localDir,
83+
nodir: true,
84+
absolute: false
85+
});
86+
87+
await map(files, async file => {
88+
const localFilePath = path.join(localDir, file);
89+
const s3Key = path.join(s3Prefix, file);
90+
91+
const fileContent = await fsPromises.readFile(localFilePath);
92+
93+
const command = new PutObjectCommand({
94+
Bucket: bucketName,
95+
Key: s3Key,
96+
Body: fileContent
97+
});
98+
99+
await s3Client.send(command);
100+
});
101+
}
102+
103+
async function uploadSingleFile(
104+
localFilePath: string,
105+
bucketName: string,
106+
s3Key: string
107+
): Promise<void> {
108+
const fileContent = await fsPromises.readFile(localFilePath);
109+
110+
const command = new PutObjectCommand({
111+
Bucket: bucketName,
112+
Key: s3Key,
113+
Body: fileContent
114+
});
115+
116+
await s3Client.send(command);
117+
}
11118

12119
export const downloadBaseImages = async () => {
13120
const bucketName = getInput('bucket-name', { required: true });
14121
const screenshotsDirectory = getInput('screenshots-directory');
15-
const baseImageExitCode = await exec(
16-
`aws s3 ls s3://${bucketName}/${BASE_IMAGES_DIRECTORY}/`,
17-
[],
18-
{ ignoreReturnCode: true }
122+
123+
const prefixExists = await checkS3PrefixExists(
124+
bucketName,
125+
`${BASE_IMAGES_DIRECTORY}/`
19126
);
20-
if (baseImageExitCode !== 0) {
127+
128+
if (!prefixExists) {
21129
info(
22130
`Base images directory does not exist in bucket ${bucketName}. Skipping download.`
23131
);
24-
await exec(`mkdir -p ${screenshotsDirectory}`);
132+
await fsPromises.mkdir(screenshotsDirectory, { recursive: true });
25133
return;
26134
}
27135

28136
const packagePaths = getInput('package-paths')?.split(',');
29137
if (packagePaths) {
30138
return Promise.all(
31139
packagePaths.map(packagePath =>
32-
exec(
33-
`aws s3 cp s3://${bucketName}/${BASE_IMAGES_DIRECTORY}/${packagePath} ${screenshotsDirectory}/${packagePath} --recursive`
140+
downloadS3Directory(
141+
bucketName,
142+
`${BASE_IMAGES_DIRECTORY}/${packagePath}/`,
143+
path.join(screenshotsDirectory, packagePath)
34144
)
35145
)
36146
);
37147
}
38148

39-
return exec(
40-
`aws s3 cp s3://${bucketName}/${BASE_IMAGES_DIRECTORY} ${screenshotsDirectory} --recursive`
149+
return downloadS3Directory(
150+
bucketName,
151+
`${BASE_IMAGES_DIRECTORY}/`,
152+
screenshotsDirectory
41153
);
42154
};
43155

44156
export const uploadAllImages = async (hash: string) => {
45157
const bucketName = getInput('bucket-name', { required: true });
46158
const screenshotsDirectory = getInput('screenshots-directory');
47159
const packagePaths = getInput('package-paths')?.split(',');
160+
48161
if (packagePaths) {
49162
return map(packagePaths, packagePath =>
50-
exec(
51-
`aws s3 cp ${screenshotsDirectory}/${packagePath} s3://${bucketName}/${NEW_IMAGES_DIRECTORY}/${hash}/${packagePath} --recursive`
163+
uploadLocalDirectory(
164+
path.join(screenshotsDirectory, packagePath),
165+
bucketName,
166+
`${NEW_IMAGES_DIRECTORY}/${hash}/${packagePath}/`
52167
)
53168
);
54169
}
55170

56-
return exec(
57-
`aws s3 cp ${screenshotsDirectory} s3://${bucketName}/${NEW_IMAGES_DIRECTORY}/${hash} --recursive`
171+
return uploadLocalDirectory(
172+
screenshotsDirectory,
173+
bucketName,
174+
`${NEW_IMAGES_DIRECTORY}/${hash}/`
58175
);
59176
};
60177

61178
export const uploadBaseImages = async (newFilePaths: string[]) => {
62179
const bucketName = getInput('bucket-name', { required: true });
63180
return map(newFilePaths, newFilePath =>
64-
exec(
65-
`aws s3 cp ${newFilePath} s3://${bucketName}/${buildBaseImagePath(newFilePath)}`
66-
)
181+
uploadSingleFile(newFilePath, bucketName, buildBaseImagePath(newFilePath))
67182
);
68183
};
69184

0 commit comments

Comments
 (0)