diff --git a/.dockerignore b/.dockerignore index a2c06ee..c0312b9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -18,4 +18,5 @@ websites example docker temp -tests \ No newline at end of file +tests +executeme.pem \ No newline at end of file diff --git a/.env.example b/.env.example index b747a3a..46eff9d 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,7 @@ -PORT=6000 \ No newline at end of file + +PORT=6000 +DOCKER_USERNAME="" +DOCKER_PASSWORD="" +VSP_HOST="" +VPS_USER="" +VPS_PRIVATE_KEY="example key" \ No newline at end of file diff --git a/.github/setup-and-load-env/action.yml b/.github/setup-and-load-env/action.yml new file mode 100644 index 0000000..af5c440 --- /dev/null +++ b/.github/setup-and-load-env/action.yml @@ -0,0 +1,141 @@ +name: "Setup and Load Environment" +description: "Generates .env from inputs and exports to GITHUB_ENV" + +inputs: + DOCKER_USERNAME: + description: "Docker username" + required: true + PACKAGE_NAME: + description: "Package name" + required: true + PACKAGE_VERSION: + description: "Package version" + required: true + EMAIL: + description: "Email" + required: true + PORT: + description: "Port" + required: true + IMAGE_TAG: + description: "Image tag" + required: true + DOCKER_PASSWORD: + description: "Docker password" + required: true + VPS_HOST: + description: "VPS host" + required: true + VPS_USER: + description: "VPS user" + required: true + VPS_SSH_PRIVATE_KEY: + description: "VPS SSH private key" + required: true + +runs: + using: "composite" + steps: + - name: ๐Ÿ”ง Generate .env file + shell: bash + run: | + ENV_FILE="$GITHUB_WORKSPACE/.env" + echo "๐Ÿ”ง Creating .env file at $ENV_FILE..." + + # Create the .env file with all variables + cat <<'EOF' > "$ENV_FILE" + DOCKER_USERNAME=${{ inputs.DOCKER_USERNAME }} + PACKAGE_NAME=${{ inputs.PACKAGE_NAME }} + PACKAGE_VERSION=${{ inputs.PACKAGE_VERSION }} + EMAIL=${{ inputs.EMAIL }} + + NODE_ENV=development + + BASE_URL=${{ inputs.BASE_URL }} + + PORT=${{ inputs.PORT }} + + IMAGE_TAG=${{ inputs.IMAGE_TAG }} + DOCKER_PASSWORD=${{ inputs.DOCKER_PASSWORD }} + + VPS_HOST=${{ inputs.VPS_HOST }} + VPS_USER=${{ inputs.VPS_USER }} + EOF + + # Handle SSH private key separately (write to file for Docker usage) + echo '${{ inputs.VPS_SSH_PRIVATE_KEY }}' > "$GITHUB_WORKSPACE/deploy_key.pem" + chmod 600 "$GITHUB_WORKSPACE/deploy_key.pem" + echo "VPS_SSH_PRIVATE_KEY_FILE=$GITHUB_WORKSPACE/deploy_key.pem" >> "$ENV_FILE" + + # Verify file creation + if [ -f "$ENV_FILE" ] && [ -s "$ENV_FILE" ]; then + FILE_SIZE=$(wc -c < "$ENV_FILE") + echo "โœ… .env file created successfully (size: ${FILE_SIZE} bytes)" + echo "๐Ÿ“ Location: $ENV_FILE" + echo "๐Ÿ”‘ SSH key written to: ssh_key.pem" + else + echo "โŒ Error: .env file creation failed" + exit 1 + fi + + - name: ๐Ÿ“ค Export variables to GITHUB_ENV (simple method) + shell: bash + env: + DOCKER_USERNAME: ${{ inputs.DOCKER_USERNAME }} + PACKAGE_NAME: ${{ inputs.PACKAGE_NAME }} + PACKAGE_VERSION: ${{ inputs.PACKAGE_VERSION }} + EMAIL: ${{ inputs.EMAIL }} + + BASE_URL: ${{ inputs.BASE_URL }} + PORT: ${{ inputs.PORT }} + + IMAGE_TAG: ${{ inputs.IMAGE_TAG }} + DOCKER_PASSWORD: ${{ inputs.DOCKER_PASSWORD }} + + VPS_HOST: ${{ inputs.VPS_HOST }} + VPS_USER: ${{ inputs.VPS_USER }} + run: | + echo "๐Ÿ“ค Exporting environment variables to GITHUB_ENV using env method..." + + echo "PACKAGE_NAME=$PACKAGE_NAME" >> $GITHUB_ENV + echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> $GITHUB_ENV + echo "DOCKER_USERNAME=$DOCKER_USERNAME" >> $GITHUB_ENV + echo "EMAIL=$EMAIL" >> $GITHUB_ENV + echo "NODE_ENV=development" >> $GITHUB_ENV + + + echo "BASE_URL=$BASE_URL" >> $GITHUB_ENV + echo "PORT=$PORT" >> $GITHUB_ENV + + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV + echo "DOCKER_PASSWORD=$DOCKER_PASSWORD" >> $GITHUB_ENV + + echo "VPS_HOST=$VPS_HOST" >> $GITHUB_ENV + echo "VPS_USER=$VPS_USER" >> $GITHUB_ENV + + echo "โœ… All environment variables exported successfully" + + - name: ๐Ÿ” Verify setup + shell: bash + run: | + ENV_FILE="$GITHUB_WORKSPACE/.env" + + if [ -f "$ENV_FILE" ]; then + VAR_COUNT=$(grep -c '^[A-Z]' "$ENV_FILE" 2>/dev/null || echo "0") + FILE_SIZE=$(wc -c < "$ENV_FILE") + echo "๐ŸŽ‰ Environment setup complete!" + echo "๐Ÿ“ .env file: $ENV_FILE" + echo "๐Ÿ”ข Variables count: $VAR_COUNT" + echo "๐Ÿ“Š File size: ${FILE_SIZE} bytes" + echo "โœ… Ready to use in subsequent steps" + + # Test a few key variables to ensure they're available + echo "๐Ÿงช Testing variable availability:" + echo " PACKAGE_NAME: ${PACKAGE_NAME:-'NOT_SET'}" + echo " PACKAGE_VERSION: ${PACKAGE_VERSION:-'NOT_SET'}" + echo " DOCKER_USERNAME: ${DOCKER_USERNAME:-'NOT_SET'}" + echo " SSH Key file: ${VPS_SSH_PRIVATE_KEY_FILE:-'NOT_SET'}" + else + echo "โŒ Error: .env file missing after generation" + exit 1 + fi diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml new file mode 100644 index 0000000..776f8f0 --- /dev/null +++ b/.github/workflows/cd.yaml @@ -0,0 +1,253 @@ +name: Deployment VPS + +on: + push: + branches: ["main"] + +jobs: + build-and-push: + name: Build & Push Docker Image ๐Ÿ—๏ธ + runs-on: ubuntu-latest + + steps: + - name: Checkout Code ๐Ÿ“ฅ + uses: actions/checkout@v4 + + - name: Set up NodeJs + uses: actions/setup-node@v3 + with: + node-version: "20" + + - name: ๐Ÿ”ง Setup and load environment + uses: ./.github/actions/setup-and-load-env + with: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + PACKAGE_NAME: ${{ secrets.PACKAGE_NAME }} + PACKAGE_VERSION: ${{ secrets.PACKAGE_VERSION }} + EMAIL: ${{ secrets.EMAIL }} + + BASE_URL: ${{ secrets.BASE_URL }} + PORT: ${{ secrets.PORT }} + + IMAGE_TAG: ${{ secrets.IMAGE_TAG }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + + VPS_HOST: ${{ secrets.VPS_HOST }} + VPS_USER: ${{ secrets.VPS_USER }} + VPS_SSH_PRIVATE_KEY: ${{ secrets.VPS_SSH_PRIVATE_KEY }} + + - name: ๐Ÿ“‹ Verify environment variables + run: | + echo "Package name: $PACKAGE_NAME" + echo "Package version: $PACKAGE_VERSION" + echo "Docker image: $IMAGE_TAG" + echo "โœ… Environment variables are accessible" + + - name: Log in to Docker Hub ๐Ÿ”‘ + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: | + echo "๐Ÿ”‘ Logging into Docker Hub..." + echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin + echo "โœ… Successfully logged into Docker Hub" + + - name: Build Docker Image ๐Ÿ”จ + run: | + echo "Building Docker image: $IMAGE_TAG" + docker compose --profile prod build + if [ $? -eq 0 ]; then + echo "โœ… Docker image built successfully" + else + echo "โŒ Failed to build Docker image" + exit 1 + fi + + - name: Push Docker Image ๐Ÿš€ + run: | + echo "Pushing Docker image: $IMAGE_TAG" + docker compose --profile prod push + if [ $? -eq 0 ]; then + echo "โœ… Docker image $IMAGE_TAG pushed successfully!" + else + echo "โŒ Failed to push Docker image" + exit 1 + fi + + deploy: + name: Deploy to VPS with Zero Downtime ๐Ÿ”„ + needs: build-and-push + runs-on: ubuntu-latest + if: ${{ success() && needs.build-and-push.result == 'success' }} + + steps: + - name: Checkout Code ๐Ÿ“ฅ + uses: actions/checkout@v4 + + - name: ๐Ÿ”ง Setup and load environment + uses: ./.github/actions/setup-and-load-env + with: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + PACKAGE_NAME: ${{ secrets.PACKAGE_NAME }} + PACKAGE_VERSION: ${{ secrets.PACKAGE_VERSION }} + EMAIL: ${{ secrets.EMAIL }} + + BASE_URL: ${{ secrets.BASE_URL }} + PORT: ${{ secrets.PORT }} + + IMAGE_TAG: ${{ secrets.IMAGE_TAG }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + + VPS_HOST: ${{ secrets.VPS_HOST }} + VPS_USER: ${{ secrets.VPS_USER }} + VPS_SSH_PRIVATE_KEY: ${{ secrets.VPS_SSH_PRIVATE_KEY }} + + - name: ๐Ÿ“‹ Verify environment variables + run: | + echo "Package name: $PACKAGE_NAME" + echo "Package version: $PACKAGE_VERSION" + echo "Docker image: $IMAGE_TAG" + echo "โœ… Environment variables are accessible" + + - name: Verify Build Outputs ๐Ÿ” + run: | + echo "๐Ÿ” Verifying build job outputs..." + echo "=== BUILD JOB OUTPUTS ===" + echo "package_name: '$PACKAGE_NAME'" + echo "package_version: '$PACKAGE_VERSION'" + echo "image_tag: '$IMAGE_TAG'" + if [ -z "$PACKAGE_TAG" ]; then + echo "โš ๏ธ IMAGE_TAG is empty, using hardcoded fallback in deployment" + else + echo "โœ… IMAGE_TAG is set to '$PACKAGE_TAG'" + fi + echo "=========================" + + - name: Setup SSH ๐Ÿ” + run: | + mkdir -p ~/.ssh + chmod 700 ~/.ssh + + echo "${{secrets.VPS_SSH_PRIVATE_KEY}}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + + ssh-keyscan -H $VPS_HOST >> ~/.ssh/known_hosts + chmod 644 ~/.ssh/known_hosts + + cat < ~/.ssh/config + Host deploy-server + HostName $VPS_HOST + User $VPS_USER + IdentityFile ~/.ssh/deploy_key + StrictHostKeyChecking no + EOF + chmod 600 ~/.ssh/config + + - name: Debug SSH key + run: | + echo "-----BEGIN KEY-----" + head -n 10 ~/.ssh/deploy_key + echo "-----END KEY-----" + + - name: Copy Files to Server ๐Ÿ“ฆ + run: | + echo "Creating directories..." + chmod 600 ~/.ssh/deploy_key + ssh deploy-server "mkdir -p ~/$PACKAGE_NAME/scripts" + echo "Copying files..." + + scp -i ~/.ssh/deploy_key docker-compose.yaml $VPS_USER@$VPS_HOST:~/$PACKAGE_NAME/ + scp -i ~/.ssh/deploy_key .env $VPS_USER@$VPS_HOST:~/$PACKAGE_NAME/ + scp -i ~/.ssh/deploy_key -r docker $VPS_USER@$VPS_HOST:~/$PACKAGE_NAME/ + scp -i ~/.ssh/deploy_key -r scripts $VPS_USER@$VPS_HOST:~/$PACKAGE_NAME/ + + echo "โœ… Files copied successfully" + + - name: Fix permissions on server ๐ŸŒ‹ + run: | + ssh deploy-server "chmod -R +x ~/$PACKAGE_NAME/scripts/*.sh" + + - name: Deploy Application ๐Ÿš€ + run: | + echo "Starting deployment..." + echo "Package: ${{secrets.PACKAGE_NAME}}" + echo "Version: ${{secrets.PACKAGE_VERSION}}" + echo "Image Tag: ${{secrets.IMAGE_TAG}}" + cat .env # Debug: show .env contents + + chmod 600 ~/.ssh/deploy_key + + ssh deploy-server bash << 'DEPLOY_EOF' + set -e + + echo "=== DEPLOYMENT STARTED ===" + echo "Package: ${{secrets.PACKAGE_NAME}}" + echo "Version: ${{secrets.PACKAGE_NAME}}" + echo "==============================" + + cd ~/${{secrets.PACKAGE_NAME}} + + # Install Docker Compose if needed + if [ ! -f ~/.docker/cli-plugins/docker-compose ]; then + echo "Installing Docker Compose..." + mkdir -p ~/.docker/cli-plugins/ + curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose + chmod +x ~/.docker/cli-plugins/docker-compose + fi + + # Login to Docker Hub + echo "Logging into Docker Hub..." + echo "${{secrets.DOCKER_PASSWORD}}" | docker login -u "${{secrets.DOCKER_USERNAME}}" --password-stdin + + # Source environment variables + if [ -f .env ]; then + echo "Sourcing .env file..." + cat .env # Debug: show .env contents on server + source .env + else + echo "โŒ .env file not found" + exit 1 + fi + + # Run the simplified deployment script + echo "Executing zero-downtime deployment..." + if ./scripts/deploy.sh --version "${{secrets.PACKAGE_VERSION}}"; then + echo "โœ… Deployment successful" + else + echo "โŒ Deployment failed - automatic rollback should have occurred" + exit 1 + fi + + # Cleanup + docker logout + docker image prune -f + + echo "๐ŸŽ‰ DEPLOYMENT COMPLETED SUCCESSFULLY!" + DEPLOY_EOF + + - name: Verify Deployment โœ… + run: | + echo "Verifying deployment..." + ssh deploy-server bash << 'VERIFY_EOF' + cd ~/${{secrets.PACKAGE_NAME}} + + echo "=== Running deployment status check ===" + ./scripts/deploy.sh status + + echo "=== Testing endpoint directly ===" + if curl -f -s --connect-timeout 5 --max-time 10 "http://localhost:${{secrets.PORT}}/" | grep -q '"status":"ok"'; then + echo "๐ŸŽ‰ Endpoint health check passed! Service is responding with status: ok" + else + echo "โŒ Endpoint health check failed!" + exit 1 + fi + + echo "=== Final verification ===" + echo "Deployment verified successfully!" + VERIFY_EOF + + - name: Cleanup ๐Ÿงน + if: always() + run: | + rm -rf ~/.ssh/deploy_key* ~/.ssh/config + rm -f .env diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 213ac7c..72d5373 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,7 +18,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: "20" # Or your preferred Node.js version + node-version: "20" - name: Install Node.js dependencies run: npm install @@ -27,20 +27,4 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Build Docker images - # This builds all services defined in your docker-compose.yaml - # It's crucial to build them before running tests. run: docker compose build - - - name: Run Vitest Battle Tests - run: echo "this will handle letter" - # # 1. Start all Docker Compose services (including your Node.js server and language executors). - # # 2. Run the Vitest tests, which will make HTTP requests to your Node.js server. - # # 3. The tests will internally use `docker run --rm` against your language executor images. - # # 4. After tests complete, the `afterAll` hook in your tests will tear down Docker Compose. - # env: - # # Pass HOST_PROJECT_ROOT to your Node.js server if it needs it during tests. - # # In GitHub Actions, GITHUB_WORKSPACE is the root of your checked-out repo. - # HOST_PROJECT_ROOT: ${{ github.workspace }} - # # Ensure your Node.js server uses the correct port for testing - # PORT: 9091 - # run: npm test diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml deleted file mode 100644 index 9f65a18..0000000 --- a/.github/workflows/deploy.yaml +++ /dev/null @@ -1,45 +0,0 @@ -name: Deployment VPS - -on: - push: - branches: ["main"] - -env: - VPS_HOST: ${{ secrets.VPS_HOST }} - VPS_SSH: ${{ secrets.VPS_SSH }} - VPS_USER: ${{ secrets.VPS_USER }} - VPS_PASSWORD: ${{ secrets.VPS_PASSWORD }} - -jobs: - deploy: - name: Deploy to VPS - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Deploy to VPS via SSH - uses: appleboy/ssh-action@v1.0.3 - with: - host: ${{ env.VPS_HOST }} - username: ${{ env.VPS_USER }} - password: ${{ env.VPS_PASSWORD }} - key: ${{ env.VPS_SSH }} - script: | - # Navigate to the executeme directory on the VPS. - cd ~/workspace/executeme - - # make sure we are in executeme directory - # ls -a - - # Pull the latest code from the 'main' branch of the GitHub repository. - git pull origin main - - # Check git status to make sure everything is up to date - # git status - - # Execute your bash script. - bash ./scripts.sh - - echo "Deployment complete!" diff --git a/.github/workflows/electron-build.yaml b/.github/workflows/electron-build.yaml index ebd7ca9..1f3fd56 100644 --- a/.github/workflows/electron-build.yaml +++ b/.github/workflows/electron-build.yaml @@ -1,10 +1,10 @@ -name: Build/release Electron app +name: ๐Ÿšง Build/๐Ÿš€ Release Electron App on: push: branches: ["main"] tags: - - v*.*.* + - v*.*.* # ๐Ÿ”– Only run on version tags like v0.0.2 jobs: release: @@ -15,37 +15,55 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] steps: - - name: Check out Git repository + - name: ๐Ÿ”„ Check out Git repository uses: actions/checkout@v3 - - name: Install Node.js + - name: ๐Ÿ“ฆ Install Node.js uses: actions/setup-node@v3 with: node-version: 20 - - name: Install Dependencies + - name: ๐Ÿ“ฅ Install Dependencies run: npm install working-directory: desktop - - name: build-linux + # ๐Ÿง Linux Build + - name: ๐Ÿง Build for Linux if: matrix.os == 'ubuntu-latest' run: npm run build:linux working-directory: desktop - - name: build-mac + # ๐ŸŽ macOS Build + - name: ๐ŸŽ Build for macOS if: matrix.os == 'macos-latest' run: npm run build:mac working-directory: desktop - - name: build-win + # ๐ŸชŸ Windows Build + - name: ๐ŸชŸ Build for Windows if: matrix.os == 'windows-latest' run: npm run build:win working-directory: desktop - - name: release + # ๐Ÿ“œ Extract changelog for the current version tag + - name: ๐Ÿ“ Extract changelog for current tag + id: extract_changelog + run: | + TAG_NAME=${{ github.ref_name }} + awk "/## ${TAG_NAME}/,/^## v[0-9]+\.[0-9]+\.[0-9]+/" changelog.md | sed '$d' > changelog.txt + shell: bash + + - name: ๐Ÿ‘€ Show extracted changelog + run: cat changelog.txt + + # ๐Ÿš€ Publish GitHub Release with Assets and Changelog + - name: ๐Ÿš€ Publish GitHub Release uses: softprops/action-gh-release@v1 + if: github.ref_type == 'tag' with: - draft: true + body_path: changelog.txt + repository: devlopersabbir/executeme + token: ${{ secrets.EXECUTE_ME_GITHUB_TOKEN }} files: | desktop/dist/*.exe desktop/dist/*.zip @@ -58,4 +76,4 @@ jobs: desktop/dist/*.yml desktop/dist/*.blockmap env: - GITHUB_TOKEN: ${{ secrets.RELEASE_NOTE_WRITE }} + GITHUB_TOKEN: ${{ secrets.EXECUTE_ME_GITHUB_TOKEN }} diff --git a/.github/workflows/release-tag.yaml b/.github/workflows/release-tag.yaml deleted file mode 100644 index 488a99f..0000000 --- a/.github/workflows/release-tag.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: Create Release - -on: - push: - branches: ["main"] - tags: - - v*.*.* - -jobs: - build: - name: Create Release - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Create Release for Tag - run: echo "TODO: we have to create a Release notes genetor CI" - env: - RELEASE_NOTE_WRITE: ${{ secrets.RELEASE_NOTE_WRITE }} diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 1e7b16f..13f8b2f 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -1,28 +1,19 @@ -# Simple workflow for deploying static content to GitHub Pages name: Deploy static content to Pages on: - # Runs on pushes targeting the default branch push: branches: ["main"] - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. concurrency: group: "pages" cancel-in-progress: false jobs: - # Single deploy job since we're just deploying deploy: environment: name: github-pages @@ -31,13 +22,15 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Setup Pages uses: actions/configure-pages@v5 + - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: # Upload entire repository - path: './browsers' + path: "./browsers" - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 0c140c0..a107c0b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules temp .env +executeme.pem \ No newline at end of file diff --git a/.secrets b/.secrets deleted file mode 100644 index 3377f1c..0000000 --- a/.secrets +++ /dev/null @@ -1 +0,0 @@ -http://145.223.97.55:6000 \ No newline at end of file diff --git a/app.js b/app.js index 3464f0c..e1e445d 100644 --- a/app.js +++ b/app.js @@ -147,6 +147,7 @@ app.post("/run", async (req, res) => { app.get(["/", "/index", "/index.html"], (_, res) => { res.sendFile(path.join(process.cwd(), "./", "views", "index.html")); }); + app.use((req, res) => { res.status(404); if (req.accepts("html")) { @@ -157,11 +158,13 @@ app.use((req, res) => { res.type("txt").send("404 not found"); } }); + function emitActiveCoder() { const activeUsers = cache.get("active_coders"); io.emit("active_coders", activeUsers); } -const PORT = process.env.PORT || 9091; -server.listen(PORT, () => { + +const PORT = process.env.PORT || 6000; +server.listen(Number(PORT), () => { console.log(`Code executor backend listening on port ${PORT}`); }); diff --git a/docker-compose.yaml b/docker-compose.yaml index 3e40b99..f018864 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,35 +1,48 @@ services: nodejs-image: + profile: + - prod build: context: . dockerfile: docker/Dockerfile.nodejs image: executor-nodejs:latest deno-image: + profile: + - prod build: context: . dockerfile: docker/Dockerfile.deno image: executor-deno:latest python-image: + profile: + - prod build: context: . dockerfile: docker/Dockerfile.python image: executor-python:latest java-image: + profile: + - prod build: context: . dockerfile: docker/Dockerfile.jvm image: executor-java:latest kotlin-image: + profile: + - prod build: context: . dockerfile: docker/Dockerfile.kotlin image: executor-kotlin:latest nodejs-server-image: + profile: + - prod + - dev build: context: . dockerfile: Dockerfile @@ -47,6 +60,8 @@ services: restart: unless-stopped executeme-nginx: + profile: + - prod image: nginx:alpine ports: - "9292:9292" diff --git a/package.json b/package.json index 9a05d85..cc6bfb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "executeme", - "version": "0.0.2", + "displayName": "Executeme Server", + "version": "0.0.3", "description": "A robust and secure backend service for executing user-submitted code in isolated Docker containers. Supports multiple programming languages like Python, Node.js, and Java, with resource limiting for safe execution. Ideal for online compilers, coding platforms, and educational tools.", "scripts": { "dev": "nodemon app.js", @@ -51,6 +52,8 @@ }, "devDependencies": { "dotenv": "^16.6.0", + "dotenv-expand": "^12.0.3", + "node": "^24.8.0", "nodemon": "^3.1.10", "vitest": "^3.2.4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0106664..ad8cf08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,12 @@ importers: dotenv: specifier: ^16.6.0 version: 16.6.0 + dotenv-expand: + specifier: ^12.0.3 + version: 12.0.3 + node: + specifier: ^24.8.0 + version: 24.8.0 nodemon: specifier: ^3.1.10 version: 3.1.10 @@ -470,6 +476,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + dotenv@16.6.0: resolution: {integrity: sha512-Omf1L8paOy2VJhILjyhrhqwLIdstqm1BvcDPKg4NGAlkwEu9ODyrFbvk8UymUOMCT+HXo31jg1lArIrVAAhuGA==} engines: {node: '>=12'} @@ -715,10 +725,18 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + node-bin-setup@1.1.4: + resolution: {integrity: sha512-vWNHOne0ZUavArqPP5LJta50+S8R261Fr5SvGul37HbEDcowvLjwdvd0ZeSr0r2lTSrPxl6okq9QUw8BFGiAxA==} + node-cache@5.1.2: resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==} engines: {node: '>= 8.0.0'} + node@24.8.0: + resolution: {integrity: sha512-96VUgFV6DNxMQwi/V/mwSFLUDId93eHJQIA4XNZ3PTGHdek7EeY1bAHEUX51hpBcA5YT9kU7Yn6LP+6TmsMtlw==} + engines: {npm: '>=5.0.0'} + hasBin: true + nodemon@3.1.10: resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==} engines: {node: '>=10'} @@ -1385,6 +1403,10 @@ snapshots: depd@2.0.0: {} + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.6.0 + dotenv@16.6.0: {} dunder-proto@1.0.1: @@ -1662,10 +1684,16 @@ snapshots: negotiator@1.0.0: {} + node-bin-setup@1.1.4: {} + node-cache@5.1.2: dependencies: clone: 2.1.2 + node@24.8.0: + dependencies: + node-bin-setup: 1.1.4 + nodemon@3.1.10: dependencies: chokidar: 3.6.0 diff --git a/scripts.sh b/scripts.sh index 0f9c019..ceab853 100644 --- a/scripts.sh +++ b/scripts.sh @@ -1,8 +1,144 @@ -## delete all containers including its volumes use -docker rm -vf $(docker ps -aq) +#!/bin/bash -## delete all the images -docker rmi -f $(docker images -aq) +ENV_FILE=".env" -## create container with new force command -docker compose up --force-recreate \ No newline at end of file +# Color constants +GREEN="\033[0;32m" +YELLOW="\033[1;33m" +RED="\033[0;31m" +BLUE="\033[0;34m" +RESET="\033[0m" + +# ------------------------- +# ๐Ÿงฐ Utility Functions +# ------------------------- + +# Remove surrounding quotes and trim whitespace +sanitize_value() { + local val="$1" + val="${val%\"}" # Remove trailing " + val="${val#\"}" # Remove leading " + val="$(echo -e "${val}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" # Trim + echo "$val" +} + +# Read value from .env file by key +read_env() { + local key="$1" + local val=$(grep -E "^${key}=" "$ENV_FILE" | cut -d '=' -f2-) + echo "$(sanitize_value "$val")" +} + +# Set value in env, preserving structure +update_env_file() { + local key="$1" + local value="$2" + local tmp_file=$(mktemp) + + local updated=0 + + while IFS= read -r line || [ -n "$line" ]; do + if [[ "$line" =~ ^[[:space:]]*# || -z "$line" ]]; then + echo "$line" >> "$tmp_file" + continue + fi + + if [[ "$line" =~ ^${key}= ]]; then + echo "${key}=${value}" >> "$tmp_file" + updated=1 + else + echo "$line" >> "$tmp_file" + fi + done < "$ENV_FILE" + + if [[ $updated -eq 0 ]]; then + echo "${key}=${value}" >> "$tmp_file" + fi + + mv "$tmp_file" "$ENV_FILE" +} + +# ------------------------- +# ๐Ÿš€ Main Script +# ------------------------- + +# 1. Check if .env file exists +if [[ ! -f "$ENV_FILE" ]]; then + echo -e "${RED}โŒ Error: $ENV_FILE file not found.${RESET}" + exit 1 +fi + +# 2. Read values from package.json +PACKAGE_NAME=$(sanitize_value "$(node -p "require('./package.json').name || 'empty_name'")") +PACKAGE_VERSION=$(sanitize_value "$(node -p "require('./package.json').version || '0.0.1'")") + +# 3. Read required values from .env +DOCKER_USERNAME=$(read_env "DOCKER_USERNAME") +EMAIL=$(read_env "EMAIL") + +if [[ -z "$DOCKER_USERNAME" || -z "$EMAIL" ]]; then + echo -e "${RED}โŒ Missing DOCKER_USERNAME or EMAIL in .env${RESET}" + exit 1 +else + echo -e "${YELLOW}โœ… Captured DOCKER_USERNAME and EMAIL${RESET}" +fi + +# 4. Compose image tag +IMAGE_TAG="${DOCKER_USERNAME}/${PACKAGE_NAME}:${PACKAGE_VERSION}" + +# 5. Update dynamic values in .env +echo -e "${BLUE}๐Ÿ”„ Updating values in .env...${RESET}" +update_env_file "PACKAGE_NAME" "$PACKAGE_NAME" +update_env_file "PACKAGE_VERSION" "$PACKAGE_VERSION" +update_env_file "IMAGE_TAG" "$IMAGE_TAG" + +# 6. Display summary +echo -e "${GREEN}โœ… Updated values:${RESET}" +echo -e " DOCKER_USERNAME: $DOCKER_USERNAME" +echo -e " PACKAGE_NAME: $PACKAGE_NAME" +echo -e " PACKAGE_VERSION: $PACKAGE_VERSION" +echo -e " EMAIL: $EMAIL" +echo -e " IMAGE_TAG: $IMAGE_TAG" + +# 7. Push to GitHub secrets +echo -e "${BLUE}๐Ÿš€ Uploading secrets to GitHub...${RESET}" + +if ! command -v gh &> /dev/null; then + echo -e "${RED}โŒ GitHub CLI (gh) not found. Please install it.${RESET}" + exit 1 +fi + +if ! gh auth status &> /dev/null; then + echo -e "${RED}โŒ GitHub CLI not authenticated. Run 'gh auth login'.${RESET}" + exit 1 +fi + +while IFS= read -r line || [ -n "$line" ]; do + if [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]]; then + continue + fi + + if [[ "$line" =~ ^([A-Z_][A-Z0-9_]*)=(.*)$ ]]; then + key="${BASH_REMATCH[1]}" + raw_value="${BASH_REMATCH[2]}" + + # Special handling for MAIL_PASS (preserve quotes) + if [[ "$key" == "MAIL_PASS" ]]; then + value="${raw_value}" + [[ "$value" != \"* ]] && value="\"$value\"" + else + value=$(sanitize_value "$raw_value") + fi + + echo -e "${GREEN}โœจ Setting secret:${RESET} ${BLUE}${key}${RESET}" + if gh secret set "$key" --body "$value" &>/dev/null; then + echo -e "${GREEN}โœ… Secret $key set successfully.${RESET}" + else + echo -e "${RED}โŒ Failed to set secret: $key${RESET}" + fi + else + echo -e "${YELLOW}โš ๏ธ Skipping invalid line: $line${RESET}" + fi +done < "$ENV_FILE" + +echo -e "${BLUE}๐ŸŽ‰ Done! All secrets uploaded.${RESET}" diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..62d2764 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,307 @@ +#!/bin/bash + +# Simplified Zero Downtime Deployment Script +# Usage: ./deploy.sh --version [--rollback] [status] + +set -e + +# Configuration +PACKAGE_NAME="${PACKAGE_NAME:-server}" +DOCKER_USERNAME="${DOCKER_USERNAME:-devlopersabbir}" +HEALTH_ENDPOINT="http://localhost:5000/" +HEALTH_TIMEOUT=30 +HEALTH_RETRIES=6 + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Logging functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to show usage +show_usage() { + echo "Usage: $0 [OPTIONS] [COMMAND]" + echo "" + echo "Commands:" + echo " status Show current deployment status" + echo " --version Deploy specified version" + echo " --rollback Rollback to previous version" + echo "" + echo "Options:" + echo " --help Show this help message" + echo "" + echo "Examples:" + echo " $0 --version 0.3.3" + echo " $0 --rollback" + echo " $0 status" +} + +# Function to get current running version +get_current_version() { + if docker ps --filter "name=${PACKAGE_NAME}_api" --format "table {{.Image}}" | grep -v "IMAGE" | head -1 | cut -d':' -f2; then + return 0 + else + echo "none" + return 1 + fi +} + +# Function to get previous version from version history file +get_previous_version() { + local version_file="./deployment_versions.txt" + if [ -f "$version_file" ] && [ -s "$version_file" ]; then + # Get the second to last version (previous version) + tail -n 2 "$version_file" | head -n 1 || echo "none" + else + echo "none" + fi +} + +# Function to save version to history +save_version_to_history() { + local version=$1 + local version_file="./deployment_versions.txt" + echo "$version" >> "$version_file" + # Keep only last 10 versions + tail -n 10 "$version_file" > "${version_file}.tmp" && mv "${version_file}.tmp" "$version_file" +} + +# Simple health check using curl +check_health() { + local container_name=$1 + local max_attempts=${2:-$HEALTH_RETRIES} + local attempt=1 + + log_info "Checking health for container: $container_name" + + while [ $attempt -le $max_attempts ]; do + log_info "Health check attempt $attempt/$max_attempts" + + # Check if container is running first + if ! docker ps --filter "name=$container_name" --format "{{.Names}}" | grep -q "^$container_name$"; then + log_error "Container $container_name is not running" + return 1 + fi + + # Simple curl check for the endpoint + if curl -f -s --connect-timeout 5 --max-time 10 "$HEALTH_ENDPOINT" | grep -q '"status":"ok"'; then + log_success "Health check passed - endpoint returned status: ok" + return 0 + fi + + log_info "Waiting for service to be ready... (${HEALTH_TIMEOUT}s)" + sleep $HEALTH_TIMEOUT + attempt=$((attempt + 1)) + done + + log_error "Health check failed after $max_attempts attempts" + return 1 +} + +# Function to stop and remove container +cleanup_container() { + local container_name=$1 + + if docker ps -a --filter "name=$container_name" --format "{{.Names}}" | grep -q "^$container_name$"; then + log_info "Stopping and removing container: $container_name" + docker stop "$container_name" 2>/dev/null || true + docker rm "$container_name" 2>/dev/null || true + log_success "Container $container_name cleaned up" + fi +} + +# Function to perform rollback +perform_rollback() { + local current_version=$(get_current_version) + local previous_version=$(get_previous_version) + + if [ "$previous_version" = "none" ]; then + log_error "No previous version found for rollback" + return 1 + fi + + log_warning "Rolling back from version $current_version to $previous_version" + + # Stop current container + cleanup_container "${PACKAGE_NAME}_api" + + # Deploy previous version + deploy_version "$previous_version" + + if [ $? -eq 0 ]; then + log_success "Rollback to version $previous_version completed successfully" + return 0 + else + log_error "Rollback failed" + return 1 + fi +} + +# Function to deploy a specific version +deploy_version() { + local version=$1 + local image_tag="${DOCKER_USERNAME}/${PACKAGE_NAME}:${version}" + local new_container="${PACKAGE_NAME}_api" + local old_container="${PACKAGE_NAME}_api_old" + + log_info "Starting deployment of version $version" + log_info "Image: $image_tag" + + # Check if image exists locally or pull it + if ! docker image inspect "$image_tag" > /dev/null 2>&1; then + log_info "Pulling image: $image_tag" + if ! docker pull "$image_tag"; then + log_error "Failed to pull image: $image_tag" + return 1 + fi + fi + + # Rename current container to old if it exists + if docker ps --filter "name=${new_container}" --format "{{.Names}}" | grep -q "^${new_container}$"; then + log_info "Renaming current container to backup" + docker rename "$new_container" "$old_container" 2>/dev/null || true + fi + + # Update .env file with new version + if [ -f .env ]; then + sed -i "s/PACKAGE_VERSION=.*/PACKAGE_VERSION=\"$version\"/" .env + log_info "Updated .env file with version $version" + fi + + # Start new container + log_info "Starting new container with version $version" + if docker compose --profile prod up -d app; then + log_success "New container started successfully" + else + log_error "Failed to start new container" + # Restore old container if it exists + if docker ps -a --filter "name=${old_container}" --format "{{.Names}}" | grep -q "^${old_container}$"; then + log_info "Restoring previous container" + docker rename "$old_container" "$new_container" 2>/dev/null || true + docker start "$new_container" 2>/dev/null || true + fi + return 1 + fi + + # Wait a bit for container to initialize + log_info "Waiting for new container to initialize..." + sleep 15 + + # Perform simple health check + if check_health "$new_container"; then + log_success "New container is healthy" + + # Clean up old container + cleanup_container "$old_container" + + # Save version to history + save_version_to_history "$version" + + log_success "Deployment of version $version completed successfully" + return 0 + else + log_error "New container failed health check, rolling back" + + # Stop and remove failed container + cleanup_container "$new_container" + + # Restore old container if it exists + if docker ps -a --filter "name=${old_container}" --format "{{.Names}}" | grep -q "^${old_container}$"; then + log_info "Restoring previous container" + docker rename "$old_container" "$new_container" + docker start "$new_container" + + # Quick check on restored container + sleep 10 + if curl -f -s --connect-timeout 5 --max-time 10 "$HEALTH_ENDPOINT" | grep -q '"status":"ok"'; then + log_success "Previous container restored successfully" + else + log_warning "Previous container restored but health check uncertain" + fi + fi + + return 1 + fi +} + +# Function to show deployment status +show_status() { + echo "=== Deployment Status ===" + + local current_version=$(get_current_version) + local previous_version=$(get_previous_version) + + echo "Current Version: $current_version" + echo "Previous Version: $previous_version" + echo "" + + # Show running containers + echo "=== Running Containers ===" + docker ps --filter "name=${PACKAGE_NAME}" --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" + echo "" + + # Show health status + echo "=== Health Status ===" + if docker ps --filter "name=${PACKAGE_NAME}_api" --format "{{.Names}}" | grep -q "${PACKAGE_NAME}_api"; then + # Test HTTP endpoint + if curl -f -s --connect-timeout 5 --max-time 10 "$HEALTH_ENDPOINT" | grep -q '"status":"ok"'; then + echo "HTTP Endpoint: โœ… Healthy (status: ok)" + else + echo "HTTP Endpoint: โŒ Unhealthy or not responding" + fi + else + echo "No running containers found" + fi + + echo "" + + # Show recent deployment history + if [ -f "./deployment_versions.txt" ]; then + echo "=== Recent Deployments ===" + tail -n 5 "./deployment_versions.txt" | nl -v1 -s'. ' | sort -r + fi +} + +# Main script logic +case "$1" in + --version) + if [ -z "$2" ]; then + log_error "Version parameter is required" + show_usage + exit 1 + fi + deploy_version "$2" + ;; + --rollback) + perform_rollback + ;; + status) + show_status + ;; + --help) + show_usage + ;; + *) + log_error "Invalid command: $1" + show_usage + exit 1 + ;; +esac \ No newline at end of file diff --git a/scripts/generate-self-signed-cert.sh b/scripts/generate-self-signed-cert.sh new file mode 100644 index 0000000..e353832 --- /dev/null +++ b/scripts/generate-self-signed-cert.sh @@ -0,0 +1,17 @@ +set -e + +CERT_DIR="nginx/selfsigned" +mkdir -p "$CERT_DIR" + +# Set default values +IP=${1:-127.0.0.1} +DAYS=365 + +echo "๐Ÿ” Generating self-signed cert for IP: $IP" + +openssl req -x509 -nodes -days $DAYS -newkey rsa:2048 \ + -keyout "$CERT_DIR/privkey.pem" \ + -out "$CERT_DIR/fullchain.pem" \ + -subj "/CN=$IP" + +echo "โœ… Self-signed certificate created at $CERT_DIR/" \ No newline at end of file diff --git a/scripts/setup-nginx.sh b/scripts/setup-nginx.sh new file mode 100644 index 0000000..a77e4ac --- /dev/null +++ b/scripts/setup-nginx.sh @@ -0,0 +1,132 @@ +#!/bin/bash +set -euo pipefail + +echo "๐Ÿš€ Starting NGINX + HTTPS deployment..." + +# === Load env === +if [[ -f .env ]]; then + echo "Loading environment variables from .env file..." + # cleanup .env + sed -i 's/\r//' .env + sed -i 's/[[:space:]]*$//' .env + sed -i '/^$/d' .env + sed -i '/^#/d' .env + + while IFS='=' read -r key value; do + if [[ -z "$key" || "$key" =~ ^# ]]; then continue; fi + key=$(echo "$key" | xargs) + value=$(echo "$value" | xargs | sed 's/^"\(.*\)"$/\1/' | sed "s/^'\(.*\)'$/\1/") + export "$key=$value" + echo "Setting: $key=$value" + done < .env +fi + +# Defaults +BASE_URL="${BASE_URL:-localhost}" +EMAIL="${EMAIL:-admin@example.com}" +PACKAGE_NAME="${PACKAGE_NAME:-myapp}" +PORT="${PORT:-5056}" + +# Determine if BASE_URL is domain or IP/localhost +if [[ "$BASE_URL" == "localhost" || "$BASE_URL" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + USE_SELF_SIGNED=true + PUBLIC_IP=$(curl -s http://checkip.amazonaws.com) + echo "๐Ÿ“ก Using IP-based access: $PUBLIC_IP" +else + USE_SELF_SIGNED=false +fi + +# Prepare folders +mkdir -p nginx/conf.d nginx/certbot/www nginx/certbot/conf nginx/selfsigned + +# NGINX config +if [[ "$USE_SELF_SIGNED" == true ]]; then + # Generate self-signed cert if not already + if [[ ! -f "nginx/selfsigned/fullchain.pem" ]]; then + bash scripts/generate-self-signed-cert.sh "$PUBLIC_IP" + fi + +cat > nginx/conf.d/default.conf < nginx/conf.d/default.conf </dev/null || true + +# Start base services +docker-compose up -d app nginx + +# Certbot only for domain +if [[ "$USE_SELF_SIGNED" == false ]]; then + CERT_PATH="nginx/certbot/conf/live/$BASE_URL/fullchain.pem" + if [[ ! -f "$CERT_PATH" ]]; then + echo "๐Ÿ” First-time certbot run..." + docker-compose run --rm certbot + else + echo "โœ… SSL already exists โ€” skipping certbot issue." + fi + + docker-compose restart nginx + docker-compose up -d certbot-renew +else + echo "๐Ÿ”’ Self-signed SSL active โ€” skipping certbot." +fi + +echo "โœ… Setup complete:" +echo " โžค Public: https://$([[ $USE_SELF_SIGNED == true ]] && echo $PUBLIC_IP || echo $BASE_URL)" \ No newline at end of file