CI with AWS CodeBuild (#5493)

This commit is contained in:
Shrikant Sharat Kandula 2021-07-08 16:26:01 +05:30 committed by GitHub
parent f471a269b1
commit 34420e7777
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 672 additions and 0 deletions

View File

@ -0,0 +1,26 @@
# Build phase script for units node in the main build graph.
set -o errexit
set -o pipefail
set -o xtrace
{
echo "$BASH_VERSION"
node --version
cd "$CODEBUILD_SRC_DIR/app/client"
npm install -g yarn
yarn install --frozen-lockfile
REACT_APP_ENVIRONMENT=PRODUCTION \
npx jest -b --no-cache --coverage --collectCoverage=true --coverageDirectory='../../' --coverageReporters='json-summary'
REACT_APP_SHOW_ONBOARDING_FORM=true \
yarn run build
mv -v build client-dist
tar -caf client-dist.tgz client-dist
aws s3 cp --no-progress client-dist.tgz "$S3_BUILDS_PREFIX/$BATCH_ID/client-dist.tgz"
} 2>&1 | tee -a "ci/logs/$CODEBUILD_BATCH_BUILD_IDENTIFIER.log"

View File

@ -0,0 +1,22 @@
# This buildspec will run unit tests on the client code.
version: 0.2
env:
shell: bash
phases:
install:
on-failure: ABORT
runtime-versions:
nodejs: 14
build:
on-failure: ABORT
commands:
- source ci/common/extra-env.sh
- mkdir -pv ci/logs
- source ci/1-client-scripts/3-build.sh
finally:
- source ci/common/upload-logs.sh

View File

@ -0,0 +1,26 @@
set -o errexit
set -o pipefail
set -o xtrace
{
wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse" \
| tee /etc/apt/sources.list.d/mongodb-org-4.4.list
add-apt-repository --yes ppa:redislabs/redis
apt-get update --yes
time apt-get install --yes maven mongodb-org-{server,shell} redis
mkdir -p "$CODEBUILD_SRC_DIR/logs"
# Start a MongoDB server.
mkdir -p /data/db
nohup mongod > "$CODEBUILD_SRC_DIR/logs/mongod.log" 2>&1 & disown $!
# Start a Redis server.
nohup redis-server > "$CODEBUILD_SRC_DIR/logs/redis.log" 2>&1 & disown $!
} 2>&1 | tee -a "ci/logs/$CODEBUILD_BATCH_BUILD_IDENTIFIER.log"

View File

@ -0,0 +1,39 @@
# Build phase script for units node in the main build graph.
set -o errexit
set -o pipefail
set -o xtrace
{
echo "BASH_VERSION: '$BASH_VERSION'"
java -version
export APPSMITH_MONGODB_URI="mongodb://localhost:27017/appsmith"
export APPSMITH_REDIS_URL="redis://localhost:6379"
export APPSMITH_ENCRYPTION_SALT=ci-salt-is-white-like-radish
export APPSMITH_ENCRYPTION_PASSWORD=ci-password-is-red-like-carrot
export APPSMITH_CLOUD_SERVICES_BASE_URL=
export APPSMITH_IS_SELF_HOSTED=false
if ! mongo --eval 'db.runCommand({ connectionStatus: 1 })' "$APPSMITH_MONGODB_URI"; then
cat "$CODEBUILD_SRC_DIR/logs/mongod.log"
fi
cd "$CODEBUILD_SRC_DIR/app/server"
# Not using `build.sh` here since it doesn't exit with a non-zero status when the build fails.
mvn package --batch-mode
mkdir -p dist/plugins
mv -v appsmith-server/target/server-1.0-SNAPSHOT.jar dist/
rsync -av --exclude "original-*.jar" appsmith-plugins/*/target/*.jar dist/plugins/
mv -v dist server-dist
tar -caf server-dist.tgz server-dist
aws s3 cp --no-progress server-dist.tgz "$S3_BUILDS_PREFIX/$BATCH_ID/server-dist.tgz"
} 2>&1 | tee -a "ci/logs/$CODEBUILD_BATCH_BUILD_IDENTIFIER.log"

View File

@ -0,0 +1,34 @@
# This buildspec will run unit tests on the server code.
version: 0.2
env:
shell: bash
phases:
install:
on-failure: ABORT
runtime-versions:
java: corretto11
commands:
- set -o pipefail
- source ci/common/extra-env.sh
- mkdir -pv ci/logs
- source ci/1-server-scripts/1-install.sh
finally:
- source ci/common/upload-logs.sh
build:
on-failure: ABORT
commands:
- set -o pipefail
- source ci/common/extra-env.sh
- mkdir -pv ci/logs
- source ci/1-server-scripts/3-build.sh
finally:
- source ci/common/upload-logs.sh
cache:
paths:
- '/root/.m2/**/*' # Server dependencies.

36
ci/2-cypress.yml Normal file
View File

@ -0,0 +1,36 @@
# This buildspec will run Cypress tests.
# It does NOT build Docker images and it does NOT run unit tests.
version: 0.2
env:
shell: bash
phases:
install:
on-failure: ABORT
runtime-versions:
java: corretto11
nodejs: 14
commands:
- set -o pipefail
- source ci/common/extra-env.sh
- mkdir -pv ci/logs
- source ci/2-scripts/1-install.sh
finally:
- source ci/common/upload-logs.sh
build:
on-failure: ABORT
commands:
- set -o pipefail
- source ci/common/extra-env.sh
- mkdir -pv ci/logs
- source ci/2-scripts/3-build.sh
finally:
- source ci/common/upload-logs.sh
cache:
paths:
- '/root/.cache/Cypress/**/*' # Cypress binary.

40
ci/2-scripts/1-install.sh Normal file
View File

@ -0,0 +1,40 @@
set -o errexit
set -o pipefail
set -o xtrace
{
wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse" \
| tee /etc/apt/sources.list.d/mongodb-org-4.4.list
add-apt-repository --yes ppa:redislabs/redis
apt-get update --yes
# Installing `gettext-base` just for `envsubst` command.
time apt-get install --yes maven gettext-base curl mongodb-org-{server,shell} redis nginx postgresql
mkdir -p "$CODEBUILD_SRC_DIR/logs"
# Start a MongoDB server.
mkdir -p /data/db
nohup mongod > "$CODEBUILD_SRC_DIR/logs/mongod.log" 2>&1 & disown $!
# Start a Redis server.
nohup redis-server > "$CODEBUILD_SRC_DIR/logs/redis.log" 2>&1 & disown $!
# Start a PostgreSQL server.
pg_ctlcluster 12 main start
su -c "psql --username=postgres --command=\"alter user postgres with password 'postgres'\"" postgres
PGPASSWORD=postgres psql \
--username=postgres \
--host=localhost \
--single-transaction \
--variable=ON_ERROR_STOP=ON \
--file="$CODEBUILD_SRC_DIR/app/client/cypress/init-pg-dump-for-test.sql"
# Start an nginx server.
nginx
} 2>&1 | tee -a "ci/logs/$CODEBUILD_BATCH_BUILD_IDENTIFIER.log"

167
ci/2-scripts/3-build.sh Normal file
View File

@ -0,0 +1,167 @@
set -o errexit
set -o pipefail
set -o xtrace
curl-fail() {
# Source: <https://superuser.com/a/1641410>.
outfile="$(mktemp)"
local code
code="$(curl --insecure --silent --show-error --output "$outfile" --write-out "%{http_code}" "$@")"
if [[ $code -lt 200 || $code -gt 302 ]] ; then
cat "$outfile" >&2
return 22
fi
cat "$outfile"
rm "$outfile"
# Need below line because cURL doesn't print a final newline and that messes the logs in CloudWatch.
echo
}
{
echo "$BASH_VERSION"
java -version
node --version
aws s3 cp --no-progress "$S3_BUILDS_PREFIX/$BATCH_ID/client-dist.tgz" .
aws s3 cp --no-progress "$S3_BUILDS_PREFIX/$BATCH_ID/server-dist.tgz" .
tar -xaf client-dist.tgz
tar -xaf server-dist.tgz
du -sh client-dist server-dist
echo Building server code
cd "$CODEBUILD_SRC_DIR/server-dist"
export APPSMITH_MONGODB_URI="mongodb://localhost:27017/appsmith"
export APPSMITH_REDIS_URL="redis://localhost:6379"
APPSMITH_ENCRYPTION_SALT=ci-salt-is-white-like-radish \
APPSMITH_ENCRYPTION_PASSWORD=ci-password-is-red-like-carrot \
APPSMITH_CLOUD_SERVICES_BASE_URL='' \
APPSMITH_IS_SELF_HOSTED=false \
java -jar server-*.jar > "$CODEBUILD_SRC_DIR/logs/server.log" 2>&1 &
# Serve the react bundle on a specific port. Nginx will proxy to this port.
echo "127.0.0.1 dev.appsmith.com" | tee -a /etc/hosts
npx serve -s "$CODEBUILD_SRC_DIR/client-dist" -p 3000 > "$CODEBUILD_SRC_DIR/logs/client.log" 2>&1 &
export APPSMITH_DISABLE_TELEMETRY=true
echo Building client code
cd "$CODEBUILD_SRC_DIR/app/client"
npm install -g yarn
time yarn install --frozen-lockfile
if [[ ! -d ~/.cache/Cypress ]]; then
npx cypress install
fi
# Substitute all the env variables in nginx
vars_to_substitute='\$'"$(env | grep -o "^APPSMITH_[A-Z0-9_]\+" | paste -s -d, - | sed 's/,/,\\$/g')"
echo "vars_to_substitute: $vars_to_substitute"
envsubst "$vars_to_substitute" < docker/templates/nginx-app.conf.template \
| sed \
-e 's|\${\(APPSMITH_[A-Z0-9_]*\)}||g' \
-e "s|__APPSMITH_CLIENT_PROXY_PASS__|http://localhost:3000|g" \
-e "s|__APPSMITH_SERVER_PROXY_PASS__|http://localhost:8080|g" \
| tee /etc/nginx/conf.d/app.conf
envsubst "$vars_to_substitute" < docker/templates/nginx-root.conf.template \
| sed \
-e 's|\${\(APPSMITH_[A-Z0-9_]*\)}||g' \
-e 's/user *nginx;/user root;/' \
| tee /etc/nginx/nginx.conf
# Create the SSL files for Nginx. Required for service workers to work properly.
# This is a self-signed certificate, and so when using cURL, we need to add the `-k` or `--insecure` argument.
mkdir -p /etc/certificate
openssl req -x509 -newkey rsa:4096 -nodes \
-out /etc/certificate/dev.appsmith.com.pem \
-keyout /etc/certificate/dev.appsmith.com-key.pem \
-days 365 \
-subj "/O=appsmith/CN=dev.appsmith.com"
if ! /etc/init.d/nginx reload; then
cat /var/log/nginx/error.log
exit 4
fi
echo "Sleeping to let the servers start"
# timeout 20s tail -500 -f "$CODEBUILD_SRC_DIR/logs/server.log" | grep -q 'Mongock has finished'
sleep 30s # TODO: Wait more intelligently, by looking at the log files for a specific line.
if ! curl-fail https://dev.appsmith.com; then
cat /var/log/nginx/access.log
cat /var/log/nginx/error.log
exit 5
fi
if ! mongo --eval 'db.runCommand({ connectionStatus: 1 })' "$APPSMITH_MONGODB_URI"; then
cat "$CODEBUILD_SRC_DIR/logs/mongod.log"
exit 6
fi
if ! curl-fail --verbose localhost:3000; then
cat "$CODEBUILD_SRC_DIR/logs/client.log"
exit 7
fi
if ! curl --insecure --verbose localhost:8080; then
cat "$CODEBUILD_SRC_DIR/logs/server.log"
exit 8
fi
# Create test users.
# Note: The USERNAME values must be valid email addresses, or the signup API calls will fail.
export CYPRESS_USERNAME=cy@example.com
export CYPRESS_PASSWORD=cypas
export CYPRESS_TESTUSERNAME1=cy1@example.com
export CYPRESS_TESTPASSWORD1=cypas1
export CYPRESS_TESTUSERNAME2=cy2@example.com
export CYPRESS_TESTPASSWORD2=cypas2
curl-fail -v -d "email=$CYPRESS_USERNAME" -d "password=$CYPRESS_PASSWORD" 'https://dev.appsmith.com/api/v1/users'
curl-fail -v -d "email=$CYPRESS_TESTUSERNAME1" -d "password=$CYPRESS_TESTPASSWORD1" 'https://dev.appsmith.com/api/v1/users'
curl-fail -v -d "email=$CYPRESS_TESTUSERNAME2" -d "password=$CYPRESS_TESTPASSWORD2" 'https://dev.appsmith.com/api/v1/users'
# Run the Cypress tests
if [[ -z $CYPRESS_RECORD_KEY || -z $CYPRESS_PROJECT_ID ]]; then
echo 'Missing CYPRESS_RECORD_KEY or CYPRESS_PROJECT_ID.' >&2
exit 2
fi
touch ../../.env # Doing this to silence a misleading error message from `cypress/plugins/index.js`.
npx cypress info
# Git information for Cypress: <https://docs.cypress.io/guides/continuous-integration/introduction#Git-information>.
branch="$(git name-rev --name-only HEAD 2>/dev/null)"
if [[ -z $branch ]]; then
echo "Unable to get branch" >&2
git name-rev --name-only HEAD
else
# When this variable is not set, Cypress will try to detect it itself, but it's not very reliable so we try our hand
# at it first.
export COMMIT_INFO_BRANCH="$branch"
fi
# COMMIT_INFO_MESSAGE="$(git log -1 --pretty=%s)" \
# COMMIT_INFO_EMAIL="$(git log -1 --pretty=%ae)" \
# COMMIT_INFO_AUTHOR="$(git log -1 --pretty=%an)" \
# COMMIT_INFO_SHA="$CODEBUILD_RESOLVED_SOURCE_VERSION" \
# COMMIT_INFO_REMOTE="$CODEBUILD_SOURCE_REPO_URL" \
export NO_COLOR=1
if ! npx cypress run --headless --browser chrome \
--record \
--ci-build-id "$CODEBUILD_INITIATOR" \
--parallel \
--group 'Electrons on CodeBuild CI' \
--env 'NODE_ENV=development' \
--tag "$CODEBUILD_WEBHOOK_TRIGGER" \
--spec 'cypress/integration/Smoke_TestSuite/**/*.js'; then
echo "Cypress tests failed, printing backend server logs."
cat "$CODEBUILD_SRC_DIR/logs/server.log"
exit 3
fi
# At end of this script, CodeBuild does some cleanup and without the below line, it throws an error.
unset -f curl-fail
} 2>&1 | tee -a "ci/logs/$CODEBUILD_BATCH_BUILD_IDENTIFIER.log"

28
ci/3-publish.yml Normal file
View File

@ -0,0 +1,28 @@
# This buildspec will build and publish Docker images.
# It does NOT run any tests.
version: 0.2
env:
shell: bash
phases:
build:
on-failure: ABORT
commands:
- set -o pipefail
- source ci/common/extra-env.sh
- mkdir -pv ci/logs
- source ci/3-scripts/3-build.sh
finally:
- source ci/common/upload-logs.sh
post_build:
commands:
- set -o pipefail
- source ci/common/extra-env.sh
- mkdir -pv ci/logs
- source ci/cleanup.sh
finally:
- source ci/common/upload-logs.sh

49
ci/3-scripts/3-build.sh Normal file
View File

@ -0,0 +1,49 @@
# Build Docker images for client and server, and push to registry/registries.
set -o errexit
set -o pipefail
set -o xtrace
{
aws --version
java -version
node --version
docker version
echo "$DOCKER_HUB_PASSWORD" | docker login --username "$DOCKER_HUB_USERNAME" --password-stdin
docker info
image_prefix=""
if [[ -z $DOCKER_HUB_ORGANIZATION ]]; then
image_prefix="$DOCKER_HUB_ORGANIZATION/"
fi
if [[ -z $DOCKER_TAG_NAME ]]; then
DOCKER_TAG_NAME="${CODEBUILD_SOURCE_VERSION:-release}"
fi
aws s3 cp --no-progress "$S3_BUILDS_PREFIX/$BATCH_ID/client-dist.tgz" .
aws s3 cp --no-progress "$S3_BUILDS_PREFIX/$BATCH_ID/server-dist.tgz" .
tar -xaf client-dist.tgz
mv -v client-dist "$CODEBUILD_SRC_DIR/app/client/build"
cd "$CODEBUILD_SRC_DIR/app/client"
docker build --tag "${image_prefix}appsmith-editor:$DOCKER_TAG_NAME" .
echo Building server code
tar -xaf server-dist.tgz
# The following is a horrible attempt at moving the jar files to their original locations, before `build.sh` moving
# them. The Dockerfile expects them to be _kind of_ in these places.
mkdir -p "$CODEBUILD_SRC_DIR/app/server/appsmith-server/target"
mv -v server-dist/server-1.0-SNAPSHOT.jar "$CODEBUILD_SRC_DIR/app/server/appsmith-server/target/"
mkdir -p "$CODEBUILD_SRC_DIR/app/server/appsmith-plugins/dummy"
mv -v server-dist/plugins "$CODEBUILD_SRC_DIR/app/server/appsmith-plugins/dummy/target"
ls "$CODEBUILD_SRC_DIR/app/server/appsmith-server/target" # Should list `server-1.0-SNAPSHOT.jar` only.
ls"$CODEBUILD_SRC_DIR/app/server/appsmith-plugins/dummy/target" # Should list all plugin jar files.
docker build --tag "${image_prefix}appsmith-server:$DOCKER_TAG_NAME" .
docker push "${image_prefix}appsmith-editor:$DOCKER_TAG_NAME"
docker push "${image_prefix}appsmith-server:$DOCKER_TAG_NAME"
} 2>&1 | tee -a "ci/logs/$CODEBUILD_BATCH_BUILD_IDENTIFIER.log"

188
ci/buildspec.yml Normal file
View File

@ -0,0 +1,188 @@
version: 0.2
env:
shell: bash
phases:
install:
runtime-versions:
java: corretto11
nodejs: 14
batch:
fail-fast: false
build-graph:
# Note: Do NOT use `-` in identifier values. There's pain on the other side of doing that.
# Run unit tests on client and server.
- identifier: client_unit_tests
buildspec: ci/1-client-unit-tests.yml
env:
compute-type: BUILD_GENERAL1_MEDIUM
debug-session: true
# # Run unit tests on client and server.
# - identifier: server_unit_tests
# buildspec: ci/1-server-unit-tests.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# # This job doesn't build any Docker images, but the backend tests use Docker to bring up database containers, so
# # we need the privileged mode for that.
# privileged-mode: true
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress01
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress02
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress03
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress04
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress05
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress06
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress07
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress08
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress09
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress10
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress11
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress12
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress13
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress14
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress15
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Run all Cypress tests.
# - identifier: cypress16
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/2-cypress.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# debug-session: true
#
# # Publish
# - identifier: publish
# depend-on: [client_unit_tests, server_unit_tests]
# buildspec: ci/3-publish.yml
# env:
# compute-type: BUILD_GENERAL1_MEDIUM
# privileged-mode: true
# debug-session: true
# depend-on:
# - cypress01
# - cypress02
# - cypress03
# - cypress04
# - cypress05
# - cypress06
# - cypress07
# - cypress08
# - cypress09
# - cypress10
# - cypress11
# - cypress12
# - cypress13
# - cypress14
# - cypress15
# - cypress16

4
ci/common/cleanup.sh Normal file
View File

@ -0,0 +1,4 @@
set -o errexit
set -o xtrace
aws s3 rm --recursive "$S3_BUILDS_PREFIX/$BATCH_ID"

8
ci/common/extra-env.sh Normal file
View File

@ -0,0 +1,8 @@
# This script should be idempotent. Meaning, sourcing it multiple times shouldn't cause problems.
set -o errexit
set -o xtrace
# Replace `/` characters to `--` in the initiator.
# Sample CODEBUILD_INITIATOR: `codebuild-appsmith-ce-service-role/AWSCodeBuild-146ccba7-69a4-42b1-935b-e5ea50fc7535`
BATCH_ID="${CODEBUILD_INITIATOR//\//--}"

5
ci/common/upload-logs.sh Normal file
View File

@ -0,0 +1,5 @@
set -o errexit
set -o xtrace
ls "$CODEBUILD_SRC_DIR/ci/logs"
aws s3 cp --no-progress --recursive "$CODEBUILD_SRC_DIR/ci/logs" "$S3_LOGS_PREFIX/$BATCH_ID/"