@smi-digital/create-smi-app 1.0.6 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +41 -11
- package/package.json +1 -1
- package/templates/.husky/pre-commit +28 -1
- package/templates/base/gitignore.template +6 -1
- package/templates/integrations/strapi-astro/.github/workflows/deploy.yml.template +9 -11
- package/templates/integrations/strapi-astro/.vault-password.template +1 -0
- package/templates/integrations/strapi-astro/Dockerfile.backend.template +23 -0
- package/templates/integrations/strapi-astro/Dockerfile.frontend.ssr.template +31 -0
- package/templates/integrations/strapi-astro/backend/src/index.ts.template +41 -0
- package/templates/integrations/strapi-astro/docker-compose.yml.template +52 -0
- package/templates/integrations/strapi-astro/frontend/src/middleware.ts.template +16 -0
- package/templates/integrations/strapi-astro/integration.config.json +37 -8
- package/templates/integrations/strapi-astro/production.backend.env.template +19 -0
- package/templates/integrations/strapi-astro/production.frontend.env.template +14 -0
package/dist/index.js
CHANGED
|
@@ -64,8 +64,7 @@ var FRAMEWORK_GENERATORS = {
|
|
|
64
64
|
"--template",
|
|
65
65
|
"minimal",
|
|
66
66
|
"--install",
|
|
67
|
-
"--git",
|
|
68
|
-
"false",
|
|
67
|
+
"--no-git",
|
|
69
68
|
"--yes"
|
|
70
69
|
];
|
|
71
70
|
}
|
|
@@ -487,6 +486,7 @@ async function createApps(projectRoot, targets) {
|
|
|
487
486
|
// src/modules/scaffoldActions/createIntegrations.ts
|
|
488
487
|
import { mkdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
489
488
|
import { dirname as dirname2, join as join4 } from "path";
|
|
489
|
+
import { randomBytes } from "crypto";
|
|
490
490
|
function toTemplateToken(key) {
|
|
491
491
|
const normalized = key.replaceAll(/[^a-zA-Z0-9]+/gv, "_").replaceAll(/^_+|_+$/gv, "").toUpperCase();
|
|
492
492
|
return `__${normalized}__`;
|
|
@@ -530,18 +530,48 @@ async function createIntegrations(options, projectRoot, templatesDir) {
|
|
|
530
530
|
integrationKey
|
|
531
531
|
);
|
|
532
532
|
const integrationRoot = join4(templatesDir, "integrations", integrationKey);
|
|
533
|
-
const filesToApply = [
|
|
534
|
-
if (
|
|
535
|
-
|
|
533
|
+
const filesToApply = [];
|
|
534
|
+
if (integrationConfig.workflows) {
|
|
535
|
+
if (integrationConfig.workflows.prValidation)
|
|
536
|
+
filesToApply.push(integrationConfig.workflows.prValidation);
|
|
537
|
+
if (options.configureCd && integrationConfig.workflows.deploy)
|
|
538
|
+
filesToApply.push(integrationConfig.workflows.deploy);
|
|
539
|
+
}
|
|
540
|
+
if (integrationConfig.files) {
|
|
541
|
+
for (const file of integrationConfig.files) {
|
|
542
|
+
if (!file.condition || file.condition === options.integrationInputs?.astro_mode) {
|
|
543
|
+
filesToApply.push(file);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (options.configureCd && integrationConfig.cdFiles) {
|
|
548
|
+
for (const file of integrationConfig.cdFiles) {
|
|
549
|
+
if (!file.condition || file.condition === options.integrationInputs?.astro_mode) {
|
|
550
|
+
filesToApply.push(file);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
536
553
|
}
|
|
537
|
-
const applyFile = async (file) => runStep(
|
|
538
|
-
|
|
539
|
-
|
|
554
|
+
const applyFile = async (file) => runStep(`Adding ${file.target}`, async () => {
|
|
555
|
+
const generateSecret = () => randomBytes(32).toString("base64url");
|
|
556
|
+
const templateValues = {
|
|
557
|
+
...options.integrationInputs,
|
|
558
|
+
/* eslint-disable camelcase */
|
|
559
|
+
astro_port: "4321",
|
|
560
|
+
// Hardcoded to SSR port
|
|
561
|
+
vault_password: generateSecret(),
|
|
562
|
+
app_keys: `${generateSecret()},${generateSecret()}`,
|
|
563
|
+
api_token_salt: generateSecret(),
|
|
564
|
+
admin_jwt_secret: generateSecret(),
|
|
565
|
+
transfer_token_salt: generateSecret(),
|
|
566
|
+
jwt_secret: generateSecret()
|
|
567
|
+
/* eslint-enable camelcase */
|
|
568
|
+
};
|
|
569
|
+
return copyTemplateFile(
|
|
540
570
|
join4(integrationRoot, file.template),
|
|
541
571
|
join4(projectRoot, file.target),
|
|
542
|
-
|
|
543
|
-
)
|
|
544
|
-
);
|
|
572
|
+
templateValues
|
|
573
|
+
);
|
|
574
|
+
});
|
|
545
575
|
const applySequentially = async (index) => {
|
|
546
576
|
const file = filesToApply[index];
|
|
547
577
|
if (!file) {
|
package/package.json
CHANGED
|
@@ -1 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
|
|
3
|
+
# 1. Run standard linters and formatters
|
|
4
|
+
npx lint-staged
|
|
5
|
+
|
|
6
|
+
# 2. Ansible-Vault Security Guard
|
|
7
|
+
# Find any staged files that end in .env and start with "production"
|
|
8
|
+
STAGED_ENV_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^production\..*\.env$')
|
|
9
|
+
|
|
10
|
+
for file in $STAGED_ENV_FILES; do
|
|
11
|
+
# Read the first line of the file
|
|
12
|
+
FIRST_LINE=$(head -n 1 "$file")
|
|
13
|
+
|
|
14
|
+
# Check if the first line indicates it is an Ansible Vault file
|
|
15
|
+
if [[ "$FIRST_LINE" != "\$ANSIBLE_VAULT;"* ]]; then
|
|
16
|
+
echo ""
|
|
17
|
+
echo "🚨 SECURITY ALERT: Unencrypted Production Secrets Detected! 🚨"
|
|
18
|
+
echo "File: $file"
|
|
19
|
+
echo ""
|
|
20
|
+
echo "You are attempting to commit a plain-text production .env file."
|
|
21
|
+
echo "Please encrypt it using Ansible-Vault before committing:"
|
|
22
|
+
echo ""
|
|
23
|
+
echo " ansible-vault encrypt $file"
|
|
24
|
+
echo ""
|
|
25
|
+
echo "Commit aborted."
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
done
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
# ==============================================================================
|
|
2
|
-
# COPY THIS FILE TO: .github/workflows/deploy.yml in your project repository
|
|
3
|
-
# ==============================================================================
|
|
4
1
|
name: Deploy Production
|
|
5
2
|
|
|
6
3
|
on:
|
|
@@ -11,16 +8,17 @@ on:
|
|
|
11
8
|
|
|
12
9
|
jobs:
|
|
13
10
|
call-central-deployer:
|
|
14
|
-
uses: smi-digital/cd-library/.github/workflows/deploy-
|
|
11
|
+
uses: smi-digital/cd-library/.github/workflows/deploy-ansible.yml@main
|
|
15
12
|
with:
|
|
16
|
-
# Replace the following Variables with the actual ones
|
|
17
13
|
project_domain: __PROJECT_DOMAIN__
|
|
18
|
-
frontend_domain: __FRONTEND_DOMAIN__
|
|
19
14
|
backend_domain: __BACKEND_DOMAIN__
|
|
20
15
|
app_name: __APP_NAME__
|
|
21
|
-
|
|
16
|
+
image_tag: latest
|
|
22
17
|
secrets:
|
|
23
|
-
# These secrets
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
# These secrets must be configured at the Organization or Repository level:
|
|
19
|
+
SERVER_SSH_KEY: ${{ secrets.SERVER_SSH_KEY }}
|
|
20
|
+
SERVER_IP: ${{ secrets.SERVER_IP }}
|
|
21
|
+
SERVER_DEPLOY_USER: ${{ secrets.SERVER_DEPLOY_USER }}
|
|
22
|
+
VAULT_PASSWORD: ${{ secrets.VAULT_PASSWORD }}
|
|
23
|
+
ZOT_USERNAME: ${{ secrets.ZOT_USERNAME }}
|
|
24
|
+
ZOT_PASSWORD: ${{ secrets.ZOT_PASSWORD }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__VAULT_PASSWORD__
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# ========================================================
|
|
2
|
+
# EXAMPLE: Dockerfile for Strapi Backend
|
|
3
|
+
# Place this in your /backend folder
|
|
4
|
+
# ========================================================
|
|
5
|
+
|
|
6
|
+
FROM node:18-alpine
|
|
7
|
+
# Installing libvips-dev for sharp Compatibility
|
|
8
|
+
RUN apk update && apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev vips-dev > /dev/null 2>&1
|
|
9
|
+
ARG NODE_ENV=production
|
|
10
|
+
ENV NODE_ENV=${NODE_ENV}
|
|
11
|
+
|
|
12
|
+
WORKDIR /opt/
|
|
13
|
+
COPY package.json ./
|
|
14
|
+
RUN npm install -g node-gyp
|
|
15
|
+
RUN npm config set fetch-retry-maxtimeout 600000 -g && npm install --only=production
|
|
16
|
+
ENV PATH /opt/node_modules/.bin:$PATH
|
|
17
|
+
|
|
18
|
+
WORKDIR /opt/app
|
|
19
|
+
COPY . .
|
|
20
|
+
RUN npm run build
|
|
21
|
+
|
|
22
|
+
EXPOSE 1337
|
|
23
|
+
CMD ["npm", "run", "start"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# ========================================================
|
|
2
|
+
# EXAMPLE: Dockerfile for Astro SSR (Server-Side Rendering)
|
|
3
|
+
# Place this in your /frontend folder and name it 'Dockerfile'
|
|
4
|
+
# ========================================================
|
|
5
|
+
|
|
6
|
+
# Stage 1: Build with Bun
|
|
7
|
+
FROM oven/bun:1 as builder
|
|
8
|
+
WORKDIR /app
|
|
9
|
+
COPY package.json bun.lock ./
|
|
10
|
+
RUN bun install
|
|
11
|
+
COPY . .
|
|
12
|
+
RUN bun run build
|
|
13
|
+
|
|
14
|
+
# Stage 2: Serve dynamic application using Bun
|
|
15
|
+
FROM oven/bun:1-alpine
|
|
16
|
+
WORKDIR /app
|
|
17
|
+
|
|
18
|
+
# Copy the built SSR server bundle and node_modules
|
|
19
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
20
|
+
COPY --from=builder /app/dist ./dist
|
|
21
|
+
COPY --from=builder /app/package.json ./
|
|
22
|
+
|
|
23
|
+
# Expose port 4321 (Matches the 'frontend_port' logic in Ansible for SSR)
|
|
24
|
+
EXPOSE 4321
|
|
25
|
+
|
|
26
|
+
# Default environment variables
|
|
27
|
+
ENV HOST=0.0.0.0
|
|
28
|
+
ENV PORT=4321
|
|
29
|
+
|
|
30
|
+
# Start the server (Adjust entrypoint depending on your Astro Node/Bun adapter)
|
|
31
|
+
CMD ["bun", "run", "./dist/server/entry.mjs"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
register(/*{ strapi }*/) {},
|
|
3
|
+
|
|
4
|
+
async bootstrap({ strapi }) {
|
|
5
|
+
// 1. Find the Public Role
|
|
6
|
+
const publicRole = await strapi
|
|
7
|
+
.query('plugin::users-permissions.role')
|
|
8
|
+
.findOne({ where: { type: 'public' } });
|
|
9
|
+
|
|
10
|
+
if (!publicRole) return;
|
|
11
|
+
|
|
12
|
+
// 2. Dynamically find all Custom APIs created by the developer
|
|
13
|
+
// strapi.api contains all user-generated content types (ignoring system plugins)
|
|
14
|
+
const customApis = Object.keys(strapi.api);
|
|
15
|
+
const permissionsToOpen: string[] = [];
|
|
16
|
+
|
|
17
|
+
// 3. Build the permission strings (find and findOne) for every custom API
|
|
18
|
+
for (const apiName of customApis) {
|
|
19
|
+
permissionsToOpen.push(`api::${apiName}.${apiName}.find`);
|
|
20
|
+
permissionsToOpen.push(`api::${apiName}.${apiName}.findOne`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 4. Grant the permissions programmatically
|
|
24
|
+
for (const action of permissionsToOpen) {
|
|
25
|
+
const existingPermission = await strapi
|
|
26
|
+
.query('plugin::users-permissions.permission')
|
|
27
|
+
.findOne({ where: { role: publicRole.id, action } });
|
|
28
|
+
|
|
29
|
+
// If the permission doesn't exist yet, create it
|
|
30
|
+
if (!existingPermission) {
|
|
31
|
+
await strapi.query('plugin::users-permissions.permission').create({
|
|
32
|
+
data: {
|
|
33
|
+
action: action,
|
|
34
|
+
role: publicRole.id,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
console.log(`[BOOTSTRAP] Automatically granted public access to: ${action}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
services:
|
|
2
|
+
__APP_NAME__-strapi:
|
|
3
|
+
container_name: __APP_NAME__-strapi
|
|
4
|
+
# The image is built in CI and pushed to Zot
|
|
5
|
+
image: registry.smi-digital.com/__APP_NAME__-strapi:${IMAGE_TAG:-latest}
|
|
6
|
+
restart: unless-stopped
|
|
7
|
+
env_file:
|
|
8
|
+
- ./backend.env
|
|
9
|
+
volumes:
|
|
10
|
+
- ./data/uploads:/opt/app/public/uploads
|
|
11
|
+
- ./data/database:/opt/app/.tmp
|
|
12
|
+
networks:
|
|
13
|
+
- agency_shared_network
|
|
14
|
+
healthcheck:
|
|
15
|
+
test: ["CMD", "curl", "-f", "http://localhost:1337/admin"]
|
|
16
|
+
interval: 10s
|
|
17
|
+
timeout: 5s
|
|
18
|
+
retries: 15
|
|
19
|
+
labels:
|
|
20
|
+
- "traefik.enable=true"
|
|
21
|
+
# Route traffic from your backend domain to this container
|
|
22
|
+
- "traefik.http.routers.__APP_NAME__-strapi.rule=Host(`__BACKEND_DOMAIN__`)"
|
|
23
|
+
# Request an SSL certificate from the global Let's Encrypt resolver
|
|
24
|
+
- "traefik.http.routers.__APP_NAME__-strapi.tls.certresolver=letsencrypt"
|
|
25
|
+
# Tell Traefik which port to send traffic to inside the container
|
|
26
|
+
- "traefik.http.services.__APP_NAME__-strapi.loadbalancer.server.port=1337"
|
|
27
|
+
# Strapi requires larger body sizes for media uploads (25MB)
|
|
28
|
+
- "traefik.http.middlewares.__APP_NAME__-strapi-limit.buffering.maxRequestBodyBytes=25000000"
|
|
29
|
+
- "traefik.http.routers.__APP_NAME__-strapi.middlewares=__APP_NAME__-strapi-limit"
|
|
30
|
+
|
|
31
|
+
__APP_NAME__-astro:
|
|
32
|
+
container_name: __APP_NAME__-astro
|
|
33
|
+
image: registry.smi-digital.com/__APP_NAME__-astro:${IMAGE_TAG:-latest}
|
|
34
|
+
restart: unless-stopped
|
|
35
|
+
env_file:
|
|
36
|
+
- ./frontend.env
|
|
37
|
+
networks:
|
|
38
|
+
- agency_shared_network
|
|
39
|
+
depends_on:
|
|
40
|
+
__APP_NAME__-strapi:
|
|
41
|
+
condition: service_healthy
|
|
42
|
+
labels:
|
|
43
|
+
- "traefik.enable=true"
|
|
44
|
+
# Listen on both www and non-www (If include_www is selected during setup, you can manually adjust this)
|
|
45
|
+
- "traefik.http.routers.__APP_NAME__-astro.rule=Host(`__PROJECT_DOMAIN__`) || Host(`www.__PROJECT_DOMAIN__`)"
|
|
46
|
+
- "traefik.http.routers.__APP_NAME__-astro.tls.certresolver=letsencrypt"
|
|
47
|
+
# Port 4321 for Bun/SSR, Port 80 if pure SSG Nginx container
|
|
48
|
+
- "traefik.http.services.__APP_NAME__-astro.loadbalancer.server.port=__ASTRO_PORT__"
|
|
49
|
+
|
|
50
|
+
networks:
|
|
51
|
+
agency_shared_network:
|
|
52
|
+
external: true
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// This middleware enforces Stale-While-Revalidate (SWR) caching for SSR pages.
|
|
2
|
+
// It ensures pages are fast for users while updating in the background.
|
|
3
|
+
import { defineMiddleware } from 'astro:middleware';
|
|
4
|
+
|
|
5
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
6
|
+
const response = await next();
|
|
7
|
+
|
|
8
|
+
// Only apply SWR caching to HTML responses (SSR pages)
|
|
9
|
+
// We do not want to aggressively cache API routes or dynamic forms
|
|
10
|
+
if (response.headers.get('content-type')?.includes('text/html')) {
|
|
11
|
+
// Cache for 10 seconds. For the next 1 hour (3600s), serve stale cache while fetching new data.
|
|
12
|
+
response.headers.set('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=3600');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return response;
|
|
16
|
+
});
|
|
@@ -15,13 +15,6 @@
|
|
|
15
15
|
"default": "my-new-client.com",
|
|
16
16
|
"required": true
|
|
17
17
|
},
|
|
18
|
-
{
|
|
19
|
-
"key": "frontend_domain",
|
|
20
|
-
"message": "What is the frontend domain?",
|
|
21
|
-
"type": "input",
|
|
22
|
-
"default": "my-new-client.com",
|
|
23
|
-
"required": true
|
|
24
|
-
},
|
|
25
18
|
{
|
|
26
19
|
"key": "backend_domain",
|
|
27
20
|
"message": "What is the backend domain?",
|
|
@@ -53,5 +46,41 @@
|
|
|
53
46
|
"template": ".github/workflows/deploy.yml.template",
|
|
54
47
|
"target": ".github/workflows/deploy.yml"
|
|
55
48
|
}
|
|
56
|
-
}
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
{
|
|
52
|
+
"template": "backend/src/index.ts.template",
|
|
53
|
+
"target": "backend/src/index.ts"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"cdFiles": [
|
|
57
|
+
{
|
|
58
|
+
"template": "docker-compose.yml.template",
|
|
59
|
+
"target": "docker-compose.yml"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"template": "Dockerfile.backend.template",
|
|
63
|
+
"target": "backend/Dockerfile"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"template": "Dockerfile.frontend.ssr.template",
|
|
67
|
+
"target": "frontend/Dockerfile"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"template": "frontend/src/middleware.ts.template",
|
|
71
|
+
"target": "frontend/src/middleware.ts"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"template": ".vault-password.template",
|
|
75
|
+
"target": ".vault-password"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"template": "production.frontend.env.template",
|
|
79
|
+
"target": "production.frontend.env"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"template": "production.backend.env.template",
|
|
83
|
+
"target": "production.backend.env"
|
|
84
|
+
}
|
|
85
|
+
]
|
|
57
86
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# PRODUCTION BACKEND SECRETS (Strapi)
|
|
3
|
+
#
|
|
4
|
+
# 🚨 SECURITY WARNING 🚨
|
|
5
|
+
# This file MUST be encrypted before committing to Git!
|
|
6
|
+
# Run: ansible-vault encrypt production.backend.env --vault-password-file .vault-password
|
|
7
|
+
# ==============================================================================
|
|
8
|
+
|
|
9
|
+
# Strapi System Keys (Automatically generated by create-smi-app)
|
|
10
|
+
APP_KEYS=__APP_KEYS__
|
|
11
|
+
API_TOKEN_SALT=__API_TOKEN_SALT__
|
|
12
|
+
ADMIN_JWT_SECRET=__ADMIN_JWT_SECRET__
|
|
13
|
+
TRANSFER_TOKEN_SALT=__TRANSFER_TOKEN_SALT__
|
|
14
|
+
JWT_SECRET=__JWT_SECRET__
|
|
15
|
+
|
|
16
|
+
# Production Settings
|
|
17
|
+
HOST=0.0.0.0
|
|
18
|
+
PORT=1337
|
|
19
|
+
NODE_ENV=production
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# PRODUCTION FRONTEND SECRETS (Astro SSR)
|
|
3
|
+
#
|
|
4
|
+
# 🚨 SECURITY WARNING 🚨
|
|
5
|
+
# This file MUST be encrypted before committing to Git!
|
|
6
|
+
# Run: ansible-vault encrypt production.frontend.env --vault-password-file .vault-password
|
|
7
|
+
# ==============================================================================
|
|
8
|
+
|
|
9
|
+
# Internal Docker Network routing (Do not change)
|
|
10
|
+
PUBLIC_API_URL=http://__APP_NAME__-strapi:1337
|
|
11
|
+
|
|
12
|
+
# Add your client-specific 3rd party secrets below
|
|
13
|
+
# STRIPE_SECRET_KEY=sk_live_...
|
|
14
|
+
# SENDGRID_API_KEY=SG...
|