Compare commits
6 commits
cfffcba187
...
d271bdc513
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d271bdc513 | ||
|
|
5e3acd9ba9 | ||
|
|
94b67a8427 | ||
|
|
6030a0efbd | ||
|
|
842fc3a66e | ||
|
|
04a8264d61 |
69 changed files with 1484 additions and 2199 deletions
|
|
@ -15,4 +15,5 @@ ROOT_USERNAME=
|
|||
ROOT_USER_EMAIL=
|
||||
ROOT_USER_PASSWORD=
|
||||
|
||||
REGISTRY_URL=docker.io
|
||||
REGISTRY_URL=forgejo.mapledeploy.ca
|
||||
CDN_URL=https://updates.mapledeploy.ca
|
||||
|
|
|
|||
103
.forgejo/workflows/build.yml
Normal file
103
.forgejo/workflows/build.yml
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
name: Build MapleDeploy Coolify Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [mapledeploy]
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
- ".github/**"
|
||||
|
||||
env:
|
||||
REGISTRY: forgejo.mapledeploy.ca
|
||||
CDN_STORAGE_ZONE: coolify-update
|
||||
CDN_PULL_ZONE_ID: "5338895"
|
||||
CDN_BASE_URL: https://updates.mapledeploy.ca
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
BASE_VERSION=$(sed -n "s/.*'version' => '\([^']*\)'.*/\1/p" config/constants.php)
|
||||
TIMESTAMP=$(date -u +%Y%m%d%H%M)
|
||||
VERSION="${BASE_VERSION}.${TIMESTAMP}"
|
||||
HELPER_VERSION=$(sed -n "s/.*'helper_version' => '\([^']*\)'.*/\1/p" config/constants.php)
|
||||
REALTIME_VERSION=$(sed -n "s/.*'realtime_version' => '\([^']*\)'.*/\1/p" config/constants.php)
|
||||
echo "VERSION=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "HELPER_VERSION=${HELPER_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "REALTIME_VERSION=${REALTIME_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "Building version: ${VERSION} (helper: ${HELPER_VERSION}, realtime: ${REALTIME_VERSION})"
|
||||
|
||||
- name: Login to Forgejo registry
|
||||
run: |
|
||||
echo "${{ secrets.FORGEJO_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ github.repository_owner }} --password-stdin
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
DOCKER_BUILDKIT=1 docker build -f docker/production/Dockerfile \
|
||||
--build-arg MAPLEDEPLOY_VERSION=${{ steps.version.outputs.VERSION }} \
|
||||
-t ${{ env.REGISTRY }}/${{ github.repository }}:${{ steps.version.outputs.VERSION }} \
|
||||
-t ${{ env.REGISTRY }}/${{ github.repository }}:latest \
|
||||
.
|
||||
|
||||
- name: Push image
|
||||
run: |
|
||||
docker push ${{ env.REGISTRY }}/${{ github.repository }}:${{ steps.version.outputs.VERSION }}
|
||||
docker push ${{ env.REGISTRY }}/${{ github.repository }}:latest
|
||||
|
||||
- name: Generate versions.json
|
||||
run: |
|
||||
cat > versions.json <<EOF
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "${{ steps.version.outputs.VERSION }}"
|
||||
},
|
||||
"helper": {
|
||||
"version": "${{ steps.version.outputs.HELPER_VERSION }}"
|
||||
},
|
||||
"realtime": {
|
||||
"version": "${{ steps.version.outputs.REALTIME_VERSION }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
echo "Generated versions.json:"
|
||||
cat versions.json
|
||||
|
||||
- name: Install curl
|
||||
run: apk add --no-cache curl
|
||||
|
||||
- name: Upload artifacts to Bunny CDN
|
||||
run: |
|
||||
STORAGE_URL="https://storage.bunnycdn.com/${{ env.CDN_STORAGE_ZONE }}/coolify"
|
||||
|
||||
upload() {
|
||||
local file="$1"
|
||||
local dest="$2"
|
||||
echo "Uploading ${file} -> ${dest}"
|
||||
curl -fsSL -X PUT "${STORAGE_URL}/${dest}" \
|
||||
-H "AccessKey: ${{ secrets.BUNNY_CDN_STORAGE_KEY }}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"${file}"
|
||||
}
|
||||
|
||||
upload versions.json versions.json
|
||||
upload scripts/upgrade.sh upgrade.sh
|
||||
upload scripts/upgrade-postgres.sh upgrade-postgres.sh
|
||||
upload docker-compose.yml docker-compose.yml
|
||||
upload docker-compose.prod.yml docker-compose.prod.yml
|
||||
upload .env.production .env.production
|
||||
|
||||
echo "All artifacts uploaded."
|
||||
|
||||
- name: Purge CDN cache
|
||||
run: |
|
||||
curl -fsSL -X POST "https://api.bunny.net/pullzone/${{ env.CDN_PULL_ZONE_ID }}/purgeCache" \
|
||||
-H "AccessKey: ${{ secrets.BUNNY_API_KEY }}" \
|
||||
-H "Content-Type: application/json"
|
||||
echo "CDN cache purged."
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
name: Lock closed Issues, Discussions, and PRs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 1 * * *'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
discussions: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
lock-threads:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Lock threads after 30 days of inactivity
|
||||
uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-inactive-days: '30'
|
||||
discussion-inactive-days: '30'
|
||||
pr-inactive-days: '30'
|
||||
182
.github/workflows/chore-manage-pr-branch.yaml
vendored
182
.github/workflows/chore-manage-pr-branch.yaml
vendored
|
|
@ -1,182 +0,0 @@
|
|||
name: Manage PR Branch
|
||||
|
||||
# Runs *after* the "PR Quality" workflow finishes. This is required because
|
||||
# PR Quality may close a PR that fails its checks, so we must wait for it to
|
||||
# complete before deciding whether to retarget the PR's base branch.
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["PR Quality"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: manage-pr-branch-${{ github.event.workflow_run.head_sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
manage-branch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Retarget PR base branch based on category
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const run = context.payload.workflow_run;
|
||||
|
||||
// Branch routing based on the "Category" section of the PR body.
|
||||
// Bug fixes and one-click service changes ship in patch releases -> main.
|
||||
// Everything else (features, improvements) -> next.
|
||||
const MAIN_BRANCH = 'main';
|
||||
const NEXT_BRANCH = 'next';
|
||||
|
||||
// Maintainers/collaborators are trusted to pick their own base branch.
|
||||
const EXEMPT_ASSOCIATIONS = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
|
||||
|
||||
// Resolve the open PR from the triggering run.
|
||||
//
|
||||
// PR Quality runs on `pull_request_target`, so `run.head_sha` is the
|
||||
// *base* branch tip, not the PR head — a commit-based lookup finds
|
||||
// nothing. Instead match on the source branch (`head_branch`) and its
|
||||
// owner (`head_repository.owner.login`), which uniquely identify the PR
|
||||
// via the `owner:branch` head filter. This also works for forked PRs,
|
||||
// where `workflow_run.pull_requests` is empty.
|
||||
const headOwner = run.head_repository?.owner?.login;
|
||||
const headBranch = run.head_branch;
|
||||
|
||||
let prRef;
|
||||
if (headOwner && headBranch) {
|
||||
const { data: openPrs } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
head: `${headOwner}:${headBranch}`,
|
||||
per_page: 100,
|
||||
});
|
||||
prRef = openPrs[0];
|
||||
}
|
||||
|
||||
// Fallback: same-repo PRs may also be resolvable by commit association.
|
||||
if (!prRef) {
|
||||
const { data: associated } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: run.head_sha,
|
||||
});
|
||||
prRef = associated.find(pr => pr.state === 'open');
|
||||
}
|
||||
|
||||
if (!prRef) {
|
||||
core.info('No open PR associated with this run (possibly closed by PR Quality). Skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the full PR to get an up-to-date body, base ref, and state.
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prRef.number,
|
||||
});
|
||||
|
||||
if (pr.state !== 'open') {
|
||||
core.info(`PR #${pr.number} is not open. Skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip PRs opened by owners/members/collaborators — they choose their own base.
|
||||
if (EXEMPT_ASSOCIATIONS.has(pr.author_association)) {
|
||||
core.info(`PR #${pr.number} author association is ${pr.author_association}. Skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if a maintainer has already changed the base branch manually.
|
||||
const timeline = await github.paginate(github.rest.issues.listEventsForTimeline, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const baseChanges = timeline.filter(e => e.event === 'base_ref_changed');
|
||||
for (const change of baseChanges) {
|
||||
const actor = change.actor?.login;
|
||||
if (!actor) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
username: actor,
|
||||
});
|
||||
// admin/maintain/write => trusted maintainer.
|
||||
if (['admin', 'maintain', 'write'].includes(perm.permission)) {
|
||||
core.info(`Base branch was changed manually by ${actor} (${perm.permission}). Skipping.`);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
core.info(`Could not resolve permission for ${actor}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the checked category checkboxes from the PR body.
|
||||
const body = pr.body ?? '';
|
||||
const checked = [];
|
||||
const checkboxRegex = /^\s*-\s*\[([ xX])\]\s*(.+?)\s*$/gm;
|
||||
let match;
|
||||
while ((match = checkboxRegex.exec(body)) !== null) {
|
||||
if (match[1].toLowerCase() === 'x') {
|
||||
checked.push(match[2].toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
const includesAny = (labels) => labels.some(label => checked.some(c => c.includes(label)));
|
||||
|
||||
const mainCategories = ['bug fix', 'adding new one click service', 'fixing or updating existing one click service'];
|
||||
const nextCategories = ['improvement', 'new feature'];
|
||||
|
||||
const wantsMain = includesAny(mainCategories);
|
||||
const wantsNext = includesAny(nextCategories);
|
||||
|
||||
if (!wantsMain && !wantsNext) {
|
||||
core.info('No category selected in the PR body. Skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
// If categories from both groups are checked, prefer next: features and
|
||||
// improvements can only be released from the development branch.
|
||||
const targetBranch = wantsNext ? NEXT_BRANCH : MAIN_BRANCH;
|
||||
|
||||
if (pr.base.ref === targetBranch) {
|
||||
core.info(`PR #${pr.number} already targets ${targetBranch}. Nothing to do.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const previousBranch = pr.base.ref;
|
||||
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number,
|
||||
base: targetBranch,
|
||||
});
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: [
|
||||
`Based on the selected category, this PR's base branch was automatically changed from \`${previousBranch}\` to \`${targetBranch}\`.`,
|
||||
'',
|
||||
targetBranch === MAIN_BRANCH
|
||||
? 'Bug fixes and one-click service changes target `main`.'
|
||||
: 'New features and improvements target `next`.',
|
||||
'',
|
||||
'If you believe this is incorrect, please let a maintainer know.',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
core.info(`Retargeted PR #${pr.number}: ${previousBranch} -> ${targetBranch}.`);
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
name: Manage Stale Issues and PRs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
manage-stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Manage stale issues and PRs
|
||||
uses: actions/stale@v9
|
||||
id: stale
|
||||
with:
|
||||
stale-issue-message: 'This issue will be automatically closed in a few days if no response is received. Please provide an update with the requested information.'
|
||||
stale-pr-message: 'This pull request requires attention. If no changes or response is received within the next few days, it will be automatically closed. Please update your PR or leave a comment with the requested information.'
|
||||
close-issue-message: 'This issue has been automatically closed due to inactivity.'
|
||||
close-pr-message: 'Thank you for your contribution. Due to inactivity, this PR was automatically closed. If you would like to continue working on this change in the future, feel free to reopen this PR or submit a new one.'
|
||||
days-before-stale: 14
|
||||
days-before-close: 7
|
||||
stale-issue-label: '⏱︎ Stale'
|
||||
stale-pr-label: '⏱︎ Stale'
|
||||
only-labels: '💤 Waiting for feedback, 💤 Waiting for changes'
|
||||
remove-stale-when-updated: true
|
||||
operations-per-run: 100
|
||||
labels-to-remove-when-unstale: '⏱︎ Stale, 💤 Waiting for feedback, 💤 Waiting for changes'
|
||||
close-issue-reason: 'not_planned'
|
||||
exempt-all-milestones: false
|
||||
52
.github/workflows/chore-pr-comments.yml
vendored
52
.github/workflows/chore-pr-comments.yml
vendored
|
|
@ -1,52 +0,0 @@
|
|||
name: Add comment based on label
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
add-comment:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- label: "⚙️ Service"
|
||||
body: |
|
||||
Hi @${{ github.event.pull_request.user.login }}! 👋
|
||||
|
||||
It appears to us that you are either adding a new service or making changes to an existing one.
|
||||
We kindly ask you to also review and update the **Coolify Documentation** to include this new service or it's new configuration needs.
|
||||
This will help ensure that our documentation remains accurate and up-to-date for all users.
|
||||
|
||||
Coolify Docs Repository: https://github.com/coollabsio/coolify-docs
|
||||
How to Contribute a new Service to the Docs: https://coolify.io/docs/get-started/contribute/service#adding-a-new-service-template-to-the-coolify-documentation
|
||||
- label: "🛠️ Feature"
|
||||
body: |
|
||||
Hi @${{ github.event.pull_request.user.login }}! 👋
|
||||
|
||||
It appears to us that you are adding a new feature to Coolify.
|
||||
We kindly ask you to also update the **Coolify Documentation** to include information about this new feature.
|
||||
This will help ensure that our documentation remains accurate and up-to-date for all users.
|
||||
|
||||
Coolify Docs Repository: https://github.com/coollabsio/coolify-docs
|
||||
How to Contribute to the Docs: https://coolify.io/docs/get-started/contribute/documentation
|
||||
# - label: "✨ Enhancement"
|
||||
# body: |
|
||||
# It appears to us that you are making an enhancement to Coolify.
|
||||
# We kindly ask you to also review and update the Coolify Documentation to include information about this enhancement if applicable.
|
||||
# This will help ensure that our documentation remains accurate and up-to-date for all users.
|
||||
steps:
|
||||
- name: Add comment
|
||||
if: >-
|
||||
(github.event.label.name == matrix.label || github.event.label.name == '📑 Waiting for Docs PR')
|
||||
&& contains(github.event.pull_request.labels.*.name, matrix.label)
|
||||
&& contains(github.event.pull_request.labels.*.name, '📑 Waiting for Docs PR')
|
||||
run: gh pr comment "$NUMBER" --body "$BODY"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
NUMBER: ${{ github.event.pull_request.number }}
|
||||
BODY: ${{ matrix.body }}
|
||||
37
.github/workflows/claude.yml
vendored
37
.github/workflows/claude.yml
vendored
|
|
@ -1,37 +0,0 @@
|
|||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
claude_args: '--model opus'
|
||||
22
.github/workflows/cleanup-ghcr-untagged.yml
vendored
22
.github/workflows/cleanup-ghcr-untagged.yml
vendored
|
|
@ -1,22 +0,0 @@
|
|||
name: Cleanup Untagged GHCR Images
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
cleanup-all-packages:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
package: ['coolify', 'coolify-helper', 'coolify-realtime', 'coolify-testing-host']
|
||||
steps:
|
||||
- name: Delete untagged ${{ matrix.package }} images
|
||||
uses: actions/delete-package-versions@v5
|
||||
with:
|
||||
package-name: ${{ matrix.package }}
|
||||
package-type: 'container'
|
||||
min-versions-to-keep: 0
|
||||
delete-only-untagged-versions: 'true'
|
||||
117
.github/workflows/coolify-helper-next.yml
vendored
117
.github/workflows/coolify-helper-next.yml
vendored
|
|
@ -1,117 +0,0 @@
|
|||
name: Coolify Helper Image Development
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "next" ]
|
||||
paths:
|
||||
- .github/workflows/coolify-helper-next.yml
|
||||
- docker/coolify-helper/Dockerfile
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify-helper"
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-helper/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next
|
||||
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
|
||||
|
||||
161
.github/workflows/coolify-helper.yml
vendored
161
.github/workflows/coolify-helper.yml
vendored
|
|
@ -1,161 +0,0 @@
|
|||
name: Coolify Helper Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- .github/workflows/coolify-helper.yml
|
||||
- docker/coolify-helper/Dockerfile
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify-helper"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Ensure version is not published
|
||||
run: |
|
||||
BASE_VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)
|
||||
VERSION="${BASE_VERSION}"
|
||||
for registry in "${DOCKER_REGISTRY}" "${GITHUB_REGISTRY}"; do
|
||||
IMAGE="${registry}/${IMAGE_NAME}:${VERSION}"
|
||||
if output=$(docker buildx imagetools inspect "$IMAGE" 2>&1); then
|
||||
echo "::error::Version $VERSION already exists in $registry"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -Eqi 'manifest unknown|not found|no such manifest' <<< "$output"; then
|
||||
echo "::error::Could not verify $IMAGE: $output"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "Version $VERSION is available in both registries"
|
||||
|
||||
build-push:
|
||||
needs: check-version
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-helper/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}
|
||||
152
.github/workflows/coolify-next-build.yml
vendored
152
.github/workflows/coolify-next-build.yml
vendored
|
|
@ -1,152 +0,0 @@
|
|||
name: Build Coolify Next
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [next]
|
||||
paths-ignore:
|
||||
- .github/workflows/coolify-helper.yml
|
||||
- .github/workflows/coolify-helper-next.yml
|
||||
- .github/workflows/coolify-realtime.yml
|
||||
- .github/workflows/coolify-realtime-next.yml
|
||||
- .github/workflows/pr-quality.yaml
|
||||
- docker/coolify-helper/Dockerfile
|
||||
- docker/coolify-realtime/Dockerfile
|
||||
- docker/testing-host/Dockerfile
|
||||
- templates/**
|
||||
- CHANGELOG.md
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: coolify-next-build
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: coollabsio/coolify
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
rc_version: ${{ steps.version.outputs.rc_version }}
|
||||
short_sha: ${{ steps.version.outputs.short_sha }}
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Resolve next version
|
||||
id: version
|
||||
run: |
|
||||
RC_VERSION=$(jq -r '.coolify.nightly.version' versions.json)
|
||||
if [[ ! "${RC_VERSION}" =~ ^[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
||||
echo "Invalid next RC version: ${RC_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
VERSION="${RC_VERSION}.${SHORT_SHA}"
|
||||
echo "rc_version=${RC_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
needs: prepare
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push next image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/production/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
build-args: |
|
||||
COOLIFY_VERSION=${{ needs.prepare.outputs.version }}
|
||||
tags: |
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next-build-${{ needs.prepare.outputs.short_sha }}-${{ matrix.arch }}
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next-build-${{ needs.prepare.outputs.short_sha }}-${{ matrix.arch }}
|
||||
|
||||
publish:
|
||||
needs: [prepare, build]
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Publish next manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
env:
|
||||
REGISTRY: ${{ env.GITHUB_REGISTRY }}
|
||||
SHA: ${{ needs.prepare.outputs.short_sha }}
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
run: |
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||
SOURCE="next-build-${SHA}"
|
||||
docker buildx imagetools create \
|
||||
"${IMAGE}:${SOURCE}-amd64" \
|
||||
"${IMAGE}:${SOURCE}-aarch64" \
|
||||
--tag "${IMAGE}:sha-${SHA}" \
|
||||
--tag "${IMAGE}:${VERSION}" \
|
||||
--tag "${IMAGE}:next"
|
||||
|
||||
- name: Publish next manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
env:
|
||||
REGISTRY: ${{ env.DOCKER_REGISTRY }}
|
||||
SHA: ${{ needs.prepare.outputs.short_sha }}
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
run: |
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||
SOURCE="next-build-${SHA}"
|
||||
docker buildx imagetools create \
|
||||
"${IMAGE}:${SOURCE}-amd64" \
|
||||
"${IMAGE}:${SOURCE}-aarch64" \
|
||||
--tag "${IMAGE}:sha-${SHA}" \
|
||||
--tag "${IMAGE}:${VERSION}" \
|
||||
--tag "${IMAGE}:next"
|
||||
304
.github/workflows/coolify-rc-release.yml
vendored
304
.github/workflows/coolify-rc-release.yml
vendored
|
|
@ -1,304 +0,0 @@
|
|||
name: Release Coolify RC
|
||||
run-name: ${{ inputs.tag }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Existing draft prerelease tag (for example, v4.4-rc.1)
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: coolify-rc-release
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: coollabsio/coolify
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
outputs:
|
||||
release_id: ${{ steps.draft.outputs.release_id }}
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Reject releases outside next
|
||||
if: ${{ github.ref != 'refs/heads/next' }}
|
||||
run: |
|
||||
echo "RC releases must run from the next branch, not ${{ github.ref }}."
|
||||
exit 1
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate version
|
||||
id: version
|
||||
env:
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
run: |
|
||||
if [[ ! "${TAG_NAME}" =~ ^v[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
||||
echo "Unsupported RC tag: ${TAG_NAME}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="${TAG_NAME#v}"
|
||||
CONFIG_VERSION=$(jq -r '.coolify.nightly.version' versions.json)
|
||||
if [[ "${CONFIG_VERSION}" != "${VERSION}" ]]; then
|
||||
echo "RC tag ${VERSION} does not match nightly version ${CONFIG_VERSION}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate and pin draft prerelease
|
||||
id: draft
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
with:
|
||||
script: |
|
||||
const releases = await github.paginate(github.rest.repos.listReleases, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
per_page: 100,
|
||||
});
|
||||
const release = releases.find((candidate) => candidate.tag_name === process.env.TAG_NAME);
|
||||
|
||||
if (!release) {
|
||||
core.setFailed(`Create a draft prerelease for ${process.env.TAG_NAME} before running this workflow.`);
|
||||
return;
|
||||
}
|
||||
if (!release.draft) {
|
||||
core.setFailed(`Release ${process.env.TAG_NAME} must still be a draft.`);
|
||||
return;
|
||||
}
|
||||
if (!release.prerelease) {
|
||||
core.setFailed(`RC release ${process.env.TAG_NAME} must be marked as a prerelease.`);
|
||||
return;
|
||||
}
|
||||
if (!release.body?.trim()) {
|
||||
core.setFailed(`Draft prerelease ${process.env.TAG_NAME} must contain reviewed release notes.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.git.getRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `tags/${process.env.TAG_NAME}`,
|
||||
});
|
||||
core.setFailed(`Git tag ${process.env.TAG_NAME} already exists.`);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error;
|
||||
}
|
||||
|
||||
await github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: release.id,
|
||||
tag_name: process.env.TAG_NAME,
|
||||
target_commitish: context.sha,
|
||||
prerelease: true,
|
||||
});
|
||||
core.setOutput('release_id', release.id);
|
||||
|
||||
build:
|
||||
needs: validate
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push RC image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/production/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
build-args: |
|
||||
COOLIFY_VERSION=${{ needs.validate.outputs.version }}
|
||||
tags: |
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:rc-release-${{ needs.validate.outputs.version }}-${{ github.sha }}-${{ matrix.arch }}
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:rc-release-${{ needs.validate.outputs.version }}-${{ github.sha }}-${{ matrix.arch }}
|
||||
|
||||
revalidate:
|
||||
needs: [validate, build]
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Revalidate draft prerelease
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
RELEASE_ID: ${{ needs.validate.outputs.release_id }}
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
with:
|
||||
script: |
|
||||
const releaseId = Number(process.env.RELEASE_ID);
|
||||
const { data: release } = await github.rest.repos.getRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: releaseId,
|
||||
});
|
||||
|
||||
if (release.tag_name !== process.env.TAG_NAME || !release.draft || !release.prerelease) {
|
||||
core.setFailed(`Draft prerelease ${process.env.TAG_NAME} changed while the images were building.`);
|
||||
return;
|
||||
}
|
||||
if (!release.body?.trim()) {
|
||||
core.setFailed(`Draft prerelease ${process.env.TAG_NAME} no longer contains release notes.`);
|
||||
return;
|
||||
}
|
||||
if (release.target_commitish !== context.sha) {
|
||||
core.setFailed(`Draft prerelease ${process.env.TAG_NAME} no longer targets ${context.sha}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.git.getRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `tags/${process.env.TAG_NAME}`,
|
||||
});
|
||||
core.setFailed(`Git tag ${process.env.TAG_NAME} was created while the images were building.`);
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error;
|
||||
}
|
||||
|
||||
publish:
|
||||
needs: [validate, build, revalidate]
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Publish RC and next on ${{ env.GITHUB_REGISTRY }}
|
||||
env:
|
||||
REGISTRY: ${{ env.GITHUB_REGISTRY }}
|
||||
VERSION: ${{ needs.validate.outputs.version }}
|
||||
run: |
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||
SOURCE="rc-release-${VERSION}-${GITHUB_SHA}"
|
||||
docker buildx imagetools create \
|
||||
"${IMAGE}:${SOURCE}-amd64" \
|
||||
"${IMAGE}:${SOURCE}-aarch64" \
|
||||
--tag "${IMAGE}:${VERSION}" \
|
||||
--tag "${IMAGE}:next"
|
||||
|
||||
- name: Publish RC and next on ${{ env.DOCKER_REGISTRY }}
|
||||
env:
|
||||
REGISTRY: ${{ env.DOCKER_REGISTRY }}
|
||||
VERSION: ${{ needs.validate.outputs.version }}
|
||||
run: |
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||
SOURCE="rc-release-${VERSION}-${GITHUB_SHA}"
|
||||
docker buildx imagetools create \
|
||||
"${IMAGE}:${SOURCE}-amd64" \
|
||||
"${IMAGE}:${SOURCE}-aarch64" \
|
||||
--tag "${IMAGE}:${VERSION}" \
|
||||
--tag "${IMAGE}:next"
|
||||
|
||||
- name: Publish reviewed draft prerelease
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
RELEASE_ID: ${{ needs.validate.outputs.release_id }}
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
with:
|
||||
script: |
|
||||
const releaseId = Number(process.env.RELEASE_ID);
|
||||
const { data: release } = await github.rest.repos.getRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: releaseId,
|
||||
});
|
||||
|
||||
if (release.tag_name !== process.env.TAG_NAME || !release.draft || !release.prerelease) {
|
||||
core.setFailed(`Draft prerelease ${process.env.TAG_NAME} changed while the images were building.`);
|
||||
return;
|
||||
}
|
||||
if (!release.body?.trim()) {
|
||||
core.setFailed(`Draft prerelease ${process.env.TAG_NAME} no longer contains release notes.`);
|
||||
return;
|
||||
}
|
||||
if (release.target_commitish !== context.sha) {
|
||||
core.setFailed(`Draft prerelease ${process.env.TAG_NAME} no longer targets ${context.sha}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.git.getRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `tags/${process.env.TAG_NAME}`,
|
||||
});
|
||||
core.setFailed(`Git tag ${process.env.TAG_NAME} was created while the images were building.`);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error;
|
||||
}
|
||||
|
||||
await github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: Number(process.env.RELEASE_ID),
|
||||
tag_name: process.env.TAG_NAME,
|
||||
target_commitish: context.sha,
|
||||
prerelease: true,
|
||||
draft: false,
|
||||
});
|
||||
120
.github/workflows/coolify-realtime-next.yml
vendored
120
.github/workflows/coolify-realtime-next.yml
vendored
|
|
@ -1,120 +0,0 @@
|
|||
name: Coolify Realtime Development
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "next" ]
|
||||
paths:
|
||||
- .github/workflows/coolify-realtime-next.yml
|
||||
- docker/coolify-realtime/Dockerfile
|
||||
- docker/coolify-realtime/terminal-server.js
|
||||
- docker/coolify-realtime/package.json
|
||||
- docker/coolify-realtime/package-lock.json
|
||||
- docker/coolify-realtime/soketi-entrypoint.sh
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify-realtime"
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-realtime/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next
|
||||
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
|
||||
161
.github/workflows/coolify-realtime.yml
vendored
161
.github/workflows/coolify-realtime.yml
vendored
|
|
@ -1,161 +0,0 @@
|
|||
name: Coolify Realtime
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- .github/workflows/coolify-realtime.yml
|
||||
- docker/coolify-realtime/**
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify-realtime"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Ensure version is not published
|
||||
run: |
|
||||
BASE_VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)
|
||||
VERSION="${BASE_VERSION}"
|
||||
for registry in "${DOCKER_REGISTRY}" "${GITHUB_REGISTRY}"; do
|
||||
IMAGE="${registry}/${IMAGE_NAME}:${VERSION}"
|
||||
if output=$(docker buildx imagetools inspect "$IMAGE" 2>&1); then
|
||||
echo "::error::Version $VERSION already exists in $registry"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -Eqi 'manifest unknown|not found|no such manifest' <<< "$output"; then
|
||||
echo "::error::Could not verify $IMAGE: $output"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "Version $VERSION is available in both registries"
|
||||
|
||||
build-push:
|
||||
needs: check-version
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-realtime/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}
|
||||
259
.github/workflows/coolify-release.yml
vendored
259
.github/workflows/coolify-release.yml
vendored
|
|
@ -1,259 +0,0 @@
|
|||
name: Release Coolify Stable
|
||||
run-name: ${{ inputs.tag }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Existing draft release tag (for example, v4.3.1)
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: coolify-fix-release
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: coollabsio/coolify
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
outputs:
|
||||
release_id: ${{ steps.draft.outputs.release_id }}
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Reject releases outside the production branch
|
||||
if: ${{ github.ref_name != 'main' }}
|
||||
run: |
|
||||
echo "Stable releases must run from main, not ${{ github.ref_name }}."
|
||||
exit 1
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate version
|
||||
id: version
|
||||
env:
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
run: |
|
||||
if [[ ! "${TAG_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Unsupported fix release tag: ${TAG_NAME}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="${TAG_NAME#v}"
|
||||
CONFIG_VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)
|
||||
|
||||
if [[ "${CONFIG_VERSION}" != "${VERSION}" ]]; then
|
||||
echo "Release tag ${VERSION} does not match config version ${CONFIG_VERSION}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate and pin draft release
|
||||
id: draft
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
with:
|
||||
script: |
|
||||
const releases = await github.paginate(github.rest.repos.listReleases, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
per_page: 100,
|
||||
});
|
||||
const release = releases.find((candidate) => candidate.tag_name === process.env.TAG_NAME);
|
||||
|
||||
if (!release) {
|
||||
core.setFailed(`Create a draft release for ${process.env.TAG_NAME} before running this workflow.`);
|
||||
return;
|
||||
}
|
||||
if (!release.draft) {
|
||||
core.setFailed(`Release ${process.env.TAG_NAME} must still be a draft.`);
|
||||
return;
|
||||
}
|
||||
if (release.prerelease) {
|
||||
core.setFailed(`Fix release ${process.env.TAG_NAME} cannot be marked as a prerelease.`);
|
||||
return;
|
||||
}
|
||||
if (!release.body?.trim()) {
|
||||
core.setFailed(`Draft release ${process.env.TAG_NAME} must contain reviewed release notes.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.git.getRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `tags/${process.env.TAG_NAME}`,
|
||||
});
|
||||
core.setFailed(`Git tag ${process.env.TAG_NAME} already exists.`);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error;
|
||||
}
|
||||
|
||||
await github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: release.id,
|
||||
tag_name: process.env.TAG_NAME,
|
||||
target_commitish: context.sha,
|
||||
});
|
||||
core.setOutput('release_id', release.id);
|
||||
|
||||
build:
|
||||
needs: validate
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push release image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/production/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
build-args: |
|
||||
COOLIFY_VERSION=${{ needs.validate.outputs.version }}
|
||||
tags: |
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:release-${{ needs.validate.outputs.version }}-${{ github.sha }}-${{ matrix.arch }}
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:release-${{ needs.validate.outputs.version }}-${{ github.sha }}-${{ matrix.arch }}
|
||||
|
||||
publish:
|
||||
needs: [validate, build]
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Publish version and latest on ${{ env.GITHUB_REGISTRY }}
|
||||
env:
|
||||
REGISTRY: ${{ env.GITHUB_REGISTRY }}
|
||||
VERSION: ${{ needs.validate.outputs.version }}
|
||||
run: |
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||
SOURCE="release-${VERSION}-${GITHUB_SHA}"
|
||||
docker buildx imagetools create \
|
||||
"${IMAGE}:${SOURCE}-amd64" \
|
||||
"${IMAGE}:${SOURCE}-aarch64" \
|
||||
--tag "${IMAGE}:${VERSION}" \
|
||||
--tag "${IMAGE}:latest"
|
||||
|
||||
- name: Publish version and latest on ${{ env.DOCKER_REGISTRY }}
|
||||
env:
|
||||
REGISTRY: ${{ env.DOCKER_REGISTRY }}
|
||||
VERSION: ${{ needs.validate.outputs.version }}
|
||||
run: |
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||
SOURCE="release-${VERSION}-${GITHUB_SHA}"
|
||||
docker buildx imagetools create \
|
||||
"${IMAGE}:${SOURCE}-amd64" \
|
||||
"${IMAGE}:${SOURCE}-aarch64" \
|
||||
--tag "${IMAGE}:${VERSION}" \
|
||||
--tag "${IMAGE}:latest"
|
||||
|
||||
- name: Publish reviewed draft release
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
RELEASE_ID: ${{ needs.validate.outputs.release_id }}
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
with:
|
||||
script: |
|
||||
const releaseId = Number(process.env.RELEASE_ID);
|
||||
const { data: release } = await github.rest.repos.getRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: releaseId,
|
||||
});
|
||||
|
||||
if (release.tag_name !== process.env.TAG_NAME || !release.draft || release.prerelease) {
|
||||
core.setFailed(`Draft release ${process.env.TAG_NAME} changed while the images were building.`);
|
||||
return;
|
||||
}
|
||||
if (!release.body?.trim()) {
|
||||
core.setFailed(`Draft release ${process.env.TAG_NAME} no longer contains release notes.`);
|
||||
return;
|
||||
}
|
||||
if (release.target_commitish !== context.sha) {
|
||||
core.setFailed(`Draft release ${process.env.TAG_NAME} no longer targets ${context.sha}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.git.getRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `tags/${process.env.TAG_NAME}`,
|
||||
});
|
||||
core.setFailed(`Git tag ${process.env.TAG_NAME} was created while the images were building.`);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error;
|
||||
}
|
||||
|
||||
await github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: Number(process.env.RELEASE_ID),
|
||||
tag_name: process.env.TAG_NAME,
|
||||
target_commitish: context.sha,
|
||||
draft: false,
|
||||
});
|
||||
109
.github/workflows/coolify-sha-build.yml
vendored
109
.github/workflows/coolify-sha-build.yml
vendored
|
|
@ -1,109 +0,0 @@
|
|||
name: Build Coolify (SHA)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify"
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
outputs:
|
||||
short_sha: ${{ steps.version.outputs.short_sha }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Resolve internal version
|
||||
id: version
|
||||
run: |
|
||||
BASE_VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)
|
||||
echo "version=${BASE_VERSION}-dev.${GITHUB_SHA::9}" >> "$GITHUB_OUTPUT"
|
||||
echo "short_sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/production/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
build-args: |
|
||||
COOLIFY_VERSION=${{ steps.version.outputs.version }}
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ steps.version.outputs.short_sha }}-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ steps.version.outputs.short_sha }}-${{ matrix.arch }}
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
env:
|
||||
REGISTRY: ${{ env.GITHUB_REGISTRY }}
|
||||
SHA: ${{ needs.build-push.outputs.short_sha }}
|
||||
run: |
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||
docker buildx imagetools create \
|
||||
"${IMAGE}:sha-${SHA}-amd64" \
|
||||
"${IMAGE}:sha-${SHA}-aarch64" \
|
||||
--tag "${IMAGE}:sha-${SHA}"
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
env:
|
||||
REGISTRY: ${{ env.DOCKER_REGISTRY }}
|
||||
SHA: ${{ needs.build-push.outputs.short_sha }}
|
||||
run: |
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||
docker buildx imagetools create \
|
||||
"${IMAGE}:sha-${SHA}-amd64" \
|
||||
"${IMAGE}:sha-${SHA}-aarch64" \
|
||||
--tag "${IMAGE}:sha-${SHA}"
|
||||
104
.github/workflows/coolify-testing-host.yml
vendored
104
.github/workflows/coolify-testing-host.yml
vendored
|
|
@ -1,104 +0,0 @@
|
|||
name: Coolify Testing Host
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "next" ]
|
||||
paths:
|
||||
- .github/workflows/coolify-testing-host.yml
|
||||
- docker/testing-host/Dockerfile
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify-testing-host"
|
||||
|
||||
jobs:
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/testing-host/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-${{ matrix.arch }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
|
||||
42
.github/workflows/generate-changelog.yml
vendored
42
.github/workflows/generate-changelog.yml
vendored
|
|
@ -1,42 +0,0 @@
|
|||
name: Generate Changelog
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- .github/workflows/coolify-helper.yml
|
||||
- .github/workflows/coolify-helper-next.yml
|
||||
- .github/workflows/coolify-realtime.yml
|
||||
- .github/workflows/coolify-realtime-next.yml
|
||||
- .github/workflows/pr-quality.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
name: Generate changelog
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate changelog
|
||||
uses: orhun/git-cliff-action@v4
|
||||
with:
|
||||
config: cliff.toml
|
||||
args: --verbose
|
||||
env:
|
||||
OUTPUT: CHANGELOG.md
|
||||
GITHUB_REPO: ${{ github.repository }}
|
||||
|
||||
- name: Commit
|
||||
run: |
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
git add CHANGELOG.md
|
||||
git commit -m "docs: update changelog"
|
||||
git push https://${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git HEAD:${GITHUB_REF_NAME}
|
||||
108
.github/workflows/pr-quality.yaml
vendored
108
.github/workflows/pr-quality.yaml
vendored
|
|
@ -1,108 +0,0 @@
|
|||
name: PR Quality
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
pr-quality:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: peakoss/anti-slop@v0
|
||||
with:
|
||||
# General Settings
|
||||
max-failures: 4
|
||||
|
||||
# PR Branch Checks
|
||||
allowed-target-branches: ""
|
||||
blocked-target-branches: ""
|
||||
allowed-source-branches: ""
|
||||
blocked-source-branches: ""
|
||||
|
||||
# PR Quality Checks
|
||||
max-negative-reactions: 0
|
||||
require-maintainer-can-modify: true
|
||||
|
||||
# PR Title Checks
|
||||
require-conventional-title: true
|
||||
|
||||
# PR Description Checks
|
||||
require-description: true
|
||||
max-description-length: 2500
|
||||
max-emoji-count: 2
|
||||
max-code-references: 5
|
||||
require-linked-issue: false
|
||||
blocked-terms: |
|
||||
STRAWBERRY
|
||||
🤖 Generated with Claude Code
|
||||
Generated with Claude Code
|
||||
blocked-issue-numbers: 8154
|
||||
|
||||
# PR Template Checks
|
||||
require-pr-template: true
|
||||
strict-pr-template-sections: "Contributor Agreement"
|
||||
optional-pr-template-sections: "Issues,Preview"
|
||||
max-additional-pr-template-sections: 2
|
||||
|
||||
# Commit Message Checks
|
||||
max-commit-message-length: 500
|
||||
require-conventional-commits: false
|
||||
require-commit-author-match: true
|
||||
blocked-commit-authors: ""
|
||||
|
||||
# File Checks
|
||||
allowed-file-extensions: ""
|
||||
allowed-paths: ""
|
||||
blocked-paths: |
|
||||
README.md
|
||||
SECURITY.md
|
||||
LICENSE
|
||||
CODE_OF_CONDUCT.md
|
||||
templates/service-templates-latest.json
|
||||
templates/service-templates.json
|
||||
require-final-newline: true
|
||||
max-added-comments: 10
|
||||
|
||||
# User Checks
|
||||
detect-spam-usernames: true
|
||||
min-account-age: 30
|
||||
max-daily-forks: 7
|
||||
min-profile-completeness: 4
|
||||
|
||||
# Merge Checks
|
||||
min-repo-merged-prs: 0
|
||||
min-repo-merge-ratio: 0
|
||||
min-global-merge-ratio: 30
|
||||
global-merge-ratio-exclude-own: false
|
||||
|
||||
# Exemptions
|
||||
exempt-draft-prs: false
|
||||
exempt-bots: |
|
||||
actions-user
|
||||
dependabot[bot]
|
||||
renovate[bot]
|
||||
github-actions[bot]
|
||||
exempt-users: ""
|
||||
exempt-author-association: "OWNER,MEMBER,COLLABORATOR"
|
||||
exempt-label: "quality/exempt"
|
||||
exempt-pr-label: ""
|
||||
exempt-all-milestones: false
|
||||
exempt-all-pr-milestones: false
|
||||
exempt-milestones: ""
|
||||
exempt-pr-milestones: ""
|
||||
|
||||
# PR Success Actions
|
||||
success-add-pr-labels: ""
|
||||
|
||||
# PR Failure Actions
|
||||
failure-remove-pr-labels: ""
|
||||
failure-remove-all-pr-labels: true
|
||||
failure-add-pr-labels: "quality/rejected"
|
||||
failure-pr-message: "This PR did not pass quality checks so it will be closed. If you believe this is a mistake please let us know."
|
||||
close-pr: true
|
||||
lock-pr: false
|
||||
62
.github/workflows/sync-main-to-next.yml
vendored
62
.github/workflows/sync-main-to-next.yml
vendored
|
|
@ -1,62 +0,0 @@
|
|||
name: Sync main to next
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: sync-main-to-next
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Merge main into next
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout next
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: next
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Merge main into next
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
||||
git fetch origin main next
|
||||
|
||||
if git merge --no-edit origin/main; then
|
||||
git push origin HEAD:next
|
||||
exit 0
|
||||
fi
|
||||
|
||||
conflicts=$(git diff --name-only --diff-filter=U)
|
||||
git merge --abort
|
||||
|
||||
if [ -z "$conflicts" ]; then
|
||||
echo 'The merge failed without conflicts, so no pull request was created.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sync_branch='automation/sync-main-to-next'
|
||||
existing_pr=$(gh pr list --base next --head "$sync_branch" --state open --json url --jq '.[0].url')
|
||||
if [ -n "$existing_pr" ]; then
|
||||
echo "A main to next pull request already exists: $existing_pr"
|
||||
else
|
||||
git push --force origin origin/main:"refs/heads/$sync_branch"
|
||||
gh pr create \
|
||||
--base next \
|
||||
--head "$sync_branch" \
|
||||
--title 'chore: merge main into next' \
|
||||
--body 'This pull request was created automatically because main could not be merged into next without conflicts. Resolve conflicts on this temporary branch; never update main with next.'
|
||||
fi
|
||||
|
||||
echo 'main could not be merged into next without conflicts.'
|
||||
exit 1
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||
|
||||
class ResetUserPassword implements ResetsUserPasswords
|
||||
|
|
@ -17,6 +18,13 @@ class ResetUserPassword implements ResetsUserPasswords
|
|||
*/
|
||||
public function reset(User $user, array $input): void
|
||||
{
|
||||
if ($user->isMapledeployRevoked()) {
|
||||
// MapleDeploy branding: dashboard-managed revocation is restored only by mapledeploy:user:set-password.
|
||||
throw ValidationException::withMessages([
|
||||
'email' => [trans('passwords.user')],
|
||||
]);
|
||||
}
|
||||
|
||||
Validator::make($input, [
|
||||
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||
])->validate();
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ public function handle(Server $server, bool $restart = false, ?string $latestVer
|
|||
$endpoint = $server->settings->ensureSentinelUrl();
|
||||
$debug = data_get($server, 'settings.is_sentinel_debug_enabled');
|
||||
$mountDir = '/data/coolify/sentinel';
|
||||
$image = coolifyRegistryUrl().'/coollabsio/sentinel:'.$version;
|
||||
// MapleDeploy branding: Sentinel is not mirrored to our Forgejo registry, so pull from ghcr.io directly (upstream image)
|
||||
$image = 'ghcr.io/coollabsio/sentinel:'.$version;
|
||||
$environments = [
|
||||
'TOKEN' => $token,
|
||||
'DEBUG' => $debug ? 'true' : 'false',
|
||||
|
|
|
|||
|
|
@ -118,7 +118,8 @@ private function update()
|
|||
{
|
||||
$latestHelperImageVersion = getHelperVersion();
|
||||
$upgradeScriptUrl = config('constants.coolify.upgrade_script_url');
|
||||
$registryUrl = coolifyRegistryUrl();
|
||||
// MapleDeploy branding: always use the fork registry default, ignoring per-instance overrides
|
||||
$registryUrl = config('constants.coolify.registry_url');
|
||||
|
||||
remote_process([
|
||||
"curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh",
|
||||
|
|
|
|||
|
|
@ -265,15 +265,11 @@ private function restoreCoolifyDbBackup()
|
|||
}
|
||||
}
|
||||
|
||||
// MapleDeploy branding: telemetry disabled — no phone-home signal
|
||||
private function sendAliveSignal()
|
||||
{
|
||||
$id = config('app.id');
|
||||
$version = config('constants.coolify.version');
|
||||
try {
|
||||
Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version");
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in sending live signal: {$e->getMessage()}\n";
|
||||
}
|
||||
// Disabled for MapleDeploy: do not send telemetry to coolify.io
|
||||
return;
|
||||
}
|
||||
|
||||
private function replaceSlashInEnvironmentName()
|
||||
|
|
|
|||
160
app/Console/Commands/Mapledeploy/UserCreate.php
Normal file
160
app/Console/Commands/Mapledeploy/UserCreate.php
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands\Mapledeploy;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UserCreate extends Command
|
||||
{
|
||||
protected $signature = 'mapledeploy:user:create
|
||||
{--email= : User email address}
|
||||
{--name= : User display name}
|
||||
{--admin : Create the first root admin user}
|
||||
{--team-role=member : Root team role for non-admin users}';
|
||||
|
||||
protected $description = 'Create a Coolify user for MapleDeploy dashboard access management';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$password = $this->readPassword();
|
||||
$input = [
|
||||
'email' => $this->option('email'),
|
||||
'name' => $this->option('name'),
|
||||
'password' => $password,
|
||||
'team_role' => $this->option('team-role'),
|
||||
];
|
||||
|
||||
$validator = Validator::make($input, [
|
||||
'email' => ['required', 'string', 'email', 'max:255'],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'password' => ['required', 'string', 'min:8'],
|
||||
'team_role' => ['required', Rule::in([Role::ADMIN->value, Role::MEMBER->value])],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->failWith('INVALID_INPUT');
|
||||
}
|
||||
|
||||
$input['email'] = Str::lower((string) $input['email']);
|
||||
|
||||
if (User::whereEmail($input['email'])->exists()) {
|
||||
return $this->failWith('EMAIL_EXISTS');
|
||||
}
|
||||
|
||||
if ($this->option('admin')) {
|
||||
return $this->createAdmin($input);
|
||||
}
|
||||
|
||||
return $this->createMember($input);
|
||||
}
|
||||
|
||||
private function createAdmin(array $input): int
|
||||
{
|
||||
if (User::count() !== 0) {
|
||||
return $this->failWith('USERS_ALREADY_EXIST');
|
||||
}
|
||||
|
||||
$user = DB::transaction(function () use ($input) {
|
||||
$user = (new User)->forceFill([
|
||||
'id' => 0,
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'password' => Hash::make($input['password']),
|
||||
]);
|
||||
$user->save();
|
||||
$user->markEmailAsVerified();
|
||||
|
||||
$settings = instanceSettings();
|
||||
$settings->is_registration_enabled = false;
|
||||
$attributes = $settings->getAttributes();
|
||||
if (array_key_exists('setup_token', $attributes)) {
|
||||
$settings->setup_token = null;
|
||||
}
|
||||
if (array_key_exists('setup_callback_url', $attributes)) {
|
||||
$settings->setup_callback_url = null;
|
||||
}
|
||||
$settings->save();
|
||||
|
||||
return $user;
|
||||
});
|
||||
|
||||
return $this->succeedWithUser($user);
|
||||
}
|
||||
|
||||
private function createMember(array $input): int
|
||||
{
|
||||
$rootTeam = Team::find(0);
|
||||
if (! $rootTeam) {
|
||||
return $this->failWith('ROOT_TEAM_MISSING');
|
||||
}
|
||||
|
||||
$user = DB::transaction(function () use ($input, $rootTeam) {
|
||||
$user = User::create([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'password' => Hash::make($input['password']),
|
||||
]);
|
||||
$user->markEmailAsVerified();
|
||||
$this->deletePersonalTeams($user);
|
||||
$user->teams()->syncWithoutDetaching([
|
||||
$rootTeam->id => ['role' => $input['team_role']],
|
||||
]);
|
||||
|
||||
return $user;
|
||||
});
|
||||
|
||||
return $this->succeedWithUser($user);
|
||||
}
|
||||
|
||||
private function deletePersonalTeams(User $user): void
|
||||
{
|
||||
// MapleDeploy branding: dashboard-managed users should only see the
|
||||
// managed instance root team, not an empty personal Coolify team.
|
||||
$personalTeams = Team::query()
|
||||
->where('teams.id', '!=', 0)
|
||||
->where('personal_team', true)
|
||||
->whereHas('members', fn ($query) => $query->whereKey($user->id))
|
||||
->get();
|
||||
|
||||
foreach ($personalTeams as $team) {
|
||||
DB::table('team_user')
|
||||
->where('team_id', $team->id)
|
||||
->where('user_id', $user->id)
|
||||
->delete();
|
||||
DB::table('teams')->where('id', $team->id)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
private function readPassword(): string
|
||||
{
|
||||
return rtrim((string) stream_get_contents(STDIN), "\n");
|
||||
}
|
||||
|
||||
private function succeedWithUser(User $user): int
|
||||
{
|
||||
$this->line(json_encode([
|
||||
'user' => [
|
||||
'id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'name' => $user->name,
|
||||
],
|
||||
], JSON_THROW_ON_ERROR));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function failWith(string $code): int
|
||||
{
|
||||
$this->line(json_encode(['error' => $code], JSON_THROW_ON_ERROR));
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
61
app/Console/Commands/Mapledeploy/UserDelete.php
Normal file
61
app/Console/Commands/Mapledeploy/UserDelete.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands\Mapledeploy;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UserDelete extends Command
|
||||
{
|
||||
protected $signature = 'mapledeploy:user:delete {user_id : Coolify user id}';
|
||||
|
||||
protected $description = 'Delete a Coolify user for MapleDeploy dashboard access management';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$rawUserId = $this->argument('user_id');
|
||||
$userId = filter_var($rawUserId, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]]);
|
||||
if ($userId === false) {
|
||||
return $this->failWith('INVALID_USER_ID');
|
||||
}
|
||||
|
||||
if ($userId === 0) {
|
||||
return $this->failWith('CANNOT_DELETE_ROOT_USER');
|
||||
}
|
||||
|
||||
$user = User::find($userId);
|
||||
if (! $user) {
|
||||
$this->line(json_encode([
|
||||
'deleted' => null,
|
||||
'alreadyDeleted' => true,
|
||||
'id' => $userId,
|
||||
], JSON_THROW_ON_ERROR));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$deleted = [
|
||||
'id' => $user->id,
|
||||
'email' => $user->email,
|
||||
];
|
||||
|
||||
DB::transaction(function () use ($user) {
|
||||
$user->tokens()->delete();
|
||||
// MapleDeploy branding: deletion must end any active browser sessions.
|
||||
DB::table('sessions')->where('user_id', $user->id)->delete();
|
||||
$user->delete();
|
||||
});
|
||||
|
||||
$this->line(json_encode(['deleted' => $deleted], JSON_THROW_ON_ERROR));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function failWith(string $code): int
|
||||
{
|
||||
$this->line(json_encode(['error' => $code], JSON_THROW_ON_ERROR));
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
40
app/Console/Commands/Mapledeploy/UserList.php
Normal file
40
app/Console/Commands/Mapledeploy/UserList.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands\Mapledeploy;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class UserList extends Command
|
||||
{
|
||||
protected $signature = 'mapledeploy:user:list';
|
||||
|
||||
protected $description = 'List Coolify users for MapleDeploy dashboard access management';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$users = User::with('teams')
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->map(fn (User $user) => [
|
||||
'id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'name' => $user->name,
|
||||
'created_at' => $user->created_at?->toISOString(),
|
||||
'teams' => $user->teams
|
||||
->map(fn ($team) => [
|
||||
'id' => $team->id,
|
||||
'name' => $team->name,
|
||||
'role' => $team->pivot?->role,
|
||||
])
|
||||
->values()
|
||||
->all(),
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$this->line(json_encode(['users' => $users], JSON_THROW_ON_ERROR));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
55
app/Console/Commands/Mapledeploy/UserRevoke.php
Normal file
55
app/Console/Commands/Mapledeploy/UserRevoke.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands\Mapledeploy;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UserRevoke extends Command
|
||||
{
|
||||
protected $signature = 'mapledeploy:user:revoke {user_id : Coolify user id}';
|
||||
|
||||
protected $description = 'Revoke a Coolify user login for MapleDeploy dashboard access management';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$userId = (int) $this->argument('user_id');
|
||||
if ($userId === 0) {
|
||||
return $this->failWith('CANNOT_REVOKE_ROOT_USER');
|
||||
}
|
||||
|
||||
$user = User::find($userId);
|
||||
if (! $user) {
|
||||
return $this->failWith('USER_NOT_FOUND');
|
||||
}
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make(Str::random(64)),
|
||||
// MapleDeploy branding: OAuth login matches by email, so keep a
|
||||
// persistent marker that the callback can reject after revocation.
|
||||
'remember_token' => 'mapledeploy-revoked:'.Str::random(40),
|
||||
])->save();
|
||||
$user->tokens()->delete();
|
||||
// MapleDeploy branding: revocation must end any active browser sessions.
|
||||
DB::table('sessions')->where('user_id', $user->id)->delete();
|
||||
|
||||
$this->line(json_encode([
|
||||
'revoked' => [
|
||||
'id' => $user->id,
|
||||
'email' => $user->email,
|
||||
],
|
||||
], JSON_THROW_ON_ERROR));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function failWith(string $code): int
|
||||
{
|
||||
$this->line(json_encode(['error' => $code], JSON_THROW_ON_ERROR));
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
107
app/Console/Commands/Mapledeploy/UserSetPassword.php
Normal file
107
app/Console/Commands/Mapledeploy/UserSetPassword.php
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands\Mapledeploy;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UserSetPassword extends Command
|
||||
{
|
||||
protected $signature = 'mapledeploy:user:set-password
|
||||
{user_id : Coolify user id}
|
||||
{--email= : New user email address}
|
||||
{--name= : New user display name}';
|
||||
|
||||
protected $description = 'Set a Coolify user password for MapleDeploy dashboard access management';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$password = rtrim((string) stream_get_contents(STDIN), "\n");
|
||||
$updatesOwner = $this->option('email') !== null || $this->option('name') !== null;
|
||||
$input = [
|
||||
'password' => $password,
|
||||
'email' => $this->option('email'),
|
||||
'name' => $this->option('name'),
|
||||
];
|
||||
$rules = ['password' => ['required', 'string', 'min:8']];
|
||||
if ($updatesOwner) {
|
||||
$rules['email'] = ['required', 'string', 'email', 'max:255'];
|
||||
$rules['name'] = ['required', 'string', 'max:255'];
|
||||
}
|
||||
$validator = Validator::make($input, $rules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->failWith('INVALID_INPUT');
|
||||
}
|
||||
|
||||
$user = User::find($this->argument('user_id'));
|
||||
if (! $user) {
|
||||
return $this->failWith('USER_NOT_FOUND');
|
||||
}
|
||||
$rootTeam = null;
|
||||
if ((int) $user->id !== 0) {
|
||||
$rootTeam = Team::find(0);
|
||||
if (! $rootTeam) {
|
||||
return $this->failWith('ROOT_TEAM_MISSING');
|
||||
}
|
||||
}
|
||||
|
||||
$changes = [
|
||||
'password' => Hash::make($password),
|
||||
// MapleDeploy branding: clear the revocation marker when the
|
||||
// dashboard intentionally restores this Coolify login.
|
||||
'remember_token' => null,
|
||||
];
|
||||
if ($updatesOwner) {
|
||||
$email = Str::lower((string) $input['email']);
|
||||
if (User::whereEmail($email)->whereKeyNot($user->id)->exists()) {
|
||||
return $this->failWith('EMAIL_EXISTS');
|
||||
}
|
||||
// MapleDeploy branding: claiming root admin transfers the Coolify
|
||||
// account identity so the previous email holder cannot recover it.
|
||||
$changes['email'] = $email;
|
||||
$changes['name'] = $input['name'];
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($user, $changes, $updatesOwner, $rootTeam) {
|
||||
$user->forceFill($changes)->save();
|
||||
if ($updatesOwner && ! $user->hasVerifiedEmail()) {
|
||||
$user->markEmailAsVerified();
|
||||
}
|
||||
if ($rootTeam) {
|
||||
// MapleDeploy branding: matching an existing Coolify user by
|
||||
// email must grant the same root-team admin access as a newly
|
||||
// dashboard-created user.
|
||||
$user->teams()->syncWithoutDetaching([
|
||||
$rootTeam->id => ['role' => Role::ADMIN->value],
|
||||
]);
|
||||
}
|
||||
// MapleDeploy branding: password resets from the dashboard should
|
||||
// end browser sessions authenticated with the previous password.
|
||||
DB::table('sessions')->where('user_id', $user->id)->delete();
|
||||
});
|
||||
|
||||
$this->line(json_encode([
|
||||
'user' => [
|
||||
'id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'name' => $user->name,
|
||||
],
|
||||
], JSON_THROW_ON_ERROR));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function failWith(string $code): int
|
||||
{
|
||||
$this->line(json_encode(['error' => $code], JSON_THROW_ON_ERROR));
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
|
@ -82,6 +82,11 @@ public function forgot_password(Request $request)
|
|||
return response()->json(['message' => 'Transactional emails are not active'], 400);
|
||||
}
|
||||
$request->validate([Fortify::email() => 'required|email']);
|
||||
$user = User::where('email', $request->input(Fortify::email()))->first();
|
||||
if ($user?->isMapledeployRevoked()) {
|
||||
// MapleDeploy branding: only the dashboard set-password path can restore revoked users.
|
||||
return app(SuccessfulPasswordResetLinkRequestResponse::class, ['status' => Password::RESET_LINK_SENT]);
|
||||
}
|
||||
$status = Password::broker(config('fortify.passwords'))->sendResetLink(
|
||||
$request->only(Fortify::email())
|
||||
);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ public function callback(string $provider)
|
|||
}
|
||||
$email = strtolower($email);
|
||||
$user = User::whereEmail($email)->first();
|
||||
// MapleDeploy branding: dashboard revocation scrambles passwords,
|
||||
// clears sessions, and marks the user so email-matched OAuth cannot
|
||||
// reopen access.
|
||||
if ($user?->isMapledeployRevoked()) {
|
||||
abort(403, 'User access has been revoked');
|
||||
}
|
||||
if (! $user) {
|
||||
$settings = instanceSettings();
|
||||
if (! $settings->is_registration_enabled) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
use App\Http\Middleware\EnsureTokenBelongsToCurrentTeamMember;
|
||||
use App\Http\Middleware\PreventRequestsDuringMaintenance;
|
||||
use App\Http\Middleware\RedirectIfAuthenticated;
|
||||
use App\Http\Middleware\RejectMapledeployRevokedUser;
|
||||
use App\Http\Middleware\TrimStrings;
|
||||
use App\Http\Middleware\TrustHosts;
|
||||
use App\Http\Middleware\TrustProxies;
|
||||
|
|
@ -75,6 +76,7 @@ class Kernel extends HttpKernel
|
|||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
RejectMapledeployRevokedUser::class,
|
||||
CheckForcePasswordReset::class,
|
||||
DecideWhatToDoWithUser::class,
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,16 @@ public function handle(Request $request, Closure $next): Response
|
|||
$currentTeam = auth()->user()?->recreate_personal_team();
|
||||
refreshSession($currentTeam);
|
||||
}
|
||||
$preferredTeam = auth()?->user()?->mapledeployPreferredTeam();
|
||||
if (
|
||||
$preferredTeam &&
|
||||
$preferredTeam->id === 0 &&
|
||||
auth()?->user()?->currentTeam()?->id !== 0
|
||||
) {
|
||||
// MapleDeploy branding: repair sessions that landed in the empty
|
||||
// personal team before dashboard-managed root-team access existed.
|
||||
refreshSession($preferredTeam);
|
||||
}
|
||||
if (auth()?->user()?->currentTeam()) {
|
||||
refreshSession(auth()->user()->currentTeam());
|
||||
} elseif (auth()?->user()?->teams?->count() > 0) {
|
||||
|
|
|
|||
37
app/Http/Middleware/RejectMapledeployRevokedUser.php
Normal file
37
app/Http/Middleware/RejectMapledeployRevokedUser.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RejectMapledeployRevokedUser
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$user = auth()->user();
|
||||
if (! $user?->isMapledeployRevoked()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// MapleDeploy branding: revocation is marked on the user row so old
|
||||
// browser sessions are rejected even when SESSION_DRIVER is not database.
|
||||
auth()->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
if ($request->routeIs('login') || $request->path() === 'login') {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
return redirect()->route('login')->withErrors([
|
||||
'email' => __('auth.failed'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ protected static function boot()
|
|||
$team = [
|
||||
'name' => $user->name."'s Team",
|
||||
'personal_team' => true,
|
||||
'show_boarding' => true,
|
||||
'show_boarding' => false,
|
||||
];
|
||||
if ($user->id === 0) {
|
||||
$team['id'] = 0;
|
||||
|
|
@ -219,7 +219,7 @@ public function recreate_personal_team()
|
|||
$team = [
|
||||
'name' => $this->name."'s Team",
|
||||
'personal_team' => true,
|
||||
'show_boarding' => true,
|
||||
'show_boarding' => false,
|
||||
];
|
||||
if ($this->id === 0) {
|
||||
$team['id'] = 0;
|
||||
|
|
@ -232,6 +232,20 @@ public function recreate_personal_team()
|
|||
return $new_team;
|
||||
}
|
||||
|
||||
public function mapledeployPreferredTeam(): ?Team
|
||||
{
|
||||
// MapleDeploy branding: dashboard-managed users are attached to the
|
||||
// root team so they can administer the customer's managed instance.
|
||||
$rootTeam = $this->teams->firstWhere('id', 0);
|
||||
$rootRole = data_get($rootTeam, 'pivot.role');
|
||||
if ($rootTeam && ($rootRole === 'admin' || $rootRole === 'owner')) {
|
||||
return $rootTeam;
|
||||
}
|
||||
|
||||
return $this->teams->firstWhere('personal_team', true)
|
||||
?? $this->teams->first();
|
||||
}
|
||||
|
||||
public function createToken(string $name, array $abilities = ['*'], ?DateTimeInterface $expiresAt = null)
|
||||
{
|
||||
$plainTextToken = sprintf(
|
||||
|
|
@ -292,9 +306,19 @@ public function sendVerificationEmail()
|
|||
|
||||
public function sendPasswordResetNotification($token): void
|
||||
{
|
||||
if ($this->isMapledeployRevoked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this?->notify(new TransactionalEmailsResetPassword($token));
|
||||
}
|
||||
|
||||
public function isMapledeployRevoked(): bool
|
||||
{
|
||||
// MapleDeploy branding: dashboard-managed revocation stores a persistent marker.
|
||||
return str_starts_with((string) $this->remember_token, 'mapledeploy-revoked:');
|
||||
}
|
||||
|
||||
public function isAdmin()
|
||||
{
|
||||
return $this->role() === 'admin' || $this->role() === 'owner';
|
||||
|
|
|
|||
|
|
@ -43,13 +43,13 @@ public function boot(): void
|
|||
{
|
||||
Fortify::createUsersUsing(CreateNewUser::class);
|
||||
Fortify::registerView(function () {
|
||||
$isFirstUser = User::count() === 0;
|
||||
|
||||
$settings = instanceSettings();
|
||||
if (! $settings->is_registration_enabled) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
$isFirstUser = User::count() === 0;
|
||||
|
||||
return view('auth.register', [
|
||||
'isFirstUser' => $isFirstUser,
|
||||
]);
|
||||
|
|
@ -59,8 +59,11 @@ public function boot(): void
|
|||
$settings = instanceSettings();
|
||||
$enabled_oauth_providers = OauthSetting::where('enabled', true)->get();
|
||||
$users = User::count();
|
||||
if ($users == 0) {
|
||||
// If there are no users, redirect to registration
|
||||
// MapleDeploy branding: public registration is disabled by default
|
||||
// because the dashboard creates the first admin over SSH. Do not
|
||||
// redirect to /register in that fail-closed state, or fresh/failed
|
||||
// provisioning loops between login and registration.
|
||||
if ($users == 0 && $settings->is_registration_enabled) {
|
||||
return redirect()->route('register');
|
||||
}
|
||||
|
||||
|
|
@ -91,8 +94,9 @@ public function boot(): void
|
|||
$user->currentTeam = $invitation->team;
|
||||
$invitation->delete();
|
||||
} else {
|
||||
// Normal login - use personal team
|
||||
$user->currentTeam = $user->teams->firstWhere('personal_team', true);
|
||||
// MapleDeploy branding: root-team admins should land in
|
||||
// the managed instance team, not their empty personal team.
|
||||
$user->currentTeam = $user->mapledeployPreferredTeam();
|
||||
if (! $user->currentTeam) {
|
||||
$user->currentTeam = $user->recreate_personal_team();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -634,13 +634,14 @@ function get_route_parameters(): array
|
|||
|
||||
function get_latest_sentinel_version(): string
|
||||
{
|
||||
// MapleDeploy branding: our versions.json omits the sentinel key, so fall back to 'latest' (matches upstream's fallback in CheckAndStartSentinelJob)
|
||||
try {
|
||||
$response = Http::get(config('constants.coolify.versions_url'));
|
||||
$versions = $response->json();
|
||||
|
||||
return data_get($versions, 'coolify.sentinel.version');
|
||||
} catch (Throwable) {
|
||||
return '0.0.0';
|
||||
return data_get($versions, 'coolify.sentinel.version') ?? 'latest';
|
||||
} catch (\Throwable) {
|
||||
return 'latest';
|
||||
}
|
||||
}
|
||||
function get_latest_version_of_coolify(): string
|
||||
|
|
|
|||
|
|
@ -1,27 +1,28 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
// MapleDeploy branding: registry pointed to Forgejo, auto-update disabled by default
|
||||
'coolify' => [
|
||||
'version' => env('COOLIFY_VERSION') ?: '4.3.12',
|
||||
'helper_version' => '1.0.16',
|
||||
'realtime_version' => '1.0.17',
|
||||
'railpack_version' => '0.23.0',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
'autoupdate' => env('AUTOUPDATE'),
|
||||
'autoupdate' => env('AUTOUPDATE', false),
|
||||
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
||||
'registry_url' => env('REGISTRY_URL', 'docker.io'),
|
||||
'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'docker.io').'/coollabsio/coolify-helper'),
|
||||
'realtime_image' => env('REALTIME_IMAGE', env('REGISTRY_URL', 'docker.io').'/coollabsio/coolify-realtime'),
|
||||
'registry_url' => env('REGISTRY_URL', 'forgejo.mapledeploy.ca'),
|
||||
'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper'),
|
||||
'realtime_image' => env('REALTIME_IMAGE', 'ghcr.io/coollabsio/coolify-realtime'),
|
||||
'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
|
||||
'cdn_url' => env('CDN_URL', 'https://cdn.coollabs.io'),
|
||||
'versions_url' => env('VERSIONS_URL', env('CDN_URL', 'https://cdn.coollabs.io').'/coolify/versions.json'),
|
||||
'upgrade_script_url' => env('UPGRADE_SCRIPT_URL', env('CDN_URL', 'https://cdn.coollabs.io').'/coolify/upgrade.sh'),
|
||||
'releases_url' => env('RELEASES_URL', 'https://cdn.coollabs.io/coolify/releases.json'),
|
||||
'cdn_url' => env('CDN_URL', 'https://updates.mapledeploy.ca'),
|
||||
'versions_url' => env('VERSIONS_URL', env('CDN_URL', 'https://updates.mapledeploy.ca').'/coolify/versions.json'),
|
||||
'upgrade_script_url' => env('UPGRADE_SCRIPT_URL', env('CDN_URL', 'https://updates.mapledeploy.ca').'/coolify/upgrade.sh'),
|
||||
'releases_url' => 'https://cdn.coolify.io/releases.json',
|
||||
],
|
||||
|
||||
'urls' => [
|
||||
'docs' => 'https://coolify.io/docs',
|
||||
'contact' => 'https://coolify.io/docs/contact',
|
||||
'docs' => 'https://mapledeploy.ca/docs',
|
||||
'contact' => 'https://mapledeploy.ca/contact',
|
||||
],
|
||||
|
||||
'services' => [
|
||||
|
|
@ -94,8 +95,9 @@
|
|||
'verification_code_expiry_minutes' => 10,
|
||||
],
|
||||
|
||||
'sentry' => [
|
||||
'sentry_dsn' => env('SENTRY_DSN'),
|
||||
// MapleDeploy branding: telemetry disabled
|
||||
'sentry' => [ // disabled by MapleDeploy
|
||||
'sentry_dsn' => null,
|
||||
],
|
||||
|
||||
'sentinel' => [
|
||||
|
|
@ -117,7 +119,6 @@
|
|||
],
|
||||
|
||||
'webhooks' => [
|
||||
'feedback_discord_webhook' => env('FEEDBACK_DISCORD_WEBHOOK'),
|
||||
'dev_webhook' => env('SERVEO_URL'),
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,13 @@
|
|||
return $fallbackHosts === [] ? ['coolify-db'] : $fallbackHosts;
|
||||
};
|
||||
|
||||
$pgsqlOptions = [];
|
||||
if (defined('Pdo\Pgsql::ATTR_DISABLE_PREPARES')) {
|
||||
$pgsqlOptions[Pgsql::ATTR_DISABLE_PREPARES] = env('DB_DISABLE_PREPARES', false);
|
||||
} elseif (defined('\PDO::PGSQL_ATTR_DISABLE_PREPARES')) {
|
||||
$pgsqlOptions[PDO::PGSQL_ATTR_DISABLE_PREPARES] = env('DB_DISABLE_PREPARES', false);
|
||||
}
|
||||
|
||||
$pgsql = [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DATABASE_URL'),
|
||||
|
|
@ -34,9 +41,7 @@
|
|||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
'options' => [
|
||||
(defined('Pdo\Pgsql::ATTR_DISABLE_PREPARES') ? Pgsql::ATTR_DISABLE_PREPARES : PDO::PGSQL_ATTR_DISABLE_PREPARES) => env('DB_DISABLE_PREPARES', false),
|
||||
],
|
||||
'options' => $pgsqlOptions,
|
||||
];
|
||||
|
||||
/*
|
||||
|
|
@ -97,7 +102,7 @@
|
|||
|
||||
'testing' => [
|
||||
'driver' => 'sqlite',
|
||||
'database' => ':memory:',
|
||||
'database' => env('DB_DATABASE', ':memory:'),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => true,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@
|
|||
|
||||
'navigate' => [
|
||||
'show_progress_bar' => true,
|
||||
'progress_bar_color' => '#6b16ed',
|
||||
'progress_bar_color' => '#fde047', // MapleDeploy branding: warning yellow
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
return [
|
||||
|
||||
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
|
||||
'dsn' => config('constants.sentry.sentry_dsn'),
|
||||
// Sentry DSN disabled by MapleDeploy.
|
||||
'dsn' => config('constants.sentry.sentry_dsn'), // disabled by MapleDeploy
|
||||
|
||||
// The release version of your application
|
||||
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
'tracing' => [
|
||||
// Trace queue jobs as their own transactions
|
||||
'queue_job_transactions' => env('SENTRY_TRACE_QUEUE_ENABLED', false),
|
||||
'queue_job_transactions' => env('SENTRY_TRACE_QUEUE_ENABLED', false), // disabled by MapleDeploy
|
||||
|
||||
// Capture queue jobs as spans when executed on the sync driver
|
||||
'queue_jobs' => true,
|
||||
|
|
@ -61,12 +61,12 @@
|
|||
'http_client_requests' => true,
|
||||
|
||||
// Capture Redis operations as spans (this enables Redis events in Laravel)
|
||||
'redis_commands' => env('SENTRY_TRACE_REDIS_COMMANDS', false),
|
||||
'redis_commands' => env('SENTRY_TRACE_REDIS_COMMANDS', false), // disabled by MapleDeploy
|
||||
|
||||
// Try to find out where the Redis command originated from and add it to the command spans
|
||||
'redis_origin' => true,
|
||||
|
||||
// Indicates if the tracing integrations supplied by Sentry should be loaded
|
||||
// Indicates if the tracing integrations supplied by Sentry should be loaded; disabled by MapleDeploy.
|
||||
'default_integrations' => true,
|
||||
|
||||
// Indicates that requests without a matching route should be traced
|
||||
|
|
@ -74,12 +74,12 @@
|
|||
],
|
||||
|
||||
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#send-default-pii
|
||||
'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false),
|
||||
'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false), // disabled by MapleDeploy
|
||||
|
||||
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate
|
||||
'enable_tracing' => env('SENTRY_ENABLE_TRACING', false),
|
||||
'enable_tracing' => env('SENTRY_ENABLE_TRACING', false), // disabled by MapleDeploy
|
||||
'traces_sample_rate' => 0.2,
|
||||
|
||||
'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float) env('SENTRY_PROFILES_SAMPLE_RATE'),
|
||||
'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float) env('SENTRY_PROFILES_SAMPLE_RATE'), // disabled by MapleDeploy
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public function up(): void
|
|||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->boolean('is_dns_validation_enabled')->default(true);
|
||||
$table->string('custom_dns_servers')->nullable()->default('1.1.1.1');
|
||||
$table->string('custom_dns_servers')->nullable()->default('149.112.121.10,149.112.122.10');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$columns = array_values(array_filter(
|
||||
['setup_token', 'setup_callback_url'],
|
||||
fn (string $column) => Schema::hasColumn('instance_settings', $column),
|
||||
));
|
||||
|
||||
if ($columns === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('instance_settings', function (Blueprint $table) use ($columns) {
|
||||
// MapleDeploy branding: remove legacy one-time setup columns from customer instances.
|
||||
$table->dropColumn($columns);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$missingColumns = array_values(array_filter(
|
||||
['setup_token', 'setup_callback_url'],
|
||||
fn (string $column) => ! Schema::hasColumn('instance_settings', $column),
|
||||
));
|
||||
|
||||
if ($missingColumns === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('instance_settings', function (Blueprint $table) use ($missingColumns) {
|
||||
if (in_array('setup_token', $missingColumns, true)) {
|
||||
$table->text('setup_token')->nullable();
|
||||
}
|
||||
|
||||
if (in_array('setup_callback_url', $missingColumns, true)) {
|
||||
$table->text('setup_callback_url')->nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -15,7 +15,9 @@ public function run(): void
|
|||
{
|
||||
InstanceSettings::create([
|
||||
'id' => 0,
|
||||
'is_registration_enabled' => true,
|
||||
// MapleDeploy branding: dashboard provisioning creates the first
|
||||
// admin over SSH, so public registration must fail closed.
|
||||
'is_registration_enabled' => false,
|
||||
'is_api_enabled' => isDev(),
|
||||
'smtp_enabled' => true,
|
||||
'smtp_host' => 'coolify-mail',
|
||||
|
|
|
|||
|
|
@ -57,6 +57,9 @@ public function run(): void
|
|||
if (InstanceSettings::find(0) == null) {
|
||||
InstanceSettings::create([
|
||||
'id' => 0,
|
||||
// MapleDeploy branding: dashboard provisioning creates the
|
||||
// first admin over SSH, so public registration must fail closed.
|
||||
'is_registration_enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
services:
|
||||
coolify:
|
||||
image: "${REGISTRY_URL:-docker.io}/coollabsio/coolify:${LATEST_IMAGE:-latest}"
|
||||
image: "${REGISTRY_URL:-forgejo.mapledeploy.ca}/rosslh/coolify:${LATEST_IMAGE:-latest}"
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /data/coolify/source/.env
|
||||
|
|
@ -62,7 +62,7 @@ services:
|
|||
retries: 10
|
||||
timeout: 2s
|
||||
soketi:
|
||||
image: '${REGISTRY_URL:-docker.io}/coollabsio/coolify-realtime:1.0.17'
|
||||
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.17' # MapleDeploy branding: pinned to ghcr.io, not mirrored
|
||||
ports:
|
||||
- "${SOKETI_PORT:-6001}:6001"
|
||||
- "6002:6002"
|
||||
|
|
|
|||
|
|
@ -147,6 +147,16 @@ COPY --chown=www-data:www-data composer.json composer.lock ./
|
|||
COPY --chown=www-data:www-data app ./app
|
||||
COPY --chown=www-data:www-data bootstrap ./bootstrap
|
||||
COPY --chown=www-data:www-data config ./config
|
||||
|
||||
# MapleDeploy: inject build version into constants.php at build time.
|
||||
# The version in git stays at the upstream value to avoid rebase conflicts.
|
||||
# CI passes the timestamped version (e.g. 4.0.0-beta.468.202603140006) as a build arg.
|
||||
ARG MAPLEDEPLOY_VERSION=""
|
||||
RUN if [ -n "$MAPLEDEPLOY_VERSION" ]; then \
|
||||
sed -i "s/'version' => '[^']*'/'version' => '$MAPLEDEPLOY_VERSION'/" config/constants.php && \
|
||||
chown www-data:www-data config/constants.php; \
|
||||
fi
|
||||
|
||||
COPY --chown=www-data:www-data database ./database
|
||||
COPY --chown=www-data:www-data lang ./lang
|
||||
COPY --chown=www-data:www-data public ./public
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
"auth.register_now": "Register",
|
||||
"auth.logout": "Logout",
|
||||
"auth.register": "Register",
|
||||
"auth.registration_disabled": "Registration is disabled. Please contact the administrator.",
|
||||
"auth.registration_disabled": "Set up server access in the MapleDeploy dashboard.",
|
||||
"auth.reset_password": "Reset password",
|
||||
"auth.failed": "These credentials do not match our records.",
|
||||
"auth.failed.callback": "Failed to process callback from login provider.",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
"auth.register_now": "S'enregistrer",
|
||||
"auth.logout": "Déconnexion",
|
||||
"auth.register": "S'enregistrer",
|
||||
"auth.registration_disabled": "L'enregistrement est désactivé. Merci de contacter l'administrateur.",
|
||||
"auth.registration_disabled": "Configurez l’accès au serveur dans le tableau de bord MapleDeploy.",
|
||||
"auth.reset_password": "Réinitialiser le mot de passe",
|
||||
"auth.failed": "Aucune correspondance n'a été trouvée pour les informations d'identification renseignées.",
|
||||
"auth.failed.callback": "Erreur lors du processus de retour de la plateforme de connexion.",
|
||||
|
|
|
|||
|
|
@ -14,36 +14,51 @@
|
|||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* MapleDeploy branding: Canadian red accent, stone greys */
|
||||
@theme {
|
||||
--font-sans: 'Geist Sans', Inter, sans-serif;
|
||||
--font-mono: 'Geist Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
--font-geist-sans: 'Geist Sans', Inter, sans-serif;
|
||||
--font-logs: 'Geist Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-display: 'Overlock', sans-serif;
|
||||
--font-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
--font-logs: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
|
||||
--color-base: #101010;
|
||||
--color-warning: #fcd452;
|
||||
--color-base: #292524;
|
||||
--color-warning: #fde047;
|
||||
--color-warning-50: #fefce8;
|
||||
--color-warning-100: #fef9c3;
|
||||
--color-warning-200: #fef08a;
|
||||
--color-warning-300: #fde047;
|
||||
--color-warning-400: #fcd452;
|
||||
--color-warning-500: #facc15;
|
||||
--color-warning-400: #facc15;
|
||||
--color-warning-500: #eab308;
|
||||
--color-warning-600: #ca8a04;
|
||||
--color-warning-700: #a16207;
|
||||
--color-warning-800: #854d0e;
|
||||
--color-warning-900: #713f12;
|
||||
--color-success: #22C55E;
|
||||
--color-error: #dc2626;
|
||||
--color-coollabs-50: #f5f0ff;
|
||||
--color-coollabs: #6b16ed;
|
||||
--color-coollabs-100: #7317ff;
|
||||
--color-coollabs-200: #5a12c7;
|
||||
--color-coollabs-300: #4a0fa3;
|
||||
--color-coolgray-100: #181818;
|
||||
--color-coolgray-200: #202020;
|
||||
--color-coolgray-300: #242424;
|
||||
--color-coolgray-400: #282828;
|
||||
--color-coolgray-500: #323232;
|
||||
/* MapleDeploy branding: red palette hue-normalized to OKLCH h=29.38 (#D52A1E) */
|
||||
--color-error: #dc281c;
|
||||
--color-coollabs-50: #fef3f1;
|
||||
--color-coollabs: #d52b1f;
|
||||
--color-coollabs-100: #f34d3d;
|
||||
--color-coollabs-200: #bc251a;
|
||||
--color-coollabs-300: #9c2117;
|
||||
/* Override Tailwind's red scale so red-* utility classes match the MapleDeploy brand hue */
|
||||
--color-red-50: #fef1ef;
|
||||
--color-red-100: #ffe3df;
|
||||
--color-red-200: #ffcec5;
|
||||
--color-red-300: #feaa9d;
|
||||
--color-red-400: #fb7968;
|
||||
--color-red-500: #f34d3d;
|
||||
--color-red-600: #d52b1f;
|
||||
--color-red-700: #bc251a;
|
||||
--color-red-800: #9c2117;
|
||||
--color-red-900: #812219;
|
||||
--color-red-950: #460d08;
|
||||
/* MapleDeploy branding: coolgray remapped to stone (coolgray-200 interpolated, see COOLIFY_FORK.md) */
|
||||
--color-coolgray-100: #1c1917;
|
||||
--color-coolgray-200: #35322f;
|
||||
--color-coolgray-300: #44403c;
|
||||
--color-coolgray-400: #57534e;
|
||||
--color-coolgray-500: #78716c;
|
||||
|
||||
/* Graphite design language (ported from ref/frontend). Layered neutral
|
||||
surfaces + translucent hairlines. See DESIGN.md. */
|
||||
|
|
@ -56,7 +71,7 @@ @theme {
|
|||
--color-fg: #f2f2f2;
|
||||
--color-fg-dim: #b4b4b8;
|
||||
--color-fg-faint: #6e6e74;
|
||||
--color-accent: #6b16ed;
|
||||
--color-accent: #d52b1f; /* MapleDeploy branding: accent → red-600 */
|
||||
--color-accent-foreground: #ffffff;
|
||||
--color-hairline: rgba(255, 255, 255, 0.08);
|
||||
--color-nav-text: #525252;
|
||||
|
|
@ -65,6 +80,33 @@ @theme {
|
|||
--color-log: #0d0d0d;
|
||||
|
||||
--shadow-modal: 0 24px 64px rgba(0, 0, 0, 0.55), 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* MapleDeploy branding: remap neutral and gray → stone for warm tone consistency with marketing/dashboard.
|
||||
Upstream uses neutral for text/borders and gray for backgrounds/surfaces. Remapping both to stone
|
||||
warms the entire UI to match our design system, covering ~1000 class references without touching
|
||||
any Blade templates. */
|
||||
--color-neutral-50: oklch(98.5% 0.001 106.423);
|
||||
--color-neutral-100: oklch(97% 0.001 106.424);
|
||||
--color-neutral-200: oklch(92.3% 0.003 48.717);
|
||||
--color-neutral-300: oklch(86.9% 0.005 56.366);
|
||||
--color-neutral-400: oklch(70.9% 0.01 56.259);
|
||||
--color-neutral-500: oklch(55.3% 0.013 58.071);
|
||||
--color-neutral-600: oklch(44.4% 0.011 73.639);
|
||||
--color-neutral-700: oklch(37.4% 0.01 67.558);
|
||||
--color-neutral-800: oklch(26.8% 0.007 34.298);
|
||||
--color-neutral-900: oklch(21.6% 0.006 56.043);
|
||||
--color-neutral-950: oklch(14.7% 0.004 49.25);
|
||||
--color-gray-50: oklch(98.5% 0.001 106.423);
|
||||
--color-gray-100: oklch(97% 0.001 106.424);
|
||||
--color-gray-200: oklch(92.3% 0.003 48.717);
|
||||
--color-gray-300: oklch(86.9% 0.005 56.366);
|
||||
--color-gray-400: oklch(70.9% 0.01 56.259);
|
||||
--color-gray-500: oklch(55.3% 0.013 58.071);
|
||||
--color-gray-600: oklch(44.4% 0.011 73.639);
|
||||
--color-gray-700: oklch(37.4% 0.01 67.558);
|
||||
--color-gray-800: oklch(26.8% 0.007 34.298);
|
||||
--color-gray-900: oklch(21.6% 0.006 56.043);
|
||||
--color-gray-950: oklch(14.7% 0.004 49.25);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -258,7 +300,8 @@ @layer components {
|
|||
*/
|
||||
html,
|
||||
body {
|
||||
@apply w-full min-h-full bg-gray-50 dark:bg-app dark:text-fg-dim;
|
||||
/* MapleDeploy branding: text-stone-800 body text matches marketing/dashboard */
|
||||
@apply w-full min-h-full text-stone-800 bg-gray-50 dark:bg-app dark:text-fg-dim;
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
@ -293,19 +336,20 @@ button[isHighlighted]:not(:disabled) {
|
|||
}
|
||||
|
||||
h1 {
|
||||
@apply text-[24px] leading-7 font-semibold tracking-tight dark:text-white;
|
||||
/* MapleDeploy branding: font-display (Overlock) on semantic headings */
|
||||
@apply text-[24px] leading-7 font-semibold tracking-tight font-display dark:text-white;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-xl font-bold dark:text-white;
|
||||
@apply text-xl font-bold font-display dark:text-white;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-lg font-bold dark:text-white;
|
||||
@apply text-lg font-bold font-display dark:text-white;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-base font-bold dark:text-white;
|
||||
@apply text-base font-bold font-display dark:text-white;
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
@ -935,7 +979,7 @@ :root {
|
|||
}
|
||||
|
||||
.dark {
|
||||
--color-accent: #fcd452;
|
||||
--color-accent: #fde047; /* MapleDeploy branding: dark-mode accent → warning */
|
||||
--coollabs-canvas: oklch(10% 0 0);
|
||||
/* elevated (card shells/headers) and recessed (input fills) sit a touch
|
||||
above the canvas so they read lighter than near-black */
|
||||
|
|
@ -954,7 +998,7 @@ .dark {
|
|||
/* Theme surfaces are derived from one brand color. Changing
|
||||
--theme-base-color is enough to generate a complete dark surface ladder. */
|
||||
html[data-theme="custom"] {
|
||||
--theme-base-color: #6b16ed;
|
||||
--theme-base-color: #d52b1f; /* MapleDeploy branding: default custom-theme base → red-600 */
|
||||
--theme-bright-color: color-mix(in srgb, var(--theme-base-color) 85%, white);
|
||||
--theme-scrollbar-thumb: color-mix(in srgb, var(--theme-bright-color) 70%, var(--theme-accent-foreground));
|
||||
--theme-border-color: color-mix(in oklab, var(--theme-base-color) 42%, #52525b);
|
||||
|
|
@ -1698,7 +1742,7 @@ .resource-heading-tabs-control:focus {
|
|||
|
||||
.resource-heading-tabs-control:focus-visible .resource-heading-tabs-control-icon {
|
||||
box-shadow:
|
||||
0 0 0 2px color-mix(in srgb, var(--color-coollabs, #6b16ed) 45%, transparent),
|
||||
0 0 0 2px color-mix(in srgb, var(--color-coollabs, #d52b1f) 45%, transparent), /* MapleDeploy branding */
|
||||
0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,18 +70,12 @@ @font-face {
|
|||
src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* MapleDeploy branding: Overlock for headings */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Geist Mono';
|
||||
font-family: 'Overlock';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
src: url('../fonts/geist-mono-variable.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Geist Sans';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
src: url('../fonts/geist-sans-variable.woff2') format('woff2');
|
||||
font-weight: 900;
|
||||
src: url('../fonts/overlock-v19-latin-900.woff2') format('woff2'),
|
||||
url('../fonts/overlock-v19-latin-900.ttf') format('truetype');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,11 +40,11 @@ @utility input-sticky {
|
|||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: inset 4px 0 0 #6b16ed, inset 0 0 0 1px #e5e5e5;
|
||||
box-shadow: inset 4px 0 0 #d52b1f, inset 0 0 0 1px #e5e5e5;
|
||||
}
|
||||
|
||||
&:where(.dark, .dark *):focus-visible {
|
||||
box-shadow: inset 4px 0 0 #fcd452, inset 0 0 0 1px #242424;
|
||||
box-shadow: inset 4px 0 0 #fde047, inset 0 0 0 1px #242424;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ @utility tag {
|
|||
}
|
||||
|
||||
@utility add-tag {
|
||||
@apply flex items-center px-2 text-xs cursor-pointer dark:text-neutral-500/20 text-neutral-500 group-hover:text-neutral-700 dark:group-hover:text-white dark:hover:bg-coolgray-300 hover:bg-neutral-200;
|
||||
@apply flex items-center px-2 text-xs cursor-pointer dark:text-neutral-500 text-neutral-500 group-hover:text-neutral-700 dark:group-hover:text-white dark:hover:bg-coolgray-300 hover:bg-neutral-200;
|
||||
}
|
||||
|
||||
@utility user-menu-item {
|
||||
|
|
@ -272,7 +272,8 @@ @utility icon {
|
|||
}
|
||||
|
||||
@utility scrollbar {
|
||||
@apply scrollbar-thumb-coollabs-100 scrollbar-track-neutral-200 dark:scrollbar-thumb-coollabs-100 dark:scrollbar-track-coolgray-200 scrollbar-thin;
|
||||
/* MapleDeploy branding: yellow scrollbar thumb instead of Coolify brand color */
|
||||
@apply scrollbar-thumb-warning scrollbar-track-neutral-200 dark:scrollbar-thumb-warning dark:scrollbar-track-coolgray-200 scrollbar-thin;
|
||||
}
|
||||
|
||||
@utility main {
|
||||
|
|
@ -332,7 +333,8 @@ @utility description {
|
|||
}
|
||||
|
||||
@utility bg-coollabs-gradient {
|
||||
@apply from-purple-500 via-pink-500 to-red-500 bg-linear-to-r;
|
||||
/* MapleDeploy branding */
|
||||
@apply from-red-700 via-red-500 to-red-400 bg-linear-to-r;
|
||||
}
|
||||
|
||||
@utility text-helper {
|
||||
|
|
@ -396,7 +398,7 @@ @utility log-warning {
|
|||
}
|
||||
|
||||
@utility log-debug {
|
||||
@apply bg-purple-500/10 dark:bg-purple-500/15;
|
||||
@apply bg-stone-500/10 dark:bg-stone-500/15;
|
||||
}
|
||||
|
||||
@utility log-info {
|
||||
|
|
|
|||
BIN
resources/fonts/overlock-v19-latin-900.ttf
Normal file
BIN
resources/fonts/overlock-v19-latin-900.ttf
Normal file
Binary file not shown.
BIN
resources/fonts/overlock-v19-latin-900.woff2
Normal file
BIN
resources/fonts/overlock-v19-latin-900.woff2
Normal file
Binary file not shown.
|
|
@ -93,7 +93,7 @@ class="auth-tooltip max-w-xs whitespace-normal">
|
|||
|
||||
<x-slot:footer>
|
||||
@if ($is_registration_enabled)
|
||||
<span>New to Coolify?</span>
|
||||
<span>New to MapleDeploy?</span> {{-- MapleDeploy branding --}}
|
||||
<a href="/register" class="auth-text-link">{{ __('auth.register_now') }}</a>
|
||||
@else
|
||||
<span>{{ __('auth.registration_disabled') }}</span>
|
||||
|
|
|
|||
|
|
@ -99,9 +99,10 @@
|
|||
{{-- Unified Input Container with Tags Inside --}}
|
||||
<div @click="$refs.searchInput.focus()" x-data="{ focused: false }" @focusin="focused = true" @focusout="focused = false"
|
||||
class="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto scrollbar py-1.5 px-2 w-full text-sm rounded-sm border-0 bg-white dark:bg-coolgray-100 cursor-text px-1 text-black dark:text-white"
|
||||
{{-- MapleDeploy branding: red accent instead of Coolify purple --}}
|
||||
:style="(() => {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const accent = isDark ? '#fcd452' : '#6b16ed';
|
||||
const accent = isDark ? '#fde047' : '#d52b1f';
|
||||
const border = isDark ? '#242424' : '#e5e5e5';
|
||||
return focused
|
||||
? 'box-shadow: inset 4px 0 0 ' + accent + ', inset 0 0 0 2px ' + border + ';'
|
||||
|
|
@ -110,7 +111,7 @@ class="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto scrollbar py-1.5 px-2 w-
|
|||
:class="{
|
||||
'opacity-50': {{ $disabled ? 'true' : 'false' }}
|
||||
}" wire:loading.class="opacity-50"
|
||||
wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]">
|
||||
wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1f,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]">
|
||||
|
||||
{{-- Selected Tags Inside Input --}}
|
||||
<template x-for="value in selected" :key="value">
|
||||
|
|
@ -240,9 +241,10 @@ class="pointer-events-none absolute inset-0 rounded-[5px] border border-neutral-
|
|||
{{-- Input Container --}}
|
||||
<div @click="openDropdown()" x-data="{ focused: false }" @focusin="focused = true" @focusout="focused = false"
|
||||
class="flex items-center gap-2 py-1.5 w-full text-sm rounded-sm border-0 bg-white dark:bg-coolgray-100 cursor-text text-black dark:text-white"
|
||||
{{-- MapleDeploy branding: red accent instead of Coolify purple --}}
|
||||
:style="(() => {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const accent = isDark ? '#fcd452' : '#6b16ed';
|
||||
const accent = isDark ? '#fde047' : '#d52b1f';
|
||||
const border = isDark ? '#242424' : '#e5e5e5';
|
||||
return focused
|
||||
? 'box-shadow: inset 4px 0 0 ' + accent + ', inset 0 0 0 2px ' + border + ';'
|
||||
|
|
@ -250,7 +252,7 @@ class="flex items-center gap-2 py-1.5 w-full text-sm rounded-sm border-0 bg-whit
|
|||
})()"
|
||||
:class="{
|
||||
'opacity-50': {{ $disabled ? 'true' : 'false' }}
|
||||
}" wire:loading.class="opacity-50" wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]">
|
||||
}" wire:loading.class="opacity-50" wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1f,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]">
|
||||
|
||||
{{-- Display Selected Value or Search Input --}}
|
||||
<div class="flex-1 flex items-center min-w-0 px-1">
|
||||
|
|
|
|||
|
|
@ -28,16 +28,6 @@
|
|||
</div>
|
||||
@elseif ($type === 'password')
|
||||
<div class="relative" x-data="{ type: 'password' }" @success.window="type = 'password'">
|
||||
<input autocomplete="{{ $autocomplete }}" value="{{ $value }}"
|
||||
x-bind:type="type"
|
||||
x-bind:class="{ 'truncate': type === 'text' && ! $el.disabled }"
|
||||
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required)
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:loading.attr="disabled"
|
||||
@readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||
aria-placeholder="{{ $attributes->get('placeholder') }}"
|
||||
@if ($autofocus) x-ref="autofocusInput" @endif>
|
||||
@if ($allowToPeak)
|
||||
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
|
||||
class="password-toggle flex absolute inset-y-0 right-0 z-10 items-center pr-2 cursor-pointer text-neutral-500 hover:text-black dark:text-neutral-400 dark:hover:text-white"
|
||||
|
|
@ -48,12 +38,22 @@ class="password-toggle flex absolute inset-y-0 right-0 z-10 items-center pr-2 cu
|
|||
<x-reicon name="eye-off2" x-cloak x-show="type === 'text'" class="size-[18px]" />
|
||||
</button>
|
||||
@endif
|
||||
<input autocomplete="{{ $autocomplete }}" value="{{ $value }}"
|
||||
x-bind:type="type"
|
||||
x-bind:class="{ 'truncate': type === 'text' && ! $el.disabled }"
|
||||
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required)
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1f,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:loading.attr="disabled"
|
||||
@readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||
aria-placeholder="{{ $attributes->get('placeholder') }}"
|
||||
@if ($autofocus) x-ref="autofocusInput" @endif>
|
||||
|
||||
</div>
|
||||
@else
|
||||
<input autocomplete="{{ $autocomplete }}" @if ($value) value="{{ $value }}" @endif
|
||||
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly)
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1f,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:loading.attr="disabled"
|
||||
type="{{ $type }}" @disabled($disabled) min="{{ $attributes->get('min') }}"
|
||||
max="{{ $attributes->get('max') }}" minlength="{{ $attributes->get('minlength') }}"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class="mb-0! flex items-center gap-1 text-sm font-medium leading-4 {{ $disabled
|
|||
@endif
|
||||
<select {{ $attributes->merge(['class' => $defaultClass]) }} @disabled($disabled) @required($required)
|
||||
wire:loading.attr="disabled" name={{ $modelBinding }} id="{{ $htmlId }}"
|
||||
@if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @else wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif>
|
||||
@if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1f,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]" @else wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1f,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]" @endif>
|
||||
{{ $slot }}
|
||||
</select>
|
||||
@error($modelBinding)
|
||||
|
|
|
|||
|
|
@ -36,21 +36,6 @@ function handleKeydown(e) {
|
|||
@else
|
||||
@if ($type === 'password')
|
||||
<div class="relative" x-data="{ type: 'password' }" @success.window="type = 'password'">
|
||||
<input x-cloak x-show="type === 'password'" value="{{ $value }}"
|
||||
{{ $attributes->merge(['class' => $defaultClassInput]) }} @required($required)
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:loading.attr="disabled"
|
||||
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||
aria-placeholder="{{ $attributes->get('placeholder') }}">
|
||||
<textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}" x-cloak x-show="type !== 'password'"
|
||||
placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $modelBinding }}" wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]"
|
||||
@else
|
||||
wire:model={{ $value ?? $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" name={{ $modelBinding }}
|
||||
@if ($autofocus) x-ref="autofocusInput" @endif></textarea>
|
||||
@if ($allowToPeak)
|
||||
<button type="button" x-on:click="type = type === 'password' ? 'text' : 'password'"
|
||||
class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer dark:hover:text-white"
|
||||
|
|
@ -71,15 +56,30 @@ class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer
|
|||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
<input x-cloak x-show="type === 'password'" value="{{ $value }}"
|
||||
{{ $attributes->merge(['class' => $defaultClassInput]) }} @required($required)
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1f,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:loading.attr="disabled"
|
||||
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||
aria-placeholder="{{ $attributes->get('placeholder') }}">
|
||||
<textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}" x-cloak x-show="type !== 'password'"
|
||||
placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $modelBinding }}" wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1f,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]"
|
||||
@else
|
||||
wire:model={{ $value ?? $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1f,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]" @endif
|
||||
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" name={{ $modelBinding }}
|
||||
@if ($autofocus) x-ref="autofocusInput" @endif></textarea>
|
||||
|
||||
</div>
|
||||
@else
|
||||
<textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}"
|
||||
{{ $allowTab ? '@keydown.tab=handleKeydown' : '' }} placeholder="{{ $placeholder }}"
|
||||
{{ !$spellcheck ? 'spellcheck=false' : '' }} {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $modelBinding }}" wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]"
|
||||
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $modelBinding }}" wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1f,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]"
|
||||
@else
|
||||
wire:model={{ $value ?? $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:model={{ $value ?? $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#d52b1f,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fde047,inset_0_0_0_2px_#242424]" @endif
|
||||
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" name={{ $modelBinding }}
|
||||
@if ($autofocus) x-ref="autofocusInput" @endif></textarea>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<div>
|
||||
@unless ($embedded)
|
||||
<x-slot:title>{{ data_get_str($application, 'name')->limit(10) }} > Deployments | Coolify</x-slot>
|
||||
<x-slot:title>{{ data_get_str($application, 'name')->limit(10) }} > Deployments | MapleDeploy</x-slot>
|
||||
<livewire:project.shared.configuration-checker :resource="$application" />
|
||||
<livewire:project.application.heading :application="$application" wire:key="application-heading-deployment-index" />
|
||||
@endunless
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@
|
|||
enabled: false,
|
||||
},
|
||||
grid: {
|
||||
borderColor: 'rgba(128, 128, 128, 0.14)',
|
||||
borderColor: gridColor, // MapleDeploy branding
|
||||
strokeDashArray: 4,
|
||||
},
|
||||
legend: {
|
||||
|
|
@ -175,6 +175,9 @@
|
|||
name: 'CPU',
|
||||
data: chartData[0].seriesData,
|
||||
}],
|
||||
grid: {
|
||||
borderColor: gridColor, // MapleDeploy branding
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
|
|
@ -257,7 +260,7 @@
|
|||
enabled: false,
|
||||
},
|
||||
grid: {
|
||||
borderColor: 'rgba(128, 128, 128, 0.14)',
|
||||
borderColor: gridColor, // MapleDeploy branding
|
||||
strokeDashArray: 4,
|
||||
},
|
||||
legend: {
|
||||
|
|
@ -322,6 +325,9 @@
|
|||
name: 'Memory',
|
||||
data: chartData[0].seriesData,
|
||||
}],
|
||||
grid: {
|
||||
borderColor: gridColor, // MapleDeploy branding
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<div>
|
||||
<x-slot:title>
|
||||
{{ data_get_str($server, 'name')->limit(10) }} > Metrics | Coolify
|
||||
{{ data_get_str($server, 'name')->limit(10) }} > Metrics | MapleDeploy
|
||||
</x-slot>
|
||||
|
||||
<livewire:server.navbar :server="$server" />
|
||||
|
|
@ -109,7 +109,7 @@ class="server-settings-workspace application-settings-workspace mt-4 grid w-full
|
|||
enabled: false,
|
||||
},
|
||||
grid: {
|
||||
borderColor: 'rgba(128, 128, 128, 0.14)',
|
||||
borderColor: gridColor, // MapleDeploy branding
|
||||
strokeDashArray: 4,
|
||||
},
|
||||
legend: {
|
||||
|
|
@ -189,6 +189,9 @@ class="server-settings-workspace application-settings-workspace mt-4 grid w-full
|
|||
name: 'CPU',
|
||||
data: chartData[0].seriesData,
|
||||
}],
|
||||
grid: {
|
||||
borderColor: gridColor, // MapleDeploy branding
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
|
|
@ -227,6 +230,9 @@ class="server-settings-workspace application-settings-workspace mt-4 grid w-full
|
|||
name: 'Memory',
|
||||
data: chartData[0].seriesData,
|
||||
}],
|
||||
grid: {
|
||||
borderColor: gridColor, // MapleDeploy branding
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<div>
|
||||
<x-slot:title>
|
||||
Advanced Settings | Coolify
|
||||
Advanced Settings | MapleDeploy
|
||||
</x-slot>
|
||||
|
||||
<x-settings.layout>
|
||||
|
|
@ -36,8 +36,8 @@
|
|||
['value' => false, 'label' => 'Disabled'],
|
||||
]" />
|
||||
<x-forms.input id="custom_dns_servers" label="Custom DNS servers"
|
||||
helper="Comma-separated resolvers. Leave empty to use system defaults."
|
||||
placeholder="1.1.1.1, 8.8.8.8" />
|
||||
helper="Comma-separated resolvers (e.g., 149.112.121.10,149.112.122.10). Leave empty to use system defaults." {{-- MapleDeploy branding: Canadian Shield examples --}}
|
||||
placeholder="149.112.121.10,149.112.122.10" />
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
<x-application.settings-section id="api-section" title="API and MCP">
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<x-forms.listbox id="is_api_enabled" label="API access"
|
||||
helper="Allow authenticated requests to the Coolify REST API." onChange="instantSave"
|
||||
helper="Allow authenticated requests to the REST API." onChange="instantSave"
|
||||
:options="[
|
||||
['value' => true, 'label' => 'Enabled'],
|
||||
['value' => false, 'label' => 'Disabled'],
|
||||
|
|
@ -127,11 +127,7 @@
|
|||
['value' => false, 'label' => 'Enabled'],
|
||||
['value' => true, 'label' => 'Disabled'],
|
||||
]" />
|
||||
<x-forms.listbox id="is_sponsorship_popup_enabled" label="Sponsorship reminders"
|
||||
helper="Show the monthly project sponsorship reminder." onChange="instantSave" :options="[
|
||||
['value' => true, 'label' => 'Enabled'],
|
||||
['value' => false, 'label' => 'Disabled'],
|
||||
]" />
|
||||
{{-- MapleDeploy branding: sponsorship popup listbox removed (popup itself already removed) --}}
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
## Do not modify this file. You will lose the ability to autoupdate!
|
||||
|
||||
CDN="https://cdn.coollabs.io/coolify"
|
||||
CDN="https://updates.mapledeploy.ca/coolify"
|
||||
LATEST_IMAGE=${1:-latest}
|
||||
LATEST_HELPER_VERSION=${2:-latest}
|
||||
ENV_FILE="/data/coolify/source/.env"
|
||||
|
|
@ -197,7 +197,7 @@ echo "3/6 Pulling Docker images..."
|
|||
echo " This may take a few minutes depending on your connection."
|
||||
|
||||
# Also pull the helper image (not in compose files but needed for upgrade)
|
||||
HELPER_IMAGE="${REGISTRY_URL:-docker.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}"
|
||||
HELPER_IMAGE="ghcr.io/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}"
|
||||
echo " - Pulling $HELPER_IMAGE..."
|
||||
log "Pulling image: $HELPER_IMAGE"
|
||||
if docker pull "$HELPER_IMAGE" >>"$LOGFILE" 2>&1; then
|
||||
|
|
@ -292,7 +292,7 @@ nohup bash -c "
|
|||
fi
|
||||
|
||||
log 'Running docker compose up...'
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-docker.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env \${COMPOSE_FILES} up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm ghcr.io/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env \${COMPOSE_FILES} up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||
log 'Docker compose up completed'
|
||||
|
||||
# Final log entry
|
||||
|
|
|
|||
|
|
@ -183,8 +183,8 @@ services:
|
|||
network:
|
||||
interface: 172.28.0.1
|
||||
dns:
|
||||
- 1.1.1.1
|
||||
- 1.0.0.1
|
||||
- 149.112.121.10
|
||||
- 149.112.122.10
|
||||
name: pterodactyl_nw
|
||||
ispn: false
|
||||
driver: bridge
|
||||
|
|
|
|||
32
tests/Feature/MapledeployInstanceSettingsSeederTest.php
Normal file
32
tests/Feature/MapledeployInstanceSettingsSeederTest.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use Database\Seeders\InstanceSettingsSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Foundation\Vite;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('MapleDeploy instance settings seeder disables public registration by default', function () {
|
||||
$this->seed(InstanceSettingsSeeder::class);
|
||||
|
||||
expect((bool) InstanceSettings::findOrFail(0)->is_registration_enabled)->toBeFalse();
|
||||
});
|
||||
|
||||
test('login page does not redirect to registration when no users exist and registration is disabled', function () {
|
||||
config()->set('app.maintenance.driver', 'file');
|
||||
$this->app->instance(Vite::class, new class
|
||||
{
|
||||
public function __invoke(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
});
|
||||
$this->seed(InstanceSettingsSeeder::class);
|
||||
|
||||
$response = $this->get(route('login'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertViewIs('auth.login');
|
||||
$response->assertViewHas('is_registration_enabled', false);
|
||||
});
|
||||
65
tests/Feature/MapledeployPreferredTeamLoginTest.php
Normal file
65
tests/Feature/MapledeployPreferredTeamLoginTest.php
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Middleware\DecideWhatToDoWithUser;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config([
|
||||
'app.maintenance.store' => 'array',
|
||||
'cache.default' => 'array',
|
||||
]);
|
||||
});
|
||||
|
||||
test('MapleDeploy root team admins log in to the managed root team', function () {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
|
||||
$rootTeam = Team::factory()->create([
|
||||
'id' => 0,
|
||||
'name' => 'Root Team',
|
||||
'personal_team' => false,
|
||||
]);
|
||||
$user = User::factory()->create([
|
||||
'email' => 'member@example.com',
|
||||
]);
|
||||
expect($user->teams()->where('personal_team', true)->exists())->toBeTrue();
|
||||
$user->teams()->syncWithoutDetaching([
|
||||
$rootTeam->id => ['role' => 'admin'],
|
||||
]);
|
||||
|
||||
$response = $this->post('/login', [
|
||||
'email' => 'member@example.com',
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
expect(session('currentTeam')?->id)->toBe(0);
|
||||
});
|
||||
|
||||
test('MapleDeploy root team admin sessions are repaired from personal team to root team', function () {
|
||||
$rootTeam = Team::factory()->create([
|
||||
'id' => 0,
|
||||
'name' => 'Root Team',
|
||||
'personal_team' => false,
|
||||
]);
|
||||
$user = User::factory()->create([
|
||||
'email' => 'member@example.com',
|
||||
]);
|
||||
$personalTeam = $user->teams()->where('personal_team', true)->firstOrFail();
|
||||
$user->teams()->syncWithoutDetaching([
|
||||
$rootTeam->id => ['role' => 'admin'],
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $personalTeam]);
|
||||
|
||||
app(DecideWhatToDoWithUser::class)->handle(
|
||||
Request::create('/'),
|
||||
fn () => response('ok'),
|
||||
);
|
||||
|
||||
expect(session('currentTeam')?->id)->toBe(0);
|
||||
});
|
||||
91
tests/Feature/MapledeployRevokedPasswordResetTest.php
Normal file
91
tests/Feature/MapledeployRevokedPasswordResetTest.php
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
use App\Actions\Fortify\ResetUserPassword;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\User;
|
||||
use App\Notifications\TransactionalEmails\ResetPassword;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Once;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Notification::fake();
|
||||
config([
|
||||
'app.maintenance.driver' => 'file',
|
||||
'cache.default' => 'array',
|
||||
'session.driver' => 'array',
|
||||
]);
|
||||
InstanceSettings::unguarded(function () {
|
||||
InstanceSettings::query()->create([
|
||||
'id' => 0,
|
||||
'smtp_enabled' => true,
|
||||
'smtp_from_address' => 'test@example.com',
|
||||
'smtp_from_name' => 'MapleDeploy',
|
||||
'smtp_host' => 'localhost',
|
||||
'smtp_port' => 1025,
|
||||
]);
|
||||
});
|
||||
Once::flush();
|
||||
});
|
||||
|
||||
test('forgot password does not create a reset token for MapleDeploy revoked users', function () {
|
||||
$user = User::factory()->create([
|
||||
'email' => 'revoked@example.com',
|
||||
'remember_token' => 'mapledeploy-revoked:abc123',
|
||||
]);
|
||||
|
||||
$response = $this->post('/forgot-password', [
|
||||
'email' => 'revoked@example.com',
|
||||
]);
|
||||
|
||||
$response->assertSessionHas('status');
|
||||
expect(DB::table('password_reset_tokens')->where('email', $user->email)->exists())->toBeFalse();
|
||||
Notification::assertNothingSent();
|
||||
});
|
||||
|
||||
test('forgot password still sends reset links for active users', function () {
|
||||
$user = User::factory()->create([
|
||||
'email' => 'active@example.com',
|
||||
'remember_token' => null,
|
||||
]);
|
||||
|
||||
$response = $this->post('/forgot-password', [
|
||||
'email' => 'active@example.com',
|
||||
]);
|
||||
|
||||
$response->assertSessionHas('status');
|
||||
expect(DB::table('password_reset_tokens')->where('email', $user->email)->exists())->toBeTrue();
|
||||
Notification::assertSentTo($user, ResetPassword::class);
|
||||
});
|
||||
|
||||
test('reset password refuses MapleDeploy revoked users even with an existing token', function () {
|
||||
$user = User::factory()->create([
|
||||
'password' => Hash::make('old-password'),
|
||||
'remember_token' => 'mapledeploy-revoked:abc123',
|
||||
]);
|
||||
|
||||
expect(fn () => app(ResetUserPassword::class)->reset($user, [
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
]))->toThrow(ValidationException::class);
|
||||
|
||||
expect(Hash::check('old-password', $user->fresh()->password))->toBeTrue()
|
||||
->and($user->fresh()->remember_token)->toBe('mapledeploy-revoked:abc123');
|
||||
});
|
||||
|
||||
test('revoked users are logged out even when sessions are not database backed', function () {
|
||||
$user = User::factory()->create([
|
||||
'remember_token' => 'mapledeploy-revoked:abc123',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get('/');
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
$this->assertGuest();
|
||||
});
|
||||
403
tests/Feature/MapledeployUserManagementCommandsTest.php
Normal file
403
tests/Feature/MapledeployUserManagementCommandsTest.php
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->originalDatabaseConfig = [
|
||||
'default' => config('database.default'),
|
||||
'testing_database' => config('database.connections.testing.database'),
|
||||
];
|
||||
$this->originalDatabaseEnvironment = [
|
||||
'DB_CONNECTION' => [
|
||||
'env' => $_ENV['DB_CONNECTION'] ?? null,
|
||||
'server' => $_SERVER['DB_CONNECTION'] ?? null,
|
||||
'process' => getenv('DB_CONNECTION') === false ? null : getenv('DB_CONNECTION'),
|
||||
],
|
||||
'DB_DATABASE' => [
|
||||
'env' => $_ENV['DB_DATABASE'] ?? null,
|
||||
'server' => $_SERVER['DB_DATABASE'] ?? null,
|
||||
'process' => getenv('DB_DATABASE') === false ? null : getenv('DB_DATABASE'),
|
||||
],
|
||||
];
|
||||
$this->databasePath = storage_path('framework/testing-mapledeploy-user-mgmt-'.bin2hex(random_bytes(6)).'.sqlite');
|
||||
touch($this->databasePath);
|
||||
|
||||
config([
|
||||
'database.default' => 'testing',
|
||||
'database.connections.testing.database' => $this->databasePath,
|
||||
]);
|
||||
$_ENV['DB_CONNECTION'] = 'testing';
|
||||
$_SERVER['DB_CONNECTION'] = 'testing';
|
||||
$_ENV['DB_DATABASE'] = $this->databasePath;
|
||||
$_SERVER['DB_DATABASE'] = $this->databasePath;
|
||||
putenv('DB_CONNECTION=testing');
|
||||
putenv("DB_DATABASE={$this->databasePath}");
|
||||
$GLOBALS['mapledeployUserMgmtDatabasePath'] = $this->databasePath;
|
||||
|
||||
DB::purge('testing');
|
||||
DB::reconnect('testing');
|
||||
Artisan::call('migrate:fresh', ['--database' => 'testing']);
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
DB::disconnect('testing');
|
||||
DB::purge('testing');
|
||||
config([
|
||||
'database.default' => $this->originalDatabaseConfig['default'] ?? null,
|
||||
'database.connections.testing.database' => $this->originalDatabaseConfig['testing_database'] ?? null,
|
||||
]);
|
||||
if (isset($this->databasePath) && file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
foreach (($this->originalDatabaseEnvironment ?? []) as $key => $values) {
|
||||
if ($values['env'] === null) {
|
||||
unset($_ENV[$key]);
|
||||
} else {
|
||||
$_ENV[$key] = $values['env'];
|
||||
}
|
||||
if ($values['server'] === null) {
|
||||
unset($_SERVER[$key]);
|
||||
} else {
|
||||
$_SERVER[$key] = $values['server'];
|
||||
}
|
||||
if ($values['process'] === null) {
|
||||
putenv($key);
|
||||
} else {
|
||||
putenv("{$key}={$values['process']}");
|
||||
}
|
||||
}
|
||||
unset($GLOBALS['mapledeployUserMgmtDatabasePath']);
|
||||
});
|
||||
|
||||
function runMapledeployUserCommand(array $arguments, string $stdin = ''): array
|
||||
{
|
||||
$process = new Process(
|
||||
[PHP_BINARY, 'artisan', ...$arguments],
|
||||
base_path(),
|
||||
[
|
||||
'APP_ENV' => 'testing',
|
||||
'APP_KEY' => config('app.key'),
|
||||
'DB_CONNECTION' => 'testing',
|
||||
'DB_DATABASE' => $GLOBALS['mapledeployUserMgmtDatabasePath'],
|
||||
'CACHE_DRIVER' => 'array',
|
||||
'SESSION_DRIVER' => 'database',
|
||||
'QUEUE_CONNECTION' => 'sync',
|
||||
'MAIL_MAILER' => 'array',
|
||||
'SELF_HOSTED' => 'true',
|
||||
],
|
||||
);
|
||||
$process->setInput($stdin);
|
||||
$process->setTimeout(30);
|
||||
$process->run();
|
||||
|
||||
$jsonLine = collect(explode("\n", $process->getOutput()))
|
||||
->map(fn (string $line) => trim($line))
|
||||
->first(fn (string $line) => str_starts_with($line, '{') && str_ends_with($line, '}'));
|
||||
|
||||
return [
|
||||
'exitCode' => $process->getExitCode(),
|
||||
'json' => $jsonLine ? json_decode($jsonLine, true, flags: JSON_THROW_ON_ERROR) : null,
|
||||
'stdout' => $process->getOutput(),
|
||||
'stderr' => $process->getErrorOutput(),
|
||||
];
|
||||
}
|
||||
|
||||
test('MapleDeploy user management commands create, list, reset, and revoke users', function () {
|
||||
$admin = runMapledeployUserCommand([
|
||||
'mapledeploy:user:create',
|
||||
'--admin',
|
||||
'--email=Owner@Example.com',
|
||||
'--name=Owner',
|
||||
], "owner-password\n");
|
||||
|
||||
expect($admin['exitCode'])->toBe(0)
|
||||
->and($admin['json']['user'])->toMatchArray([
|
||||
'id' => 0,
|
||||
'email' => 'owner@example.com',
|
||||
'name' => 'Owner',
|
||||
])
|
||||
->and((bool) InstanceSettings::findOrFail(0)->is_registration_enabled)->toBeFalse()
|
||||
->and(Hash::check('owner-password', User::findOrFail(0)->password))->toBeTrue();
|
||||
|
||||
$duplicateOwner = runMapledeployUserCommand([
|
||||
'mapledeploy:user:create',
|
||||
'--email=OWNER@Example.com',
|
||||
'--name=Duplicate Owner',
|
||||
], "duplicate-password\n");
|
||||
|
||||
expect($duplicateOwner['exitCode'])->toBe(1)
|
||||
->and($duplicateOwner['json'])->toBe(['error' => 'EMAIL_EXISTS']);
|
||||
|
||||
$member = runMapledeployUserCommand([
|
||||
'mapledeploy:user:create',
|
||||
'--email=Member@Example.com',
|
||||
'--name=Member',
|
||||
'--team-role=admin',
|
||||
], "member-password\n");
|
||||
|
||||
expect($member['exitCode'])->toBe(0)
|
||||
->and($member['json']['user']['email'])->toBe('member@example.com');
|
||||
|
||||
$memberUser = User::whereEmail('member@example.com')->firstOrFail();
|
||||
expect($memberUser->teams()->where('teams.id', 0)->first()?->pivot?->role)->toBe('admin')
|
||||
->and($memberUser->teams()->pluck('teams.id')->all())->toBe([0]);
|
||||
|
||||
$list = runMapledeployUserCommand(['mapledeploy:user:list']);
|
||||
expect($list['exitCode'])->toBe(0)
|
||||
->and(collect($list['json']['users'])->pluck('email')->all())
|
||||
->toBe(['owner@example.com', 'member@example.com']);
|
||||
|
||||
$resetOtherUser = User::factory()->create();
|
||||
DB::table('sessions')->insert([
|
||||
[
|
||||
'id' => 'member-reset-session',
|
||||
'user_id' => $memberUser->id,
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Test Browser',
|
||||
'payload' => base64_encode('member-reset-payload'),
|
||||
'last_activity' => now()->timestamp,
|
||||
],
|
||||
[
|
||||
'id' => 'other-reset-session',
|
||||
'user_id' => $resetOtherUser->id,
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Test Browser',
|
||||
'payload' => base64_encode('other-reset-payload'),
|
||||
'last_activity' => now()->timestamp,
|
||||
],
|
||||
]);
|
||||
|
||||
$reset = runMapledeployUserCommand([
|
||||
'mapledeploy:user:set-password',
|
||||
(string) $memberUser->id,
|
||||
], "new-member-password\n");
|
||||
expect($reset['exitCode'])->toBe(0)
|
||||
->and(Hash::check('new-member-password', $memberUser->fresh()->password))->toBeTrue();
|
||||
expect(DB::table('sessions')->where('user_id', $memberUser->id)->count())->toBe(0)
|
||||
->and(DB::table('sessions')->where('user_id', $resetOtherUser->id)->count())->toBe(1);
|
||||
|
||||
DB::table('personal_access_tokens')->insert([
|
||||
'tokenable_type' => User::class,
|
||||
'tokenable_id' => $memberUser->id,
|
||||
'name' => 'e2e-token',
|
||||
'token' => hash('sha256', 'e2e-token'),
|
||||
'team_id' => '0',
|
||||
'abilities' => json_encode(['*'], JSON_THROW_ON_ERROR),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
expect($memberUser->tokens()->count())->toBe(1);
|
||||
$otherUser = User::factory()->create();
|
||||
DB::table('sessions')->insert([
|
||||
[
|
||||
'id' => 'member-session',
|
||||
'user_id' => $memberUser->id,
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Test Browser',
|
||||
'payload' => base64_encode('member-payload'),
|
||||
'last_activity' => now()->timestamp,
|
||||
],
|
||||
[
|
||||
'id' => 'other-session',
|
||||
'user_id' => $otherUser->id,
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Test Browser',
|
||||
'payload' => base64_encode('other-payload'),
|
||||
'last_activity' => now()->timestamp,
|
||||
],
|
||||
]);
|
||||
expect(DB::table('sessions')->where('user_id', $memberUser->id)->count())->toBe(1);
|
||||
|
||||
$revokeRoot = runMapledeployUserCommand(['mapledeploy:user:revoke', '0']);
|
||||
expect($revokeRoot['exitCode'])->toBe(1)
|
||||
->and($revokeRoot['json'])->toBe(['error' => 'CANNOT_REVOKE_ROOT_USER']);
|
||||
|
||||
$revoke = runMapledeployUserCommand([
|
||||
'mapledeploy:user:revoke',
|
||||
(string) $memberUser->id,
|
||||
]);
|
||||
expect($revoke['exitCode'])->toBe(0)
|
||||
->and($revoke['json']['revoked']['email'])->toBe('member@example.com')
|
||||
->and($memberUser->fresh()->tokens()->count())->toBe(0);
|
||||
expect(str_starts_with((string) $memberUser->fresh()->remember_token, 'mapledeploy-revoked:'))->toBeTrue();
|
||||
expect(DB::table('sessions')->where('user_id', $memberUser->id)->count())->toBe(0)
|
||||
->and(DB::table('sessions')->where('user_id', $otherUser->id)->count())->toBe(1);
|
||||
|
||||
$restore = runMapledeployUserCommand([
|
||||
'mapledeploy:user:set-password',
|
||||
(string) $memberUser->id,
|
||||
], "restored-member-password\n");
|
||||
expect($restore['exitCode'])->toBe(0)
|
||||
->and(Hash::check('restored-member-password', $memberUser->fresh()->password))->toBeTrue()
|
||||
->and($memberUser->fresh()->remember_token)->toBeNull();
|
||||
});
|
||||
|
||||
test('MapleDeploy user delete command removes non-root users', function () {
|
||||
runMapledeployUserCommand([
|
||||
'mapledeploy:user:create',
|
||||
'--admin',
|
||||
'--email=owner@example.com',
|
||||
'--name=Owner',
|
||||
], "owner-password\n");
|
||||
|
||||
$member = runMapledeployUserCommand([
|
||||
'mapledeploy:user:create',
|
||||
'--email=delete-me@example.com',
|
||||
'--name=Delete Me',
|
||||
'--team-role=admin',
|
||||
], "member-password\n");
|
||||
$memberUser = User::findOrFail($member['json']['user']['id']);
|
||||
|
||||
DB::table('personal_access_tokens')->insert([
|
||||
'tokenable_type' => User::class,
|
||||
'tokenable_id' => $memberUser->id,
|
||||
'name' => 'delete-token',
|
||||
'token' => hash('sha256', 'delete-token'),
|
||||
'team_id' => '0',
|
||||
'abilities' => json_encode(['*'], JSON_THROW_ON_ERROR),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
DB::table('sessions')->insert([
|
||||
'id' => 'delete-session',
|
||||
'user_id' => $memberUser->id,
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Test Browser',
|
||||
'payload' => base64_encode('delete-payload'),
|
||||
'last_activity' => now()->timestamp,
|
||||
]);
|
||||
|
||||
$deleteRoot = runMapledeployUserCommand(['mapledeploy:user:delete', '0']);
|
||||
expect($deleteRoot['exitCode'])->toBe(1)
|
||||
->and($deleteRoot['json'])->toBe(['error' => 'CANNOT_DELETE_ROOT_USER']);
|
||||
|
||||
$invalid = runMapledeployUserCommand(['mapledeploy:user:delete', 'not-a-user-id']);
|
||||
expect($invalid['exitCode'])->toBe(1)
|
||||
->and($invalid['json'])->toBe(['error' => 'INVALID_USER_ID']);
|
||||
|
||||
$delete = runMapledeployUserCommand([
|
||||
'mapledeploy:user:delete',
|
||||
(string) $memberUser->id,
|
||||
]);
|
||||
|
||||
expect($delete['exitCode'])->toBe(0)
|
||||
->and($delete['json']['deleted'])->toBe([
|
||||
'id' => $memberUser->id,
|
||||
'email' => 'delete-me@example.com',
|
||||
])
|
||||
->and(User::find($memberUser->id))->toBeNull()
|
||||
->and(DB::table('personal_access_tokens')->where('tokenable_id', $memberUser->id)->count())->toBe(0)
|
||||
->and(DB::table('sessions')->where('user_id', $memberUser->id)->count())->toBe(0);
|
||||
|
||||
$missing = runMapledeployUserCommand([
|
||||
'mapledeploy:user:delete',
|
||||
(string) $memberUser->id,
|
||||
]);
|
||||
expect($missing['exitCode'])->toBe(0)
|
||||
->and($missing['json'])->toBe([
|
||||
'deleted' => null,
|
||||
'alreadyDeleted' => true,
|
||||
'id' => $memberUser->id,
|
||||
]);
|
||||
});
|
||||
|
||||
test('MapleDeploy password command can transfer root ownership identity', function () {
|
||||
runMapledeployUserCommand([
|
||||
'mapledeploy:user:create',
|
||||
'--admin',
|
||||
'--email=old-owner@example.com',
|
||||
'--name=Old Owner',
|
||||
], "old-owner-password\n");
|
||||
DB::table('sessions')->insert([
|
||||
'id' => 'root-session',
|
||||
'user_id' => 0,
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Test Browser',
|
||||
'payload' => base64_encode('root-payload'),
|
||||
'last_activity' => now()->timestamp,
|
||||
]);
|
||||
|
||||
$claim = runMapledeployUserCommand([
|
||||
'mapledeploy:user:set-password',
|
||||
'0',
|
||||
'--email=New.Owner@Example.com',
|
||||
'--name=New Owner',
|
||||
], "new-owner-password\n");
|
||||
|
||||
$root = User::findOrFail(0);
|
||||
expect($claim['exitCode'])->toBe(0)
|
||||
->and($claim['json']['user'])->toMatchArray([
|
||||
'id' => 0,
|
||||
'email' => 'new.owner@example.com',
|
||||
'name' => 'New Owner',
|
||||
])
|
||||
->and($root->email)->toBe('new.owner@example.com')
|
||||
->and($root->name)->toBe('New Owner')
|
||||
->and(Hash::check('new-owner-password', $root->password))->toBeTrue()
|
||||
->and($root->remember_token)->toBeNull()
|
||||
->and($root->email_verified_at)->not->toBeNull();
|
||||
expect(User::whereEmail('old-owner@example.com')->exists())->toBeFalse();
|
||||
expect(DB::table('sessions')->where('user_id', 0)->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('MapleDeploy password command promotes matched native users to root team admin', function () {
|
||||
runMapledeployUserCommand([
|
||||
'mapledeploy:user:create',
|
||||
'--admin',
|
||||
'--email=owner@example.com',
|
||||
'--name=Owner',
|
||||
], "owner-password\n");
|
||||
$nativeUser = User::factory()->create([
|
||||
'email' => 'native-member@example.com',
|
||||
'name' => 'Native Member',
|
||||
]);
|
||||
expect($nativeUser->teams()->where('teams.id', 0)->exists())->toBeFalse();
|
||||
|
||||
$reset = runMapledeployUserCommand([
|
||||
'mapledeploy:user:set-password',
|
||||
(string) $nativeUser->id,
|
||||
], "native-member-password\n");
|
||||
|
||||
expect($reset['exitCode'])->toBe(0)
|
||||
->and(Hash::check('native-member-password', $nativeUser->fresh()->password))->toBeTrue()
|
||||
->and($nativeUser->fresh()->teams()->where('teams.id', 0)->first()?->pivot?->role)->toBe('admin');
|
||||
});
|
||||
|
||||
test('MapleDeploy password command rejects ownership transfer to an existing email', function () {
|
||||
runMapledeployUserCommand([
|
||||
'mapledeploy:user:create',
|
||||
'--admin',
|
||||
'--email=old-owner@example.com',
|
||||
'--name=Old Owner',
|
||||
], "old-owner-password\n");
|
||||
$existing = User::factory()->create(['email' => 'new.owner@example.com']);
|
||||
|
||||
$claim = runMapledeployUserCommand([
|
||||
'mapledeploy:user:set-password',
|
||||
'0',
|
||||
'--email=new.owner@example.com',
|
||||
'--name=New Owner',
|
||||
], "new-owner-password\n");
|
||||
|
||||
expect($claim['exitCode'])->toBe(1)
|
||||
->and($claim['json'])->toBe(['error' => 'EMAIL_EXISTS'])
|
||||
->and(User::findOrFail(0)->email)->toBe('old-owner@example.com')
|
||||
->and($existing->fresh()->email)->toBe('new.owner@example.com');
|
||||
});
|
||||
|
||||
test('MapleDeploy user creation command reports validation errors as JSON', function () {
|
||||
$invalid = runMapledeployUserCommand([
|
||||
'mapledeploy:user:create',
|
||||
'--email=not-an-email',
|
||||
'--name=Invalid',
|
||||
], "short\n");
|
||||
|
||||
expect($invalid['exitCode'])->toBe(1)
|
||||
->and($invalid['json'])->toBe(['error' => 'INVALID_INPUT']);
|
||||
});
|
||||
Loading…
Reference in a new issue