feat: prepare supporter self-hosting
This commit is contained in:
50
.github/workflows/docker-build.yml
vendored
50
.github/workflows/docker-build.yml
vendored
@@ -23,6 +23,7 @@ jobs:
|
|||||||
api: ${{ steps.filter.outputs.api }}
|
api: ${{ steps.filter.outputs.api }}
|
||||||
worker: ${{ steps.filter.outputs.worker }}
|
worker: ${{ steps.filter.outputs.worker }}
|
||||||
public: ${{ steps.filter.outputs.public }}
|
public: ${{ steps.filter.outputs.public }}
|
||||||
|
dashboard: ${{ steps.filter.outputs.dashboard }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dorny/paths-filter@v2
|
- uses: dorny/paths-filter@v2
|
||||||
@@ -42,10 +43,14 @@ jobs:
|
|||||||
- 'apps/public/**'
|
- 'apps/public/**'
|
||||||
- 'packages/**'
|
- 'packages/**'
|
||||||
- '.github/workflows/**'
|
- '.github/workflows/**'
|
||||||
|
dashboard:
|
||||||
|
- 'apps/start/**'
|
||||||
|
- 'packages/**'
|
||||||
|
- '.github/workflows/**'
|
||||||
|
|
||||||
lint-and-test:
|
lint-and-test:
|
||||||
needs: changes
|
needs: changes
|
||||||
if: ${{ needs.changes.outputs.api == 'true' || needs.changes.outputs.worker == 'true' || needs.changes.outputs.public == 'true' }}
|
if: ${{ needs.changes.outputs.api == 'true' || needs.changes.outputs.worker == 'true' || needs.changes.outputs.public == 'true' || needs.changes.outputs.dashboard == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
@@ -181,3 +186,46 @@ jobs:
|
|||||||
ghcr.io/${{ env.repo_owner }}/worker:${{ steps.tags.outputs.branch_name }}-${{ steps.tags.outputs.short_sha }}
|
ghcr.io/${{ env.repo_owner }}/worker:${{ steps.tags.outputs.branch_name }}-${{ steps.tags.outputs.short_sha }}
|
||||||
build-args: |
|
build-args: |
|
||||||
DATABASE_URL=postgresql://dummy:dummy@localhost:5432/dummy
|
DATABASE_URL=postgresql://dummy:dummy@localhost:5432/dummy
|
||||||
|
|
||||||
|
build-and-push-dashboard:
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
needs: [changes, lint-and-test]
|
||||||
|
if: ${{ needs.changes.outputs.dashboard == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Generate tags
|
||||||
|
id: tags
|
||||||
|
run: |
|
||||||
|
# Sanitize branch name by replacing / with -
|
||||||
|
BRANCH_NAME=$(echo "${{ github.ref_name }}" | sed 's/\//-/g')
|
||||||
|
# Get first 4 characters of commit SHA
|
||||||
|
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-4)
|
||||||
|
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||||
|
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: apps/start/Dockerfile
|
||||||
|
push: true
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ env.repo_owner }}/dashboard:${{ steps.tags.outputs.branch_name }}-${{ steps.tags.outputs.short_sha }}
|
||||||
|
build-args: |
|
||||||
|
NO_CLOUDFLARE=1
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export async function healthcheck(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const redisRes = await getRedisCache().ping();
|
const redisRes = await getRedisCache().ping();
|
||||||
const dbRes = await db.project.findFirst();
|
const dbRes = await db.$executeRaw`SELECT 1`;
|
||||||
const chRes = await chQuery('SELECT 1');
|
const chRes = await chQuery('SELECT 1');
|
||||||
const status = redisRes && dbRes && chRes ? 200 : 503;
|
const status = redisRes && dbRes && chRes ? 200 : 503;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
---
|
||||||
|
title: Supporter Access - Latest Docker Images
|
||||||
|
description: Get access to OpenPanel's latest private Docker images with bleeding-edge features built on every commit.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Thank You for Your Support! 🙏
|
||||||
|
|
||||||
|
First and foremost, **thank you** for supporting OpenPanel! Your contribution means the world to us and helps keep this project alive, maintained, and constantly improving. Every dollar you contribute goes directly into development, infrastructure, and making OpenPanel better for everyone.
|
||||||
|
|
||||||
|
As a supporter, you get exclusive access to our private Docker images that are built with every commit, giving you the absolute latest features and fixes before they're publicly released.
|
||||||
|
|
||||||
|
## Why Latest Images Matter
|
||||||
|
|
||||||
|
### Bleeding-Edge Features
|
||||||
|
- **Instant access**: Get new features the moment they're merged
|
||||||
|
- **Early bug fixes**: Patches and fixes deployed immediately
|
||||||
|
- **Continuous improvements**: Performance enhancements and optimizations in real-time
|
||||||
|
- **Stay ahead**: Run the most advanced version of OpenPanel available
|
||||||
|
|
||||||
|
### Built on Every Commit
|
||||||
|
We maintain a continuous integration pipeline that builds new Docker images with every single commit to our repository. This means:
|
||||||
|
- Zero delay between development and deployment
|
||||||
|
- Production-ready images tested and validated automatically
|
||||||
|
- Access to features weeks or months before stable releases
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
Our private Docker images are hosted on GitHub's container registry and protected from public access. As a supporter, you get an API key that grants you access to our private Docker proxy at `docker.openpanel.dev`, which seamlessly pulls these images for you.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Step 1: Become a Supporter
|
||||||
|
|
||||||
|
Support starts at just **$20/month** and includes:
|
||||||
|
- Access to all private Docker images
|
||||||
|
- Priority support in our Discord community
|
||||||
|
- Direct impact on OpenPanel's development
|
||||||
|
- Our eternal gratitude ❤️
|
||||||
|
|
||||||
|
### Step 2: Get Your API Key
|
||||||
|
|
||||||
|
Once you become a supporter, you'll receive a unique API key that grants access to our Docker proxy.
|
||||||
|
|
||||||
|
### Step 3: Login to Docker Registry
|
||||||
|
|
||||||
|
On your server, authenticate with our Docker proxy using your API key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "your_api_key" | docker login docker.openpanel.dev -u user --password-stdin
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `your_api_key` with the actual API key provided to you.
|
||||||
|
|
||||||
|
<Callout>
|
||||||
|
Make sure to keep your API key secure and never commit it to version control!
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
### Step 4: Update to Latest Images
|
||||||
|
|
||||||
|
We've created a convenient script to make updating your images effortless. Navigate to your self-hosting folder and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./get_latest_images apply
|
||||||
|
```
|
||||||
|
|
||||||
|
This script will:
|
||||||
|
- Check that you're authenticated with our Docker registry
|
||||||
|
- Fetch the latest Git tags from our repository
|
||||||
|
- Update your `docker-compose.yml` with the new image tags pointing to the latest builds
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
The script will automatically check if you're logged in. If not, it will provide you with instructions to authenticate first.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
You can also check what tags are available without applying them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./get_latest_images # Show latest tags
|
||||||
|
./get_latest_images --list # List all available tags
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Restart Your Services
|
||||||
|
|
||||||
|
After pulling the latest images, simply restart your services:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./restart
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it! Your OpenPanel instance is now running the latest and greatest version.
|
||||||
|
|
||||||
|
## Quick Update Workflow
|
||||||
|
|
||||||
|
Once you're set up, updating to the latest version is as simple as:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/self-hosting
|
||||||
|
./get_latest_images apply
|
||||||
|
./restart
|
||||||
|
```
|
||||||
|
|
||||||
|
This entire process takes less than a minute, giving you instant access to new features and fixes.
|
||||||
|
|
||||||
|
The `get_latest_images` script will:
|
||||||
|
1. Verify you're logged into the Docker registry
|
||||||
|
2. Fetch the latest tags from GitHub
|
||||||
|
3. Update your `docker-compose.yml` with images pointing to the latest commit SHAs
|
||||||
|
4. Create a backup of your docker-compose file before making changes
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- **Stability**: While these images are tested, they may occasionally contain bugs that haven't been discovered yet. We recommend having a backup strategy.
|
||||||
|
- **Breaking changes**: We strive to maintain backwards compatibility, but occasionally breaking changes may occur. Always check the [changelog](/docs/self-hosting/changelog) before updating.
|
||||||
|
- **Support**: As a supporter, you have priority access to support. If you encounter any issues, reach out to us on Discord or via email.
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
If you have any questions or run into issues:
|
||||||
|
- Join our [Discord community](https://go.openpanel.dev/discord) (supporters get a special role!)
|
||||||
|
- Email us at [hello@openpanel.dev](mailto:hello@openpanel.dev)
|
||||||
|
- Check our [GitHub repository](https://github.com/Openpanel-dev/openpanel)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Your Impact
|
||||||
|
|
||||||
|
Every contribution helps us:
|
||||||
|
- Dedicate more time to development
|
||||||
|
- Maintain and improve infrastructure
|
||||||
|
- Provide better documentation and support
|
||||||
|
- Keep OpenPanel free and open-source for everyone
|
||||||
|
|
||||||
|
Thank you for being an essential part of OpenPanel's journey. We couldn't do this without supporters like you! 💙
|
||||||
|
|
||||||
140
apps/start/Dockerfile
Normal file
140
apps/start/Dockerfile
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
ARG NODE_VERSION=22.20.0
|
||||||
|
|
||||||
|
FROM node:${NODE_VERSION}-slim AS base
|
||||||
|
|
||||||
|
# FIX: Bad workaround (https://github.com/nodejs/corepack/issues/612)
|
||||||
|
ENV COREPACK_INTEGRITY_KEYS=0
|
||||||
|
|
||||||
|
RUN corepack enable && apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
openssl \
|
||||||
|
libssl3 \
|
||||||
|
curl \
|
||||||
|
&& apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV NITRO=1
|
||||||
|
ENV SELF_HOSTED=1
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Workspace - Copy package.json files for caching
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
# Apps
|
||||||
|
COPY apps/start/package.json ./apps/start/
|
||||||
|
# Packages - Only copy what start app needs
|
||||||
|
COPY packages/trpc/package.json packages/trpc/
|
||||||
|
COPY packages/json/package.json packages/json/
|
||||||
|
COPY packages/common/package.json packages/common/
|
||||||
|
COPY packages/payments/package.json packages/payments/
|
||||||
|
COPY packages/constants/package.json packages/constants/
|
||||||
|
COPY packages/validation/package.json packages/validation/
|
||||||
|
COPY packages/integrations/package.json packages/integrations/
|
||||||
|
COPY packages/sdks/sdk/package.json packages/sdks/sdk/
|
||||||
|
COPY packages/sdks/_info/package.json packages/sdks/_info/
|
||||||
|
COPY patches ./patches
|
||||||
|
|
||||||
|
# BUILD
|
||||||
|
FROM base AS build
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install all dependencies (including dev dependencies for build)
|
||||||
|
RUN pnpm install --frozen-lockfile && \
|
||||||
|
pnpm store prune
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY apps/start ./apps/start
|
||||||
|
COPY packages ./packages
|
||||||
|
COPY tooling ./tooling
|
||||||
|
|
||||||
|
# Generate Prisma client and build the app
|
||||||
|
RUN pnpm --filter start run build
|
||||||
|
|
||||||
|
# PROD - Install only production dependencies
|
||||||
|
FROM base AS prod
|
||||||
|
|
||||||
|
ENV npm_config_build_from_source=true
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/package.json ./
|
||||||
|
COPY --from=build /app/pnpm-lock.yaml ./
|
||||||
|
COPY --from=build /app/pnpm-workspace.yaml ./
|
||||||
|
|
||||||
|
# Copy package.json files for production install
|
||||||
|
COPY --from=build /app/apps/start/package.json ./apps/start/
|
||||||
|
COPY --from=build /app/packages/db/package.json ./packages/db/
|
||||||
|
COPY --from=build /app/packages/trpc/package.json ./packages/trpc/
|
||||||
|
COPY --from=build /app/packages/auth/package.json ./packages/auth/
|
||||||
|
COPY --from=build /app/packages/json/package.json ./packages/json/
|
||||||
|
COPY --from=build /app/packages/common/package.json ./packages/common/
|
||||||
|
COPY --from=build /app/packages/payments/package.json ./packages/payments/
|
||||||
|
COPY --from=build /app/packages/constants/package.json ./packages/constants/
|
||||||
|
COPY --from=build /app/packages/validation/package.json ./packages/validation/
|
||||||
|
COPY --from=build /app/packages/integrations/package.json ./packages/integrations/
|
||||||
|
COPY --from=build /app/packages/sdks/sdk/package.json ./packages/sdks/sdk/
|
||||||
|
COPY --from=build /app/packages/sdks/_info/package.json ./packages/sdks/_info/
|
||||||
|
COPY --from=build /app/patches ./patches
|
||||||
|
|
||||||
|
# Install production dependencies only
|
||||||
|
RUN pnpm install --frozen-lockfile --prod && \
|
||||||
|
pnpm rebuild && \
|
||||||
|
pnpm store prune
|
||||||
|
|
||||||
|
# FINAL - Minimal runtime image
|
||||||
|
FROM base AS runner
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV npm_config_build_from_source=true
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy workspace files
|
||||||
|
COPY --from=build /app/package.json ./
|
||||||
|
COPY --from=build /app/pnpm-workspace.yaml ./
|
||||||
|
|
||||||
|
# Copy production node_modules
|
||||||
|
COPY --from=prod /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
# Copy built app with .output directory
|
||||||
|
COPY --from=build /app/apps/start/.output ./apps/start/.output
|
||||||
|
COPY --from=build /app/apps/start/dist ./apps/start/dist
|
||||||
|
COPY --from=build /app/apps/start/package.json ./apps/start/
|
||||||
|
|
||||||
|
# Copy necessary packages
|
||||||
|
COPY --from=build /app/packages/db ./packages/db
|
||||||
|
COPY --from=build /app/packages/trpc ./packages/trpc
|
||||||
|
COPY --from=build /app/packages/auth ./packages/auth
|
||||||
|
COPY --from=build /app/packages/json ./packages/json
|
||||||
|
COPY --from=build /app/packages/common ./packages/common
|
||||||
|
COPY --from=build /app/packages/payments ./packages/payments
|
||||||
|
COPY --from=build /app/packages/constants ./packages/constants
|
||||||
|
COPY --from=build /app/packages/validation ./packages/validation
|
||||||
|
COPY --from=build /app/packages/integrations ./packages/integrations
|
||||||
|
COPY --from=build /app/packages/sdks/sdk ./packages/sdks/sdk
|
||||||
|
COPY --from=build /app/packages/sdks/_info ./packages/sdks/_info
|
||||||
|
COPY --from=build /app/tooling/typescript ./tooling/typescript
|
||||||
|
|
||||||
|
WORKDIR /app/apps/start
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Start the Tanstack Start server
|
||||||
|
CMD ["node", ".output/server/index.mjs"]
|
||||||
@@ -65,6 +65,7 @@
|
|||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@tailwindcss/vite": "^4.0.6",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||||
|
"@tanstack/nitro-v2-vite-plugin": "^1.133.19",
|
||||||
"@tanstack/react-devtools": "^0.7.6",
|
"@tanstack/react-devtools": "^0.7.6",
|
||||||
"@tanstack/react-query": "^5.90.2",
|
"@tanstack/react-query": "^5.90.2",
|
||||||
"@tanstack/react-query-devtools": "^5.90.2",
|
"@tanstack/react-query-devtools": "^5.90.2",
|
||||||
|
|||||||
@@ -12,21 +12,12 @@ import { ThemeProvider } from './theme-provider';
|
|||||||
export function Providers({ children }: { children: React.ReactNode }) {
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
const storeRef = useRef<AppStore>(undefined);
|
const storeRef = useRef<AppStore>(undefined);
|
||||||
if (!storeRef.current) {
|
if (!storeRef.current) {
|
||||||
// Create the store instance the first time this renders
|
|
||||||
storeRef.current = makeStore();
|
storeRef.current = makeStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NuqsAdapter>
|
<NuqsAdapter>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
{/* {import.meta.env.VITE_OP_CLIENT_ID && (
|
|
||||||
<OpenPanelComponent
|
|
||||||
clientId={import.meta.env.VITE_OP_CLIENT_ID}
|
|
||||||
trackScreenViews
|
|
||||||
trackOutgoingLinks
|
|
||||||
trackAttributes
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
<ReduxProvider store={storeRef.current}>
|
<ReduxProvider store={storeRef.current}>
|
||||||
<TooltipProvider delayDuration={200}>
|
<TooltipProvider delayDuration={200}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
|
import { useAppContext } from '@/hooks/use-app-context';
|
||||||
import { useTRPC } from '@/integrations/trpc/react';
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import type { IServiceOrganization } from '@openpanel/db';
|
import type { IServiceOrganization } from '@openpanel/db';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import {
|
import { Link, useLocation, useParams } from '@tanstack/react-router';
|
||||||
Link,
|
|
||||||
useLocation,
|
|
||||||
useParams,
|
|
||||||
useRouteContext,
|
|
||||||
} from '@tanstack/react-router';
|
|
||||||
import { MenuIcon, XIcon } from 'lucide-react';
|
import { MenuIcon, XIcon } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { FeedbackButton } from './feedback-button';
|
import { FeedbackButton } from './feedback-button';
|
||||||
@@ -81,6 +77,7 @@ export function SidebarContainer({
|
|||||||
}: SidebarContainerProps) {
|
}: SidebarContainerProps) {
|
||||||
const [active, setActive] = useState(false);
|
const [active, setActive] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { isSelfHosted } = useAppContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActive(false);
|
setActive(false);
|
||||||
@@ -135,7 +132,7 @@ export function SidebarContainer({
|
|||||||
|
|
||||||
<div className="mt-auto w-full ">
|
<div className="mt-auto w-full ">
|
||||||
<FeedbackButton />
|
<FeedbackButton />
|
||||||
{import.meta.env.VITE_SELF_HOSTED === 'true' && (
|
{isSelfHosted && (
|
||||||
<div className={cn('text-sm w-full text-center')}>
|
<div className={cn('text-sm w-full text-center')}>
|
||||||
Self-hosted instance
|
Self-hosted instance
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ export function useAppContext() {
|
|||||||
strict: false,
|
strict: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!params.apiUrl || !params.dashboardUrl) {
|
if (!params.apiUrl || !params.dashboardUrl || !params.isSelfHosted) {
|
||||||
throw new Error('API URL or dashboard URL is not set');
|
throw new Error('API URL or dashboard URL is not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
apiUrl: params.apiUrl,
|
apiUrl: params.apiUrl,
|
||||||
dashboardUrl: params.dashboardUrl,
|
dashboardUrl: params.dashboardUrl,
|
||||||
|
isSelfHosted: params.isSelfHosted,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { Route as PublicRouteImport } from './routes/_public'
|
|||||||
import { Route as LoginRouteImport } from './routes/_login'
|
import { Route as LoginRouteImport } from './routes/_login'
|
||||||
import { Route as AppRouteImport } from './routes/_app'
|
import { Route as AppRouteImport } from './routes/_app'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
|
import { Route as ApiHealthcheckRouteImport } from './routes/api/healthcheck'
|
||||||
|
import { Route as ApiConfigRouteImport } from './routes/api/config'
|
||||||
import { Route as PublicOnboardingRouteImport } from './routes/_public.onboarding'
|
import { Route as PublicOnboardingRouteImport } from './routes/_public.onboarding'
|
||||||
import { Route as LoginResetPasswordRouteImport } from './routes/_login.reset-password'
|
import { Route as LoginResetPasswordRouteImport } from './routes/_login.reset-password'
|
||||||
import { Route as LoginLoginRouteImport } from './routes/_login.login'
|
import { Route as LoginLoginRouteImport } from './routes/_login.login'
|
||||||
@@ -112,6 +114,16 @@ const IndexRoute = IndexRouteImport.update({
|
|||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ApiHealthcheckRoute = ApiHealthcheckRouteImport.update({
|
||||||
|
id: '/api/healthcheck',
|
||||||
|
path: '/api/healthcheck',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const ApiConfigRoute = ApiConfigRouteImport.update({
|
||||||
|
id: '/api/config',
|
||||||
|
path: '/api/config',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const PublicOnboardingRoute = PublicOnboardingRouteImport.update({
|
const PublicOnboardingRoute = PublicOnboardingRouteImport.update({
|
||||||
id: '/onboarding',
|
id: '/onboarding',
|
||||||
path: '/onboarding',
|
path: '/onboarding',
|
||||||
@@ -459,6 +471,8 @@ export interface FileRoutesByFullPath {
|
|||||||
'/login': typeof LoginLoginRoute
|
'/login': typeof LoginLoginRoute
|
||||||
'/reset-password': typeof LoginResetPasswordRoute
|
'/reset-password': typeof LoginResetPasswordRoute
|
||||||
'/onboarding': typeof PublicOnboardingRoute
|
'/onboarding': typeof PublicOnboardingRoute
|
||||||
|
'/api/config': typeof ApiConfigRoute
|
||||||
|
'/api/healthcheck': typeof ApiHealthcheckRoute
|
||||||
'/$organizationId/$projectId': typeof AppOrganizationIdProjectIdRoute
|
'/$organizationId/$projectId': typeof AppOrganizationIdProjectIdRoute
|
||||||
'/$organizationId/billing': typeof AppOrganizationIdBillingRoute
|
'/$organizationId/billing': typeof AppOrganizationIdBillingRoute
|
||||||
'/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
|
'/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
|
||||||
@@ -513,6 +527,8 @@ export interface FileRoutesByTo {
|
|||||||
'/login': typeof LoginLoginRoute
|
'/login': typeof LoginLoginRoute
|
||||||
'/reset-password': typeof LoginResetPasswordRoute
|
'/reset-password': typeof LoginResetPasswordRoute
|
||||||
'/onboarding': typeof PublicOnboardingRoute
|
'/onboarding': typeof PublicOnboardingRoute
|
||||||
|
'/api/config': typeof ApiConfigRoute
|
||||||
|
'/api/healthcheck': typeof ApiHealthcheckRoute
|
||||||
'/$organizationId/$projectId': typeof AppOrganizationIdProjectIdRoute
|
'/$organizationId/$projectId': typeof AppOrganizationIdProjectIdRoute
|
||||||
'/$organizationId/billing': typeof AppOrganizationIdBillingRoute
|
'/$organizationId/billing': typeof AppOrganizationIdBillingRoute
|
||||||
'/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
|
'/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
|
||||||
@@ -566,6 +582,8 @@ export interface FileRoutesById {
|
|||||||
'/_login/login': typeof LoginLoginRoute
|
'/_login/login': typeof LoginLoginRoute
|
||||||
'/_login/reset-password': typeof LoginResetPasswordRoute
|
'/_login/reset-password': typeof LoginResetPasswordRoute
|
||||||
'/_public/onboarding': typeof PublicOnboardingRoute
|
'/_public/onboarding': typeof PublicOnboardingRoute
|
||||||
|
'/api/config': typeof ApiConfigRoute
|
||||||
|
'/api/healthcheck': typeof ApiHealthcheckRoute
|
||||||
'/_app/$organizationId/$projectId': typeof AppOrganizationIdProjectIdRoute
|
'/_app/$organizationId/$projectId': typeof AppOrganizationIdProjectIdRoute
|
||||||
'/_app/$organizationId/billing': typeof AppOrganizationIdBillingRoute
|
'/_app/$organizationId/billing': typeof AppOrganizationIdBillingRoute
|
||||||
'/_app/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
|
'/_app/$organizationId/settings': typeof AppOrganizationIdSettingsRoute
|
||||||
@@ -630,6 +648,8 @@ export interface FileRouteTypes {
|
|||||||
| '/login'
|
| '/login'
|
||||||
| '/reset-password'
|
| '/reset-password'
|
||||||
| '/onboarding'
|
| '/onboarding'
|
||||||
|
| '/api/config'
|
||||||
|
| '/api/healthcheck'
|
||||||
| '/$organizationId/$projectId'
|
| '/$organizationId/$projectId'
|
||||||
| '/$organizationId/billing'
|
| '/$organizationId/billing'
|
||||||
| '/$organizationId/settings'
|
| '/$organizationId/settings'
|
||||||
@@ -684,6 +704,8 @@ export interface FileRouteTypes {
|
|||||||
| '/login'
|
| '/login'
|
||||||
| '/reset-password'
|
| '/reset-password'
|
||||||
| '/onboarding'
|
| '/onboarding'
|
||||||
|
| '/api/config'
|
||||||
|
| '/api/healthcheck'
|
||||||
| '/$organizationId/$projectId'
|
| '/$organizationId/$projectId'
|
||||||
| '/$organizationId/billing'
|
| '/$organizationId/billing'
|
||||||
| '/$organizationId/settings'
|
| '/$organizationId/settings'
|
||||||
@@ -736,6 +758,8 @@ export interface FileRouteTypes {
|
|||||||
| '/_login/login'
|
| '/_login/login'
|
||||||
| '/_login/reset-password'
|
| '/_login/reset-password'
|
||||||
| '/_public/onboarding'
|
| '/_public/onboarding'
|
||||||
|
| '/api/config'
|
||||||
|
| '/api/healthcheck'
|
||||||
| '/_app/$organizationId/$projectId'
|
| '/_app/$organizationId/$projectId'
|
||||||
| '/_app/$organizationId/billing'
|
| '/_app/$organizationId/billing'
|
||||||
| '/_app/$organizationId/settings'
|
| '/_app/$organizationId/settings'
|
||||||
@@ -799,6 +823,8 @@ export interface RootRouteChildren {
|
|||||||
LoginRoute: typeof LoginRouteWithChildren
|
LoginRoute: typeof LoginRouteWithChildren
|
||||||
PublicRoute: typeof PublicRouteWithChildren
|
PublicRoute: typeof PublicRouteWithChildren
|
||||||
StepsRoute: typeof StepsRouteWithChildren
|
StepsRoute: typeof StepsRouteWithChildren
|
||||||
|
ApiConfigRoute: typeof ApiConfigRoute
|
||||||
|
ApiHealthcheckRoute: typeof ApiHealthcheckRoute
|
||||||
ShareOverviewShareIdRoute: typeof ShareOverviewShareIdRoute
|
ShareOverviewShareIdRoute: typeof ShareOverviewShareIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -839,6 +865,20 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexRouteImport
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/api/healthcheck': {
|
||||||
|
id: '/api/healthcheck'
|
||||||
|
path: '/api/healthcheck'
|
||||||
|
fullPath: '/api/healthcheck'
|
||||||
|
preLoaderRoute: typeof ApiHealthcheckRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/api/config': {
|
||||||
|
id: '/api/config'
|
||||||
|
path: '/api/config'
|
||||||
|
fullPath: '/api/config'
|
||||||
|
preLoaderRoute: typeof ApiConfigRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/_public/onboarding': {
|
'/_public/onboarding': {
|
||||||
id: '/_public/onboarding'
|
id: '/_public/onboarding'
|
||||||
path: '/onboarding'
|
path: '/onboarding'
|
||||||
@@ -1631,6 +1671,8 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
LoginRoute: LoginRouteWithChildren,
|
LoginRoute: LoginRouteWithChildren,
|
||||||
PublicRoute: PublicRouteWithChildren,
|
PublicRoute: PublicRouteWithChildren,
|
||||||
StepsRoute: StepsRouteWithChildren,
|
StepsRoute: StepsRouteWithChildren,
|
||||||
|
ApiConfigRoute: ApiConfigRoute,
|
||||||
|
ApiHealthcheckRoute: ApiHealthcheckRoute,
|
||||||
ShareOverviewShareIdRoute: ShareOverviewShareIdRoute,
|
ShareOverviewShareIdRoute: ShareOverviewShareIdRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ interface MyRouterContext {
|
|||||||
trpc: TRPCOptionsProxy<AppRouter>;
|
trpc: TRPCOptionsProxy<AppRouter>;
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
dashboardUrl: string;
|
dashboardUrl: string;
|
||||||
|
isSelfHosted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||||
|
|||||||
19
apps/start/src/routes/api/config.tsx
Normal file
19
apps/start/src/routes/api/config.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Sidebar } from '@/components/sidebar';
|
||||||
|
import { getServerEnvs } from '@/server/get-envs';
|
||||||
|
import { Outlet, createFileRoute, redirect } from '@tanstack/react-router';
|
||||||
|
|
||||||
|
// Nothing sensitive here, its client environment variables which is good for debugging
|
||||||
|
export const Route = createFileRoute('/api/config')({
|
||||||
|
server: {
|
||||||
|
handlers: {
|
||||||
|
GET: async () => {
|
||||||
|
const envs = await getServerEnvs();
|
||||||
|
return new Response(JSON.stringify(envs), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
11
apps/start/src/routes/api/healthcheck.tsx
Normal file
11
apps/start/src/routes/api/healthcheck.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/api/healthcheck')({
|
||||||
|
server: {
|
||||||
|
handlers: {
|
||||||
|
GET: async () => {
|
||||||
|
return new Response('OK');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -7,6 +7,7 @@ export const getServerEnvs = createServerFn().handler(async () => {
|
|||||||
dashboardUrl: String(
|
dashboardUrl: String(
|
||||||
process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL,
|
process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL,
|
||||||
),
|
),
|
||||||
|
isSelfHosted: process.env.SELF_HOSTED !== undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return envs;
|
return envs;
|
||||||
|
|||||||
@@ -1,21 +1,34 @@
|
|||||||
import { cloudflare } from '@cloudflare/vite-plugin';
|
import { cloudflare } from '@cloudflare/vite-plugin';
|
||||||
import { wrapVinxiConfigWithSentry } from '@sentry/tanstackstart-react';
|
import { wrapVinxiConfigWithSentry } from '@sentry/tanstackstart-react';
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import { nitroV2Plugin } from '@tanstack/nitro-v2-vite-plugin';
|
||||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
|
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
|
||||||
import viteReact from '@vitejs/plugin-react';
|
import viteReact from '@vitejs/plugin-react';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import viteTsConfigPaths from 'vite-tsconfig-paths';
|
import viteTsConfigPaths from 'vite-tsconfig-paths';
|
||||||
|
|
||||||
const config = defineConfig({
|
const plugins = [
|
||||||
plugins: [
|
viteTsConfigPaths({
|
||||||
cloudflare({ viteEnvironment: { name: 'ssr' } }),
|
projects: ['./tsconfig.json'],
|
||||||
viteTsConfigPaths({
|
}),
|
||||||
projects: ['./tsconfig.json'],
|
tailwindcss(),
|
||||||
|
tanstackStart(),
|
||||||
|
viteReact(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (process.env.NITRO) {
|
||||||
|
plugins.unshift(
|
||||||
|
nitroV2Plugin({
|
||||||
|
preset: 'node-server',
|
||||||
|
compatibilityDate: '2025-10-21',
|
||||||
}),
|
}),
|
||||||
tailwindcss(),
|
);
|
||||||
tanstackStart(),
|
} else {
|
||||||
viteReact(),
|
plugins.unshift(cloudflare({ viteEnvironment: { name: 'ssr' } }));
|
||||||
],
|
}
|
||||||
|
|
||||||
|
const config = defineConfig({
|
||||||
|
plugins,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default wrapVinxiConfigWithSentry(config, {
|
export default wrapVinxiConfigWithSentry(config, {
|
||||||
|
|||||||
@@ -59,6 +59,9 @@
|
|||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"nuqs": "patches/nuqs.patch"
|
"nuqs": "patches/nuqs.patch"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"rolldown": "1.0.0-beta.43"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ export const eventsGroupQueue = new GroupQueue<
|
|||||||
>({
|
>({
|
||||||
logger: queueLogger,
|
logger: queueLogger,
|
||||||
namespace: 'group_events',
|
namespace: 'group_events',
|
||||||
|
// @ts-expect-error - TODO: Fix this in groupmq
|
||||||
redis: getRedisGroupQueue(),
|
redis: getRedisGroupQueue(),
|
||||||
orderingMethod: 'in-memory',
|
orderingMethod: 'in-memory',
|
||||||
orderingWindowMs,
|
orderingWindowMs,
|
||||||
|
|||||||
2030
pnpm-lock.yaml
generated
2030
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
270
self-hosting/get_latest_images
Executable file
270
self-hosting/get_latest_images
Executable file
@@ -0,0 +1,270 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# GitHub repository (update if needed)
|
||||||
|
REPO="${GITHUB_REPO:-Openpanel-dev/openpanel}"
|
||||||
|
|
||||||
|
# Components to find tags for
|
||||||
|
COMPONENTS=("worker" "api" "dashboard")
|
||||||
|
|
||||||
|
# Docker compose file path
|
||||||
|
DOCKER_COMPOSE_FILE="${DOCKER_COMPOSE_FILE:-./docker-compose.yml}"
|
||||||
|
|
||||||
|
# Color codes for output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GRAY='\033[0;90m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Show usage
|
||||||
|
show_usage() {
|
||||||
|
echo "Usage: $0 [COMMAND] [OPTIONS]"
|
||||||
|
echo ""
|
||||||
|
echo "Fetches the latest Git tags for worker, api, and dashboard components"
|
||||||
|
echo ""
|
||||||
|
echo "Commands:"
|
||||||
|
echo " apply Apply the latest tags to docker-compose.yml"
|
||||||
|
echo " (none) Show the latest tags (default)"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " --list, -l List all available tags"
|
||||||
|
echo " --repo REPO Specify GitHub repository (default: $REPO)"
|
||||||
|
echo " --file FILE Specify docker-compose file (default: $DOCKER_COMPOSE_FILE)"
|
||||||
|
echo " --help, -h Show this help message"
|
||||||
|
echo ""
|
||||||
|
echo "Environment variables:"
|
||||||
|
echo " GITHUB_REPO Set the GitHub repository"
|
||||||
|
echo " DOCKER_COMPOSE_FILE Set the docker-compose file path"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 # Show latest tags"
|
||||||
|
echo " $0 apply # Update docker-compose.yml with latest tags"
|
||||||
|
echo " $0 --list # List all available tags"
|
||||||
|
echo ""
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
LIST_ALL=false
|
||||||
|
APPLY_MODE=false
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
apply)
|
||||||
|
APPLY_MODE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--list|-l)
|
||||||
|
LIST_ALL=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--repo)
|
||||||
|
REPO="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--file)
|
||||||
|
DOCKER_COMPOSE_FILE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
show_usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Unknown option: $1${NC}"
|
||||||
|
show_usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check if user needs to be logged in (for apply mode)
|
||||||
|
if [ "$APPLY_MODE" = true ]; then
|
||||||
|
# Check if Docker is available
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
echo -e "${RED}Error: Docker is not installed or not in PATH${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if logged into docker.openpanel.dev
|
||||||
|
echo -e "${BLUE}Checking Docker registry authentication...${NC}\n"
|
||||||
|
|
||||||
|
if ! docker info 2>/dev/null | grep -q "docker.openpanel.dev" && ! grep -q "docker.openpanel.dev" ~/.docker/config.json 2>/dev/null; then
|
||||||
|
echo -e "${YELLOW}⚠ You need to login to the OpenPanel Docker registry first!${NC}\n"
|
||||||
|
echo -e "${CYAN}To access the latest Docker images, you need:${NC}"
|
||||||
|
echo -e " 1. Be a supporter (starts at \$20/month)"
|
||||||
|
echo -e " 2. Get your API key from your supporter dashboard"
|
||||||
|
echo -e " 3. Login to the registry with:\n"
|
||||||
|
echo -e "${GREEN} echo \"your_api_key\" | docker login docker.openpanel.dev -u user --password-stdin${NC}\n"
|
||||||
|
echo -e "${GRAY}For more info: https://openpanel.dev/docs/self-hosting/supporter-access-latest-docker-images${NC}\n"
|
||||||
|
|
||||||
|
read -p "$(echo -e ${YELLOW}Have you already logged in? [y/N]:${NC} )" -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo -e "${RED}Please login first and try again.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✓ Docker registry authentication OK${NC}\n"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}Fetching tags from ${REPO}...${NC}\n"
|
||||||
|
|
||||||
|
# Fetch all tags from GitHub API
|
||||||
|
TAGS_JSON=$(curl -s "https://api.github.com/repos/${REPO}/tags")
|
||||||
|
|
||||||
|
# Check if we got valid JSON response
|
||||||
|
if [ -z "$TAGS_JSON" ]; then
|
||||||
|
echo -e "${RED}Failed to fetch tags from GitHub API${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if repository has any tags
|
||||||
|
TAGS_CLEAN=$(echo "$TAGS_JSON" | tr -d '[:space:]')
|
||||||
|
if [ "$TAGS_CLEAN" == "[]" ]; then
|
||||||
|
echo -e "${RED}No tags found in repository${NC}"
|
||||||
|
echo -e "${YELLOW}Create tags using: git tag <tag-name> && git push origin <tag-name>${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${GRAY}Example tag naming patterns:${NC}"
|
||||||
|
echo -e " ${GRAY}- worker-v1.0.0${NC}"
|
||||||
|
echo -e " ${GRAY}- api-v1.0.0${NC}"
|
||||||
|
echo -e " ${GRAY}- dashboard-v1.0.0${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# List all tags if requested
|
||||||
|
if [ "$LIST_ALL" = true ]; then
|
||||||
|
echo -e "${GREEN}All available tags:${NC}\n"
|
||||||
|
if command -v jq &> /dev/null; then
|
||||||
|
echo "$TAGS_JSON" | jq -r '.[] | " \(.name) (\(.commit.sha[0:7]))"'
|
||||||
|
else
|
||||||
|
echo "$TAGS_JSON" | grep "\"name\":" | sed 's/.*"name": "\([^"]*\)".*/ \1/'
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Function to find latest tag matching a component
|
||||||
|
get_latest_tag() {
|
||||||
|
local component=$1
|
||||||
|
local output_var_tag=$2
|
||||||
|
local output_var_sha=$3
|
||||||
|
|
||||||
|
if command -v jq &> /dev/null; then
|
||||||
|
# Use jq for better JSON parsing
|
||||||
|
local tag=$(echo "$TAGS_JSON" | jq -r "[.[] | select(.name | contains(\"${component}\"))] | .[0] | .name" 2>/dev/null)
|
||||||
|
local sha=$(echo "$TAGS_JSON" | jq -r "[.[] | select(.name | contains(\"${component}\"))] | .[0] | .commit.sha" 2>/dev/null)
|
||||||
|
else
|
||||||
|
# Fallback to grep/sed
|
||||||
|
local tag=$(echo "$TAGS_JSON" | grep -o "\"name\": \"[^\"]*${component}[^\"]*\"" | head -1 | cut -d'"' -f4)
|
||||||
|
local sha=$(echo "$TAGS_JSON" | grep -B5 "\"name\": \"${tag}\"" | grep "\"sha\"" | head -1 | cut -d'"' -f4)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$tag" ] || [ "$tag" == "null" ]; then
|
||||||
|
echo -e "${RED}✗${NC} ${component}: No matching tag found"
|
||||||
|
echo
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓${NC} ${component}:"
|
||||||
|
echo -e " Tag: ${YELLOW}${tag}${NC}"
|
||||||
|
echo -e " SHA: ${sha}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Return values via eval (for compatibility with older bash)
|
||||||
|
if [ -n "$output_var_tag" ]; then
|
||||||
|
eval "$output_var_tag='$tag'"
|
||||||
|
fi
|
||||||
|
if [ -n "$output_var_sha" ]; then
|
||||||
|
eval "$output_var_sha='$sha'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to apply tags to docker-compose.yml
|
||||||
|
apply_tags() {
|
||||||
|
echo -e "${CYAN}Applying tags to ${DOCKER_COMPOSE_FILE}...${NC}\n"
|
||||||
|
|
||||||
|
# Check if docker-compose file exists
|
||||||
|
if [ ! -f "$DOCKER_COMPOSE_FILE" ]; then
|
||||||
|
echo -e "${RED}Error: Docker compose file not found: ${DOCKER_COMPOSE_FILE}${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create a backup
|
||||||
|
local backup_file="${DOCKER_COMPOSE_FILE}.backup.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
cp "$DOCKER_COMPOSE_FILE" "$backup_file"
|
||||||
|
echo -e "${GRAY}Created backup: ${backup_file}${NC}\n"
|
||||||
|
|
||||||
|
local updated=0
|
||||||
|
local failed=0
|
||||||
|
|
||||||
|
for component in "${COMPONENTS[@]}"; do
|
||||||
|
# Get tag and SHA for this component
|
||||||
|
local component_tag=""
|
||||||
|
local component_sha=""
|
||||||
|
get_latest_tag "$component" component_tag component_sha >/dev/null 2>&1
|
||||||
|
|
||||||
|
if [ -z "$component_sha" ] || [ "$component_sha" == "null" ]; then
|
||||||
|
echo -e "${RED}✗${NC} ${component}: Skipping (no tag found)"
|
||||||
|
((failed++))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get first 4 characters of SHA
|
||||||
|
local short_sha="${component_sha:0:4}"
|
||||||
|
|
||||||
|
# New image tag format
|
||||||
|
local new_image="docker.openpanel.dev/openpanel-dev/${component}:main-${short_sha}"
|
||||||
|
|
||||||
|
# Find and replace the image line in docker-compose.yml
|
||||||
|
# Look for lines like: image: something{component}something
|
||||||
|
if grep -q "image:.*${component}" "$DOCKER_COMPOSE_FILE"; then
|
||||||
|
# Use sed to replace the entire image line
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
# macOS sed syntax
|
||||||
|
sed -i '' "s|image:.*${component}.*|image: ${new_image}|g" "$DOCKER_COMPOSE_FILE"
|
||||||
|
else
|
||||||
|
# Linux sed syntax
|
||||||
|
sed -i "s|image:.*${component}.*|image: ${new_image}|g" "$DOCKER_COMPOSE_FILE"
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}✓${NC} Updated ${component}: ${CYAN}${new_image}${NC}"
|
||||||
|
((updated++))
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠${NC} ${component}: No matching image line found in docker-compose.yml"
|
||||||
|
((failed++))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}Summary:${NC}"
|
||||||
|
echo -e " Updated: ${GREEN}${updated}${NC}"
|
||||||
|
echo -e " Failed: ${RED}${failed}${NC}"
|
||||||
|
echo -e " Backup: ${GRAY}${backup_file}${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ $updated -gt 0 ]; then
|
||||||
|
echo -e "${GREEN}Successfully updated docker-compose.yml!${NC}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main execution
|
||||||
|
if [ "$APPLY_MODE" = true ]; then
|
||||||
|
# Apply mode: update docker-compose.yml
|
||||||
|
# First, show all tags
|
||||||
|
for component in "${COMPONENTS[@]}"; do
|
||||||
|
get_latest_tag "$component"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Then apply them
|
||||||
|
apply_tags
|
||||||
|
else
|
||||||
|
# Default mode: just show the tags
|
||||||
|
for component in "${COMPONENTS[@]}"; do
|
||||||
|
get_latest_tag "$component"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${GRAY}Tip: Use --list to see all available tags, or 'apply' to update docker-compose.yml${NC}"
|
||||||
|
echo -e "${BLUE}Done!${NC}"
|
||||||
|
fi
|
||||||
3
self-hosting/restart
Executable file
3
self-hosting/restart
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker compose restart
|
||||||
Reference in New Issue
Block a user