Improve deployment process and add login redirect logic

Deployment improvements:
- Add template env files (.envs.example/) for documentation
- Create init-production.sh for one-time server setup
- Create build-activepieces.sh for building/deploying AP image
- Update deploy.sh with --deploy-ap flag
- Make custom-pieces-metadata.sql idempotent
- Update DEPLOYMENT.md with comprehensive instructions

Frontend:
- Redirect logged-in business owners from root domain to tenant dashboard
- Redirect logged-in users from /login to /dashboard on their tenant
- Log out customers on wrong subdomain instead of redirecting

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-20 23:13:56 -05:00
parent 2a33e4cf57
commit f8d8419622
38 changed files with 2471 additions and 396 deletions

View File

@@ -1 +1 @@
1766209168989
1766280110308

View File

@@ -1,11 +1,15 @@
FROM node:20.19-bullseye-slim AS base
# Set environment variables early for better layer caching
# Memory optimizations for low-RAM servers (2GB):
# - Limit Node.js heap to 1536MB to leave room for system
# - Disable NX daemon and cloud to reduce overhead
ENV LANG=en_US.UTF-8 \
LANGUAGE=en_US:en \
LC_ALL=en_US.UTF-8 \
NX_DAEMON=false \
NX_NO_CLOUD=true
NX_NO_CLOUD=true \
NODE_OPTIONS="--max-old-space-size=1536"
# Install all system dependencies in a single layer with cache mounts
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
@@ -63,7 +67,7 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \
COPY . .
# Build all projects including custom pieces
RUN npx nx run-many --target=build --projects=react-ui,server-api,pieces-smoothschedule,pieces-python-code,pieces-ruby-code --configuration production --parallel=2 --skip-nx-cache --verbose
RUN npx nx run-many --target=build --projects=react-ui,server-api,pieces-smoothschedule,pieces-python-code,pieces-ruby-code,pieces-interfaces --configuration production --parallel=2 --skip-nx-cache
# Install production dependencies only for the backend API
RUN --mount=type=cache,target=/root/.bun/install/cache \
@@ -77,6 +81,8 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \
cd ../python-code && \
bun install --production && \
cd ../ruby-code && \
bun install --production && \
cd ../interfaces && \
bun install --production
### STAGE 2: Run ###
@@ -84,24 +90,30 @@ FROM base AS run
WORKDIR /usr/src/app
# Install Nginx and gettext in a single layer with cache mount
# Install Nginx, gettext, and PostgreSQL client in a single layer with cache mount
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends nginx gettext
apt-get install -y --no-install-recommends nginx gettext postgresql-client
# Copy static configuration files first (better layer caching)
COPY nginx.react.conf /etc/nginx/nginx.conf
COPY --from=build /usr/src/app/packages/server/api/src/assets/default.cf /usr/local/etc/isolate
COPY docker-entrypoint.sh .
COPY custom-pieces-metadata.sql .
COPY publish-pieces.sh .
# Create all necessary directories in one layer
# Also create symlink for AP_DEV_PIECES to find pieces in dist folder
# Structure: /packages/pieces/community -> /dist/packages/pieces/community
RUN mkdir -p \
/usr/src/app/dist/packages/server \
/usr/src/app/dist/packages/engine \
/usr/src/app/dist/packages/shared \
/usr/src/app/dist/packages/pieces && \
chmod +x docker-entrypoint.sh
/usr/src/app/dist/packages/pieces \
/usr/src/app/packages/pieces && \
ln -sf /usr/src/app/dist/packages/pieces/community /usr/src/app/packages/pieces/community && \
chmod +x docker-entrypoint.sh publish-pieces.sh
# Copy built artifacts from build stage
COPY --from=build /usr/src/app/LICENSE .

View File

