diff --git a/.github/workflows/supreme-ai-deploy.yml b/.github/workflows/supreme-ai-deploy.yml new file mode 100644 index 0000000..3214c05 --- /dev/null +++ b/.github/workflows/supreme-ai-deploy.yml @@ -0,0 +1,185 @@ +name: Reusable Supreme AI Deploy + +on: + workflow_call: + inputs: + node-version: + description: Node.js version used during validation + required: false + default: '20' + type: string + app-port: + description: Container application port + required: false + default: 3000 + type: number + host-port: + description: Host port exposed on the deploy server + required: false + default: 80 + type: number + bind-ip: + description: Bind address on deploy host (127.0.0.1 recommended) + required: false + default: '127.0.0.1' + type: string + healthcheck-url: + description: Health endpoint URL checked from deploy host + required: false + default: 'http://localhost/health' + type: string + secrets: + DEPLOY_HOST: + required: true + DEPLOY_USER: + required: true + DEPLOY_SSH_KEY: + required: true + GHCR_USERNAME: + required: true + GHCR_TOKEN: + required: true + + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: supreme-ai-deploy-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + CONTAINER_NAME: supreme-ai-hub + +jobs: + validate: + name: Validate, test, and build + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version || '20' }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint --if-present + + - name: Unit tests + run: npm test --if-present -- --ci + + - name: Build + run: npm run build --if-present + + containerize: + name: Build and push image + runs-on: ubuntu-latest + needs: validate + timeout-minutes: 30 + permissions: + contents: read + packages: write + outputs: + image_tag: ${{ steps.vars.outputs.image_tag }} + image_repo: ${{ steps.vars.outputs.image_repo }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set image variables + id: vars + run: | + IMAGE_REPO=$(echo "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]') + echo "image_repo=${IMAGE_REPO}" >> "$GITHUB_OUTPUT" + echo "image_tag=sha-${GITHUB_SHA}" >> "$GITHUB_OUTPUT" + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ steps.vars.outputs.image_repo }} + tags: | + type=raw,value=${{ steps.vars.outputs.image_tag }} + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + deploy: + name: Deploy to server + runs-on: ubuntu-latest + needs: containerize + timeout-minutes: 20 + environment: + name: production + + steps: + - name: Deploy and verify + uses: appleboy/ssh-action@v1.0.3 + env: + IMAGE_REPO: ${{ needs.containerize.outputs.image_repo }} + IMAGE_TAG: ${{ needs.containerize.outputs.image_tag }} + GHCR_USER: ${{ secrets.GHCR_USERNAME }} + GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} + HEALTHCHECK_URL: ${{ inputs.healthcheck-url || 'http://localhost/health' }} + DEPLOY_BIND_IP: ${{ inputs.bind-ip || '127.0.0.1' }} + APP_PORT: ${{ inputs.app-port || 3000 }} + HOST_PORT: ${{ inputs.host-port || 80 }} + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + script_stop: true + envs: IMAGE_REPO,IMAGE_TAG,GHCR_USER,GHCR_TOKEN,HEALTHCHECK_URL,DEPLOY_BIND_IP,APP_PORT,HOST_PORT + script: | + set -euxo pipefail + IMAGE="${{ env.REGISTRY }}/${IMAGE_REPO}:${IMAGE_TAG}" + + printf '%s' "$GHCR_TOKEN" | docker login ${{ env.REGISTRY }} -u "$GHCR_USER" --password-stdin + docker pull "$IMAGE" + + docker stop "${{ env.CONTAINER_NAME }}" || true + docker rm "${{ env.CONTAINER_NAME }}" || true + + docker run -d --name "${{ env.CONTAINER_NAME }}" --restart unless-stopped \ + -p ${DEPLOY_BIND_IP}:${HOST_PORT}:${APP_PORT} \ + "$IMAGE" + + for attempt in {1..12}; do + if curl -fsS "$HEALTHCHECK_URL" > /dev/null; then + echo "Health check passed on attempt ${attempt}" + docker image prune -f || true + exit 0 + fi + sleep 5 + done + + echo "Health check failed for ${HEALTHCHECK_URL}" >&2 + docker logs --tail 100 "${{ env.CONTAINER_NAME }}" || true + exit 1 diff --git a/.github/workflows/vercel-app-deploy-example.yml b/.github/workflows/vercel-app-deploy-example.yml new file mode 100644 index 0000000..5644a0d --- /dev/null +++ b/.github/workflows/vercel-app-deploy-example.yml @@ -0,0 +1,25 @@ +name: Vercel Deploy (Example Caller) + +on: + workflow_dispatch: + +jobs: + deploy: + uses: wesship/.github/.github/workflows/vercel-deploy.yml@main + with: + environment: production + production: true + working-directory: . + node-version: '20' + vercel-version: 'latest' + secrets: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + + announce: + runs-on: ubuntu-latest + needs: deploy + steps: + - name: Print deployed URL + run: echo "Deployed URL -> ${{ needs.deploy.outputs.deployment_url }}" diff --git a/.github/workflows/vercel-deploy.yml b/.github/workflows/vercel-deploy.yml new file mode 100644 index 0000000..d48ce2c --- /dev/null +++ b/.github/workflows/vercel-deploy.yml @@ -0,0 +1,111 @@ +name: Reusable Vercel Deploy + +on: + workflow_call: + inputs: + environment: + description: Vercel target environment + required: false + default: production + type: string + working-directory: + description: Directory of app source in caller repository + required: false + default: . + type: string + production: + description: Deploy to production (`--prod`) + required: false + default: true + type: boolean + node-version: + description: Node.js version used for CLI execution + required: false + default: '20' + type: string + vercel-version: + description: Vercel CLI version + required: false + default: 'latest' + type: string + secrets: + VERCEL_TOKEN: + required: true + VERCEL_ORG_ID: + required: true + VERCEL_PROJECT_ID: + required: true + outputs: + deployment_url: + description: URL returned by Vercel deploy + value: ${{ jobs.deploy.outputs.deployment_url }} + +permissions: + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 20 + outputs: + deployment_url: ${{ steps.deploy.outputs.deployment_url }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: Install Vercel CLI + run: npm install --global "vercel@${{ inputs.vercel-version }}" + + - name: Validate working directory + working-directory: ${{ inputs.working-directory }} + run: | + test -f package.json || { + echo "package.json not found in $PWD" >&2 + exit 1 + } + + - name: Pull Vercel environment + working-directory: ${{ inputs.working-directory }} + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + run: | + vercel pull --yes --environment=${{ inputs.environment }} --token="$VERCEL_TOKEN" + + - name: Build with Vercel + working-directory: ${{ inputs.working-directory }} + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: | + vercel build --token="$VERCEL_TOKEN" + + - name: Deploy prebuilt output + id: deploy + working-directory: ${{ inputs.working-directory }} + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: | + set -euo pipefail + DEPLOY_ARGS="--prebuilt --yes --token=$VERCEL_TOKEN" + if [ "${{ inputs.production }}" = "true" ]; then + DEPLOY_ARGS="$DEPLOY_ARGS --prod" + fi + + DEPLOY_OUTPUT=$(vercel deploy $DEPLOY_ARGS 2>&1) + echo "$DEPLOY_OUTPUT" + + DEPLOYMENT_URL=$(printf '%s\n' "$DEPLOY_OUTPUT" | grep -Eo "https://[A-Za-z0-9.-]+\.vercel\.app" | tail -n 1) + if [ -z "$DEPLOYMENT_URL" ]; then + echo "Could not detect deployment URL from Vercel output" >&2 + exit 1 + fi + + echo "deployment_url=$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT" + echo "### Vercel Deployment\n- URL: $DEPLOYMENT_URL" >> "$GITHUB_STEP_SUMMARY" diff --git a/profile/README.md b/profile/README.md index 103e942..11b2ff5 100644 --- a/profile/README.md +++ b/profile/README.md @@ -19,3 +19,44 @@ n8n is a low-code automation tool. With over 220 pre-built integrations and a ge - 🌱 We were Sequoia's first seed investment in Germany, and recently raised a $12m Series A round, led by Felicis Ventures We're on a mission to give technical superpowers to everyone with a computer. Join us! + +## Supreme AI Deployment Hub: debug + deploy workflow + +If your `wesship/supreme-ai-deployment-hub` app is failing in production, this repo now provides reusable workflows that your app repo can call. + +Available workflows: +- `.github/workflows/supreme-ai-deploy.yml`: reusable CI/CD workflow for validate -> containerize -> SSH deploy. +- `.github/workflows/vercel-deploy.yml`: reusable Vercel workflow for `vercel pull`, `vercel build`, and `vercel deploy --prebuilt`. + +For Supreme AI server deploy callers, provide these required secrets from the app repo: +- `DEPLOY_HOST` +- `DEPLOY_USER` +- `DEPLOY_SSH_KEY` +- `GHCR_USERNAME` +- `GHCR_TOKEN` (PAT with `read:packages`) + +Recommended inputs when calling `supreme-ai-deploy.yml`: +- `healthcheck-url` (default: `http://localhost/health`) +- `bind-ip` (default: `127.0.0.1`; set `0.0.0.0` only when you intentionally expose publicly) +- `app-port` / `host-port` (defaults: `3000` / `80`) + +Deployability checklist: +- Ensure your app repo includes a valid `package.json`, lockfile, and `Dockerfile`. +- Ensure target VM has Docker + curl installed and SSH key access configured. +- Ensure GHCR package visibility and token permissions allow pull from deploy host. + +Security hardening included: +- Deployment binds service to `127.0.0.1` by default to reduce accidental public exposure. +- GHCR login on the server uses `--password-stdin` to avoid leaking tokens in process args. + +Vercel deployment (for `https://vercel.com/wesships-projects`): +- Required app-repo secrets: `VERCEL_TOKEN`, `VERCEL_ORG_ID`, `VERCEL_PROJECT_ID`. +- Supports Lovable-generated apps too (example project: `https://lovable.dev/projects/b5eb8a4d-3709-4e3f-930c-ab5ab4b96560`) by deploying from a configurable `working-directory`. +- The reusable workflow exports `deployment_url` so callers can post the deployed URL to PR comments/check summaries. + +Go-live quick start: +- 1) Add required secrets in your app repo. +- 2) Copy `.github/workflows/vercel-app-deploy-example.yml` into your app repo. +- 3) Add a second caller workflow in the app repo for `wesship/.github/.github/workflows/supreme-ai-deploy.yml@main`. +- 4) Run one manual `workflow_dispatch` in each workflow and confirm health checks / deployment URL output. +