Archived
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87251ba65b |
@@ -1,47 +0,0 @@
|
|||||||
services:
|
|
||||||
trashscan-db:
|
|
||||||
image: postgres:14-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: ${TRASHSCAN_DB_USER:-trashscan}
|
|
||||||
POSTGRES_PASSWORD: ${TRASHSCAN_DB_PASSWORD:-password}
|
|
||||||
POSTGRES_DB: ${TRASHSCAN_DB_NAME:-trashscan}
|
|
||||||
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C"
|
|
||||||
volumes:
|
|
||||||
- trashscan_db_data:/var/lib/postgresql/data
|
|
||||||
ports:
|
|
||||||
- "5432"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "pg_isready", "-U", "trashscan"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
start_period: 5s
|
|
||||||
|
|
||||||
trashscan-explorer:
|
|
||||||
image: cerc/trashscan-explorer:local
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
trashscan-db:
|
|
||||||
condition: service_healthy
|
|
||||||
environment:
|
|
||||||
NODE_ENV: production
|
|
||||||
DATABASE_URL: ${DATABASE_URL:-postgres://trashscan:password@trashscan-db:5432/trashscan}
|
|
||||||
PORT: ${TRASHSCAN_PORT:-5000}
|
|
||||||
SESSION_SECRET: ${SESSION_SECRET:-change-me-in-production}
|
|
||||||
RPC_URL: ${RPC_URL:-https://rpc.trashscan.io/}
|
|
||||||
RUN_MIGRATIONS: ${RUN_MIGRATIONS:-true}
|
|
||||||
CERC_SCRIPT_DEBUG: ${CERC_SCRIPT_DEBUG}
|
|
||||||
ports:
|
|
||||||
- "${TRASHSCAN_HOST_PORT:-5001}:5000"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "nc", "-z", "localhost", "5000"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 10
|
|
||||||
start_period: 30s
|
|
||||||
extra_hosts:
|
|
||||||
- "host.docker.internal:host-gateway"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
trashscan_db_data:
|
|
||||||
@@ -26,14 +26,8 @@ fi
|
|||||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
WORK_DIR="${1:-/app}"
|
WORK_DIR="${1:-/app}"
|
||||||
|
|
||||||
if [ -f "${WORK_DIR}/build-webapp.sh" ]; then
|
|
||||||
echo "Building webapp with ${WORK_DIR}/build-webapp.sh ..."
|
|
||||||
cd "${WORK_DIR}" || exit 1
|
cd "${WORK_DIR}" || exit 1
|
||||||
|
|
||||||
./build-webapp.sh || exit 1
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "next.config.mjs" ]; then
|
if [ -f "next.config.mjs" ]; then
|
||||||
NEXT_CONFIG_JS="next.config.mjs"
|
NEXT_CONFIG_JS="next.config.mjs"
|
||||||
IMPORT_OR_REQUIRE="import"
|
IMPORT_OR_REQUIRE="import"
|
||||||
|
|||||||
+26
-34
@@ -30,44 +30,36 @@ fi
|
|||||||
CERC_WEBAPP_FILES_DIR="${CERC_WEBAPP_FILES_DIR:-/app}"
|
CERC_WEBAPP_FILES_DIR="${CERC_WEBAPP_FILES_DIR:-/app}"
|
||||||
cd "$CERC_WEBAPP_FILES_DIR"
|
cd "$CERC_WEBAPP_FILES_DIR"
|
||||||
|
|
||||||
if [ -f "./run-webapp.sh" ]; then
|
"$SCRIPT_DIR/apply-runtime-env.sh" "`pwd`" .next .next-r
|
||||||
echo "Running webapp with run-webapp.sh ..."
|
mv .next .next.old
|
||||||
cd "${WORK_DIR}" || exit 1
|
mv .next-r/.next .
|
||||||
./run-webapp.sh &
|
|
||||||
tpid=$!
|
|
||||||
wait $tpid
|
|
||||||
else
|
|
||||||
"$SCRIPT_DIR/apply-runtime-env.sh" "`pwd`" .next .next-r
|
|
||||||
mv .next .next.old
|
|
||||||
mv .next-r/.next .
|
|
||||||
|
|
||||||
if [ "$CERC_NEXTJS_SKIP_GENERATE" != "true" ]; then
|
if [ "$CERC_NEXTJS_SKIP_GENERATE" != "true" ]; then
|
||||||
jq -e '.scripts.cerc_generate' package.json >/dev/null
|
jq -e '.scripts.cerc_generate' package.json >/dev/null
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
npm run cerc_generate > gen.out 2>&1 &
|
npm run cerc_generate > gen.out 2>&1 &
|
||||||
tail -f gen.out &
|
tail -f gen.out &
|
||||||
tpid=$!
|
tpid=$!
|
||||||
|
|
||||||
count=0
|
count=0
|
||||||
generate_done="false"
|
generate_done="false"
|
||||||
while [ $count -lt $CERC_MAX_GENERATE_TIME ] && [ "$generate_done" == "false" ]; do
|
while [ $count -lt $CERC_MAX_GENERATE_TIME ] && [ "$generate_done" == "false" ]; do
|
||||||
sleep 1
|
sleep 1
|
||||||
count=$((count + 1))
|
count=$((count + 1))
|
||||||
grep 'rendered as static' gen.out > /dev/null
|
grep 'rendered as static' gen.out > /dev/null
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
generate_done="true"
|
generate_done="true"
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $generate_done != "true" ]; then
|
|
||||||
echo "ERROR: 'npm run cerc_generate' not successful within CERC_MAX_GENERATE_TIME" 1>&2
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
kill $tpid $(ps -ef | grep node | grep next | grep generate | awk '{print $2}') 2>/dev/null
|
if [ $generate_done != "true" ]; then
|
||||||
tpid=""
|
echo "ERROR: 'npm run cerc_generate' not successful within CERC_MAX_GENERATE_TIME" 1>&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
$CERC_BUILD_TOOL start . -- -p ${CERC_LISTEN_PORT:-80}
|
kill $tpid $(ps -ef | grep node | grep next | grep generate | awk '{print $2}') 2>/dev/null
|
||||||
|
tpid=""
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
$CERC_BUILD_TOOL start . -- -p ${CERC_LISTEN_PORT:-80}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
FROM cerc/trashscan-explorer-base:local
|
|
||||||
|
|
||||||
COPY ./scripts/start-explorer.sh /scripts/
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
|
||||||
CMD nc -z localhost 5000 || exit 1
|
|
||||||
|
|
||||||
CMD ["/scripts/start-explorer.sh"]
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
# Multi-stage build for TrashScan Explorer - Base image
|
|
||||||
# Stage 1: Build
|
|
||||||
FROM node:20-bullseye-slim AS builder
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
python3 \
|
|
||||||
make \
|
|
||||||
g++ \
|
|
||||||
git \
|
|
||||||
imagemagick \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Create placeholder assets if missing (required for build)
|
|
||||||
RUN mkdir -p attached_assets && \
|
|
||||||
cd attached_assets && \
|
|
||||||
for f in sticker2_1755759744667.webp sticker23_1755237287920.webp gormain_1759315091728.webp; do \
|
|
||||||
[ ! -f "$f" ] && convert -size 200x200 xc:gray "$f" 2>/dev/null || true; \
|
|
||||||
done && \
|
|
||||||
for f in canvas_1758470431079.png 6940a1305f6e525130bf_1762958059343.png icon_1763016116252.png \
|
|
||||||
image_1763473142686.png image_1763533198459.png trash-coin-with-text_1763537605889.png \
|
|
||||||
trashbun-logo.png Perps_1755290078134.png e6bf0fb3-0a22-485b-acd2-46e987e98c8f_1755633493962.png \
|
|
||||||
LendingBorrwing_1755289821275.png LaunchPad_1755289631741.png breakgorbagana_1755290274068.png \
|
|
||||||
"Gor Swap_1755289631741.png" bridge_1755289631741.png NFTMarketplace_1755289909254.png; do \
|
|
||||||
[ ! -f "$f" ] && convert -size 200x200 xc:gray "$f" 2>/dev/null || true; \
|
|
||||||
done && \
|
|
||||||
for f in gor-incinerator-logo.jpg; do \
|
|
||||||
[ ! -f "$f" ] && convert -size 200x200 xc:gray "$f" 2>/dev/null || true; \
|
|
||||||
done && \
|
|
||||||
[ ! -f "maintenance1_1759314383900.mp3" ] && printf '\xff\xfb\x90\x00' > maintenance1_1759314383900.mp3 || true
|
|
||||||
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Stage 2: Production runtime
|
|
||||||
FROM node:20-bullseye-slim
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
netcat-openbsd \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY --from=builder /app/package*.json ./
|
|
||||||
COPY --from=builder /app/drizzle.config.ts ./
|
|
||||||
COPY --from=builder /app/shared ./shared
|
|
||||||
# Install production deps + vite + drizzle-kit (needed for migrations and server runtime)
|
|
||||||
RUN npm ci --omit=dev && npm install vite drizzle-kit
|
|
||||||
|
|
||||||
COPY --from=builder /app/dist ./dist
|
|
||||||
COPY --from=builder /app/public ./public
|
|
||||||
COPY --from=builder /app/attached_assets ./attached_assets
|
|
||||||
|
|
||||||
EXPOSE 5000
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Build the TrashScan Explorer image
|
|
||||||
source ${CERC_CONTAINER_BASE_DIR}/build-base.sh
|
|
||||||
|
|
||||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
|
||||||
|
|
||||||
# Two-stage build: base image from repo, final image with local scripts
|
|
||||||
docker build -t cerc/trashscan-explorer-base:local \
|
|
||||||
${build_command_args} \
|
|
||||||
-f ${SCRIPT_DIR}/Dockerfile.base \
|
|
||||||
${CERC_REPO_BASE_DIR}/TrashScan-Explorer
|
|
||||||
|
|
||||||
if [[ $? -ne 0 ]]; then
|
|
||||||
echo "FATAL: Base container build failed, exiting"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker build -t cerc/trashscan-explorer:local \
|
|
||||||
${build_command_args} \
|
|
||||||
-f ${SCRIPT_DIR}/Dockerfile \
|
|
||||||
${SCRIPT_DIR}
|
|
||||||
-44
@@ -1,44 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ -n "$CERC_SCRIPT_DEBUG" ]; then
|
|
||||||
set -x
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "TrashScan Explorer starting..."
|
|
||||||
|
|
||||||
# Wait for database to be ready
|
|
||||||
if [ -n "$DATABASE_URL" ]; then
|
|
||||||
echo "Waiting for database to be ready..."
|
|
||||||
|
|
||||||
# Parse DATABASE_URL: postgres://user:pass@host:port/db
|
|
||||||
DB_HOST=$(echo $DATABASE_URL | sed -e 's|.*@||' -e 's|:.*||' -e 's|/.*||')
|
|
||||||
DB_PORT=$(echo $DATABASE_URL | sed -e 's|.*@[^:]*:||' -e 's|/.*||')
|
|
||||||
|
|
||||||
if [ -z "$DB_PORT" ] || [ "$DB_PORT" = "$DB_HOST" ]; then
|
|
||||||
DB_PORT=5432
|
|
||||||
fi
|
|
||||||
|
|
||||||
timeout=60
|
|
||||||
counter=0
|
|
||||||
until nc -z "$DB_HOST" "$DB_PORT" 2>/dev/null; do
|
|
||||||
counter=$((counter + 1))
|
|
||||||
if [ $counter -ge $timeout ]; then
|
|
||||||
echo "Error: Database not available after ${timeout} seconds"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Waiting for database at ${DB_HOST}:${DB_PORT}... ($counter/$timeout)"
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo "Database is available!"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run database migrations if needed
|
|
||||||
if [ "${RUN_MIGRATIONS:-true}" = "true" ]; then
|
|
||||||
echo "Running database migrations..."
|
|
||||||
npx drizzle-kit push --config=drizzle.config.ts 2>&1 || echo "Migration warning (tables may already exist), continuing..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Start the application
|
|
||||||
echo "Starting TrashScan Explorer on port ${PORT:-5000}..."
|
|
||||||
exec node dist/index.js
|
|
||||||
@@ -60,4 +60,3 @@ cerc/nitro-rpc-client
|
|||||||
cerc/watcher-merkl-sushiswap-v3
|
cerc/watcher-merkl-sushiswap-v3
|
||||||
cerc/watcher-sushiswap-v3
|
cerc/watcher-sushiswap-v3
|
||||||
cerc/uniswap-interface
|
cerc/uniswap-interface
|
||||||
cerc/trashscan-explorer
|
|
||||||
|
|||||||
@@ -45,4 +45,3 @@ ponder
|
|||||||
ipld-eth-server-payments
|
ipld-eth-server-payments
|
||||||
merkl-sushiswap-v3
|
merkl-sushiswap-v3
|
||||||
sushiswap-v3
|
sushiswap-v3
|
||||||
trashscan-explorer
|
|
||||||
|
|||||||
@@ -50,4 +50,3 @@ github.com/cerc-io/ponder
|
|||||||
github.com/cerc-io/merkl-sushiswap-v3-watcher-ts
|
github.com/cerc-io/merkl-sushiswap-v3-watcher-ts
|
||||||
github.com/cerc-io/sushiswap-v3-watcher-ts
|
github.com/cerc-io/sushiswap-v3-watcher-ts
|
||||||
github.com/cerc-io/uniswap-interface
|
github.com/cerc-io/uniswap-interface
|
||||||
github.com/gorbagana-dev/TrashScan-Explorer
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
# along with this program. If not, see <http:#www.gnu.org/licenses/>.
|
# along with this program. If not, see <http:#www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from stack_orchestrator.deploy.deployment_context import DeploymentContext
|
from stack_orchestrator.deploy.deployment_context import DeploymentContext
|
||||||
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
|
|
||||||
def create(context: DeploymentContext, extra_args):
|
def create(context: DeploymentContext, extra_args):
|
||||||
@@ -22,12 +23,17 @@ def create(context: DeploymentContext, extra_args):
|
|||||||
# deterministic-deployment-proxy contract, which itself is a prereq for Optimism contract deployment
|
# deterministic-deployment-proxy contract, which itself is a prereq for Optimism contract deployment
|
||||||
fixturenet_eth_compose_file = context.deployment_dir.joinpath('compose', 'docker-compose-fixturenet-eth.yml')
|
fixturenet_eth_compose_file = context.deployment_dir.joinpath('compose', 'docker-compose-fixturenet-eth.yml')
|
||||||
|
|
||||||
|
with open(fixturenet_eth_compose_file, 'r') as yaml_file:
|
||||||
|
yaml = YAML()
|
||||||
|
yaml_data = yaml.load(yaml_file)
|
||||||
|
|
||||||
new_script = '../config/fixturenet-optimism/run-geth.sh:/opt/testnet/run.sh'
|
new_script = '../config/fixturenet-optimism/run-geth.sh:/opt/testnet/run.sh'
|
||||||
|
|
||||||
def add_geth_volume(yaml_data):
|
if new_script not in yaml_data['services']['fixturenet-eth-geth-1']['volumes']:
|
||||||
if new_script not in yaml_data['services']['fixturenet-eth-geth-1']['volumes']:
|
yaml_data['services']['fixturenet-eth-geth-1']['volumes'].append(new_script)
|
||||||
yaml_data['services']['fixturenet-eth-geth-1']['volumes'].append(new_script)
|
|
||||||
|
|
||||||
context.modify_yaml(fixturenet_eth_compose_file, add_geth_volume)
|
with open(fixturenet_eth_compose_file, 'w') as yaml_file:
|
||||||
|
yaml = YAML()
|
||||||
|
yaml.dump(yaml_data, yaml_file)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ version: "1.0"
|
|||||||
name: test
|
name: test
|
||||||
description: "A test stack"
|
description: "A test stack"
|
||||||
repos:
|
repos:
|
||||||
|
- git.vdb.to/cerc-io/laconicd
|
||||||
- git.vdb.to/cerc-io/test-project@test-branch
|
- git.vdb.to/cerc-io/test-project@test-branch
|
||||||
containers:
|
containers:
|
||||||
- cerc/test-container
|
- cerc/test-container
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
# TrashScan Explorer Stack
|
|
||||||
|
|
||||||
TrashScan is a blockchain explorer for Gorbagana mainnet (Solana fork).
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Setup repositories (clones TrashScan-Explorer to ~/cerc/)
|
|
||||||
laconic-so --stack trashscan-explorer setup-repositories
|
|
||||||
|
|
||||||
# 2. Build containers
|
|
||||||
laconic-so --stack trashscan-explorer build-containers
|
|
||||||
|
|
||||||
# 3. Deploy
|
|
||||||
laconic-so --stack trashscan-explorer deploy-system up
|
|
||||||
|
|
||||||
# 4. Verify
|
|
||||||
docker ps --filter "name=trashscan"
|
|
||||||
curl http://localhost:5001/
|
|
||||||
|
|
||||||
# 5. View logs
|
|
||||||
laconic-so --stack trashscan-explorer deploy-system logs -f
|
|
||||||
|
|
||||||
# 6. Stop
|
|
||||||
laconic-so --stack trashscan-explorer deploy-system down
|
|
||||||
```
|
|
||||||
|
|
||||||
## Access
|
|
||||||
|
|
||||||
After deployment, access the explorer at: **http://localhost:5001**
|
|
||||||
|
|
||||||
Note: Default port is 5001 to avoid conflict with macOS AirPlay Receiver on port 5000.
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
| Service | Image | Port | Description |
|
|
||||||
|---------|-------|------|-------------|
|
|
||||||
| trashscan-explorer | cerc/trashscan-explorer:local | 5001 | React/Express blockchain explorer |
|
|
||||||
| trashscan-db | postgres:14-alpine | (internal) | PostgreSQL database |
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Environment variables can be set in your deployment configuration:
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
|
||||||
|----------|---------|-------------|
|
|
||||||
| NODE_ENV | production | Node environment |
|
|
||||||
| DATABASE_URL | postgres://trashscan:password@trashscan-db:5432/trashscan | Database connection string |
|
|
||||||
| TRASHSCAN_HOST_PORT | 5001 | Host port for explorer |
|
|
||||||
| SESSION_SECRET | change-me-in-production | Express session secret |
|
|
||||||
| RPC_URL | https://rpc.trashscan.io/ | Gorbagana RPC endpoint |
|
|
||||||
| RUN_MIGRATIONS | true | Run database migrations on startup |
|
|
||||||
|
|
||||||
## External Dependencies
|
|
||||||
|
|
||||||
The explorer connects to the Gorbagana RPC at https://rpc.trashscan.io/ by default.
|
|
||||||
|
|
||||||
## Files in This Stack
|
|
||||||
|
|
||||||
```
|
|
||||||
stack-orchestrator/stack_orchestrator/data/
|
|
||||||
├── stacks/trashscan-explorer/
|
|
||||||
│ ├── stack.yml # Stack definition
|
|
||||||
│ └── README.md # This file
|
|
||||||
├── container-build/cerc-trashscan-explorer/
|
|
||||||
│ ├── Dockerfile.base # Multi-stage build (base)
|
|
||||||
│ ├── Dockerfile # Final image with scripts
|
|
||||||
│ ├── build.sh # Build script
|
|
||||||
│ └── scripts/
|
|
||||||
│ └── start-explorer.sh # Container startup script
|
|
||||||
└── compose/
|
|
||||||
└── docker-compose-trashscan-explorer.yml # Docker Compose
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verification Checklist
|
|
||||||
|
|
||||||
After deployment, verify:
|
|
||||||
|
|
||||||
- [ ] `docker ps` shows both containers as `(healthy)`
|
|
||||||
- [ ] `curl http://localhost:5001/` returns HTTP 200
|
|
||||||
- [ ] Logs show "TrashScan Explorer starting..." and "Database is available!"
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Port 5000 conflict (macOS)
|
|
||||||
macOS AirPlay Receiver uses port 5000. This stack defaults to 5001.
|
|
||||||
To use a different port: `export TRASHSCAN_HOST_PORT=8080`
|
|
||||||
|
|
||||||
### Missing assets error during build
|
|
||||||
The upstream TrashScan-Explorer repo may be missing the `attached_assets/` directory.
|
|
||||||
Create placeholder images if needed:
|
|
||||||
```bash
|
|
||||||
cd ~/cerc/TrashScan-Explorer
|
|
||||||
mkdir -p attached_assets
|
|
||||||
# Create placeholder images for any missing assets
|
|
||||||
```
|
|
||||||
|
|
||||||
### "Cannot find package 'vite'" error
|
|
||||||
The Dockerfile.base includes vite in production deps to handle this.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
version: "1.0"
|
|
||||||
name: trashscan-explorer
|
|
||||||
description: "TrashScan blockchain explorer for Gorbagana mainnet"
|
|
||||||
repos:
|
|
||||||
- github.com/gorbagana-dev/TrashScan-Explorer
|
|
||||||
containers:
|
|
||||||
- cerc/trashscan-explorer
|
|
||||||
pods:
|
|
||||||
- trashscan-explorer
|
|
||||||
@@ -45,22 +45,20 @@ class DeploymentContext:
|
|||||||
def get_compose_dir(self):
|
def get_compose_dir(self):
|
||||||
return self.deployment_dir.joinpath(constants.compose_dir_name)
|
return self.deployment_dir.joinpath(constants.compose_dir_name)
|
||||||
|
|
||||||
def get_compose_file(self, name: str):
|
|
||||||
return self.get_compose_dir() / f"docker-compose-{name}.yml"
|
|
||||||
|
|
||||||
def get_cluster_id(self):
|
def get_cluster_id(self):
|
||||||
return self.id
|
return self.id
|
||||||
|
|
||||||
def init(self, dir: Path):
|
def init(self, dir):
|
||||||
self.deployment_dir = dir.absolute()
|
self.deployment_dir = dir
|
||||||
self.spec = Spec()
|
self.spec = Spec()
|
||||||
self.spec.init_from_file(self.get_spec_file())
|
self.spec.init_from_file(self.get_spec_file())
|
||||||
self.stack = Stack(self.spec.obj["stack"])
|
self.stack = Stack(self.spec.obj["stack"])
|
||||||
self.stack.init_from_file(self.get_stack_file())
|
self.stack.init_from_file(self.get_stack_file())
|
||||||
deployment_file_path = self.get_deployment_file()
|
deployment_file_path = self.get_deployment_file()
|
||||||
if deployment_file_path.exists():
|
if deployment_file_path.exists():
|
||||||
obj = get_yaml().load(open(deployment_file_path, "r"))
|
with deployment_file_path:
|
||||||
self.id = obj[constants.cluster_id_key]
|
obj = get_yaml().load(open(deployment_file_path, "r"))
|
||||||
|
self.id = obj[constants.cluster_id_key]
|
||||||
# Handle the case of a legacy deployment with no file
|
# Handle the case of a legacy deployment with no file
|
||||||
# Code below is intended to match the output from _make_default_cluster_name()
|
# Code below is intended to match the output from _make_default_cluster_name()
|
||||||
# TODO: remove when we no longer need to support legacy deployments
|
# TODO: remove when we no longer need to support legacy deployments
|
||||||
@@ -69,19 +67,3 @@ class DeploymentContext:
|
|||||||
unique_cluster_descriptor = f"{path},{self.get_stack_file()},None,None"
|
unique_cluster_descriptor = f"{path},{self.get_stack_file()},None,None"
|
||||||
hash = hashlib.md5(unique_cluster_descriptor.encode()).hexdigest()[:16]
|
hash = hashlib.md5(unique_cluster_descriptor.encode()).hexdigest()[:16]
|
||||||
self.id = f"{constants.cluster_name_prefix}{hash}"
|
self.id = f"{constants.cluster_name_prefix}{hash}"
|
||||||
|
|
||||||
def modify_yaml(self, file_path: Path, modifier_func):
|
|
||||||
"""
|
|
||||||
Load a YAML from the deployment, apply a modification function, and write it back.
|
|
||||||
"""
|
|
||||||
if not file_path.absolute().is_relative_to(self.deployment_dir):
|
|
||||||
raise ValueError(f"File is not inside deployment directory: {file_path}")
|
|
||||||
|
|
||||||
yaml = get_yaml()
|
|
||||||
with open(file_path, 'r') as f:
|
|
||||||
yaml_data = yaml.load(f)
|
|
||||||
|
|
||||||
modifier_func(yaml_data)
|
|
||||||
|
|
||||||
with open(file_path, 'w') as f:
|
|
||||||
yaml.dump(yaml_data, f)
|
|
||||||
|
|||||||
@@ -443,16 +443,18 @@ def _check_volume_definitions(spec):
|
|||||||
@click.command()
|
@click.command()
|
||||||
@click.option("--spec-file", required=True, help="Spec file to use to create this deployment")
|
@click.option("--spec-file", required=True, help="Spec file to use to create this deployment")
|
||||||
@click.option("--deployment-dir", help="Create deployment files in this directory")
|
@click.option("--deployment-dir", help="Create deployment files in this directory")
|
||||||
@click.argument('extra_args', nargs=-1, type=click.UNPROCESSED)
|
# TODO: Hack
|
||||||
|
@click.option("--network-dir", help="Network configuration supplied in this directory")
|
||||||
|
@click.option("--initial-peers", help="Initial set of persistent peers")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def create(ctx, spec_file, deployment_dir, extra_args):
|
def create(ctx, spec_file, deployment_dir, network_dir, initial_peers):
|
||||||
deployment_command_context = ctx.obj
|
deployment_command_context = ctx.obj
|
||||||
return create_operation(deployment_command_context, spec_file, deployment_dir, extra_args)
|
return create_operation(deployment_command_context, spec_file, deployment_dir, network_dir, initial_peers)
|
||||||
|
|
||||||
|
|
||||||
# The init command's implementation is in a separate function so that we can
|
# The init command's implementation is in a separate function so that we can
|
||||||
# call it from other commands, bypassing the click decoration stuff
|
# call it from other commands, bypassing the click decoration stuff
|
||||||
def create_operation(deployment_command_context, spec_file, deployment_dir, extra_args):
|
def create_operation(deployment_command_context, spec_file, deployment_dir, network_dir, initial_peers):
|
||||||
parsed_spec = Spec(os.path.abspath(spec_file), get_parsed_deployment_spec(spec_file))
|
parsed_spec = Spec(os.path.abspath(spec_file), get_parsed_deployment_spec(spec_file))
|
||||||
_check_volume_definitions(parsed_spec)
|
_check_volume_definitions(parsed_spec)
|
||||||
stack_name = parsed_spec["stack"]
|
stack_name = parsed_spec["stack"]
|
||||||
@@ -539,7 +541,7 @@ def create_operation(deployment_command_context, spec_file, deployment_dir, extr
|
|||||||
deployer_config_generator = getDeployerConfigGenerator(deployment_type, deployment_context)
|
deployer_config_generator = getDeployerConfigGenerator(deployment_type, deployment_context)
|
||||||
# TODO: make deployment_dir_path a Path above
|
# TODO: make deployment_dir_path a Path above
|
||||||
deployer_config_generator.generate(deployment_dir_path)
|
deployer_config_generator.generate(deployment_dir_path)
|
||||||
call_stack_deploy_create(deployment_context, extra_args)
|
call_stack_deploy_create(deployment_context, [network_dir, initial_peers, deployment_command_context])
|
||||||
|
|
||||||
|
|
||||||
# TODO: this code should be in the stack .py files but
|
# TODO: this code should be in the stack .py files but
|
||||||
|
|||||||
@@ -92,8 +92,9 @@ class Spec:
|
|||||||
return self.obj.get(item, default)
|
return self.obj.get(item, default)
|
||||||
|
|
||||||
def init_from_file(self, file_path: Path):
|
def init_from_file(self, file_path: Path):
|
||||||
self.obj = get_yaml().load(open(file_path, "r"))
|
with file_path:
|
||||||
self.file_path = file_path
|
self.obj = get_yaml().load(open(file_path, "r"))
|
||||||
|
self.file_path = file_path
|
||||||
|
|
||||||
def get_image_registry(self):
|
def get_image_registry(self):
|
||||||
return self.obj.get(constants.image_registry_key)
|
return self.obj.get(constants.image_registry_key)
|
||||||
|
|||||||
@@ -27,4 +27,5 @@ class Stack:
|
|||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
def init_from_file(self, file_path: Path):
|
def init_from_file(self, file_path: Path):
|
||||||
self.obj = get_yaml().load(open(file_path, "r"))
|
with file_path:
|
||||||
|
self.obj = get_yaml().load(open(file_path, "r"))
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ def create_deployment(ctx, deployment_dir, image, url, kube_config, image_regist
|
|||||||
spec_file_name,
|
spec_file_name,
|
||||||
deployment_dir,
|
deployment_dir,
|
||||||
None,
|
None,
|
||||||
|
None
|
||||||
)
|
)
|
||||||
# Fix up the container tag inside the deployment compose file
|
# Fix up the container tag inside the deployment compose file
|
||||||
_fixup_container_tag(deployment_dir, image)
|
_fixup_container_tag(deployment_dir, image)
|
||||||
|
|||||||
@@ -342,6 +342,17 @@ def dump_known_requests(filename, requests, status="SEEN"):
|
|||||||
help="Requests must have a minimum payment to be processed (in alnt)",
|
help="Requests must have a minimum payment to be processed (in alnt)",
|
||||||
default=0,
|
default=0,
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--atom-payment-address",
|
||||||
|
help="Cosmos ATOM address to receive payments",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--min-atom-payment",
|
||||||
|
help="Minimum required ATOM payment amount",
|
||||||
|
default=1,
|
||||||
|
type=float,
|
||||||
|
)
|
||||||
@click.option("--lrn", help="The LRN of this deployer.", required=True)
|
@click.option("--lrn", help="The LRN of this deployer.", required=True)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--all-requests",
|
"--all-requests",
|
||||||
@@ -394,6 +405,8 @@ def command( # noqa: C901
|
|||||||
recreate_on_deploy,
|
recreate_on_deploy,
|
||||||
log_dir,
|
log_dir,
|
||||||
min_required_payment,
|
min_required_payment,
|
||||||
|
atom_payment_address,
|
||||||
|
min_atom_payment,
|
||||||
lrn,
|
lrn,
|
||||||
config_upload_dir,
|
config_upload_dir,
|
||||||
private_key_file,
|
private_key_file,
|
||||||
@@ -630,6 +643,8 @@ def command( # noqa: C901
|
|||||||
payment_address,
|
payment_address,
|
||||||
min_required_payment,
|
min_required_payment,
|
||||||
main_logger,
|
main_logger,
|
||||||
|
atom_payment_address,
|
||||||
|
min_atom_payment,
|
||||||
):
|
):
|
||||||
main_logger.log(f"{r.id}: Payment confirmed.")
|
main_logger.log(f"{r.id}: Payment confirmed.")
|
||||||
requests_to_execute.append(r)
|
requests_to_execute.append(r)
|
||||||
|
|||||||
@@ -46,6 +46,17 @@ from stack_orchestrator.deploy.webapp.util import LaconicRegistryClient
|
|||||||
help="List the minimum required payment (in alnt) to process a deployment request.",
|
help="List the minimum required payment (in alnt) to process a deployment request.",
|
||||||
default=0,
|
default=0,
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--atom-payment-address",
|
||||||
|
help="The Cosmos ATOM address to which payments should be made.",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--min-atom-payment",
|
||||||
|
help="List the minimum required payment (in ATOM) to process a deployment request.",
|
||||||
|
default=1,
|
||||||
|
type=float,
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--dry-run",
|
"--dry-run",
|
||||||
help="Don't publish anything, just report what would be done.",
|
help="Don't publish anything, just report what would be done.",
|
||||||
@@ -60,6 +71,8 @@ def command( # noqa: C901
|
|||||||
lrn,
|
lrn,
|
||||||
payment_address,
|
payment_address,
|
||||||
min_required_payment,
|
min_required_payment,
|
||||||
|
atom_payment_address,
|
||||||
|
min_atom_payment,
|
||||||
dry_run,
|
dry_run,
|
||||||
):
|
):
|
||||||
laconic = LaconicRegistryClient(laconic_config)
|
laconic = LaconicRegistryClient(laconic_config)
|
||||||
@@ -84,6 +97,10 @@ def command( # noqa: C901
|
|||||||
"minimumPayment"
|
"minimumPayment"
|
||||||
] = f"{min_required_payment}alnt"
|
] = f"{min_required_payment}alnt"
|
||||||
|
|
||||||
|
if atom_payment_address:
|
||||||
|
webapp_deployer_record["record"]["atomPaymentAddress"] = atom_payment_address
|
||||||
|
webapp_deployer_record["record"]["minimumAtomPayment"] = min_atom_payment
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
yaml.dump(webapp_deployer_record, sys.stdout)
|
yaml.dump(webapp_deployer_record, sys.stdout)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -801,7 +801,7 @@ def skip_by_tag(r, include_tags, exclude_tags):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def confirm_payment(laconic: LaconicRegistryClient, record, payment_address, min_amount, logger):
|
def confirm_payment(laconic: LaconicRegistryClient, record, payment_address, min_amount, logger, atom_payment_address=None, atom_min_amount=None):
|
||||||
req_owner = laconic.get_owner(record)
|
req_owner = laconic.get_owner(record)
|
||||||
if req_owner == payment_address:
|
if req_owner == payment_address:
|
||||||
# No need to confirm payment if the sender and recipient are the same account.
|
# No need to confirm payment if the sender and recipient are the same account.
|
||||||
@@ -811,67 +811,114 @@ def confirm_payment(laconic: LaconicRegistryClient, record, payment_address, min
|
|||||||
logger.log(f"{record.id}: no payment tx info")
|
logger.log(f"{record.id}: no payment tx info")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Try to verify as a laconic payment first
|
||||||
tx = laconic.get_tx(record.attributes.payment)
|
tx = laconic.get_tx(record.attributes.payment)
|
||||||
if not tx:
|
if tx:
|
||||||
logger.log(f"{record.id}: cannot locate payment tx")
|
if tx.code != 0:
|
||||||
return False
|
logger.log(
|
||||||
|
f"{record.id}: payment tx {tx.hash} was not successful - code: {tx.code}, log: {tx.log}"
|
||||||
if tx.code != 0:
|
)
|
||||||
logger.log(
|
|
||||||
f"{record.id}: payment tx {tx.hash} was not successful - code: {tx.code}, log: {tx.log}"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if tx.sender != req_owner:
|
|
||||||
logger.log(
|
|
||||||
f"{record.id}: payment sender {tx.sender} in tx {tx.hash} does not match deployment "
|
|
||||||
f"request owner {req_owner}"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if tx.recipient != payment_address:
|
|
||||||
logger.log(
|
|
||||||
f"{record.id}: payment recipient {tx.recipient} in tx {tx.hash} does not match {payment_address}"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
pay_denom = "".join([i for i in tx.amount if not i.isdigit()])
|
|
||||||
if pay_denom != "alnt":
|
|
||||||
logger.log(
|
|
||||||
f"{record.id}: {pay_denom} in tx {tx.hash} is not an expected payment denomination"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
pay_amount = int("".join([i for i in tx.amount if i.isdigit()]))
|
|
||||||
if pay_amount < min_amount:
|
|
||||||
logger.log(
|
|
||||||
f"{record.id}: payment amount {tx.amount} is less than minimum {min_amount}"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check if the payment was already used on a deployment
|
|
||||||
used = laconic.app_deployments(
|
|
||||||
{"deployer": record.attributes.deployer, "payment": tx.hash}, all=True
|
|
||||||
)
|
|
||||||
if len(used):
|
|
||||||
# Fetch the app name from request record
|
|
||||||
used_request = laconic.get_record(used[0].attributes.request, require=True)
|
|
||||||
|
|
||||||
# Check that payment was used for deployment of same application
|
|
||||||
if record.attributes.application != used_request.attributes.application:
|
|
||||||
logger.log(f"{record.id}: payment {tx.hash} already used on a different application deployment {used}")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
used = laconic.app_deployment_removals(
|
if tx.sender != req_owner:
|
||||||
{"deployer": record.attributes.deployer, "payment": tx.hash}, all=True
|
logger.log(
|
||||||
)
|
f"{record.id}: payment sender {tx.sender} in tx {tx.hash} does not match deployment "
|
||||||
if len(used):
|
f"request owner {req_owner}"
|
||||||
logger.log(
|
)
|
||||||
f"{record.id}: payment {tx.hash} already used on deployment removal {used}"
|
return False
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
if tx.recipient != payment_address:
|
||||||
|
logger.log(
|
||||||
|
f"{record.id}: payment recipient {tx.recipient} in tx {tx.hash} does not match {payment_address}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
pay_denom = "".join([i for i in tx.amount if not i.isdigit()])
|
||||||
|
if pay_denom != "alnt":
|
||||||
|
logger.log(
|
||||||
|
f"{record.id}: {pay_denom} in tx {tx.hash} is not an expected payment denomination"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
pay_amount = int("".join([i for i in tx.amount if i.isdigit()]))
|
||||||
|
if pay_amount < min_amount:
|
||||||
|
logger.log(
|
||||||
|
f"{record.id}: payment amount {tx.amount} is less than minimum {min_amount}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if the payment was already used on a deployment
|
||||||
|
used = laconic.app_deployments(
|
||||||
|
{"deployer": record.attributes.deployer, "payment": tx.hash}, all=True
|
||||||
|
)
|
||||||
|
if len(used):
|
||||||
|
# Fetch the app name from request record
|
||||||
|
used_request = laconic.get_record(used[0].attributes.request, require=True)
|
||||||
|
|
||||||
|
# Check that payment was used for deployment of same application
|
||||||
|
if record.attributes.application != used_request.attributes.application:
|
||||||
|
logger.log(f"{record.id}: payment {tx.hash} already used on a different application deployment {used}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
used = laconic.app_deployment_removals(
|
||||||
|
{"deployer": record.attributes.deployer, "payment": tx.hash}, all=True
|
||||||
|
)
|
||||||
|
if len(used):
|
||||||
|
logger.log(
|
||||||
|
f"{record.id}: payment {tx.hash} already used on deployment removal {used}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# If we get here, the transaction hash wasn't found in the laconic testnet
|
||||||
|
# Let's check if it's a valid Cosmos ATOM payment if configuration is available
|
||||||
|
if atom_payment_address:
|
||||||
|
logger.log(f"{record.id}: checking if payment is a valid Cosmos ATOM transaction")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Use the webapp-deployment-status-api to verify the ATOM payment
|
||||||
|
deployer_record = laconic.get_record(record.attributes.deployer)
|
||||||
|
if not deployer_record or not deployer_record.attributes.apiUrl:
|
||||||
|
logger.log(f"{record.id}: cannot find deployer API URL to verify ATOM payment")
|
||||||
|
return False
|
||||||
|
|
||||||
|
api_url = deployer_record.attributes.apiUrl
|
||||||
|
verify_url = f"{api_url}/verify/atom-payment"
|
||||||
|
|
||||||
|
# Make a request to the API to verify the ATOM payment
|
||||||
|
# Pass markAsUsed=true to prevent this transaction from being used again
|
||||||
|
response = requests.post(
|
||||||
|
verify_url,
|
||||||
|
json={
|
||||||
|
"txHash": record.attributes.payment,
|
||||||
|
"minAmount": atom_min_amount,
|
||||||
|
"markAsUsed": True
|
||||||
|
},
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.log(f"{record.id}: ATOM payment verification API request failed with status {response.status_code}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
if not result.get("valid", False):
|
||||||
|
logger.log(f"{record.id}: ATOM payment verification failed: {result.get('reason', 'unknown reason')}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Payment is valid
|
||||||
|
logger.log(f"{record.id}: ATOM payment verified successfully, amount: {result.get('amount')} ATOM")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.log(f"{record.id}: error verifying ATOM payment: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.log(f"{record.id}: payment tx {record.attributes.payment} not found in laconic testnet and ATOM payment verification not configured")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def confirm_auction(laconic: LaconicRegistryClient, record, deployer_lrn, payment_address, logger):
|
def confirm_auction(laconic: LaconicRegistryClient, record, deployer_lrn, payment_address, logger):
|
||||||
|
|||||||
@@ -180,7 +180,9 @@ def get_k8s_dir():
|
|||||||
def get_parsed_deployment_spec(spec_file):
|
def get_parsed_deployment_spec(spec_file):
|
||||||
spec_file_path = Path(spec_file)
|
spec_file_path = Path(spec_file)
|
||||||
try:
|
try:
|
||||||
return get_yaml().load(open(spec_file_path, "r"))
|
with spec_file_path:
|
||||||
|
deploy_spec = get_yaml().load(open(spec_file_path, "r"))
|
||||||
|
return deploy_spec
|
||||||
except FileNotFoundError as error:
|
except FileNotFoundError as error:
|
||||||
# We try here to generate a useful diagnostic error
|
# We try here to generate a useful diagnostic error
|
||||||
print(f"Error: spec file: {spec_file_path} does not exist")
|
print(f"Error: spec file: {spec_file_path} does not exist")
|
||||||
|
|||||||
@@ -14,13 +14,8 @@ delete_cluster_exit () {
|
|||||||
|
|
||||||
# Test basic stack-orchestrator deploy
|
# Test basic stack-orchestrator deploy
|
||||||
echo "Running stack-orchestrator deploy test"
|
echo "Running stack-orchestrator deploy test"
|
||||||
|
# Bit of a hack, test the most recent package
|
||||||
if [ "$1" == "from-path" ]; then
|
TEST_TARGET_SO=$( ls -t1 ./package/laconic-so* | head -1 )
|
||||||
TEST_TARGET_SO="laconic-so"
|
|
||||||
else
|
|
||||||
TEST_TARGET_SO=$( ls -t1 ./package/laconic-so* | head -1 )
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set a non-default repo dir
|
# Set a non-default repo dir
|
||||||
export CERC_REPO_BASE_DIR=~/stack-orchestrator-test/repo-base-dir
|
export CERC_REPO_BASE_DIR=~/stack-orchestrator-test/repo-base-dir
|
||||||
echo "Testing this package: $TEST_TARGET_SO"
|
echo "Testing this package: $TEST_TARGET_SO"
|
||||||
|
|||||||
Reference in New Issue
Block a user