diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..77f39858f --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,22 @@ +{ + "worktree": { + "baseRef": "fresh" + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "EnterWorktree", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR/scripts/install-worktree-deps.sh\"" + }, + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR/scripts/assign-worktree-port.sh\"" + } + ] + } + ] + } +} diff --git a/.claude/skills/fix-issue/SKILL.md b/.claude/skills/fix-issue/SKILL.md new file mode 100644 index 000000000..9418a626b --- /dev/null +++ b/.claude/skills/fix-issue/SKILL.md @@ -0,0 +1,55 @@ +--- +name: fix-issue +description: Implement a GitHub issue with reproduction and verification +allowed-tools: Bash, Edit, Write, Read, Glob, Grep, mcp__playwright__*, mcp__dokploy__* +--- + +The issue number is passed as $1. + +## Instance + +No instance is running yet — start your own, isolated to this worktree: + +1. Check `apps/dokploy/.env` for `PORT` (assigned per-worktree already). +2. If nothing is listening on that port, start it: `pnpm dokploy:dev` in the + background, then poll `curl -s -o /dev/null -w '%{http_code}' http://localhost:$PORT` + until it answers (usually ~10-15s). +3. Use `http://localhost:$PORT` as the base URL for Playwright navigation. + +Note: `mcp__dokploy__*` resolves its URL from +`$DOKPLOY_BASE_URL` once, at session startup — it cannot pick up a port +discovered mid-session. If those tools are unavailable or point at the wrong +instance, fall back to `curl`/`gh api` for API-level checks, or ask the user +to relaunch with `DOKPLOY_BASE_URL` exported first. + +## Tools + +- `mcp__dokploy__*` — the Dokploy API of the running instance. Use it to set up + state (create a project, an app, an env var) and to verify backend behavior. + Search for the tool you need; they are not all loaded upfront. +- `mcp__playwright__*` — the browser at $DOKPLOY_BASE_URL. Use it for anything + a user would see or click. + +Pick by where the bug lives, not by convenience: + +- Bug in the UI (rendering, forms, navigation, state) → reproduce in Playwright. + The API returning correct data proves nothing here. +- Bug in the API, deploy logic, or data → reproduce with the Dokploy MCP. + A green screenshot proves nothing here. +- Unclear → do both. + +Use the MCP to reach the state you need quickly, then verify in the UI. Do not +click through ten screens to create a project the API can create in one call. + +## Steps + +1. Run `gh issue view $1` and read the full issue, including comments. +2. Reproduce the bug with the appropriate tool. If you cannot reproduce it, + comment on the issue explaining what you tried and STOP. + Do not implement anything. +3. Implement the fix. Keep the change minimal and scoped to the issue. +4. Run `pnpm test`, then re-run the same reproduction from step 2. +5. Only if both pass: commit and run `gh pr create`. The PR description must + include the before/after reproduction steps and reference the issue. + +Never skip step 2. A fix you cannot reproduce and then verify is not a fix. \ No newline at end of file diff --git a/.claude/skills/frontend-design/SKILL.md b/.claude/skills/frontend-design/SKILL.md new file mode 100644 index 000000000..600b6db41 --- /dev/null +++ b/.claude/skills/frontend-design/SKILL.md @@ -0,0 +1,42 @@ +--- +name: frontend-design +description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics. +license: Complete terms in LICENSE.txt +--- + +This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. + +The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints. + +## Design Thinking + +Before coding, understand the context and commit to a BOLD aesthetic direction: +- **Purpose**: What problem does this interface solve? Who uses it? +- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. +- **Constraints**: Technical requirements (framework, performance, accessibility). +- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? + +**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. + +Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: +- Production-grade and functional +- Visually striking and memorable +- Cohesive with a clear aesthetic point-of-view +- Meticulously refined in every detail + +## Frontend Aesthetics Guidelines + +Focus on: +- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font. +- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. +- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise. +- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density. +- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays. + +NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character. + +Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations. + +**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well. + +Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision. \ No newline at end of file diff --git a/.github/workflows/create-pr.yml b/.github/workflows/create-pr.yml index 248b98d5a..7cf6fa136 100644 --- a/.github/workflows/create-pr.yml +++ b/.github/workflows/create-pr.yml @@ -14,34 +14,34 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Get version from package.json - run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV + run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> "$GITHUB_ENV" - name: Get latest GitHub tag run: | LATEST_TAG=$(git ls-remote --tags origin | awk -F'/' '{print $3}' | sort -V | tail -n1) - echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV - echo $LATEST_TAG + echo "LATEST_TAG=$LATEST_TAG" >> "$GITHUB_ENV" + echo "$LATEST_TAG" - name: Compare versions run: | - if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then + if [ "$VERSION" != "$LATEST_TAG" ]; then VERSION_CHANGED="true" else VERSION_CHANGED="false" fi - echo "VERSION_CHANGED=$VERSION_CHANGED" >> $GITHUB_ENV + echo "VERSION_CHANGED=$VERSION_CHANGED" >> "$GITHUB_ENV" echo "Comparing versions:" - echo "Current version: ${{ env.VERSION }}" - echo "Latest tag: ${{ env.LATEST_TAG }}" + echo "Current version: $VERSION" + echo "Latest tag: $LATEST_TAG" echo "Version changed: $VERSION_CHANGED" - name: Check if a PR already exists run: | PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length') - echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV + echo "PR_EXISTS=$PR_EXISTS" >> "$GITHUB_ENV" env: GH_TOKEN: ${{ secrets.GH_PAT }} @@ -56,12 +56,12 @@ jobs: git push origin canary gh pr create \ - --title "🚀 Release ${{ env.VERSION }}" \ - --body ' - This PR promotes changes from `canary` to `main` for version ${{ env.VERSION }}. + --title "🚀 Release $VERSION" \ + --body " + This PR promotes changes from \`canary\` to \`main\` for version $VERSION. ### 🔍 Changes Include: - - Version bump to ${{ env.VERSION }} + - Version bump to $VERSION - All changes from canary branch ### ✅ Pre-merge Checklist: @@ -69,7 +69,7 @@ jobs: - [ ] Documentation updated - [ ] Docker images built and tested - > 🤖 This PR was automatically generated by [GitHub Actions](https://github.com/actions)' \ + > 🤖 This PR was automatically generated by [GitHub Actions](https://github.com/actions)" \ --base main \ --head canary \ --label "release" --label "automated pr" || true \ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 321fb2029..49428320e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,33 +5,38 @@ on: branches: [main, canary] workflow_dispatch: +permissions: + contents: read + jobs: build-and-push-cloud-image: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Set tag and version id: meta-cloud run: | VERSION=$(jq -r .version apps/dokploy/package.json) - echo "version=$VERSION" >> $GITHUB_OUTPUT - if [ "${{ github.ref }}" = "refs/heads/main" ]; then - echo "tags=siumauricio/cloud:latest,siumauricio/cloud:${VERSION}" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + if [ "$GITHUB_REF" = "refs/heads/main" ]; then + echo "tags=siumauricio/cloud:latest,siumauricio/cloud:${VERSION}" >> "$GITHUB_OUTPUT" else - echo "tags=siumauricio/cloud:canary" >> $GITHUB_OUTPUT + echo "tags=siumauricio/cloud:canary" >> "$GITHUB_OUTPUT" fi - name: Log in to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile.cloud @@ -48,26 +53,28 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Set tag and version id: meta-schedule run: | VERSION=$(jq -r .version apps/dokploy/package.json) - if [ "${{ github.ref }}" = "refs/heads/main" ]; then - echo "tags=siumauricio/schedule:latest,siumauricio/schedule:${VERSION}" >> $GITHUB_OUTPUT + if [ "$GITHUB_REF" = "refs/heads/main" ]; then + echo "tags=siumauricio/schedule:latest,siumauricio/schedule:${VERSION}" >> "$GITHUB_OUTPUT" else - echo "tags=siumauricio/schedule:canary" >> $GITHUB_OUTPUT + echo "tags=siumauricio/schedule:canary" >> "$GITHUB_OUTPUT" fi - name: Log in to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile.schedule @@ -80,26 +87,28 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Set tag and version id: meta-server run: | VERSION=$(jq -r .version apps/dokploy/package.json) - if [ "${{ github.ref }}" = "refs/heads/main" ]; then - echo "tags=siumauricio/server:latest,siumauricio/server:${VERSION}" >> $GITHUB_OUTPUT + if [ "$GITHUB_REF" = "refs/heads/main" ]; then + echo "tags=siumauricio/server:latest,siumauricio/server:${VERSION}" >> "$GITHUB_OUTPUT" else - echo "tags=siumauricio/server:canary" >> $GITHUB_OUTPUT + echo "tags=siumauricio/server:canary" >> "$GITHUB_OUTPUT" fi - name: Log in to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile.server diff --git a/.github/workflows/dokploy.yml b/.github/workflows/dokploy.yml index 542944611..175543205 100644 --- a/.github/workflows/dokploy.yml +++ b/.github/workflows/dokploy.yml @@ -8,18 +8,23 @@ on: env: IMAGE_NAME: dokploy/dokploy +permissions: + contents: read + jobs: docker-amd: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -27,15 +32,15 @@ jobs: - name: Set tag and version id: meta run: | - if [ "${{ github.ref }}" = "refs/heads/main" ]; then + if [ "$GITHUB_REF" = "refs/heads/main" ]; then TAG="latest" VERSION=$(node -p "require('./apps/dokploy/package.json').version") - elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + elif [ "$GITHUB_REF" = "refs/heads/canary" ]; then TAG="canary" else TAG="feature" fi - echo "tags=${IMAGE_NAME}:${TAG}-amd64" >> $GITHUB_OUTPUT + echo "tags=${IMAGE_NAME}:${TAG}-amd64" >> "$GITHUB_OUTPUT" - name: Prepare env file run: | @@ -43,7 +48,7 @@ jobs: cp apps/dokploy/.env.production.example apps/dokploy/.env.production - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64 @@ -53,13 +58,15 @@ jobs: runs-on: ubuntu-24.04-arm steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -68,15 +75,15 @@ jobs: id: meta run: | VERSION=$(node -p "require('./apps/dokploy/package.json').version") - if [ "${{ github.ref }}" = "refs/heads/main" ]; then + if [ "$GITHUB_REF" = "refs/heads/main" ]; then TAG="latest" VERSION=$(node -p "require('./apps/dokploy/package.json').version") - elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + elif [ "$GITHUB_REF" = "refs/heads/canary" ]; then TAG="canary" else TAG="feature" fi - echo "tags=${IMAGE_NAME}:${TAG}-arm64" >> $GITHUB_OUTPUT + echo "tags=${IMAGE_NAME}:${TAG}-arm64" >> "$GITHUB_OUTPUT" - name: Prepare env file run: | @@ -84,7 +91,7 @@ jobs: cp apps/dokploy/.env.production.example apps/dokploy/.env.production - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: . platforms: linux/arm64 @@ -96,20 +103,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Create and push manifests run: | - if [ "${{ github.ref }}" = "refs/heads/main" ]; then + if [ "$GITHUB_REF" = "refs/heads/main" ]; then VERSION=$(node -p "require('./apps/dokploy/package.json').version") TAG="latest" @@ -121,7 +130,7 @@ jobs: ${IMAGE_NAME}:${TAG}-amd64 \ ${IMAGE_NAME}:${TAG}-arm64 - elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + elif [ "$GITHUB_REF" = "refs/heads/canary" ]; then TAG="canary" docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \ ${IMAGE_NAME}:${TAG}-amd64 \ @@ -138,28 +147,45 @@ jobs: needs: [combine-manifests] if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + permissions: + contents: write outputs: version: ${{ steps.get_version.outputs.version }} + npm_version: ${{ steps.get_version.outputs.npm_version }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 + persist-credentials: false - name: Get version id: get_version run: | VERSION=$(node -p "require('./apps/dokploy/package.json').version") - echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "npm_version=${VERSION#v}" >> "$GITHUB_OUTPUT" + + - name: Fetch install.sh + env: + RELEASE_VERSION: ${{ steps.get_version.outputs.version }} + run: | + curl -fsSL https://raw.githubusercontent.com/Dokploy/website/main/apps/website/public/install.sh -o install.sh + head -1 install.sh | grep -q '^#!' || { echo "Downloaded install.sh is not a shell script"; exit 1; } + grep -q 'DOKPLOY_VERSION' install.sh || { echo "install.sh no longer supports DOKPLOY_VERSION pinning"; exit 1; } + { head -1 install.sh; echo "DOKPLOY_VERSION=\"\${DOKPLOY_VERSION:-${RELEASE_VERSION}}\""; tail -n +2 install.sh; } > install-pinned.sh + mv install-pinned.sh install.sh - name: Create Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ steps.get_version.outputs.version }} + target_commitish: ${{ github.sha }} name: ${{ steps.get_version.outputs.version }} generate_release_notes: true draft: false prerelease: false + files: install.sh env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -167,75 +193,90 @@ jobs: needs: [generate-release] if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + env: + NPM_VERSION: ${{ needs.generate-release.outputs.npm_version }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 + with: + persist-credentials: false + + - uses: pnpm/action-setup@v6 + with: + version: 10.22.0 + + - uses: actions/setup-node@v7 + with: + node-version: 24.4.0 + cache: pnpm + + - name: Generate OpenAPI specification + run: | + pnpm install --frozen-lockfile + pnpm generate:openapi - name: Sync version to MCP repository run: | git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git /tmp/mcp-repo cd /tmp/mcp-repo - jq --arg v "${{ needs.generate-release.outputs.version }}" '.version = $v' package.json > package.json.tmp + jq --arg v "$NPM_VERSION" '.version = $v' package.json > package.json.tmp mv package.json.tmp package.json - npm install -g pnpm + cp $GITHUB_WORKSPACE/openapi.json src/generated/openapi.json pnpm install - pnpm run fetch-openapi pnpm run generate git config user.name "Dokploy Bot" git config user.email "bot@dokploy.com" git add -A - git commit -m "chore: bump version to ${{ needs.generate-release.outputs.version }}" \ - -m "Source: ${{ github.repository }}@${{ github.sha }}" \ + git commit -m "chore: bump version to $NPM_VERSION" \ + -m "Source: $GITHUB_REPOSITORY@$GITHUB_SHA" \ --allow-empty git push - echo "✅ MCP repo synced to version ${{ needs.generate-release.outputs.version }}" + echo "✅ MCP repo synced to version $NPM_VERSION" - name: Sync version to CLI repository run: | git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git /tmp/cli-repo cd /tmp/cli-repo - jq --arg v "${{ needs.generate-release.outputs.version }}" '.version = $v' package.json > package.json.tmp + jq --arg v "$NPM_VERSION" '.version = $v' package.json > package.json.tmp mv package.json.tmp package.json - cp ${{ github.workspace }}/openapi.json ./openapi.json - npm install -g pnpm + cp $GITHUB_WORKSPACE/openapi.json ./openapi.json pnpm install pnpm run generate git config user.name "Dokploy Bot" git config user.email "bot@dokploy.com" git add -A - git commit -m "chore: bump version to ${{ needs.generate-release.outputs.version }}" \ - -m "Source: ${{ github.repository }}@${{ github.sha }}" \ + git commit -m "chore: bump version to $NPM_VERSION" \ + -m "Source: $GITHUB_REPOSITORY@$GITHUB_SHA" \ --allow-empty git push - echo "✅ CLI repo synced to version ${{ needs.generate-release.outputs.version }}" + echo "✅ CLI repo synced to version $NPM_VERSION" - name: Sync version to SDK repository run: | git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/sdk.git /tmp/sdk-repo cd /tmp/sdk-repo - jq --arg v "${{ needs.generate-release.outputs.version }}" '.version = $v' package.json > package.json.tmp + jq --arg v "$NPM_VERSION" '.version = $v' package.json > package.json.tmp mv package.json.tmp package.json - cp ${{ github.workspace }}/openapi.json ./openapi.json - npm install -g pnpm + cp $GITHUB_WORKSPACE/openapi.json ./openapi.json pnpm install pnpm run generate git config user.name "Dokploy Bot" git config user.email "bot@dokploy.com" git add -A - git commit -m "chore: bump version to ${{ needs.generate-release.outputs.version }}" \ - -m "Source: ${{ github.repository }}@${{ github.sha }}" \ + git commit -m "chore: bump version to $NPM_VERSION" \ + -m "Source: $GITHUB_REPOSITORY@$GITHUB_SHA" \ --allow-empty git push - echo "✅ SDK repo synced to version ${{ needs.generate-release.outputs.version }}" + echo "✅ SDK repo synced to version $NPM_VERSION" diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index cfddad7b2..fd0823a7a 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -6,17 +6,22 @@ on: pull_request: branches: [canary] +permissions: + contents: read + jobs: format: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup biomeJs - uses: biomejs/setup-biome@v2 + uses: biomejs/setup-biome@v2 - name: Run Biome formatter run: biome format --write - - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 # v1.3.2 + - uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a # v1.3.4 diff --git a/.github/workflows/hotfix-cherry-pick.yml b/.github/workflows/hotfix-cherry-pick.yml new file mode 100644 index 000000000..30a3dda7c --- /dev/null +++ b/.github/workflows/hotfix-cherry-pick.yml @@ -0,0 +1,51 @@ +name: Hotfix Cherry-Pick + +on: + pull_request_target: + types: [closed, labeled] + +concurrency: + group: hotfix-to-main + cancel-in-progress: false + +jobs: + cherry-pick: + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'hotfix') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout main + uses: actions/checkout@v7 + with: + ref: main + fetch-depth: 0 + token: ${{ secrets.HOTFIX_PUSH_TOKEN }} + + - name: Cherry-pick fix to main + env: + MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + run: | + git config user.name "Dokploy Bot" + git config user.email "bot@dokploy.com" + SHA="$MERGE_SHA" + if [ "$(git rev-list --parents -n1 "$SHA" | wc -w)" -gt 2 ]; then + git cherry-pick -x -m 1 "$SHA" + else + git cherry-pick -x "$SHA" + fi + git commit --amend -m "$(git log -1 --format=%B)" -m "[skip ci]" + git push origin main + + - name: Sync cherry-pick back into canary + run: | + git config user.name "Dokploy Bot" + git config user.email "bot@dokploy.com" + git fetch origin canary main + git checkout -B canary origin/canary + if ! git merge origin/main -m "chore: sync hotfix from main into canary [skip ci]"; then + git merge --abort + echo "::error::Could not auto-sync the hotfix into canary (real conflict, not just history divergence). Merge main into canary manually to avoid a conflict on the next release PR." + exit 1 + fi + git push origin canary diff --git a/.github/workflows/hotfix-release.yml b/.github/workflows/hotfix-release.yml new file mode 100644 index 000000000..3271df861 --- /dev/null +++ b/.github/workflows/hotfix-release.yml @@ -0,0 +1,32 @@ +name: Hotfix Release + +on: + workflow_dispatch: + +concurrency: + group: hotfix-to-main + cancel-in-progress: false + +# Push to main goes through HOTFIX_PUSH_TOKEN, not the workflow token. +permissions: + contents: read + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout main + uses: actions/checkout@v7 + with: + ref: main + token: ${{ secrets.HOTFIX_PUSH_TOKEN }} + + - name: Bump patch version and push + run: | + git config user.name "Dokploy Bot" + git config user.email "bot@dokploy.com" + CURRENT=$(node -p "require('./apps/dokploy/package.json').version") + NEW=$(echo "$CURRENT" | awk -F. -v OFS=. '{$NF++; print}') + sed -i "s/\"version\": \"$CURRENT\"/\"version\": \"$NEW\"/" apps/dokploy/package.json + git commit -am "chore: release ${NEW}" + git push origin main diff --git a/.github/workflows/monitoring.yml b/.github/workflows/monitoring.yml index 378b019d4..df0a70e0a 100644 --- a/.github/workflows/monitoring.yml +++ b/.github/workflows/monitoring.yml @@ -7,18 +7,23 @@ on: env: IMAGE_NAME: dokploy/monitoring +permissions: + contents: read + jobs: docker-amd: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -26,17 +31,17 @@ jobs: - name: Set tag id: meta run: | - if [ "${{ github.ref }}" = "refs/heads/main" ]; then + if [ "$GITHUB_REF" = "refs/heads/main" ]; then TAG="latest" - elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + elif [ "$GITHUB_REF" = "refs/heads/canary" ]; then TAG="canary" else TAG="feature" fi - echo "tags=${IMAGE_NAME}:${TAG}-amd64" >> $GITHUB_OUTPUT + echo "tags=${IMAGE_NAME}:${TAG}-amd64" >> "$GITHUB_OUTPUT" - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile.monitoring @@ -47,13 +52,15 @@ jobs: runs-on: ubuntu-24.04-arm steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -61,17 +68,17 @@ jobs: - name: Set id: meta run: | - if [ "${{ github.ref }}" = "refs/heads/main" ]; then + if [ "$GITHUB_REF" = "refs/heads/main" ]; then TAG="latest" - elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + elif [ "$GITHUB_REF" = "refs/heads/canary" ]; then TAG="canary" else TAG="feature" fi - echo "tags=${IMAGE_NAME}:${TAG}-arm64" >> $GITHUB_OUTPUT + echo "tags=${IMAGE_NAME}:${TAG}-arm64" >> "$GITHUB_OUTPUT" - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile.monitoring @@ -84,27 +91,29 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Create and push manifests run: | - if [ "${{ github.ref }}" = "refs/heads/main" ]; then + if [ "$GITHUB_REF" = "refs/heads/main" ]; then TAG="latest" docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \ ${IMAGE_NAME}:${TAG}-amd64 \ ${IMAGE_NAME}:${TAG}-arm64 - elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + elif [ "$GITHUB_REF" = "refs/heads/canary" ]; then TAG="canary" docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \ ${IMAGE_NAME}:${TAG}-amd64 \ diff --git a/.github/workflows/pr-quality.yml b/.github/workflows/pr-quality.yml deleted file mode 100644 index 6de85fa27..000000000 --- a/.github/workflows/pr-quality.yml +++ /dev/null @@ -1,21 +0,0 @@ - -name: PR Quality - -permissions: - contents: read - issues: read - pull-requests: write - -on: - pull_request_target: - types: [opened, reopened] - -jobs: - anti-slop: - runs-on: ubuntu-latest - steps: - - uses: peakoss/anti-slop@v0 - with: - blocked-commit-authors: "claude,copilot" - require-description: true - min-account-age: 5 diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 2ad24fc0c..d1cd96364 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -14,9 +14,11 @@ jobs: matrix: job: [build, test, typecheck] steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 with: node-version: 24.4.0 cache: "pnpm" diff --git a/.github/workflows/sync-openapi-docs.yml b/.github/workflows/sync-openapi-docs.yml index 147d8d97e..f378f7dca 100644 --- a/.github/workflows/sync-openapi-docs.yml +++ b/.github/workflows/sync-openapi-docs.yml @@ -12,17 +12,20 @@ on: workflow_dispatch: +permissions: + contents: read + jobs: generate-and-commit: name: Generate OpenAPI and commit to Dokploy repo runs-on: ubuntu-latest steps: - name: Checkout Dokploy repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: - token: ${{ secrets.GITHUB_TOKEN }} - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + persist-credentials: false + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 with: node-version: 24.4.0 cache: "pnpm" diff --git a/.github/workflows/upgrade-integration-test.yml b/.github/workflows/upgrade-integration-test.yml new file mode 100644 index 000000000..eea581175 --- /dev/null +++ b/.github/workflows/upgrade-integration-test.yml @@ -0,0 +1,442 @@ +# Upgrade Integration Test +# +# Tests that Dokploy can upgrade from version A to version B while keeping +# user projects (Postgres, MongoDB, two web apps, one static site) alive. +# +# Generates upgrade pairs: each stable tag in [floor_version, target_version) +# is paired with target_version. If target_version is empty, the highest tag +# available on Docker Hub is used as the target. +# +# Only triggered manually to avoid burning Actions minutes. + +name: Upgrade Integration Test + +on: + workflow_dispatch: + inputs: + floor_version: + description: 'Oldest version tag to include in pairs (e.g. v0.29.4)' + required: false + default: 'v0.29.4' + target_version: + description: 'Target version to upgrade to (version B). Leave empty to use the highest available.' + required: false + default: '' + +env: + DOKPLOY_IMAGE: dokploy/dokploy + DOKPLOY_SERVICE: dokploy + DOKPLOY_PORT: 3000 + +permissions: + contents: read + +# ────────────────────────────────────────────────────────────────────────────── +jobs: + + # ── 1. Build the matrix ───────────────────────────────────────────────────── + build-matrix: + name: Build upgrade pair matrix + runs-on: ubuntu-latest + outputs: + pairs: ${{ steps.pairs.outputs.pairs }} + + steps: + - name: Generate pairs + id: pairs + env: + FLOOR: ${{ inputs.floor_version }} + TARGET: ${{ inputs.target_version }} + run: | + set -euo pipefail + + # semver "a >= b" comparison helper (vX.Y.Z, strips leading v) + ge() { + [ "$(printf '%s\n%s\n' "${1#v}" "${2#v}" | sort -V | tail -n1)" = "${1#v}" ] + } + + # Fetch all semver tags from Docker Hub (dokploy/dokploy) + ALL_TAGS=$(curl -fsSL \ + "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags?page_size=100" | \ + jq -r '.results[].name' | \ + grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \ + sort -V) + + echo "All tags found:" + echo "$ALL_TAGS" + + # Target version (version B): explicit input, else highest available. + if [ -n "$TARGET" ]; then + TO="$TARGET" + echo "Using user-supplied target version: $TO" + else + TO=$(echo "$ALL_TAGS" | tail -n1) + echo "No target supplied; using highest available: $TO" + fi + + # Floor semver components + FLOOR_CLEAN="${FLOOR#v}" + IFS='.' read -r F_MAJ F_MIN F_PAT <<< "$FLOOR_CLEAN" + + # Each tag in [FLOOR, TO) → TO + PAIRS='[]' + while read -r TAG; do + [ -z "$TAG" ] && continue + # skip tags above-or-equal to the target (incl. the target itself) + ge "$TAG" "$TO" && continue + TAG_CLEAN="${TAG#v}" + IFS='.' read -r T_MAJ T_MIN T_PAT <<< "$TAG_CLEAN" + if [ "$T_MAJ" -gt "$F_MAJ" ] || \ + { [ "$T_MAJ" -eq "$F_MAJ" ] && [ "$T_MIN" -gt "$F_MIN" ]; } || \ + { [ "$T_MAJ" -eq "$F_MAJ" ] && [ "$T_MIN" -eq "$F_MIN" ] && [ "$T_PAT" -ge "$F_PAT" ]; }; then + PAIRS=$(echo "$PAIRS" | jq -c \ + --arg f "$TAG" --arg t "$TO" \ + '. + [{"from":$f,"to":$t}]') + fi + done <<< "$ALL_TAGS" + + COUNT=$(echo "$PAIRS" | jq 'length') + echo "Total pairs: $COUNT" + echo "$PAIRS" | jq -r '.[] | " \(.from) → \(.to)"' + + echo "pairs=$PAIRS" >> "$GITHUB_OUTPUT" + + + # ── 2. Run one upgrade test per pair ──────────────────────────────────────── + upgrade-test: + name: "${{ matrix.pair.from }} → ${{ matrix.pair.to }}" + needs: build-matrix + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + pair: ${{ fromJSON(needs.build-matrix.outputs.pairs) }} + + steps: + + # ── Environment setup ────────────────────────────────────────────────── + - name: Free disk space + run: | + sudo rm -rf \ + /usr/share/dotnet /opt/ghc /usr/local/share/boost \ + "$AGENT_TOOLSDIRECTORY" /usr/local/lib/android \ + /usr/local/share/chromium /opt/hostedtoolcache + docker system prune -af --volumes + df -h + + # ── Install Dokploy directly at VERSION A ────────────────────────────── + # install.sh: + # • requires root (run via sudo bash) + # • respects DOKPLOY_VERSION → installs that tag directly + # (so we don't need a separate "downgrade" step that would risk + # running B's migrations on A's expected schema) + # • respects ADVERTISE_ADDR → skip the external IP lookup + # • initializes Docker Swarm + dokploy-network itself + - name: Install Dokploy at VERSION A (${{ matrix.pair.from }}) + run: | + curl -fsSL https://dokploy.com/install.sh -o /tmp/install.sh + chmod +x /tmp/install.sh + sudo -E env \ + DOKPLOY_VERSION="${{ matrix.pair.from }}" \ + ADVERTISE_ADDR="127.0.0.1" \ + bash /tmp/install.sh + + - name: Wait for dokploy service to converge on ${{ matrix.pair.from }} + run: | + echo "Waiting for 'dokploy' Swarm service to reach 1/1..." + timeout 240 bash -c ' + until docker service ls --filter name=dokploy \ + --format "{{.Name}} {{.Replicas}}" \ + | grep "^dokploy " | grep -q " 1/1"; do + sleep 4 + done + ' + docker service ls + echo "✅ Service running on ${{ matrix.pair.from }}" + + - name: Wait for Dokploy API to accept requests + run: | + timeout 180 bash -c ' + until curl -sf -o /dev/null \ + "http://localhost:${{ env.DOKPLOY_PORT }}"; do + sleep 3 + done + ' + echo "✅ Dokploy API is up" + + # ── Bootstrap admin user ─────────────────────────────────────────────── + - name: Register first admin user + id: auth + run: | + COOKIE_JAR="$RUNNER_TEMP/dokploy-cookies.txt" + : > "$COOKIE_JAR" + chmod 600 "$COOKIE_JAR" + echo "cookie_jar=$COOKIE_JAR" >> "$GITHUB_OUTPUT" + + # better-auth: sign-up/email (allowed only before any owner exists) + set -x + curl -sS -i -X POST \ + "http://localhost:${{ env.DOKPLOY_PORT }}/api/auth/sign-up/email" \ + -H "Content-Type: application/json" \ + -c "$COOKIE_JAR" -b "$COOKIE_JAR" \ + -d '{"name":"CI Admin","email":"ci@dokploy.test","password":"CiTest1234!"}' \ + | tee /tmp/signup.out + set +x + + # Verify we got a session cookie + if ! grep -qE '(better-auth|auth)\.session' "$COOKIE_JAR"; then + echo "⚠️ No session cookie matched expected name; jar contents:" + cat "$COOKIE_JAR" + fi + + # ── Create test resources ────────────────────────────────────────────── + - name: Create project, databases and applications + id: create + env: + BASE: "http://localhost:${{ env.DOKPLOY_PORT }}" + COOKIE: ${{ steps.auth.outputs.cookie_jar }} + run: | + set -euo pipefail + + # --- tRPC POST helper --- + trpc_mut() { + curl -sf -X POST "$BASE/api/trpc/$1" \ + -H "Content-Type: application/json" \ + -b "$COOKIE" -c "$COOKIE" \ + -d "{\"json\":$2}" + } + + # --- Project --- + PROJECT=$(trpc_mut project.create \ + '{"name":"ci-upgrade-test","description":"CI upgrade integration test"}') + echo "project.create → $PROJECT" + + # project.create in v0.29+ returns nested {project:{projectId}, environment:{environmentId}} + PROJECT_ID=$(echo "$PROJECT" | jq -r \ + '.result.data.json.project.projectId // .result.data.json.projectId // empty') + ENV_ID=$(echo "$PROJECT" | jq -r \ + '.result.data.json.environment.environmentId // empty') + + if [ -z "$PROJECT_ID" ] || [ "$PROJECT_ID" = "null" ]; then + echo "❌ Could not extract projectId from project.create response:" + echo "$PROJECT" | jq . + exit 1 + fi + if [ -z "$ENV_ID" ] || [ "$ENV_ID" = "null" ]; then + echo "❌ Could not extract environmentId — this version may not support environments." + echo " project.create response: $PROJECT" + exit 1 + fi + + echo "project_id=$PROJECT_ID" >> "$GITHUB_OUTPUT" + echo "env_id=$ENV_ID" >> "$GITHUB_OUTPUT" + echo "Detected PROJECT_ID=$PROJECT_ID ENV_ID=$ENV_ID" + + # --- PostgreSQL 15 --- + PG=$(trpc_mut postgres.create \ + "{\"name\":\"ci-pg\",\"appName\":\"ci-pg-db\",\ + \"databaseName\":\"cidb\",\"databaseUser\":\"ciuser\",\ + \"databasePassword\":\"CiPg1pass\",\ + \"dockerImage\":\"postgres:15\",\"environmentId\":\"$ENV_ID\"}") + echo "postgres.create → $PG" + PG_ID=$(echo "$PG" | jq -r '.result.data.json.postgresId') + # deploy (not start) — deploy creates the Swarm service; start only + # scales an already-deployed service and 500s on a fresh resource. + trpc_mut postgres.deploy "{\"postgresId\":\"$PG_ID\"}" > /dev/null + echo "pg_id=$PG_ID" >> "$GITHUB_OUTPUT" + + # --- MongoDB 7.0 --- + MG=$(trpc_mut mongo.create \ + "{\"name\":\"ci-mongo\",\"appName\":\"ci-mongo-db\",\ + \"databaseName\":\"cidb\",\"databaseUser\":\"ciuser\",\ + \"databasePassword\":\"CiMg1pass\",\ + \"dockerImage\":\"mongo:7.0\",\"environmentId\":\"$ENV_ID\"}") + echo "mongo.create → $MG" + MG_ID=$(echo "$MG" | jq -r '.result.data.json.mongoId') + trpc_mut mongo.deploy "{\"mongoId\":\"$MG_ID\"}" > /dev/null + echo "mg_id=$MG_ID" >> "$GITHUB_OUTPUT" + + # --- Docker-image application helper --- + make_app() { + local DISP_NAME=$1 APP_NAME=$2 IMAGE=$3 + APP=$(trpc_mut application.create \ + "{\"name\":\"$DISP_NAME\",\"appName\":\"$APP_NAME\",\"environmentId\":\"$ENV_ID\"}") + APP_ID=$(echo "$APP" | jq -r '.result.data.json.applicationId') + trpc_mut application.saveDockerProvider \ + "{\"applicationId\":\"$APP_ID\",\"dockerImage\":\"$IMAGE\",\ + \"username\":\"\",\"password\":\"\",\"registryUrl\":\"\"}" \ + > /dev/null + trpc_mut application.deploy "{\"applicationId\":\"$APP_ID\"}" \ + > /dev/null + echo "$APP_ID" + } + + # Static site (nginx) + APP_STATIC=$(make_app "ci-static" "ci-static-app" "nginx:alpine") + echo "app_static_id=$APP_STATIC" >> "$GITHUB_OUTPUT" + + # Node.js hello-world (echo server image — small, no args needed) + APP_NODE=$(make_app "ci-node" "ci-node-app" "ealen/echo-server:latest") + echo "app_node_id=$APP_NODE" >> "$GITHUB_OUTPUT" + + # Go HTTP server (traefik/whoami is a tiny Go binary, port 80) + APP_GO=$(make_app "ci-go" "ci-go-app" "traefik/whoami:latest") + echo "app_go_id=$APP_GO" >> "$GITHUB_OUTPUT" + + echo "✅ All resources created" + + # ── Pre-upgrade health check ─────────────────────────────────────────── + - name: Wait for all services → 'done' (pre-upgrade) + env: + BASE: "http://localhost:${{ env.DOKPLOY_PORT }}" + COOKIE: ${{ steps.auth.outputs.cookie_jar }} + PG_ID: ${{ steps.create.outputs.pg_id }} + MG_ID: ${{ steps.create.outputs.mg_id }} + APP_STATIC_ID: ${{ steps.create.outputs.app_static_id }} + APP_NODE_ID: ${{ steps.create.outputs.app_node_id }} + APP_GO_ID: ${{ steps.create.outputs.app_go_id }} + run: | + wait_done() { + local NAME=$1 ENDPOINT=$2 ID_KEY=$3 ID=$4 STATUS_KEY=$5 + echo "Waiting for $NAME to reach 'done'..." + timeout 360 bash -c " + until [ \"\$(curl -sf -G '$BASE/api/trpc/$ENDPOINT' \ + --data-urlencode 'input={\"json\":{\"$ID_KEY\":\"$ID\"}}' \ + -b '$COOKIE' | \ + jq -r '.result.data.json.$STATUS_KEY // \"unknown\"')\" \ + = 'done' ]; do + sleep 5 + done + " + echo "✅ $NAME is done" + } + + wait_done postgres postgres.one postgresId "$PG_ID" applicationStatus + wait_done mongo mongo.one mongoId "$MG_ID" applicationStatus + wait_done static application.one applicationId "$APP_STATIC_ID" applicationStatus + wait_done node-app application.one applicationId "$APP_NODE_ID" applicationStatus + wait_done go-app application.one applicationId "$APP_GO_ID" applicationStatus + + - name: Assert Docker Swarm services healthy (pre-upgrade) + run: | + echo "=== docker service ls ===" + docker service ls + + FAIL=$(docker service ls --format '{{.Name}} {{.Replicas}}' | \ + grep -E '^ci-' | grep -v ' 1/1' || true) + if [ -n "$FAIL" ]; then + echo "❌ User services not healthy before upgrade:" + echo "$FAIL" + exit 1 + fi + echo "✅ All user services healthy before upgrade" + + # ── Upgrade ──────────────────────────────────────────────────────────── + - name: Upgrade Dokploy to VERSION B (${{ matrix.pair.to }}) + run: | + docker service update \ + --image "${{ env.DOKPLOY_IMAGE }}:${{ matrix.pair.to }}" \ + --force \ + "${{ env.DOKPLOY_SERVICE }}" + + echo "Waiting for service to converge on ${{ matrix.pair.to }}..." + timeout 240 bash -c ' + until ! docker service inspect dokploy \ + --format "{{.UpdateStatus.State}}" 2>/dev/null \ + | grep -q "^updating$"; do + sleep 4 + done + until docker service ls --filter name=dokploy \ + --format "{{.Name}} {{.Replicas}}" \ + | grep "^dokploy " | grep -q " 1/1"; do + sleep 4 + done + ' + echo "✅ Service running on ${{ matrix.pair.to }}" + + - name: Wait for Dokploy API to respond post-upgrade + run: | + timeout 180 bash -c ' + until curl -sf -o /dev/null \ + "http://localhost:${{ env.DOKPLOY_PORT }}"; do + sleep 3 + done + ' + echo "✅ Dokploy API is up after upgrade" + + # ── Post-upgrade health check ────────────────────────────────────────── + - name: Verify all services still healthy (post-upgrade) + env: + BASE: "http://localhost:${{ env.DOKPLOY_PORT }}" + COOKIE: ${{ steps.auth.outputs.cookie_jar }} + PG_ID: ${{ steps.create.outputs.pg_id }} + MG_ID: ${{ steps.create.outputs.mg_id }} + APP_STATIC_ID: ${{ steps.create.outputs.app_static_id }} + APP_NODE_ID: ${{ steps.create.outputs.app_node_id }} + APP_GO_ID: ${{ steps.create.outputs.app_go_id }} + run: | + check_status() { + local NAME=$1 ENDPOINT=$2 ID_KEY=$3 ID=$4 STATUS_KEY=$5 + STATUS=$(curl -sf -G "$BASE/api/trpc/$ENDPOINT" \ + --data-urlencode "input={\"json\":{\"$ID_KEY\":\"$ID\"}}" \ + -b "$COOKIE" | \ + jq -r ".result.data.json.$STATUS_KEY // \"unknown\"") + if [ "$STATUS" != "done" ]; then + echo "❌ $NAME status after upgrade: $STATUS" + return 1 + fi + echo "✅ $NAME: $STATUS" + } + + check_status postgres postgres.one postgresId "$PG_ID" applicationStatus + check_status mongo mongo.one mongoId "$MG_ID" applicationStatus + check_status static application.one applicationId "$APP_STATIC_ID" applicationStatus + check_status node-app application.one applicationId "$APP_NODE_ID" applicationStatus + check_status go-app application.one applicationId "$APP_GO_ID" applicationStatus + + echo "=== docker service ls (post-upgrade) ===" + docker service ls + + FAIL=$(docker service ls --format '{{.Name}} {{.Replicas}}' | \ + grep -E '^ci-' | grep -v ' 1/1' || true) + if [ -n "$FAIL" ]; then + echo "❌ User services not healthy after upgrade:" + echo "$FAIL" + exit 1 + fi + echo "✅ All services healthy after upgrade to ${{ matrix.pair.to }}" + + # ── Diagnostics on failure ───────────────────────────────────────────── + - name: Dump state on failure + if: failure() + run: | + echo "=== docker service ls ===" && docker service ls || true + echo "=== dokploy service tasks ===" && \ + docker service ps "${{ env.DOKPLOY_SERVICE }}" --no-trunc || true + echo "=== dokploy logs (last 200) ===" && \ + docker service logs "${{ env.DOKPLOY_SERVICE }}" --tail 200 2>&1 || true + echo "=== install.sh tail ===" && \ + tail -100 /tmp/install.sh 2>&1 || true + echo "=== signup response ===" && \ + cat /tmp/signup.out 2>&1 || true + echo "=== disk usage ===" && df -h + + # ── Job summary ──────────────────────────────────────────────────────── + - name: Write job summary + if: always() + env: + JOB_STATUS: ${{ job.status }} + run: | + STATUS="$JOB_STATUS" + ICON="✅"; [ "$STATUS" != "success" ] && ICON="❌" + cat >> "$GITHUB_STEP_SUMMARY" < { + it("rejects an empty name", () => { + const result = apiKeyNameSchema.safeParse(""); + expect(result.success).toBe(false); + }); + + it("accepts a name at the maximum length", () => { + const name = "a".repeat(API_KEY_NAME_MAX_LENGTH); + const result = apiKeyNameSchema.safeParse(name); + expect(result.success).toBe(true); + }); + + it("rejects a name over the maximum length instead of passing it to better-auth", () => { + const name = "a".repeat(API_KEY_NAME_MAX_LENGTH + 1); + const result = apiKeyNameSchema.safeParse(name); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0]?.message).toBe( + `Name must be at most ${API_KEY_NAME_MAX_LENGTH} characters`, + ); + } + }); +}); diff --git a/apps/dokploy/__test__/api/session-management.test.ts b/apps/dokploy/__test__/api/session-management.test.ts new file mode 100644 index 000000000..48f7eb257 --- /dev/null +++ b/apps/dokploy/__test__/api/session-management.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; + +const revokeSessionSchema = z.object({ + sessionId: z.string(), +}); + +describe("revokeSession input validation", () => { + it("accepts a valid session id", () => { + const result = revokeSessionSchema.safeParse({ sessionId: "abc123" }); + expect(result.success).toBe(true); + }); + + it("rejects missing sessionId", () => { + const result = revokeSessionSchema.safeParse({}); + expect(result.success).toBe(false); + }); + + it("rejects non-string sessionId", () => { + const result = revokeSessionSchema.safeParse({ sessionId: 123 }); + expect(result.success).toBe(false); + }); +}); diff --git a/apps/dokploy/__test__/backups/db-backup-restore-injection.test.ts b/apps/dokploy/__test__/backups/db-backup-restore-injection.test.ts new file mode 100644 index 000000000..d48644c95 --- /dev/null +++ b/apps/dokploy/__test__/backups/db-backup-restore-injection.test.ts @@ -0,0 +1,106 @@ +import { execSync } from "node:child_process"; +import { chmodSync, existsSync, rmSync, writeFileSync } from "node:fs"; +import { + getLibsqlBackupCommand, + getMariadbBackupCommand, + getMongoBackupCommand, + getMysqlBackupCommand, + getPostgresBackupCommand, +} from "@dokploy/server/utils/backups/utils"; +import { + getMariadbRestoreCommand, + getMongoRestoreCommand, + getMysqlRestoreCommand, + getPostgresRestoreCommand, +} from "@dokploy/server/utils/restore/utils"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +// A stub replacing the real `docker` binary. It ignores exec/-i/$CONTAINER_ID, +// exports the -e VAR=val pairs, and runs the inner `sh -c +