@@ -0,0 +1,123 @@
-- ==============================================================================
-- Custom SmoothSchedule Pieces Metadata
-- ==============================================================================
-- This script registers custom pieces in the Activepieces database.
-- It runs on container startup via publish-pieces.sh.
--
-- IMPORTANT:
-- - Pieces use pieceType=CUSTOM with platformId to avoid being deleted by sync
-- - This script is IDEMPOTENT - safe to run multiple times
-- - If platform doesn't exist yet, this script will silently skip
-- ==============================================================================
-- Get the platform ID dynamically and only proceed if platform exists
DO $$
DECLARE
platform_id varchar(21);
platform_count integer;
BEGIN
-- Check if platform table exists and has data
SELECT COUNT(*) INTO platform_count FROM platform;
IF platform_count = 0 THEN
RAISE NOTICE 'No platform found yet - skipping piece metadata registration';
RAISE NOTICE 'Pieces will be registered on next container restart after platform is created';
RETURN;
END IF;
SELECT id INTO platform_id FROM platform LIMIT 1;
RAISE NOTICE 'Registering custom pieces for platform: %', platform_id;
-- Pin our custom pieces in the platform so they appear first
UPDATE platform
SET "pinnedPieces" = ARRAY[
'@activepieces/piece-smoothschedule',
'@activepieces/piece-python-code',
'@activepieces/piece-ruby-code'
]::varchar[]
WHERE id = platform_id
AND ("pinnedPieces" = '{}' OR "pinnedPieces" IS NULL OR NOT '@activepieces/piece-smoothschedule' = ANY("pinnedPieces"));
-- Delete existing entries for our custom pieces (to avoid ID conflicts)
DELETE FROM piece_metadata WHERE name IN (
'@activepieces/piece-smoothschedule',
'@activepieces/piece-python-code',
'@activepieces/piece-ruby-code',
'@activepieces/piece-interfaces'
);
-- SmoothSchedule piece
INSERT INTO piece_metadata (
id, name, "displayName", "logoUrl", description, version,
"minimumSupportedRelease", "maximumSupportedRelease",
actions, triggers, auth, "pieceType", "packageType", categories, authors, "projectUsage", "platformId"
) VALUES (
'smoothschedule001',
'@activepieces/piece-smoothschedule',
'SmoothSchedule',
'https://api.smoothschedule.com/images/logo-branding.png',
'Scheduling and appointment management for your business',
'0.0.1',
'0.36.1',
'99999.99999.9999',
'{"create_event":{"name":"create_event","displayName":"Create Event","description":"Create a new event/appointment","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"update_event":{"name":"update_event","displayName":"Update Event","description":"Update an existing event","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"cancel_event":{"name":"cancel_event","displayName":"Cancel Event","description":"Cancel an event","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"find_events":{"name":"find_events","displayName":"Find Events","description":"Search for events","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"list_resources":{"name":"list_resources","displayName":"List Resources","description":"List all resources","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"list_services":{"name":"list_services","displayName":"List Services","description":"List all services","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"list_inactive_customers":{"name":"list_inactive_customers","displayName":"List Inactive Customers","description":"List customers who havent booked recently","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"send_email":{"name":"send_email","displayName":"Send Email","description":"Send an email using a SmoothSchedule email template","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"list_email_templates":{"name":"list_email_templates","displayName":"List Email Templates","description":"Get all available email templates","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"custom_api_call":{"name":"custom_api_call","displayName":"Custom API Call","description":"Make a custom API request","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}}}',
'{"event_created":{"name":"event_created","displayName":"Event Created","description":"Triggers when a new event is created","props":{},"type":"WEBHOOK","handshakeConfiguration":{"strategy":"NONE"},"requireAuth":true,"testStrategy":"SIMULATION"},"event_updated":{"name":"event_updated","displayName":"Event Updated","description":"Triggers when an event is updated","props":{},"type":"WEBHOOK","handshakeConfiguration":{"strategy":"NONE"},"requireAuth":true,"testStrategy":"SIMULATION"},"event_cancelled":{"name":"event_cancelled","displayName":"Event Cancelled","description":"Triggers when an event is cancelled","props":{},"type":"WEBHOOK","handshakeConfiguration":{"strategy":"NONE"},"requireAuth":true,"testStrategy":"SIMULATION"},"event_status_changed":{"name":"event_status_changed","displayName":"Event Status Changed","description":"Triggers when event status changes","props":{},"type":"WEBHOOK","handshakeConfiguration":{"strategy":"NONE"},"requireAuth":true,"testStrategy":"SIMULATION"}}',
'{"type":"CUSTOM_AUTH","displayName":"Connection","description":"Connect to your SmoothSchedule account","required":true,"props":{"baseUrl":{"displayName":"API URL","description":"Your SmoothSchedule API URL","required":true,"type":"SECRET_TEXT"},"apiToken":{"displayName":"API Token","description":"Your API token from Settings","required":true,"type":"SECRET_TEXT"}}}',
'CUSTOM',
'REGISTRY',
ARRAY['PRODUCTIVITY', 'SALES_AND_CRM'],
ARRAY['smoothschedule'],
100,
platform_id
);
-- Python Code piece
INSERT INTO piece_metadata (
id, name, "displayName", "logoUrl", description, version,
"minimumSupportedRelease", "maximumSupportedRelease",
actions, triggers, auth, "pieceType", "packageType", categories, authors, "projectUsage", "platformId"
) VALUES (
'pythoncode00001',
'@activepieces/piece-python-code',
'Python Code',
'https://api.smoothschedule.com/images/python-logo.svg',
'Execute custom Python code in your workflows',
'0.0.1',
'0.36.1',
'99999.99999.9999',
'{"run_python":{"name":"run_python","displayName":"Run Python Code","description":"Execute Python code and return results","props":{"code":{"displayName":"Python Code","description":"The Python code to execute","required":true,"type":"LONG_TEXT"}},"requireAuth":false,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}}}',
'{}',
NULL,
'CUSTOM',
'REGISTRY',
ARRAY['DEVELOPER_TOOLS'],
ARRAY['smoothschedule'],
0,
platform_id
);
-- Ruby Code piece
INSERT INTO piece_metadata (
id, name, "displayName", "logoUrl", description, version,
"minimumSupportedRelease", "maximumSupportedRelease",
actions, triggers, auth, "pieceType", "packageType", categories, authors, "projectUsage", "platformId"
) VALUES (
'rubycode000001',
'@activepieces/piece-ruby-code',
'Ruby Code',
'https://api.smoothschedule.com/images/ruby-logo.svg',
'Execute custom Ruby code in your workflows',
'0.0.1',
'0.36.1',
'99999.99999.9999',
'{"run_ruby":{"name":"run_ruby","displayName":"Run Ruby Code","description":"Execute Ruby code and return results","props":{"code":{"displayName":"Ruby Code","description":"The Ruby code to execute","required":true,"type":"LONG_TEXT"}},"requireAuth":false,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}}}',
'{}',
NULL,
'CUSTOM',
'REGISTRY',
ARRAY['DEVELOPER_TOOLS'],
ARRAY['smoothschedule'],
0,
platform_id
);
END $$;

