diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index 8af8625d..c2129493 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -107,7 +107,7 @@ jobs:
- name: Build and push Docker images (Docker Hub)
run: |
TAG=${{ env.TAG }}
- make build-release tag=$TAG
+ make -j4 build-release tag=$TAG
echo "Built & pushed to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
shell: bash
diff --git a/.github/workflows/restart-runners.yml b/.github/workflows/restart-runners.yml
new file mode 100644
index 00000000..14bbcefb
--- /dev/null
+++ b/.github/workflows/restart-runners.yml
@@ -0,0 +1,39 @@
+name: Restart Runners
+
+on:
+ schedule:
+ - cron: '0 0 */7 * *'
+
+permissions:
+ id-token: write
+ contents: read
+
+jobs:
+ ec2-maintenance-prod:
+ runs-on: ubuntu-latest
+ permissions: write-all
+ steps:
+ - name: Configure AWS credentials
+ uses: aws-actions/configure-aws-credentials@v2
+ with:
+ role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
+ role-duration-seconds: 3600
+ aws-region: ${{ secrets.AWS_REGION }}
+
+ - name: Verify AWS identity
+ run: aws sts get-caller-identity
+
+ - name: Start EC2 instance
+ run: |
+ aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
+ aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
+ echo "EC2 instances started"
+
+ - name: Wait
+ run: sleep 600
+
+ - name: Stop EC2 instance
+ run: |
+ aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
+ aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
+ echo "EC2 instances stopped"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 3b0627cc..41d43bd9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -12,11 +12,12 @@ on:
jobs:
test:
runs-on: ubuntu-latest
-
steps:
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - name: Checkout repository
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
+ - name: Install Node
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: '22'
@@ -57,8 +58,26 @@ jobs:
echo "App failed to start"
exit 1
+ build-sqlite:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+
+ - name: Copy config file
+ run: cp config/config.example.yml config/config.yml
+
- name: Build Docker image sqlite
- run: make build-sqlite
+ run: make dev-build-sqlite
+
+ build-postgres:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+
+ - name: Copy config file
+ run: cp config/config.example.yml config/config.yml
- name: Build Docker image pg
- run: make build-pg
+ run: make dev-build-pg
diff --git a/Dockerfile b/Dockerfile
index fa2d71c0..c59490b6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -43,23 +43,25 @@ RUN test -f dist/server.mjs
RUN npm run build:cli
+# Prune dev dependencies and clean up to prepare for copy to runner
+RUN npm prune --omit=dev && npm cache clean --force
+
FROM node:24-alpine AS runner
WORKDIR /app
-# Curl used for the health checks
-# Python and build tools needed for better-sqlite3 native compilation
-RUN apk add --no-cache curl tzdata python3 make g++
+# Only curl and tzdata needed at runtime - no build tools!
+RUN apk add --no-cache curl tzdata
-# COPY package.json package-lock.json ./
-COPY package*.json ./
-
-RUN npm ci --omit=dev && npm cache clean --force
+# Copy pre-built node_modules from builder (already pruned to production only)
+# This includes the compiled native modules like better-sqlite3
+COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/init ./dist/init
+COPY --from=builder /app/package.json ./package.json
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
diff --git a/Makefile b/Makefile
index 6c538a47..1519aec7 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,13 @@
-.PHONY: build build-pg build-release build-arm build-x86 test clean
+.PHONY: build dev-build-sqlite dev-build-pg build-release build-arm build-x86 test clean
major_tag := $(shell echo $(tag) | cut -d. -f1)
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
-build-release:
+
+.PHONY: build-release build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
+
+build-release: build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
+
+build-sqlite:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-release tag=








{t("remoteExitNodeQuestionRemove")}
{t("remoteExitNodeMessageRemove")}
diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index e391922f..97dd4a03 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -289,7 +289,7 @@ export default function GeneralPage() { setIsDeleteModalOpen(val); }} dialog={ -{t("orgQuestionRemove")}
{t("orgMessageRemove")}
{t("securityPolicyChangeDescription")}
{t("licenseQuestionRemove")}
{t("licenseMessageRemove")} @@ -360,7 +360,8 @@ export default function LicensePage() {
{t("userQuestionRemove")}
{t("userMessageRemove")}
diff --git a/src/app/auth/layout.tsx b/src/app/auth/layout.tsx index 70439824..6a72006b 100644 --- a/src/app/auth/layout.tsx +++ b/src/app/auth/layout.tsx @@ -23,6 +23,7 @@ export default async function AuthLayout({ children }: AuthLayoutProps) { const t = await getTranslations(); let hideFooter = false; + let licenseStatus: GetLicenseStatusResponse | null = null; if (build == "enterprise") { const licenseStatusRes = await cache( async () => @@ -30,10 +31,12 @@ export default async function AuthLayout({ children }: AuthLayoutProps) { "/license/status" ) )(); + licenseStatus = licenseStatusRes.data.data; if ( env.branding.hideAuthLayoutFooter && licenseStatusRes.data.data.isHostLicensed && - licenseStatusRes.data.data.isLicenseValid + licenseStatusRes.data.data.isLicenseValid && + licenseStatusRes.data.data.tier !== "personal" ) { hideFooter = true; } @@ -83,6 +86,23 @@ export default async function AuthLayout({ children }: AuthLayoutProps) { ? t("enterpriseEdition") : t("pangolinCloud")} + {build === "enterprise" && + licenseStatus?.isHostLicensed && + licenseStatus?.isLicenseValid && + licenseStatus?.tier === "personal" ? ( + <> +
{t("idpQuestionRemove", {
name: selectedIdp.name
diff --git a/src/components/AdminUsersTable.tsx b/src/components/AdminUsersTable.tsx
index 9c741cee..f12d21fc 100644
--- a/src/components/AdminUsersTable.tsx
+++ b/src/components/AdminUsersTable.tsx
@@ -269,7 +269,7 @@ export default function UsersTable({ users }: Props) {
{t("userQuestionRemove", {
selectedUser:
diff --git a/src/components/ApiKeysTable.tsx b/src/components/ApiKeysTable.tsx
index c3202277..8987fa2c 100644
--- a/src/components/ApiKeysTable.tsx
+++ b/src/components/ApiKeysTable.tsx
@@ -182,7 +182,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
setSelected(null);
}}
dialog={
- {t("apiKeysQuestionRemove")} {t("apiKeysMessageRemove")} {t("resourceQuestionRemove")} {t("resourceMessageRemove")}
+ {t("portRestrictions")}
+
+
diff --git a/src/components/Credenza.tsx b/src/components/Credenza.tsx
index 8176273f..6a48fc54 100644
--- a/src/components/Credenza.tsx
+++ b/src/components/Credenza.tsx
@@ -131,7 +131,7 @@ const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
const CredenzaHeader = isDesktop ? DialogHeader : SheetHeader;
return (
-