@django-stack/cli 0.10.0 → 0.11.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/bin.js +38 -18
- package/dist/index.d.ts +11 -4
- package/dist/index.js +1 -1
- package/dist/{production-Ct_qoeXk.js → production-WThC1aS2.js} +115 -23
- package/package.json +1 -1
- package/templates/backend/Dockerfile +7 -2
- package/templates/backend/config/settings.py +12 -0
- package/templates/backend/config/urls.py +11 -0
- package/templates/backend/content/admin.py +14 -5
- package/templates/backend/pyproject.toml +1 -1
- package/templates/base/.github/workflows/deploy.yml +10 -1
- package/templates/base/AGENTS.md +78 -0
- package/templates/base/Caddyfile +24 -0
- package/templates/base/README.md +71 -3
- package/templates/base/SECURITY.md +2 -0
- package/templates/base/compose.yaml +21 -2
- package/templates/base/env.example +25 -0
- package/templates/frontend-astro/package.json +1 -1
- package/templates/frontend-next/package.json +3 -3
- package/templates/frontend-nuxt/package.json +2 -2
- package/templates/frontend-sveltekit/package.json +2 -2
- package/templates/frontend-templates/template.json +5 -0
- package/templates/frontend-templates/website/__init__.py +0 -0
- package/templates/frontend-templates/website/apps.py +6 -0
- package/templates/frontend-templates/website/context_processors.py +16 -0
- package/templates/frontend-templates/website/forms.py +20 -0
- package/templates/frontend-templates/website/static/website/favicon.svg +1 -0
- package/templates/frontend-templates/website/static/website/global.css +232 -0
- package/templates/frontend-templates/website/templates/website/404.html +10 -0
- package/templates/frontend-templates/website/templates/website/base.html +44 -0
- package/templates/frontend-templates/website/templates/website/blocks/call_to_action.html +5 -0
- package/templates/frontend-templates/website/templates/website/blocks/image.html +4 -0
- package/templates/frontend-templates/website/templates/website/blocks/quote.html +4 -0
- package/templates/frontend-templates/website/templates/website/blocks/text.html +2 -0
- package/templates/frontend-templates/website/templates/website/home.html +8 -0
- package/templates/frontend-templates/website/templates/website/includes/contact_form.html +21 -0
- package/templates/frontend-templates/website/templates/website/includes/post_card.html +9 -0
- package/templates/frontend-templates/website/templates/website/includes/post_list.html +11 -0
- package/templates/frontend-templates/website/templates/website/page.html +10 -0
- package/templates/frontend-templates/website/templates/website/post.html +20 -0
- package/templates/frontend-templates/website/templates/website/post_list.html +11 -0
- package/templates/frontend-templates/website/templates/website/robots.txt +5 -0
- package/templates/frontend-templates/website/templates/website/rss.xml +16 -0
- package/templates/frontend-templates/website/templates/website/sitemap.xml +8 -0
- package/templates/frontend-templates/website/tests/__init__.py +0 -0
- package/templates/frontend-templates/website/tests/test_site.py +102 -0
- package/templates/frontend-templates/website/urls.py +17 -0
- package/templates/frontend-templates/website/views.py +176 -0
package/dist/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as envPorts, A as compose, B as NAMED_PRESETS, C as PACKAGE_NAME, D as RUN_DOMAIN, E as CAPTCHA_APP, F as projectUrls, H as PROMPT_ORDER, I as readLocalRun, J as resolvePreset, K as encodePreset, L as readProject, M as findProjectDirectory, N as parseProfiles, O as RUN_ENV, P as projectStack, Q as describeMove, R as runningServices, S as CLI_VERSION, T as generateSecret, U as STACK_OPTIONS, V as NODE_FRONTENDS, W as applies, X as DEV_PORTS, Y as stackOption, Z as chooseFreePorts, _ as umamiEnv, a as LOCAL_S3, at as isDockerDown, b as validateProjectName, c as localS3, ct as setEnvValues, et as portValues, f as relativeDirectory, g as toProjectName, h as timeZones, it as dockerRunning, j as composeOutput, k as activeProject, l as lockBackend, m as setNotificationsApp, n as productionEnv$1, nt as checkTools, o as cacheEnv, ot as parseEnv, p as scaffold, q as pendingChoices, r as validateDomain, rt as dockerNotRunning, s as frontendPackageManager, st as quoteEnvValue, t as missingS3, tt as runDoctor, u as lockFrontend, v as updateFrontendLock, w as generateProjectSecrets, x as validateTimeZone, y as usesValkey, z as DEFAULT_STACK } from "./production-
|
|
2
|
+
import { $ as envPorts, A as compose, B as NAMED_PRESETS, C as PACKAGE_NAME, D as RUN_DOMAIN, E as CAPTCHA_APP, F as projectUrls, H as PROMPT_ORDER, I as readLocalRun, J as resolvePreset, K as encodePreset, L as readProject, M as findProjectDirectory, N as parseProfiles, O as RUN_ENV, P as projectStack, Q as describeMove, R as runningServices, S as CLI_VERSION, T as generateSecret, U as STACK_OPTIONS, V as NODE_FRONTENDS, W as applies, X as DEV_PORTS, Y as stackOption, Z as chooseFreePorts, _ as umamiEnv, a as LOCAL_S3, at as isDockerDown, b as validateProjectName, c as localS3, ct as setEnvValues, et as portValues, f as relativeDirectory, g as toProjectName, h as timeZones, it as dockerRunning, j as composeOutput, k as activeProject, l as lockBackend, m as setNotificationsApp, n as productionEnv$1, nt as checkTools, o as cacheEnv, ot as parseEnv, p as scaffold, q as pendingChoices, r as validateDomain, rt as dockerNotRunning, s as frontendPackageManager, st as quoteEnvValue, t as missingS3, tt as runDoctor, u as lockFrontend, v as updateFrontendLock, w as generateProjectSecrets, x as validateTimeZone, y as usesValkey, z as DEFAULT_STACK } from "./production-WThC1aS2.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, delimiter, dirname, join, posix, relative, resolve, sep } from "node:path";
|
|
@@ -3930,7 +3930,8 @@ function choices(key, stack = {}) {
|
|
|
3930
3930
|
/**
|
|
3931
3931
|
* The stack from, in order of precedence, the flags, the preset, the prompts
|
|
3932
3932
|
* and the defaults. `askDatabase` asks for the Postgres version together with
|
|
3933
|
-
* the external database choice; it returns the version,
|
|
3933
|
+
* the external database choice; it returns the version, and the URL for
|
|
3934
|
+
* production when there is one.
|
|
3934
3935
|
*/
|
|
3935
3936
|
async function chooseStack(options, interactive, askDatabase) {
|
|
3936
3937
|
let preset;
|
|
@@ -3949,10 +3950,9 @@ async function chooseStack(options, interactive, askDatabase) {
|
|
|
3949
3950
|
else if (!interactive || !applies(key, stack)) continue;
|
|
3950
3951
|
else if (key === "postgres" && askDatabase && !options.databaseUrl) {
|
|
3951
3952
|
const answer = await askDatabase(stack[key]);
|
|
3952
|
-
if (
|
|
3953
|
-
|
|
3954
|
-
} else
|
|
3955
|
-
else {
|
|
3953
|
+
if (answer.databaseUrl) databaseUrl = answer.databaseUrl;
|
|
3954
|
+
stack[key] = answer.postgres;
|
|
3955
|
+
} else {
|
|
3956
3956
|
const initialValue = key === "storage" && stack.media === "s3" ? "s3" : stack[key];
|
|
3957
3957
|
stack[key] = unwrap(await p.select({
|
|
3958
3958
|
message: option.message,
|
|
@@ -4018,19 +4018,31 @@ async function init(directoryArgument, options) {
|
|
|
4018
4018
|
label: `Bundled Postgres, ${value.label.toLowerCase()}`
|
|
4019
4019
|
})), {
|
|
4020
4020
|
value: "external",
|
|
4021
|
-
label: "External Postgres",
|
|
4022
|
-
hint: "a URL to your
|
|
4021
|
+
label: "External Postgres in production",
|
|
4022
|
+
hint: "a URL to your server; development runs the bundled one"
|
|
4023
4023
|
}]
|
|
4024
4024
|
}));
|
|
4025
4025
|
if (choice !== "external") return { postgres: choice };
|
|
4026
|
-
|
|
4027
|
-
message: "Postgres URL",
|
|
4026
|
+
const databaseUrl = unwrap(await p.text({
|
|
4027
|
+
message: "Postgres URL for production",
|
|
4028
4028
|
placeholder: "postgres://user:password@host:5432/name",
|
|
4029
4029
|
validate: (value) => value?.startsWith("postgres") ? void 0 : "Enter a postgres:// URL."
|
|
4030
|
-
}))
|
|
4030
|
+
}));
|
|
4031
|
+
return {
|
|
4032
|
+
postgres: unwrap(await p.select({
|
|
4033
|
+
message: "Its Postgres version",
|
|
4034
|
+
initialValue: initial,
|
|
4035
|
+
options: choices("postgres")
|
|
4036
|
+
})),
|
|
4037
|
+
databaseUrl
|
|
4038
|
+
};
|
|
4031
4039
|
});
|
|
4032
4040
|
const stack = chosen.stack;
|
|
4033
4041
|
const databaseUrl = options.databaseUrl ?? chosen.databaseUrl;
|
|
4042
|
+
if (!applies("analytics", stack) && stack.analytics !== "off") {
|
|
4043
|
+
if (options.fromCli("analytics")) fail("--analytics umami needs a frontend: a headless project has no pages to count.");
|
|
4044
|
+
stack.analytics = "off";
|
|
4045
|
+
}
|
|
4034
4046
|
const pending = pendingChoices(stack);
|
|
4035
4047
|
const unavailable = pending.filter((item) => !item.isDefault);
|
|
4036
4048
|
if (unavailable.length) fail(`${unavailable.map((item) => item.choice).join(", ")} ${unavailable.length > 1 ? "are" : "is"} not available yet. Leave ${unavailable.length > 1 ? "them" : "it"} at the default.`);
|
|
@@ -4066,6 +4078,7 @@ async function init(directoryArgument, options) {
|
|
|
4066
4078
|
frontend: stack.frontend,
|
|
4067
4079
|
demoContent,
|
|
4068
4080
|
databaseUrl,
|
|
4081
|
+
postgres: stack.postgres,
|
|
4069
4082
|
localS3: stack.storage === "s3",
|
|
4070
4083
|
analytics: stack.analytics === "umami",
|
|
4071
4084
|
cache: stack.cache,
|
|
@@ -4127,7 +4140,7 @@ async function init(directoryArgument, options) {
|
|
|
4127
4140
|
command(`cd ${relativeDirectory(target)}`),
|
|
4128
4141
|
command(startCommand),
|
|
4129
4142
|
"",
|
|
4130
|
-
`${pc.bold("Site")} ${url(`http://localhost:${ports.ports.SITE_PORT}`)}`,
|
|
4143
|
+
stack.frontend === "headless" ? `${pc.bold("API")} ${url(`http://localhost:${ports.ports.SITE_PORT}/api/`)}` : `${pc.bold("Site")} ${url(`http://localhost:${ports.ports.SITE_PORT}`)}`,
|
|
4131
4144
|
`${pc.bold("Admin")} ${url(`http://localhost:${ports.ports.SITE_PORT}/admin/`)}`,
|
|
4132
4145
|
`${pc.bold("Mail")} ${url(`http://localhost:${ports.ports.MAIL_PORT}`)}`,
|
|
4133
4146
|
"",
|
|
@@ -4541,6 +4554,7 @@ function applyStack(envText, env, from, to) {
|
|
|
4541
4554
|
if (from[key] === to[key]) continue;
|
|
4542
4555
|
const value = option.values.find((candidate) => candidate.value === to[key]);
|
|
4543
4556
|
if (!value) throw new Error(`${JSON.stringify(to[key])} is not a ${option.key}.`);
|
|
4557
|
+
if (key === "postgres") throw new Error("--postgres can't change on an existing project: a new major version needs a backup and a restore. See the docs on backups.");
|
|
4544
4558
|
if (!SWITCHABLE.includes(key) || !value.generated) throw new Error(`${option.flag} ${to[key]} can't be set on an existing project in this version yet.`);
|
|
4545
4559
|
if (key === "storage") {
|
|
4546
4560
|
if (profiles.includes("prod")) throw new Error("--storage only changes development; this .env runs the prod profile.");
|
|
@@ -4639,7 +4653,8 @@ function ensureRunEnv(project) {
|
|
|
4639
4653
|
if (!existsSync(path)) {
|
|
4640
4654
|
const result = productionEnv$1(readFileSync(join(project.directory, ".env"), "utf8"), {
|
|
4641
4655
|
domain: RUN_DOMAIN,
|
|
4642
|
-
emailUrl: "console://"
|
|
4656
|
+
emailUrl: "console://",
|
|
4657
|
+
bundledDatabase: true
|
|
4643
4658
|
});
|
|
4644
4659
|
writeFileSync(path, result.text, { mode: 384 });
|
|
4645
4660
|
p.log.success(`Wrote ${pc.bold(RUN_ENV)}: the prod profile at ${url(`https://${RUN_DOMAIN}`)}, with its own secrets and data.`);
|
|
@@ -4695,7 +4710,8 @@ async function dev(options) {
|
|
|
4695
4710
|
intro("Develop");
|
|
4696
4711
|
project = await freeDevPorts(project);
|
|
4697
4712
|
urlsNote(project, "Starting");
|
|
4698
|
-
|
|
4713
|
+
const reloads = project.services.frontend ? "backend/ and frontend/" : "backend/";
|
|
4714
|
+
p.log.message(pc.dim(`Changes in ${reloads} reload. Press Ctrl+C to stop.`));
|
|
4699
4715
|
exitWith(compose(project, ["up", ...options.build ? ["--build"] : []]));
|
|
4700
4716
|
}
|
|
4701
4717
|
/** `dstack stop`: stop the containers of the project and of its local run; volumes and data stay. */
|
|
@@ -4844,6 +4860,10 @@ async function types(options) {
|
|
|
4844
4860
|
"--file",
|
|
4845
4861
|
"openapi.json"
|
|
4846
4862
|
], false)) !== 0) fail("The backend could not write its schema.");
|
|
4863
|
+
if (!project.services.frontend) {
|
|
4864
|
+
p.outro(`Wrote ${pc.bold("backend/openapi.json")}`);
|
|
4865
|
+
return;
|
|
4866
|
+
}
|
|
4847
4867
|
const generate = [
|
|
4848
4868
|
"npx",
|
|
4849
4869
|
"headless",
|
|
@@ -4931,7 +4951,7 @@ function checkSteps(project, tests) {
|
|
|
4931
4951
|
],
|
|
4932
4952
|
devOnly: true
|
|
4933
4953
|
});
|
|
4934
|
-
return steps.filter((step) => !(step.devOnly && project.prod));
|
|
4954
|
+
return steps.filter((step) => !(step.devOnly && project.prod) && (step.service !== "frontend" || project.services.frontend));
|
|
4935
4955
|
}
|
|
4936
4956
|
/** `dstack check`: the doctor's checks, Django's checks, the linters and, with --tests, the tests. */
|
|
4937
4957
|
async function check(options) {
|
|
@@ -5029,8 +5049,8 @@ async function update(options) {
|
|
|
5029
5049
|
];
|
|
5030
5050
|
if (compose(target, build) !== 0) fail("Building the images failed.");
|
|
5031
5051
|
}
|
|
5032
|
-
const volume = project.prod ? void 0 : nodeModulesVolume(project);
|
|
5033
|
-
if (volume) {
|
|
5052
|
+
const volume = project.prod || !project.services.frontend ? void 0 : nodeModulesVolume(project);
|
|
5053
|
+
if (volume && project.services.frontend) {
|
|
5034
5054
|
compose(project, [
|
|
5035
5055
|
"rm",
|
|
5036
5056
|
"--stop",
|
|
@@ -5313,7 +5333,7 @@ function createProgram() {
|
|
|
5313
5333
|
styleArgumentText: (text) => pc.yellow(text),
|
|
5314
5334
|
styleDescriptionText: (text) => text
|
|
5315
5335
|
}).configureOutput({ outputError: (text, write) => write(pc.red(text)) });
|
|
5316
|
-
const initCommand = program.command("init").alias("create").description("Create a project").argument("[directory]", "Where to create the project").option("--name <name>", "The project name (default: from the directory)").option("--preset <preset>", `A preset code from a previous init, or ${Object.keys(NAMED_PRESETS).join(", ")}`).option("--database-url <url>", "
|
|
5336
|
+
const initCommand = program.command("init").alias("create").description("Create a project").argument("[directory]", "Where to create the project").option("--name <name>", "The project name (default: from the directory)").option("--preset <preset>", `A preset code from a previous init, or ${Object.keys(NAMED_PRESETS).join(", ")}`).option("--database-url <url>", "An external Postgres for production; development runs the bundled one").option("--time-zone <zone>", "The time zone of the backend, such as Europe/Amsterdam (default: UTC)").addOption(new Option("--local-s3", "Same as --storage s3").hideHelp()).option("--no-demo-content", "Start with an empty site").option("--no-git", "Don't create a git repository").option("--start", "Run `docker compose up` when done").option("-y, --yes", "Accept the defaults without asking").addOption(new Option("--link <repository>", "Take the packages from a DjangoStack checkout").hideHelp()).action((directory, options, cmd) => init(directory, {
|
|
5317
5337
|
...options,
|
|
5318
5338
|
installPrompt: program.opts().installPrompt,
|
|
5319
5339
|
fromCli: (option) => cmd.getOptionValueSource(option) === "cli"
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ interface ProductionOptions {
|
|
|
20
20
|
emailUrl?: string;
|
|
21
21
|
/** Where uploads go: `s3` asks for the S3 settings (the `media` stack option). */
|
|
22
22
|
media?: string;
|
|
23
|
+
/** Keep the bundled database even with PRODUCTION_DATABASE_URL: the local run on this machine. */
|
|
24
|
+
bundledDatabase?: boolean;
|
|
23
25
|
}
|
|
24
26
|
interface ProductionEnv {
|
|
25
27
|
text: string;
|
|
@@ -54,12 +56,12 @@ export declare const STACK_OPTIONS: readonly [{
|
|
|
54
56
|
readonly value: "headless";
|
|
55
57
|
readonly label: "Headless";
|
|
56
58
|
readonly hint: "just the API and the admin";
|
|
57
|
-
readonly generated:
|
|
59
|
+
readonly generated: true;
|
|
58
60
|
}, {
|
|
59
61
|
readonly value: "templates";
|
|
60
62
|
readonly label: "Django templates";
|
|
61
63
|
readonly hint: "no Node, Django renders the site";
|
|
62
|
-
readonly generated:
|
|
64
|
+
readonly generated: true;
|
|
63
65
|
}, {
|
|
64
66
|
readonly value: "nuxt";
|
|
65
67
|
readonly label: "Nuxt";
|
|
@@ -86,7 +88,7 @@ export declare const STACK_OPTIONS: readonly [{
|
|
|
86
88
|
readonly value: "latest";
|
|
87
89
|
readonly label: "Latest";
|
|
88
90
|
readonly hint: "Postgres 18";
|
|
89
|
-
readonly generated:
|
|
91
|
+
readonly generated: true;
|
|
90
92
|
}];
|
|
91
93
|
}, {
|
|
92
94
|
readonly key: "analytics";
|
|
@@ -290,8 +292,13 @@ interface ScaffoldOptions {
|
|
|
290
292
|
frontend: Frontend;
|
|
291
293
|
/** Demo pages and posts on the first start. */
|
|
292
294
|
demoContent: boolean;
|
|
293
|
-
/**
|
|
295
|
+
/**
|
|
296
|
+
* An external Postgres URL for production, saved as PRODUCTION_DATABASE_URL;
|
|
297
|
+
* development runs the bundled Postgres either way.
|
|
298
|
+
*/
|
|
294
299
|
databaseUrl?: string;
|
|
300
|
+
/** The bundled Postgres's version, and the backend's Postgres client; stable by default. */
|
|
301
|
+
postgres?: Stack["postgres"];
|
|
295
302
|
/** Store uploads in the local S3 service in development, like S3 in production. */
|
|
296
303
|
localS3?: boolean;
|
|
297
304
|
/** Page analytics with the bundled Umami (the `analytics` profile). */
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { z as DEFAULT_STACK, i as FRONTENDS, B as NAMED_PRESETS, U as STACK_OPTIONS, S as VERSION, o as cacheEnv, G as decodePreset, K as encodePreset, w as generateProjectSecrets, T as generateSecret, d as normalizeDatabaseUrl, ot as parseEnv, q as pendingChoices, n as productionEnv, J as resolvePreset, tt as runDoctor, p as scaffold, ct as setEnvValues, m as setNotificationsApp, g as toProjectName, y as usesValkey, r as validateDomain, b as validateProjectName } from "./production-
|
|
1
|
+
export { z as DEFAULT_STACK, i as FRONTENDS, B as NAMED_PRESETS, U as STACK_OPTIONS, S as VERSION, o as cacheEnv, G as decodePreset, K as encodePreset, w as generateProjectSecrets, T as generateSecret, d as normalizeDatabaseUrl, ot as parseEnv, q as pendingChoices, n as productionEnv, J as resolvePreset, tt as runDoctor, p as scaffold, ct as setEnvValues, m as setNotificationsApp, g as toProjectName, y as usesValkey, r as validateDomain, b as validateProjectName } from "./production-WThC1aS2.js";
|
|
@@ -258,9 +258,10 @@ async function runDoctor(directory) {
|
|
|
258
258
|
const services = running ? running.split("\n").filter(Boolean) : [];
|
|
259
259
|
const backendService = prod ? "backend-prod" : "backend";
|
|
260
260
|
const frontendService = prod ? "frontend-prod" : "frontend";
|
|
261
|
+
const frontend = existsSync(join(directory, "frontend", "package.json"));
|
|
261
262
|
checks.push({
|
|
262
263
|
name: "Services are running",
|
|
263
|
-
ok: services.includes(backendService) && services.includes(frontendService),
|
|
264
|
+
ok: services.includes(backendService) && (!frontend || services.includes(frontendService)),
|
|
264
265
|
warning: services.length === 0,
|
|
265
266
|
detail: services.length ? services.join(", ") : `Nothing is running. Start the project with \`docker compose up${prod ? " -d" : ""}\`.`
|
|
266
267
|
});
|
|
@@ -278,11 +279,12 @@ async function runDoctor(directory) {
|
|
|
278
279
|
detail: health ? `The ${backendService} container is ${health}` : `See \`docker compose logs ${backendService}\`.`
|
|
279
280
|
});
|
|
280
281
|
const site = (env.SITE_URL ?? "http://localhost:3000").replace(/\/$/, "");
|
|
281
|
-
const
|
|
282
|
+
const status = await fetchStatus(site);
|
|
283
|
+
const logs = prod ? "proxy" : frontend ? frontendService : backendService;
|
|
282
284
|
checks.push({
|
|
283
285
|
name: "The site answers",
|
|
284
|
-
ok:
|
|
285
|
-
detail:
|
|
286
|
+
ok: status !== void 0 && status < 500,
|
|
287
|
+
detail: status ? `${site} answered ${status}` : `No answer on ${site}. See \`docker compose logs ${logs}\`.`
|
|
286
288
|
});
|
|
287
289
|
const api = await fetchStatus(`${site}/api/content.sitesettings`, { "X-Secret-Key": env.API_READ_KEY ?? "" });
|
|
288
290
|
checks.push({
|
|
@@ -461,13 +463,13 @@ const STACK_OPTIONS = [
|
|
|
461
463
|
value: "headless",
|
|
462
464
|
label: "Headless",
|
|
463
465
|
hint: "just the API and the admin",
|
|
464
|
-
generated:
|
|
466
|
+
generated: true
|
|
465
467
|
},
|
|
466
468
|
{
|
|
467
469
|
value: "templates",
|
|
468
470
|
label: "Django templates",
|
|
469
471
|
hint: "no Node, Django renders the site",
|
|
470
|
-
generated:
|
|
472
|
+
generated: true
|
|
471
473
|
},
|
|
472
474
|
{
|
|
473
475
|
value: "nuxt",
|
|
@@ -498,7 +500,7 @@ const STACK_OPTIONS = [
|
|
|
498
500
|
value: "latest",
|
|
499
501
|
label: "Latest",
|
|
500
502
|
hint: "Postgres 18",
|
|
501
|
-
generated:
|
|
503
|
+
generated: true
|
|
502
504
|
}]
|
|
503
505
|
},
|
|
504
506
|
{
|
|
@@ -726,9 +728,13 @@ const NODE_FRONTENDS = [
|
|
|
726
728
|
"nuxt",
|
|
727
729
|
"sveltekit"
|
|
728
730
|
];
|
|
729
|
-
/**
|
|
731
|
+
/**
|
|
732
|
+
* Whether an option applies to a stack: the package manager only with a Node
|
|
733
|
+
* frontend, page analytics only with a frontend for its tracker.
|
|
734
|
+
*/
|
|
730
735
|
function applies(key, stack) {
|
|
731
736
|
if (key === "packageManager") return stack.frontend === void 0 || NODE_FRONTENDS.includes(stack.frontend);
|
|
737
|
+
if (key === "analytics") return stack.frontend !== "headless";
|
|
732
738
|
return true;
|
|
733
739
|
}
|
|
734
740
|
const DIGITS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
@@ -916,6 +922,10 @@ function projectName(directory) {
|
|
|
916
922
|
} catch {}
|
|
917
923
|
return basename(directory).toLowerCase();
|
|
918
924
|
}
|
|
925
|
+
/** Whether the project has a frontend: a headless one has no `frontend/`. */
|
|
926
|
+
function hasFrontend(directory) {
|
|
927
|
+
return existsSync(join(directory, "frontend", "package.json"));
|
|
928
|
+
}
|
|
919
929
|
function fromEnv(directory, name, envFile, localRun) {
|
|
920
930
|
const env = parseEnv(readFileSync(join(directory, envFile), "utf8"));
|
|
921
931
|
const profiles = parseProfiles(env.COMPOSE_PROFILES);
|
|
@@ -930,7 +940,7 @@ function fromEnv(directory, name, envFile, localRun) {
|
|
|
930
940
|
localRun,
|
|
931
941
|
services: {
|
|
932
942
|
backend: `backend${suffix}`,
|
|
933
|
-
frontend: `frontend${suffix}
|
|
943
|
+
...hasFrontend(directory) ? { frontend: `frontend${suffix}` } : {},
|
|
934
944
|
worker: `worker${suffix}`
|
|
935
945
|
},
|
|
936
946
|
composeArgs: localRun ? [
|
|
@@ -1161,6 +1171,23 @@ const PACKAGE_NAME = "@django-stack/cli";
|
|
|
1161
1171
|
// src/scaffold.ts
|
|
1162
1172
|
/** The frontends this version of the CLI generates. */
|
|
1163
1173
|
const FRONTENDS = stackOption("frontend").values.filter((value) => value.generated).map((value) => value.value);
|
|
1174
|
+
/** The Postgres major version of each `--postgres` value. */
|
|
1175
|
+
const POSTGRES_VERSIONS = {
|
|
1176
|
+
stable: "17",
|
|
1177
|
+
latest: "18"
|
|
1178
|
+
};
|
|
1179
|
+
/**
|
|
1180
|
+
* The template values of a Postgres version. From 18 the image keeps its data
|
|
1181
|
+
* in a versioned directory under /var/lib/postgresql, so the volume is
|
|
1182
|
+
* mounted there; before 18 at /var/lib/postgresql/data.
|
|
1183
|
+
*/
|
|
1184
|
+
function postgresValues(postgres) {
|
|
1185
|
+
const version = POSTGRES_VERSIONS[postgres];
|
|
1186
|
+
return {
|
|
1187
|
+
postgres_version: version,
|
|
1188
|
+
postgres_data: Number(version) >= 18 ? "/var/lib/postgresql" : "/var/lib/postgresql/data"
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1164
1191
|
/** The pnpm and Bun versions the frontend image installs with. */
|
|
1165
1192
|
const PNPM_VERSION = "12.18.0";
|
|
1166
1193
|
const BUN_VERSION = "1.4.4";
|
|
@@ -1289,7 +1316,7 @@ function localS3(port = 8333) {
|
|
|
1289
1316
|
};
|
|
1290
1317
|
}
|
|
1291
1318
|
const LOCAL_S3 = localS3();
|
|
1292
|
-
const TEXT_EXTENSIONS = /\.(py|ts|tsx|mjs|js|jsx|json|astro|vue|svelte|css|md|yaml|yml|toml|txt|html|sh|svg)$|Dockerfile$|gitignore$|\.dockerignore$|env\.example$/;
|
|
1319
|
+
const TEXT_EXTENSIONS = /\.(py|ts|tsx|mjs|js|jsx|json|astro|vue|svelte|css|md|yaml|yml|toml|txt|html|sh|svg)$|Dockerfile$|Caddyfile$|gitignore$|\.dockerignore$|env\.example$/;
|
|
1293
1320
|
function defaultTemplatesDir() {
|
|
1294
1321
|
return fileURLToPath(new URL("../templates", import.meta.url));
|
|
1295
1322
|
}
|
|
@@ -1323,6 +1350,35 @@ function walk(directory) {
|
|
|
1323
1350
|
return statSync(path).isDirectory() ? walk(path) : [path];
|
|
1324
1351
|
});
|
|
1325
1352
|
}
|
|
1353
|
+
const BLOCK_START = /^\s*(?:#|<!--)\s*@if\s+(!?)([a-z]+)\s*(?:-->)?\s*$/;
|
|
1354
|
+
const BLOCK_END = /^\s*(?:#|<!--)\s*@end\s*(?:-->)?\s*$/;
|
|
1355
|
+
/**
|
|
1356
|
+
* Keeps or drops the template's conditional blocks: the lines between a
|
|
1357
|
+
* comment line `# @if frontend` (or `<!-- @if !frontend -->`) and `# @end`
|
|
1358
|
+
* stay when the flag is on (off with `!`). The marker lines always go.
|
|
1359
|
+
*/
|
|
1360
|
+
function selectBlocks(text, flags) {
|
|
1361
|
+
if (!/@(if|end)\b/.test(text)) return text;
|
|
1362
|
+
const kept = [];
|
|
1363
|
+
const open = [];
|
|
1364
|
+
for (const line of text.split("\n")) {
|
|
1365
|
+
const start = line.match(BLOCK_START);
|
|
1366
|
+
if (start) {
|
|
1367
|
+
const flag = flags[start[2]];
|
|
1368
|
+
if (flag === void 0) throw new Error(`Unknown template flag ${start[2]}.`);
|
|
1369
|
+
open.push(start[1] ? !flag : flag);
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
if (BLOCK_END.test(line)) {
|
|
1373
|
+
if (!open.length) throw new Error("@end without @if.");
|
|
1374
|
+
open.pop();
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
if (open.every(Boolean)) kept.push(line);
|
|
1378
|
+
}
|
|
1379
|
+
if (open.length) throw new Error("@if without @end.");
|
|
1380
|
+
return kept.join("\n");
|
|
1381
|
+
}
|
|
1326
1382
|
function render(text, values) {
|
|
1327
1383
|
return text.replace(/\{\{([a-z_]+)\}\}/g, (match, key) => values[key] ?? match);
|
|
1328
1384
|
}
|
|
@@ -1334,6 +1390,16 @@ function validateTimeZone(value) {
|
|
|
1334
1390
|
if (timeZones().includes(value)) return void 0;
|
|
1335
1391
|
return `${JSON.stringify(value)} is not a time zone. Use a name such as Europe/Amsterdam or UTC.`;
|
|
1336
1392
|
}
|
|
1393
|
+
/** The root package.json of a headless project: the scripts without the frontend's steps. */
|
|
1394
|
+
function headlessScripts(text) {
|
|
1395
|
+
const manifest = JSON.parse(text);
|
|
1396
|
+
const scripts = manifest.scripts;
|
|
1397
|
+
scripts.test = "docker compose exec backend pytest";
|
|
1398
|
+
scripts.lint = "docker compose exec backend ruff check .";
|
|
1399
|
+
scripts["generate:types"] = (scripts["generate:types"] ?? "").split(" && ")[0] ?? "";
|
|
1400
|
+
manifest.description = manifest.description.replace(", Content Studio and Headless,", " and Content Studio,");
|
|
1401
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
1402
|
+
}
|
|
1337
1403
|
function scaffold(options) {
|
|
1338
1404
|
const templates = options.templatesDir ?? defaultTemplatesDir();
|
|
1339
1405
|
const target = options.directory;
|
|
@@ -1344,17 +1410,28 @@ function scaffold(options) {
|
|
|
1344
1410
|
const zoneError = validateTimeZone(options.timeZone);
|
|
1345
1411
|
if (zoneError) throw new Error(zoneError);
|
|
1346
1412
|
}
|
|
1413
|
+
const headless = options.frontend === "headless";
|
|
1414
|
+
const djangoTemplates = options.frontend === "templates";
|
|
1415
|
+
const jsFrontend = !headless && !djangoTemplates;
|
|
1347
1416
|
const frontendTemplate = join(templates, `frontend-${options.frontend}`);
|
|
1348
|
-
if (!existsSync(frontendTemplate)) throw new Error(`This version of the CLI has no ${options.frontend} frontend.`);
|
|
1417
|
+
if (!headless && !existsSync(frontendTemplate)) throw new Error(`This version of the CLI has no ${options.frontend} frontend.`);
|
|
1349
1418
|
mkdirSync(target, { recursive: true });
|
|
1350
1419
|
cpSync(join(templates, "base"), target, { recursive: true });
|
|
1351
1420
|
cpSync(join(templates, "backend"), join(target, "backend"), { recursive: true });
|
|
1352
|
-
|
|
1421
|
+
const frontendDir = join(target, djangoTemplates ? "backend" : "frontend");
|
|
1422
|
+
if (!headless) cpSync(frontendTemplate, frontendDir, { recursive: true });
|
|
1353
1423
|
renameSync(join(target, "gitignore"), join(target, ".gitignore"));
|
|
1354
1424
|
makeExecutable(target);
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1425
|
+
let frontend = {
|
|
1426
|
+
label: "Headless",
|
|
1427
|
+
url: "",
|
|
1428
|
+
security: ""
|
|
1429
|
+
};
|
|
1430
|
+
if (!headless) {
|
|
1431
|
+
const about = join(frontendDir, "template.json");
|
|
1432
|
+
frontend = JSON.parse(readFileSync(about, "utf8"));
|
|
1433
|
+
rmSync(about);
|
|
1434
|
+
}
|
|
1358
1435
|
const packageManager = options.packageManager ?? "pnpm";
|
|
1359
1436
|
const values = {
|
|
1360
1437
|
project_name: options.name,
|
|
@@ -1366,17 +1443,24 @@ function scaffold(options) {
|
|
|
1366
1443
|
preset: options.preset ?? encodePreset(DEFAULT_STACK),
|
|
1367
1444
|
site_port: String(options.ports?.SITE_PORT ?? 3e3),
|
|
1368
1445
|
mail_port: String(options.ports?.MAIL_PORT ?? 8025),
|
|
1369
|
-
...packageManagerValues(packageManager)
|
|
1446
|
+
...packageManagerValues(packageManager),
|
|
1447
|
+
...postgresValues(options.postgres ?? DEFAULT_STACK.postgres)
|
|
1370
1448
|
};
|
|
1371
1449
|
const files = walk(target);
|
|
1372
1450
|
for (const file of files) {
|
|
1373
1451
|
if (!TEXT_EXTENSIONS.test(file)) continue;
|
|
1374
1452
|
const text = readFileSync(file, "utf8");
|
|
1375
|
-
const rendered = render(text,
|
|
1453
|
+
const rendered = render(selectBlocks(text, {
|
|
1454
|
+
frontend: jsFrontend,
|
|
1455
|
+
headless,
|
|
1456
|
+
templates: djangoTemplates,
|
|
1457
|
+
site: !headless
|
|
1458
|
+
}), values);
|
|
1376
1459
|
if (rendered !== text) writeFileSync(file, rendered);
|
|
1377
1460
|
}
|
|
1461
|
+
if (!jsFrontend) writeFileSync(join(target, "package.json"), headlessScripts(readFileSync(join(target, "package.json"), "utf8")));
|
|
1378
1462
|
if (packageManager !== "pnpm") rmSync(join(target, "frontend", "pnpm-workspace.yaml"), { force: true });
|
|
1379
|
-
if (packageManager !== "npm") {
|
|
1463
|
+
if (jsFrontend && packageManager !== "npm") {
|
|
1380
1464
|
const packageJson = join(target, "frontend", "package.json");
|
|
1381
1465
|
const manifest = JSON.parse(readFileSync(packageJson, "utf8"));
|
|
1382
1466
|
const version = packageManager === "pnpm" ? PNPM_VERSION : BUN_VERSION;
|
|
@@ -1398,14 +1482,13 @@ function scaffold(options) {
|
|
|
1398
1482
|
const secrets = generateProjectSecrets();
|
|
1399
1483
|
let env = template.replace(/\{\{secret:([a-z_]+)\}\}/g, (match, key) => secrets[key] ?? match);
|
|
1400
1484
|
const envExample = template.replace(/\{\{secret:[a-z_]+\}\}/g, "");
|
|
1401
|
-
const profiles = ["dev"];
|
|
1402
|
-
if (options.databaseUrl) env = setEnvValues(env, {
|
|
1403
|
-
else profiles.push("db");
|
|
1485
|
+
const profiles = ["dev", "db"];
|
|
1486
|
+
if (options.databaseUrl) env = setEnvValues(env, { PRODUCTION_DATABASE_URL: normalizeDatabaseUrl(options.databaseUrl) });
|
|
1404
1487
|
if (options.localS3) {
|
|
1405
1488
|
profiles.push("s3");
|
|
1406
1489
|
env = setEnvValues(env, LOCAL_S3);
|
|
1407
1490
|
}
|
|
1408
|
-
if (options.analytics) {
|
|
1491
|
+
if (options.analytics && !headless) {
|
|
1409
1492
|
profiles.push("analytics");
|
|
1410
1493
|
env = setEnvValues(env, umamiEnv(parseEnv(env).DATABASE_URL ?? "", secrets));
|
|
1411
1494
|
}
|
|
@@ -1456,6 +1539,10 @@ function linkPackages(target, monorepo) {
|
|
|
1456
1539
|
const pyproject = join(target, "backend", "pyproject.toml");
|
|
1457
1540
|
writeFileSync(pyproject, `${readFileSync(pyproject, "utf8").trimEnd()}\n\n[tool.uv.sources]\ndjango-stack = { path = "vendor/${wheel}" }\n`);
|
|
1458
1541
|
const packageJson = join(target, "frontend", "package.json");
|
|
1542
|
+
if (!existsSync(packageJson)) {
|
|
1543
|
+
rmSync(frontendVendor, { recursive: true });
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1459
1546
|
const manifest = JSON.parse(readFileSync(packageJson, "utf8"));
|
|
1460
1547
|
const packages = Object.keys(manifest.dependencies ?? {}).filter((name) => name.startsWith("@django-stack/"));
|
|
1461
1548
|
for (const name of packages) {
|
|
@@ -1607,7 +1694,12 @@ function productionEnv(devEnv, options) {
|
|
|
1607
1694
|
ADMIN_PASSWORD: secrets.admin ?? "",
|
|
1608
1695
|
DEFAULT_FROM_EMAIL: `${current.PROJECT_NAME ?? "site"} <noreply@${options.domain.toLowerCase()}>`
|
|
1609
1696
|
};
|
|
1610
|
-
|
|
1697
|
+
const external = options.bundledDatabase ? "" : current.PRODUCTION_DATABASE_URL ?? "";
|
|
1698
|
+
if (external) {
|
|
1699
|
+
values.DATABASE_URL = external;
|
|
1700
|
+
values.POSTGRES_PASSWORD = "";
|
|
1701
|
+
values.COMPOSE_PROFILES = [...new Set(profiles.filter((profile) => profile !== "db"))].join(",");
|
|
1702
|
+
} else if (profiles.includes("db")) {
|
|
1611
1703
|
const user = current.POSTGRES_USER || "app";
|
|
1612
1704
|
const name = current.POSTGRES_DB || "app";
|
|
1613
1705
|
values.POSTGRES_PASSWORD = secrets.postgres ?? "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@django-stack/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "The production-ready Django, batteries included stack. Create and manage DjangoStack projects: a Django backend, a frontend, Postgres and Docker Compose in one command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Leon van der Grient",
|
|
@@ -10,9 +10,14 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
|
10
10
|
UV_PROJECT_ENVIRONMENT=/venv \
|
|
11
11
|
PATH="/venv/bin:$PATH"
|
|
12
12
|
COPY --from=ghcr.io/astral-sh/uv:0.11 /uv /bin/uv
|
|
13
|
-
# The Postgres client, for the backups (pg_dump and pg_restore)
|
|
13
|
+
# The Postgres client, for the backups (pg_dump and pg_restore), from the
|
|
14
|
+
# PostgreSQL apt repository: its major version must match the server's, as
|
|
15
|
+
# pg_dump refuses a newer server. Change it with the database.
|
|
16
|
+
ARG POSTGRES_VERSION={{postgres_version}}
|
|
14
17
|
RUN apt-get update \
|
|
15
|
-
&& apt-get install --yes --no-install-recommends postgresql-
|
|
18
|
+
&& apt-get install --yes --no-install-recommends postgresql-common \
|
|
19
|
+
&& /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y \
|
|
20
|
+
&& apt-get install --yes --no-install-recommends "postgresql-client-${POSTGRES_VERSION}" \
|
|
16
21
|
&& rm -rf /var/lib/apt/lists/*
|
|
17
22
|
RUN groupadd --system app && useradd --system --gid app --home-dir /app app
|
|
18
23
|
WORKDIR /app
|
|
@@ -42,6 +42,10 @@ INSTALLED_APPS = [
|
|
|
42
42
|
"django_stack",
|
|
43
43
|
"django_stack.contrib.captcha", # the captcha of the contact form (CAPTCHA=off turns it off)
|
|
44
44
|
"content",
|
|
45
|
+
# @if templates
|
|
46
|
+
"django_stack.contrib.site", # preview, the page cache, redirects and the template tags of the site
|
|
47
|
+
"website", # the site's pages
|
|
48
|
+
# @end
|
|
45
49
|
# Last: builds the API routes once every model is exposed.
|
|
46
50
|
"headless.rest",
|
|
47
51
|
]
|
|
@@ -56,6 +60,10 @@ MIDDLEWARE = [
|
|
|
56
60
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
57
61
|
"django.contrib.messages.middleware.MessageMiddleware",
|
|
58
62
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
63
|
+
# @if templates
|
|
64
|
+
# The redirects editors manage in the admin, for paths that do not exist.
|
|
65
|
+
"django_stack.contrib.site.middleware.RedirectMiddleware",
|
|
66
|
+
# @end
|
|
59
67
|
]
|
|
60
68
|
|
|
61
69
|
ROOT_URLCONF = "config.urls"
|
|
@@ -72,6 +80,10 @@ TEMPLATES = [
|
|
|
72
80
|
"django.template.context_processors.request",
|
|
73
81
|
"django.contrib.auth.context_processors.auth",
|
|
74
82
|
"django.contrib.messages.context_processors.messages",
|
|
83
|
+
# @if templates
|
|
84
|
+
"django_stack.contrib.site.context_processors.site", # preview, analytics_website_id
|
|
85
|
+
"website.context_processors.site", # site, main_menu, footer_menu
|
|
86
|
+
# @end
|
|
75
87
|
],
|
|
76
88
|
},
|
|
77
89
|
},
|
|
@@ -6,4 +6,15 @@ urlpatterns = [
|
|
|
6
6
|
path("api/", include("headless.rest.urls")),
|
|
7
7
|
path("", include("django_stack.urls")), # /healthz and /readyz
|
|
8
8
|
*media_urlpatterns(), # uploaded files, in development only
|
|
9
|
+
# @if headless
|
|
10
|
+
path("", include("django_stack.start")), # a start page with links to the admin and the API schema
|
|
11
|
+
# @end
|
|
12
|
+
# @if templates
|
|
13
|
+
path("", include("website.urls")), # the site's pages, last: they include a catch-all for page paths
|
|
14
|
+
# @end
|
|
9
15
|
]
|
|
16
|
+
# @if templates
|
|
17
|
+
|
|
18
|
+
# The site's 404 page, with its layout (when DJANGO_DEBUG is off).
|
|
19
|
+
handler404 = "website.views.not_found"
|
|
20
|
+
# @end
|
|
@@ -7,6 +7,7 @@ from content_studio import register
|
|
|
7
7
|
from content_studio.admin import ModelStudio
|
|
8
8
|
from content_studio.analytics import Analytics
|
|
9
9
|
from content_studio.form import ActivityLog, Field, FormSet
|
|
10
|
+
from django_stack.analytics import UmamiProvider
|
|
10
11
|
from django_stack.studio import PreviewButton
|
|
11
12
|
|
|
12
13
|
from .models import Author, Category, FormSubmission, Menu, Page, Post, Redirect, SiteSettings, Tag
|
|
@@ -17,9 +18,17 @@ POST_PREVIEW = PreviewButton(component_id="8d0f6a52-3c51-4a8e-9a2b-6f1f0c8f2a02"
|
|
|
17
18
|
PAGE_ACTIVITY = ActivityLog(component_id="8d0f6a52-3c51-4a8e-9a2b-6f1f0c8f2a03")
|
|
18
19
|
POST_ACTIVITY = ActivityLog(component_id="8d0f6a52-3c51-4a8e-9a2b-6f1f0c8f2a04")
|
|
19
20
|
# The visitors of the page or post, from the analytics (see config/studio.py).
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
# Only with analytics on: Content Studio refuses them without its
|
|
22
|
+
# AnalyticsExtension.
|
|
23
|
+
ANALYTICS = UmamiProvider.is_configured()
|
|
24
|
+
PAGE_ANALYTICS = (
|
|
25
|
+
[Analytics(path_field="path", component_id="8d0f6a52-3c51-4a8e-9a2b-6f1f0c8f2a05")] if ANALYTICS else []
|
|
26
|
+
)
|
|
27
|
+
POST_ANALYTICS = (
|
|
28
|
+
[Analytics(path_field="slug", path_prefix="/blog/", component_id="8d0f6a52-3c51-4a8e-9a2b-6f1f0c8f2a06")]
|
|
29
|
+
if ANALYTICS
|
|
30
|
+
else []
|
|
31
|
+
)
|
|
23
32
|
SEO = FormSet(
|
|
24
33
|
title="Search and sharing",
|
|
25
34
|
description="Leave empty to use the title and the site settings.",
|
|
@@ -42,7 +51,7 @@ class PageStudio(ModelStudio):
|
|
|
42
51
|
FormSet(title="Body", fields=["body"]),
|
|
43
52
|
SEO,
|
|
44
53
|
]
|
|
45
|
-
edit_sidebar = ["status", "published_at", PAGE_PREVIEW, "order", PAGE_ANALYTICS, PAGE_ACTIVITY]
|
|
54
|
+
edit_sidebar = ["status", "published_at", PAGE_PREVIEW, "order", *PAGE_ANALYTICS, PAGE_ACTIVITY]
|
|
46
55
|
|
|
47
56
|
|
|
48
57
|
@register(Post)
|
|
@@ -67,7 +76,7 @@ class PostStudio(ModelStudio):
|
|
|
67
76
|
"author",
|
|
68
77
|
"categories",
|
|
69
78
|
"tags",
|
|
70
|
-
POST_ANALYTICS,
|
|
79
|
+
*POST_ANALYTICS,
|
|
71
80
|
POST_ACTIVITY,
|
|
72
81
|
]
|
|
73
82
|
|
|
@@ -5,7 +5,7 @@ description = "The Django backend of {{project_name}}."
|
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"Django>=6.1,<6.2",
|
|
8
|
-
"django-stack[server,s3]>=0.
|
|
8
|
+
"django-stack[server,s3]>=0.11.0",
|
|
9
9
|
"django-headless[schema]>=1.1.1",
|
|
10
10
|
"django-content-studio[images,huey]>=1.11.0",
|
|
11
11
|
]
|
|
@@ -24,7 +24,9 @@ permissions:
|
|
|
24
24
|
|
|
25
25
|
env:
|
|
26
26
|
BACKEND_IMAGE: ghcr.io/${{ github.repository }}-backend:${{ github.sha }}
|
|
27
|
+
# @if frontend
|
|
27
28
|
FRONTEND_IMAGE: ghcr.io/${{ github.repository }}-frontend:${{ github.sha }}
|
|
29
|
+
# @end
|
|
28
30
|
|
|
29
31
|
jobs:
|
|
30
32
|
deploy:
|
|
@@ -38,9 +40,11 @@ jobs:
|
|
|
38
40
|
- name: Build and push the images
|
|
39
41
|
run: |
|
|
40
42
|
docker build --target prod --tag "${BACKEND_IMAGE,,}" backend
|
|
41
|
-
docker build --target prod --tag "${FRONTEND_IMAGE,,}" frontend
|
|
42
43
|
docker push "${BACKEND_IMAGE,,}"
|
|
44
|
+
# @if frontend
|
|
45
|
+
docker build --target prod --tag "${FRONTEND_IMAGE,,}" frontend
|
|
43
46
|
docker push "${FRONTEND_IMAGE,,}"
|
|
47
|
+
# @end
|
|
44
48
|
|
|
45
49
|
- name: Set up SSH
|
|
46
50
|
run: |
|
|
@@ -63,7 +67,12 @@ jobs:
|
|
|
63
67
|
set -e
|
|
64
68
|
cd '$DEPLOY_PATH'
|
|
65
69
|
echo '$REGISTRY_TOKEN' | docker login ghcr.io --username '${{ github.actor }}' --password-stdin
|
|
70
|
+
# @if frontend
|
|
66
71
|
export BACKEND_IMAGE='${BACKEND_IMAGE,,}' FRONTEND_IMAGE='${FRONTEND_IMAGE,,}'
|
|
72
|
+
# @end
|
|
73
|
+
# @if !frontend
|
|
74
|
+
export BACKEND_IMAGE='${BACKEND_IMAGE,,}'
|
|
75
|
+
# @end
|
|
67
76
|
docker compose pull --quiet --ignore-buildable
|
|
68
77
|
docker compose up -d --no-build --remove-orphans
|
|
69
78
|
docker logout ghcr.io
|