View File

@@ -12,6 +12,10 @@ echo "AP_FAVICON_URL: $AP_FAVICON_URL"
envsubst '${AP_APP_TITLE} ${AP_FAVICON_URL}' < /usr/share/nginx/html/index.html > /usr/share/nginx/html/index.html.tmp && \
mv /usr/share/nginx/html/index.html.tmp /usr/share/nginx/html/index.html
# Register custom pieces (publish to Verdaccio and insert metadata)
if [ -f /usr/src/app/publish-pieces.sh ]; then
/usr/src/app/publish-pieces.sh || echo "Warning: Custom pieces registration had issues"
fi
# Start Nginx server
nginx -g "daemon off;" &

View File

@@ -31,7 +31,7 @@ http {
proxy_send_timeout 900s;
}
location ~* ^/(?!api/).*.(css|js|jpg|jpeg|png|gif|ico|svg)$ {
location ~* ^/(?!api/).*\.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
root /usr/share/nginx/html;
add_header Expires "0";
add_header Cache-Control "public, max-age=31536000, immutable";

View File

@@ -0,0 +1,4 @@
{
"name": "@activepieces/piece-interfaces",
"version": "0.0.1"
}

View File

@@ -0,0 +1,50 @@
{
"name": "pieces-interfaces",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/pieces/community/interfaces/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": [
"{options.outputPath}"
],
"options": {
"outputPath": "dist/packages/pieces/community/interfaces",
"tsConfig": "packages/pieces/community/interfaces/tsconfig.lib.json",
"packageJson": "packages/pieces/community/interfaces/package.json",
"main": "packages/pieces/community/interfaces/src/index.ts",
"assets": [],
"buildableProjectDepsInPackageJsonType": "dependencies",
"updateBuildableProjectDepsInPackageJson": true
},
"dependsOn": [
"^build",
"prebuild"
]
},
"publish": {
"command": "node tools/scripts/publish.mjs pieces-interfaces {args.ver} {args.tag}",
"dependsOn": [
"build"
]
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": [
"{options.outputFile}"
]
},
"prebuild": {
"executor": "nx:run-commands",
"options": {
"cwd": "packages/pieces/community/interfaces",
"command": "bun install --no-save --silent"
},
"dependsOn": [
"^build"
]
}
},
"tags": []
}

View File

@@ -0,0 +1,14 @@
import { createPiece, PieceAuth } from '@activepieces/pieces-framework';
import { PieceCategory } from '@activepieces/shared';
export const interfaces = createPiece({
displayName: 'Interfaces',
description: 'Create custom forms and interfaces for your workflows.',
auth: PieceAuth.None(),
categories: [PieceCategory.CORE],
minimumSupportedRelease: '0.52.0',
logoUrl: 'https://cdn.activepieces.com/pieces/interfaces.svg',
authors: ['activepieces'],
actions: [],
triggers: [],
});

View File

@@ -0,0 +1,19 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

View File

@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
}

View File

@@ -5,6 +5,8 @@ import { createEventAction, findEventsAction, updateEventAction, cancelEventActi
import { listResourcesAction } from './lib/actions/list-resources';
import { listServicesAction } from './lib/actions/list-services';
import { listInactiveCustomersAction } from './lib/actions/list-inactive-customers';
import { sendEmailAction } from './lib/actions/send-email';
import { listEmailTemplatesAction } from './lib/actions/list-email-templates';
import { eventCreatedTrigger, eventUpdatedTrigger, eventCancelledTrigger, eventStatusChangedTrigger } from './lib/triggers';
import { API_URL } from './lib/common';
@@ -75,6 +77,8 @@ export const smoothSchedule = createPiece({
listResourcesAction,
listServicesAction,
listInactiveCustomersAction,
sendEmailAction,
listEmailTemplatesAction,
createCustomApiCallAction({
auth: smoothScheduleAuth,
baseUrl: (auth) => (auth as SmoothScheduleAuth)?.props?.baseUrl ?? '',

View File

@@ -0,0 +1,23 @@
import { createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { makeRequest } from '../common';
export const listEmailTemplatesAction = createAction({
auth: smoothScheduleAuth,
name: 'list_email_templates',
displayName: 'List Email Templates',
description: 'Get all available email templates (system and custom)',
props: {},
async run(context) {
const auth = context.auth as SmoothScheduleAuth;
const response = await makeRequest(
auth,
HttpMethod.GET,
'/emails/templates/'
);
return response;
},
});

View File

@@ -0,0 +1,112 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { makeRequest } from '../common';
export const sendEmailAction = createAction({
auth: smoothScheduleAuth,
name: 'send_email',
displayName: 'Send Email',
description: 'Send an email using a SmoothSchedule email template',
props: {
template_type: Property.StaticDropdown({
displayName: 'Template Type',
description: 'Choose whether to use a system template or a custom template',
required: true,
options: {
options: [
{ label: 'System Template', value: 'system' },
{ label: 'Custom Template', value: 'custom' },
],
},
}),
email_type: Property.StaticDropdown({
displayName: 'System Email Type',
description: 'Select a system email template',
required: false,
options: {
options: [
{ label: 'Appointment Confirmation', value: 'appointment_confirmation' },
{ label: 'Appointment Reminder', value: 'appointment_reminder' },
{ label: 'Appointment Rescheduled', value: 'appointment_rescheduled' },
{ label: 'Appointment Cancelled', value: 'appointment_cancelled' },
{ label: 'Welcome Email', value: 'welcome_email' },
{ label: 'Password Reset', value: 'password_reset' },
{ label: 'Invoice', value: 'invoice' },
{ label: 'Payment Receipt', value: 'payment_receipt' },
{ label: 'Staff Invitation', value: 'staff_invitation' },
{ label: 'Customer Winback', value: 'customer_winback' },
],
},
}),
template_slug: Property.ShortText({
displayName: 'Custom Template Slug',
description: 'The slug/identifier of your custom email template',
required: false,
}),
to_email: Property.ShortText({
displayName: 'Recipient Email',
description: 'The email address to send to',
required: true,
}),
subject_override: Property.ShortText({
displayName: 'Subject Override',
description: 'Override the template subject (optional)',
required: false,
}),
reply_to: Property.ShortText({
displayName: 'Reply-To Email',
description: 'Reply-to email address (optional)',
required: false,
}),
context: Property.Object({
displayName: 'Template Variables',
description: 'Variables to replace in the template (e.g., customer_name, appointment_date)',
required: false,
}),
},
async run(context) {
const { template_type, email_type, template_slug, to_email, subject_override, reply_to, context: templateContext } = context.propsValue;
const auth = context.auth as SmoothScheduleAuth;
// Validate that the right template identifier is provided based on type
if (template_type === 'system' && !email_type) {
throw new Error('System Email Type is required when using System Template');
}
if (template_type === 'custom' && !template_slug) {
throw new Error('Custom Template Slug is required when using Custom Template');
}
// Build the request body
const requestBody: Record<string, unknown> = {
to_email,
};
if (template_type === 'system') {
requestBody['email_type'] = email_type;
} else {
requestBody['template_slug'] = template_slug;
}
if (subject_override) {
requestBody['subject_override'] = subject_override;
}
if (reply_to) {
requestBody['reply_to'] = reply_to;
}
if (templateContext && Object.keys(templateContext).length > 0) {
requestBody['context'] = templateContext;
}
const response = await makeRequest(
auth,
HttpMethod.POST,
'/emails/send/',
requestBody
);
return response;
},
});

View File

@@ -1,6 +1,6 @@
import { t } from 'i18next';
import { Plus, Globe } from 'lucide-react';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useFormContext } from 'react-hook-form';
import { AutoFormFieldWrapper } from '@/app/builder/piece-properties/auto-form-field-wrapper';
@@ -80,6 +80,27 @@ function ConnectionSelect(params: ConnectionSelectProps) {
PropertyExecutionType.DYNAMIC;
const isPLatformAdmin = useIsPlatformAdmin();
// Auto-select connection with autoSelect metadata if no connection is selected
useEffect(() => {
if (isLoadingConnections || !connections?.data) return;
const currentAuth = form.getValues().settings.input.auth;
// Only auto-select if no connection is currently selected
if (currentAuth && removeBrackets(currentAuth)) return;
// Find a connection with autoSelect metadata
const autoSelectConnection = connections.data.find(
(connection) => (connection as any).metadata?.autoSelect === true
);
if (autoSelectConnection) {
form.setValue('settings.input.auth', addBrackets(autoSelectConnection.externalId), {
shouldValidate: true,
shouldDirty: true,
});
}
}, [connections?.data, isLoadingConnections, form]);
return (
<FormField
control={form.control}

View File

@@ -22,8 +22,8 @@ import { ScrollArea } from '@/components/ui/scroll-area';
import { LoadingSpinner } from '@/components/ui/spinner';
import { TemplateCard } from '@/features/templates/components/template-card';
import { TemplateDetailsView } from '@/features/templates/components/template-details-view';
import { useTemplates } from '@/features/templates/hooks/templates-hook';
import { Template, TemplateType } from '@activepieces/shared';
import { useAllTemplates } from '@/features/templates/hooks/templates-hook';
import { Template } from '@activepieces/shared';
const SelectFlowTemplateDialog = ({
children,
@@ -32,9 +32,7 @@ const SelectFlowTemplateDialog = ({
children: React.ReactNode;
folderId: string;
}) => {
const { filteredTemplates, isLoading, search, setSearch } = useTemplates({
type: TemplateType.CUSTOM,
});
const { filteredTemplates, isLoading, search, setSearch } = useAllTemplates();
const carousel = useRef<CarouselApi>();
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
null,

View File

@@ -247,6 +247,23 @@ export const appConnectionService = (log: FastifyBaseLogger) => ({
},
async delete(params: DeleteParams): Promise<void> {
// Check if connection is protected before deleting
const connection = await appConnectionsRepo().findOneBy({
id: params.id,
platformId: params.platformId,
scope: params.scope,
...(params.projectId ? { projectIds: ArrayContains([params.projectId]) } : {}),
})
if (connection?.metadata?.protected) {
throw new ActivepiecesError({
code: ErrorCode.VALIDATION,
params: {
message: 'This connection is protected and cannot be deleted. It is required for SmoothSchedule integration.',
},
})
}
await appConnectionsRepo().delete({
id: params.id,
platformId: params.platformId,

View File

@@ -0,0 +1,164 @@
#!/bin/sh
# Publish custom pieces to Verdaccio and register metadata in database
# This script runs on container startup
set -e
VERDACCIO_URL="${VERDACCIO_URL:-http://verdaccio:4873}"
PIECES_DIR="/usr/src/app/dist/packages/pieces/community"
CUSTOM_PIECES="smoothschedule python-code ruby-code interfaces"
# Wait for Verdaccio to be ready
wait_for_verdaccio() {
echo "Waiting for Verdaccio to be ready..."
max_attempts=30
attempt=0
while [ $attempt -lt $max_attempts ]; do
if curl -sf "$VERDACCIO_URL/-/ping" > /dev/null 2>&1; then
echo "Verdaccio is ready!"
return 0
fi
attempt=$((attempt + 1))
echo "Attempt $attempt/$max_attempts - Verdaccio not ready yet..."
sleep 2
done
echo "Warning: Verdaccio not available after $max_attempts attempts"
return 1
}
# Configure npm/bun to use Verdaccio with authentication
configure_registry() {
echo "Configuring npm registry to use Verdaccio..."
# Register user with Verdaccio first
echo "Registering npm user with Verdaccio..."
RESPONSE=$(curl -sf -X PUT "$VERDACCIO_URL/-/user/org.couchdb.user:publisher" \
-H "Content-Type: application/json" \
-d '{"name":"publisher","password":"publisher","email":"publisher@smoothschedule.com"}' 2>&1) || true
echo "Registration response: $RESPONSE"
# Extract token from response if available
TOKEN=$(echo "$RESPONSE" | node -pe "JSON.parse(require('fs').readFileSync('/dev/stdin').toString()).token" 2>/dev/null || echo "")
if [ -n "$TOKEN" ] && [ "$TOKEN" != "undefined" ]; then
echo "Using token from registration"
cat > ~/.npmrc << EOF
registry=$VERDACCIO_URL
//verdaccio:4873/:_authToken=$TOKEN
EOF
else
echo "Using basic auth"
# Use legacy _auth format (base64 of username:password)
AUTH=$(echo -n "publisher:publisher" | base64)
cat > ~/.npmrc << EOF
registry=$VERDACCIO_URL
//verdaccio:4873/:_auth=$AUTH
always-auth=true
EOF
fi
# Create bunfig.toml for bun
mkdir -p ~/.bun
cat > ~/.bun/bunfig.toml << EOF
[install]
registry = "$VERDACCIO_URL"
EOF
echo "Registry configured: $VERDACCIO_URL"
}
# Publish a piece to Verdaccio
publish_piece() {
piece_name=$1
piece_dir="$PIECES_DIR/$piece_name"
if [ ! -d "$piece_dir" ]; then
echo "Warning: Piece directory not found: $piece_dir"
return 1
fi
cd "$piece_dir"
# Get package name and version
pkg_name=$(node -p "require('./package.json').name")
pkg_version=$(node -p "require('./package.json').version")
echo "Publishing $pkg_name@$pkg_version to Verdaccio..."
# Check if already published
if npm view "$pkg_name@$pkg_version" --registry "$VERDACCIO_URL" > /dev/null 2>&1; then
echo " $pkg_name@$pkg_version already published, skipping..."
return 0
fi
# Publish to Verdaccio (--force to allow republishing)
if npm publish --registry "$VERDACCIO_URL" 2>&1; then
echo " Successfully published $pkg_name@$pkg_version"
else
echo " Warning: Could not publish $pkg_name (may already exist)"
fi
cd /usr/src/app
}
# Insert piece metadata into database
insert_metadata() {
if [ -z "$AP_POSTGRES_HOST" ] || [ -z "$AP_POSTGRES_DATABASE" ]; then
echo "Warning: Database configuration not available, skipping metadata insertion"
return 1
fi
echo "Inserting custom piece metadata into database..."
echo " Host: $AP_POSTGRES_HOST"
echo " Database: $AP_POSTGRES_DATABASE"
echo " User: $AP_POSTGRES_USERNAME"
# Wait for PostgreSQL to be ready
max_attempts=30
attempt=0
while [ $attempt -lt $max_attempts ]; do
if PGPASSWORD="$AP_POSTGRES_PASSWORD" psql -h "$AP_POSTGRES_HOST" -p "${AP_POSTGRES_PORT:-5432}" -U "$AP_POSTGRES_USERNAME" -d "$AP_POSTGRES_DATABASE" -c "SELECT 1" > /dev/null 2>&1; then
break
fi
attempt=$((attempt + 1))
echo "Waiting for PostgreSQL... ($attempt/$max_attempts)"
sleep 2
done
if [ $attempt -eq $max_attempts ]; then
echo "Warning: PostgreSQL not available, skipping metadata insertion"
return 1
fi
# Run the SQL file
PGPASSWORD="$AP_POSTGRES_PASSWORD" psql -h "$AP_POSTGRES_HOST" -p "${AP_POSTGRES_PORT:-5432}" -U "$AP_POSTGRES_USERNAME" -d "$AP_POSTGRES_DATABASE" -f /usr/src/app/custom-pieces-metadata.sql
echo "Piece metadata inserted successfully!"
}
# Main execution
main() {
echo "============================================"
echo "Custom Pieces Registration"
echo "============================================"
# Configure registry first (needed for both Verdaccio and fallback to npm)
if wait_for_verdaccio; then
configure_registry
# Publish each custom piece
for piece in $CUSTOM_PIECES; do
publish_piece "$piece" || true
done
else
echo "Skipping Verdaccio publishing - will use npm registry"
fi
# Insert metadata into database
insert_metadata || true
echo "============================================"
echo "Custom Pieces Registration Complete"
echo "============================================"
}
main "$@"