@msn-control/liftoff 0.6.2 → 0.8.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.
Files changed (111) hide show
  1. package/README.md +15 -1
  2. package/assets/governance/single-maintainer-gitflow/policy.md +705 -0
  3. package/assets/locks/frontend/package-lock.json +1012 -1694
  4. package/assets/locks/frontend/package.json +5 -6
  5. package/assets/locks/go-backend/go.mod +17 -0
  6. package/assets/locks/go-backend/go.sum +159 -0
  7. package/assets/locks/node-backend/package-lock.json +1473 -2320
  8. package/assets/locks/node-backend/package.json +8 -7
  9. package/assets/locks/opentofu-azure/.terraform.lock.hcl +39 -0
  10. package/assets/locks/opentofu-azure/versions.tf +10 -0
  11. package/assets/locks/python-genai/function-requirements.txt +1734 -0
  12. package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/PKG-INFO +25 -0
  13. package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/SOURCES.txt +6 -0
  14. package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/dependency_links.txt +1 -0
  15. package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/requires.txt +23 -0
  16. package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/top_level.txt +1 -0
  17. package/assets/locks/python-genai/pyproject.toml +42 -0
  18. package/assets/locks/python-genai/uv.lock +2536 -0
  19. package/assets/locks/python-standard/pyproject.toml +36 -0
  20. package/assets/locks/python-standard/uv.lock +1780 -0
  21. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/catalog.json +4 -4
  22. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package-lock.json +667 -1108
  23. package/assets/supported-stack.json +711 -0
  24. package/dist/args.js +26 -1
  25. package/dist/args.js.map +1 -1
  26. package/dist/catalogs.d.ts +3 -1
  27. package/dist/catalogs.js +29 -7
  28. package/dist/catalogs.js.map +1 -1
  29. package/dist/cli.js +2 -1
  30. package/dist/cli.js.map +1 -1
  31. package/dist/commands.d.ts +6 -0
  32. package/dist/commands.js +364 -172
  33. package/dist/commands.js.map +1 -1
  34. package/dist/container-validation.d.ts +2 -0
  35. package/dist/container-validation.js +26 -0
  36. package/dist/container-validation.js.map +1 -0
  37. package/dist/file-system.d.ts +1 -0
  38. package/dist/file-system.js +141 -7
  39. package/dist/file-system.js.map +1 -1
  40. package/dist/framework-adapters.js +1 -0
  41. package/dist/framework-adapters.js.map +1 -1
  42. package/dist/go-template-assets.d.ts +2 -0
  43. package/dist/go-template-assets.js +14 -0
  44. package/dist/go-template-assets.js.map +1 -0
  45. package/dist/init-filesystem.js +5 -4
  46. package/dist/init-filesystem.js.map +1 -1
  47. package/dist/interactive.d.ts +0 -5
  48. package/dist/interactive.js +11 -73
  49. package/dist/interactive.js.map +1 -1
  50. package/dist/opentofu-template-assets.d.ts +2 -0
  51. package/dist/opentofu-template-assets.js +10 -0
  52. package/dist/opentofu-template-assets.js.map +1 -0
  53. package/dist/package-identity.d.ts +8 -0
  54. package/dist/package-identity.js +16 -0
  55. package/dist/package-identity.js.map +1 -0
  56. package/dist/planner.js +42 -3
  57. package/dist/planner.js.map +1 -1
  58. package/dist/power-apps-templates.js +6 -0
  59. package/dist/power-apps-templates.js.map +1 -1
  60. package/dist/project-dependencies.d.ts +1 -1
  61. package/dist/project-dependencies.js +15 -34
  62. package/dist/project-dependencies.js.map +1 -1
  63. package/dist/published-verifier.js +11 -1
  64. package/dist/published-verifier.js.map +1 -1
  65. package/dist/python-template-assets.d.ts +4 -0
  66. package/dist/python-template-assets.js +34 -0
  67. package/dist/python-template-assets.js.map +1 -0
  68. package/dist/repository-governance.d.ts +17 -0
  69. package/dist/repository-governance.js +507 -0
  70. package/dist/repository-governance.js.map +1 -0
  71. package/dist/runtime.d.ts +1 -1
  72. package/dist/runtime.js +3 -1
  73. package/dist/runtime.js.map +1 -1
  74. package/dist/self-upgrade.d.ts +77 -0
  75. package/dist/self-upgrade.js +499 -0
  76. package/dist/self-upgrade.js.map +1 -0
  77. package/dist/stable-release.d.ts +18 -0
  78. package/dist/stable-release.js +66 -0
  79. package/dist/stable-release.js.map +1 -0
  80. package/dist/standard-templates.js +28 -97
  81. package/dist/standard-templates.js.map +1 -1
  82. package/dist/supported-stack.d.ts +115 -0
  83. package/dist/supported-stack.js +439 -0
  84. package/dist/supported-stack.js.map +1 -0
  85. package/dist/telemetry/contract.d.ts +1 -1
  86. package/dist/telemetry/contract.js +1 -0
  87. package/dist/telemetry/contract.js.map +1 -1
  88. package/dist/templates.js +216 -125
  89. package/dist/templates.js.map +1 -1
  90. package/dist/types.d.ts +18 -1
  91. package/dist/workstation-catalog.js +15 -12
  92. package/dist/workstation-catalog.js.map +1 -1
  93. package/dist/workstation.js +16 -6
  94. package/dist/workstation.js.map +1 -1
  95. package/docs/cli-reference.md +99 -22
  96. package/docs/configuration-and-manifests.md +14 -7
  97. package/docs/existing-repositories.md +27 -6
  98. package/docs/getting-started.md +23 -7
  99. package/docs/prerequisites.md +30 -4
  100. package/docs/project-structure.md +20 -1
  101. package/docs/repository-governance.md +91 -0
  102. package/docs/safety-and-consent.md +42 -17
  103. package/docs/spec-workflows-and-agents.md +22 -8
  104. package/docs/supported-stack.md +89 -0
  105. package/docs/telemetry.md +6 -0
  106. package/docs/troubleshooting.md +84 -8
  107. package/docs/workloads.md +18 -5
  108. package/package.json +14 -6
  109. package/dist/update-impact.d.ts +0 -23
  110. package/dist/update-impact.js +0 -127
  111. package/dist/update-impact.js.map +0 -1
package/dist/templates.js CHANGED
@@ -1,10 +1,15 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { addGenAiExtensionArtifacts } from './genai-templates.js';
3
+ import { assertImmutableGeneratedContainerReferences } from './container-validation.js';
3
4
  import { addPowerAppsCodeAppArtifacts } from './power-apps-templates.js';
4
5
  import { addStandardStackArtifacts, renderStandardDockerfile, renderStandardEnv } from './standard-templates.js';
5
6
  import { liftoffVersion } from './version.js';
6
7
  import { renderNpmLock, renderNpmPackage } from './npm-template-assets.js';
8
+ import { renderOpenTofuProviderLock, renderOpenTofuVersions } from './opentofu-template-assets.js';
7
9
  import { formatCommand } from './process-runner.js';
10
+ import { formatContainerImage, supportedStack } from './supported-stack.js';
11
+ import { buildRepositoryGovernanceArtifacts, governancePolicyVersion } from './repository-governance.js';
12
+ import { renderFunctionRequirementsAsset, renderPythonLock, renderPythonPyprojectAsset } from './python-template-assets.js';
8
13
  import { workstationRequirementCatalog } from './workstation-catalog.js';
9
14
  const contentHash = (content) => `sha256:${createHash('sha256').update(content, 'utf8').digest('hex')}`;
10
15
  const DEFAULT_FUNCTION_WORKER_QUEUE_NAME = 'events';
@@ -77,10 +82,14 @@ export function buildArtifacts(plan) {
77
82
  addPowerAppsWorkloadArtifacts(add, plan);
78
83
  break;
79
84
  }
80
- addGovernanceArtifacts(add, plan);
85
+ addSpecWorkflowArtifacts(add, plan);
86
+ for (const artifact of buildRepositoryGovernanceArtifacts(plan)) {
87
+ add(artifact.logicalName, artifact.category, artifact.pathParts, artifact.content);
88
+ }
81
89
  if (plan.workload !== 'power-apps-code-app' && plan.includeFrontend) {
82
90
  addFrontendArtifacts(add, plan);
83
91
  }
92
+ assertImmutableGeneratedContainerReferences(artifacts);
84
93
  const manifest = buildManifest(plan, artifacts);
85
94
  artifacts.push({
86
95
  logicalName: 'manifest',
@@ -154,7 +163,7 @@ export function buildManifest(plan, artifacts, options = {}) {
154
163
  environments: plan.environments.map((environment) => environment.id)
155
164
  };
156
165
  return {
157
- artifactVersion: 4,
166
+ artifactVersion: 5,
158
167
  generatedBy: 'Mission Control Liftoff',
159
168
  liftoffVersion,
160
169
  project: {
@@ -169,6 +178,16 @@ export function buildManifest(plan, artifacts, options = {}) {
169
178
  adapter: plan.framework.id,
170
179
  ...(frameworkState === 'initialized' ? { contractVersion: plan.framework.version } : {})
171
180
  },
181
+ governance: plan.governanceProfile.id === 'none'
182
+ ? {
183
+ profile: 'none',
184
+ state: 'disabled'
185
+ }
186
+ : {
187
+ profile: plan.governanceProfile.id,
188
+ policyVersion: governancePolicyVersion,
189
+ state: 'handoff-generated'
190
+ },
172
191
  artifacts: artifacts
173
192
  .filter((artifact) => artifact.category !== 'seed' && artifact.category !== 'framework')
174
193
  .map((artifact) => ({
@@ -193,7 +212,8 @@ function addBaseArtifacts(add, plan) {
193
212
  environments: plan.environments.map((environment) => environment.id),
194
213
  specWorkflow: plan.specWorkflow.id,
195
214
  agents: plan.agents.map((agent) => agent.id),
196
- ...(plan.defaultAgent ? { defaultAgent: plan.defaultAgent.id } : {})
215
+ ...(plan.defaultAgent ? { defaultAgent: plan.defaultAgent.id } : {}),
216
+ governanceProfile: plan.governanceProfile.id
197
217
  }, null, 2));
198
218
  add('env-example', 'configuration', ['.env.example'], renderEnvExample(plan));
199
219
  add('backend-dockerfile', 'runtime', ['Dockerfile'], plan.workload === 'genai' ? renderBackendDockerfile() : renderStandardDockerfile(plan));
@@ -205,12 +225,14 @@ function addPowerAppsProjectConfig(add, plan) {
205
225
  specWorkflow: plan.specWorkflow.id,
206
226
  agents: plan.agents.map((agent) => agent.id),
207
227
  ...(plan.defaultAgent ? { defaultAgent: plan.defaultAgent.id } : {}),
208
- codeAppsPlugin: plan.codeAppsPlugin
228
+ codeAppsPlugin: plan.codeAppsPlugin,
229
+ governanceProfile: plan.governanceProfile.id
209
230
  }, null, 2));
210
231
  }
211
232
  function addBackendArtifacts(add, plan) {
212
233
  const routeModule = pyModule(genAiPattern(plan).id);
213
234
  add('backend-pyproject', 'backend', ['backend', 'pyproject.toml'], renderBackendPyproject(plan));
235
+ add('backend-uv-lock', 'backend', ['backend', 'uv.lock'], renderPythonLock('genai', `${plan.safeProjectName}-backend`));
214
236
  add('backend-package', 'backend', ['backend', '__init__.py'], '');
215
237
  add('backend-api-package', 'backend', ['backend', 'apis', '__init__.py'], '');
216
238
  add('backend-main', 'backend', ['backend', 'apis', 'main.py'], renderFastApiMain(plan, routeModule));
@@ -285,7 +307,8 @@ function addDockerArtifacts(add, plan) {
285
307
  }
286
308
  function addInfrastructureArtifacts(add, plan) {
287
309
  const base = ['infrastructure', 'opentofu', 'azure'];
288
- add('opentofu-versions', 'infrastructure', [...base, 'versions.tf'], renderTofuVersions());
310
+ add('opentofu-versions', 'infrastructure', [...base, 'versions.tf'], renderOpenTofuVersions());
311
+ add('opentofu-provider-lock', 'infrastructure', [...base, '.terraform.lock.hcl'], renderOpenTofuProviderLock());
289
312
  add('opentofu-providers', 'infrastructure', [...base, 'providers.tf'], renderTofuProviders());
290
313
  add('opentofu-variables', 'infrastructure', [...base, 'variables.tf'], renderTofuVariables(plan));
291
314
  add('opentofu-main', 'infrastructure', [...base, 'main.tf'], renderTofuMain(plan));
@@ -297,7 +320,7 @@ function addInfrastructureArtifacts(add, plan) {
297
320
  add(`opentofu-${environment.id}-tfvars`, 'infrastructure', [...base, 'environments', `${environment.id}.tfvars`], renderTofuTfvars(plan, environment.id));
298
321
  }
299
322
  }
300
- function addGovernanceArtifacts(add, plan) {
323
+ function addSpecWorkflowArtifacts(add, plan) {
301
324
  if (plan.workload === 'power-apps-code-app') {
302
325
  if (plan.specWorkflow.id === 'openspec') {
303
326
  const changeName = `bootstrap-${plan.safeProjectName}`;
@@ -425,10 +448,11 @@ function addFrontendArtifacts(add, plan) {
425
448
  function renderDirectBuildAndTestGuide(plan) {
426
449
  let backendCommands;
427
450
  if (plan.workload === 'genai' || plan.apiStack.id === 'python-fastapi') {
428
- backendCommands = `# macOS and Linux
429
- python3 -m venv .venv
430
- .venv/bin/python -m pip install -e "./backend[test]"
431
- (cd backend && python -m pytest -q)`;
451
+ const extras = plan.workload === 'genai' && hasFunctionWorker(plan)
452
+ ? ' --extra functions'
453
+ : '';
454
+ backendCommands = `uv sync --frozen --project backend --extra test${extras}
455
+ uv run --project backend python -m pytest -q backend/tests`;
432
456
  }
433
457
  else if (plan.apiStack.id === 'node-fastify') {
434
458
  backendCommands = `cd backend
@@ -452,24 +476,12 @@ npm ci
452
476
  npm run build
453
477
  \`\`\`
454
478
  ` : '';
455
- const windowsPythonCommands = plan.workload === 'genai' || plan.apiStack.id === 'python-fastapi'
456
- ? `
457
-
458
- Equivalent Windows dependency commands:
459
-
460
- \`\`\`powershell
461
- py -3 -m venv .venv
462
- .venv\\Scripts\\python.exe -m pip install -e "./backend[test]"
463
- \`\`\`
464
- `
465
- : '';
466
479
  const functionCommands = plan.workload === 'genai' && hasFunctionWorker(plan) ? `
467
480
 
468
- Run the Function worker unit tests from the same Python virtual environment:
481
+ Run the Function worker unit tests from the same locked Python environment:
469
482
 
470
483
  \`\`\`bash
471
- .venv/bin/python -m pip install -r functions/${functionWorkerName(plan)}/requirements.txt
472
- (cd functions/${functionWorkerName(plan)} && ../../.venv/bin/python -m pytest -q)
484
+ (cd functions/${functionWorkerName(plan)} && uv run --project ../../backend --directory . python -m pytest -q)
473
485
  \`\`\`
474
486
  ` : '';
475
487
  return `## Project Dependencies, Build, And Test
@@ -480,7 +492,7 @@ Run the Function worker unit tests from the same Python virtual environment:
480
492
  ${backendCommands}
481
493
  \`\`\`
482
494
 
483
- ${windowsPythonCommands}${frontendCommands}${functionCommands}`;
495
+ ${frontendCommands}${functionCommands}`;
484
496
  }
485
497
  function renderAdvisoryReadinessGuide(plan) {
486
498
  const selected = [
@@ -553,9 +565,13 @@ Copy \`.env.example\` to \`.env\`, then configure only the integrations you use:
553
565
  function renderGeneratedUpdateGuide() {
554
566
  return `## Safe Liftoff Updates
555
567
 
556
- \`liftoff update\` reports drift and, in an interactive terminal, shows the overwrite impact before a default-No prompt for safe managed changes. Local or user-owned conflicts require a separate default-No confirmation. With redirected input or output, or with \`--json\`, update remains a prompt-free read-only check; \`liftoff update --apply\` and \`liftoff update --apply --force\` provide explicit automation consent.
568
+ \`liftoff upgrade\` replaces a supported global Liftoff CLI installation; it does not inspect or modify this project. Check and apply CLI replacement separately with \`liftoff upgrade --check\` and \`liftoff upgrade\`.
569
+
570
+ \`single-maintainer-gitflow\` repository governance generates a local handoff only. Review \`.liftoff/governance/README.md\`, commit and push the project, then run a selected-agent launcher for read-only Phase 0. Live enforcement requires a separately approved governance change.
571
+
572
+ \`liftoff update\` immediately applies safe managed changes without prompting in interactive, redirected, and automated environments. Use \`liftoff update --check\` for a read-only human drift report, or \`liftoff update --check --json\` as an automation gate that exits 0 when clean and 2 when drift exists.
557
573
 
558
- An occupied destination with different user bytes is reported and preserved without conflict consent, while an identical destination is adopted without rewriting it. Update never deletes orphans or installs dependencies. A failed transaction is rolled back, but Liftoff retains no backup after a successful overwrite, so commit or copy local work first.
574
+ Local or user-owned conflicts are skipped by default. Review the reported paths, commit or copy local work, and use \`liftoff update --force\` only when every listed overwrite is intended. An occupied destination with different user bytes remains preserved without force, while an identical destination is adopted without rewriting it. Update never deletes orphans or installs dependencies. A failed transaction is rolled back, but Liftoff retains no backup after a successful overwrite.
559
575
 
560
576
  Liftoff rejects malformed, traversal, absolute, drive-qualified, UNC, separator-containing, or symlink-escaping manifest paths before artifact access. If the manifest is unsafe or malformed, restore \`liftoff.manifest.json\` from version control or regenerate the project with a matching Liftoff version; do not hand-edit unsafe paths. Run \`liftoff <command> --help\` for command-specific syntax because unknown flags, subcommands, values, and extra arguments fail before any write.
561
577
  `;
@@ -694,16 +710,26 @@ LANGFUSE_SECRET_KEY=
694
710
  `;
695
711
  }
696
712
  function renderBackendDockerfile() {
697
- return `FROM python:3.12-slim
713
+ return `FROM ${formatContainerImage(supportedStack.containers['uv-tool'])} AS uv
714
+ FROM ${formatContainerImage(supportedStack.containers['python-runtime'])}
698
715
 
699
716
  WORKDIR /app
700
717
 
701
718
  ENV PYTHONDONTWRITEBYTECODE=1
702
719
  ENV PYTHONUNBUFFERED=1
703
720
  ENV PYTHONPATH=/app
721
+ ENV PATH=/app/backend/.venv/bin:$PATH
722
+ COPY --from=uv /uv /uvx /bin/
704
723
 
724
+ ARG UV_DEFAULT_INDEX=https://pypi.org/simple
705
725
  COPY backend/pyproject.toml /app/backend/pyproject.toml
706
- RUN pip install --no-cache-dir /app/backend
726
+ COPY backend/uv.lock /app/backend/uv.lock
727
+ RUN --mount=type=cache,target=/root/.cache/uv \\
728
+ uv export --frozen --no-dev --no-emit-project --project /app/backend --output-file /tmp/requirements.txt \\
729
+ && uv venv /app/backend/.venv \\
730
+ && uv pip install --python /app/backend/.venv/bin/python --require-hashes \\
731
+ --default-index "$UV_DEFAULT_INDEX" --requirements /tmp/requirements.txt \\
732
+ && rm /tmp/requirements.txt
707
733
 
708
734
  COPY backend /app/backend
709
735
  COPY database /app/database
@@ -713,43 +739,7 @@ CMD ["uvicorn", "backend.apis.main:app", "--host", "0.0.0.0", "--port", "8000"]
713
739
  `;
714
740
  }
715
741
  function renderBackendPyproject(plan) {
716
- return `[project]
717
- name = "${plan.safeProjectName}-backend"
718
- version = "0.1.0"
719
- requires-python = ">=3.12"
720
- dependencies = [
721
- "fastapi>=0.111",
722
- "uvicorn[standard]>=0.30",
723
- "pydantic>=2.7",
724
- "pydantic-settings>=2.3",
725
- "pydantic-ai-slim[openai]==1.107.1",
726
- "scalar-fastapi>=1.0",
727
- "sqlalchemy[asyncio]>=2.0",
728
- "asyncpg>=0.29",
729
- "psycopg[binary]>=3.2",
730
- "alembic>=1.13",
731
- "redis>=5.0",
732
- "langfuse==2.60.10",
733
- "azure-servicebus>=7.12",
734
- "azure-identity>=1.17",
735
- "azure-storage-blob>=12.20",
736
- "azure-communication-email>=1.0"
737
- ]
738
-
739
- [project.optional-dependencies]
740
- test = ["pytest>=8.2", "httpx>=0.27"]
741
-
742
- [build-system]
743
- requires = ["setuptools>=70"]
744
- build-backend = "setuptools.build_meta"
745
-
746
- [tool.setuptools]
747
- packages = []
748
-
749
- [tool.pytest.ini_options]
750
- pythonpath = [".."]
751
- testpaths = ["tests"]
752
- `;
742
+ return renderPythonPyprojectAsset('genai', `${plan.safeProjectName}-backend`);
753
743
  }
754
744
  function renderFastApiMain(plan, routeModule) {
755
745
  return `from fastapi import FastAPI
@@ -1136,8 +1126,12 @@ class LangfuseTracer:
1136
1126
 
1137
1127
  @asynccontextmanager
1138
1128
  async def trace(self, name: str, input_data: Any = None):
1139
- remote_trace = self._client.trace(name=name, input=input_data)
1140
- remote_id = getattr(remote_trace, "id", None)
1129
+ remote_trace = self._client.start_observation(
1130
+ name=name,
1131
+ as_type="span",
1132
+ input=input_data,
1133
+ )
1134
+ remote_id = getattr(remote_trace, "trace_id", None)
1141
1135
  handle = TraceHandle(
1142
1136
  enabled=True,
1143
1137
  trace_id=str(remote_id) if remote_id is not None else None,
@@ -1149,6 +1143,8 @@ class LangfuseTracer:
1149
1143
  raise
1150
1144
  else:
1151
1145
  remote_trace.update(output=handle.output)
1146
+ finally:
1147
+ remote_trace.end()
1152
1148
 
1153
1149
 
1154
1150
  def build_tracer(
@@ -1293,21 +1289,25 @@ from backend.observability.tracing import (
1293
1289
 
1294
1290
 
1295
1291
  class FakeRemoteTrace:
1296
- id = "trace-123"
1292
+ trace_id = "trace-123"
1297
1293
 
1298
1294
  def __init__(self):
1299
1295
  self.updates = []
1296
+ self.ended = False
1300
1297
 
1301
1298
  def update(self, **values):
1302
1299
  self.updates.append(values)
1303
1300
 
1301
+ def end(self):
1302
+ self.ended = True
1303
+
1304
1304
 
1305
1305
  class FakeLangfuse:
1306
1306
  def __init__(self):
1307
1307
  self.calls = []
1308
1308
  self.remote_trace = FakeRemoteTrace()
1309
1309
 
1310
- def trace(self, **values):
1310
+ def start_observation(self, **values):
1311
1311
  self.calls.append(values)
1312
1312
  return self.remote_trace
1313
1313
 
@@ -1337,8 +1337,11 @@ def test_configured_tracing_updates_langfuse_operation():
1337
1337
  trace.set_output({"answer": "world"})
1338
1338
 
1339
1339
  asyncio.run(scenario())
1340
- assert client.calls == [{"name": "agent.run", "input": {"prompt": "hello"}}]
1340
+ assert client.calls == [
1341
+ {"name": "agent.run", "as_type": "span", "input": {"prompt": "hello"}}
1342
+ ]
1341
1343
  assert client.remote_trace.updates == [{"output": {"answer": "world"}}]
1344
+ assert client.remote_trace.ended is True
1342
1345
 
1343
1346
 
1344
1347
  def test_partial_langfuse_configuration_fails(monkeypatch):
@@ -1712,15 +1715,11 @@ Deployed triggers use \`ServiceBusConnection__fullyQualifiedNamespace\` and \`Se
1712
1715
  ## Local Development
1713
1716
 
1714
1717
  \`\`\`bash
1715
- python -m venv .venv
1716
- source .venv/bin/activate
1717
- pip install -r requirements.txt
1718
+ uv sync --frozen --project ../../backend --extra test --extra functions
1718
1719
  cp local.settings.example.json local.settings.json
1719
- python -m pytest -q
1720
+ uv run --project ../../backend --directory . python -m pytest -q
1720
1721
  func start
1721
1722
  \`\`\`
1722
-
1723
- On Windows, activate the virtual environment with \`.venv\\Scripts\\activate\`.
1724
1723
  `;
1725
1724
  }
1726
1725
  function renderFunctionHostJson() {
@@ -1747,9 +1746,7 @@ function renderFunctionLocalSettings(plan) {
1747
1746
  }, null, 2);
1748
1747
  }
1749
1748
  function renderFunctionRequirements() {
1750
- return `azure-functions>=1.21.0
1751
- pytest>=8.2
1752
- `;
1749
+ return renderFunctionRequirementsAsset();
1753
1750
  }
1754
1751
  function renderFunctionApp(plan) {
1755
1752
  const pattern = genAiPattern(plan);
@@ -1861,7 +1858,9 @@ function renderDockerCompose(plan) {
1861
1858
  depends_on:
1862
1859
  - backend
1863
1860
  ` : '';
1864
- const postgresImage = plan.workload === 'genai' ? 'pgvector/pgvector:pg16' : 'postgres:16-alpine';
1861
+ const postgresImage = plan.workload === 'genai'
1862
+ ? formatContainerImage(supportedStack.containers.pgvector)
1863
+ : formatContainerImage(supportedStack.containers.postgres);
1865
1864
  return `services:
1866
1865
  backend:
1867
1866
  build:
@@ -1888,46 +1887,136 @@ ${frontendService}
1888
1887
  POSTGRES_DB: ${plan.safeProjectName.replace(/-/g, '_')}
1889
1888
  ports:
1890
1889
  - "5432:5432"
1890
+ healthcheck:
1891
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
1892
+ interval: 3s
1893
+ timeout: 3s
1894
+ retries: 10
1891
1895
 
1892
1896
  redis:
1893
- image: redis:7-alpine
1897
+ image: ${formatContainerImage(supportedStack.containers.redis)}
1894
1898
  ports:
1895
1899
  - "6379:6379"
1896
1900
 
1897
1901
  azurite:
1898
- image: mcr.microsoft.com/azure-storage/azurite
1902
+ image: ${formatContainerImage(supportedStack.containers.azurite)}
1899
1903
  command: azurite --blobHost 0.0.0.0
1900
1904
  ports:
1901
1905
  - "10000:10000"
1902
1906
 
1903
1907
  mailpit:
1904
- image: axllent/mailpit:latest
1908
+ image: ${formatContainerImage(supportedStack.containers.mailpit)}
1905
1909
  ports:
1906
1910
  - "8025:8025"
1907
1911
 
1908
- ${plan.workload === 'genai' ? ` langfuse:
1909
- image: langfuse/langfuse:2
1912
+ ${plan.workload === 'genai' ? ` langfuse-worker:
1913
+ image: ${formatContainerImage(supportedStack.containers['langfuse-worker'])}
1910
1914
  profiles:
1911
1915
  - observability
1912
- environment:
1916
+ depends_on: &langfuse-dependencies
1917
+ postgres:
1918
+ condition: service_healthy
1919
+ langfuse-redis:
1920
+ condition: service_healthy
1921
+ clickhouse:
1922
+ condition: service_healthy
1923
+ minio:
1924
+ condition: service_healthy
1925
+ environment: &langfuse-environment
1913
1926
  DATABASE_URL: postgresql://postgres:postgres@postgres:5432/${plan.safeProjectName.replace(/-/g, '_')}
1914
- NEXTAUTH_SECRET: local-development-placeholder
1915
- SALT: local-development-placeholder
1927
+ NEXTAUTH_URL: http://localhost:3000
1928
+ SALT: local-development-salt
1929
+ ENCRYPTION_KEY: 0000000000000000000000000000000000000000000000000000000000000000
1930
+ TELEMETRY_ENABLED: "false"
1931
+ CLICKHOUSE_MIGRATION_URL: clickhouse://clickhouse:9000
1932
+ CLICKHOUSE_URL: http://clickhouse:8123
1933
+ CLICKHOUSE_USER: clickhouse
1934
+ CLICKHOUSE_PASSWORD: clickhouse
1935
+ REDIS_HOST: langfuse-redis
1936
+ REDIS_PORT: 6379
1937
+ REDIS_AUTH: langfuse-redis
1938
+ LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse
1939
+ LANGFUSE_S3_EVENT_UPLOAD_REGION: auto
1940
+ LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: minio
1941
+ LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: miniosecret
1942
+ LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: http://minio:9000
1943
+ LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true"
1944
+ LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: langfuse
1945
+ LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto
1946
+ LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: minio
1947
+ LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: miniosecret
1948
+ LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://minio:9000
1949
+ LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "true"
1950
+
1951
+ langfuse:
1952
+ image: ${formatContainerImage(supportedStack.containers['langfuse-web'])}
1953
+ profiles:
1954
+ - observability
1955
+ depends_on: *langfuse-dependencies
1956
+ environment:
1957
+ <<: *langfuse-environment
1958
+ NEXTAUTH_SECRET: local-development-secret
1959
+ LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://localhost:9090
1916
1960
  ports:
1917
1961
  - "3000:3000"
1962
+
1963
+ clickhouse:
1964
+ image: ${formatContainerImage(supportedStack.containers.clickhouse)}
1965
+ profiles:
1966
+ - observability
1967
+ user: "101:101"
1968
+ environment:
1969
+ CLICKHOUSE_DB: default
1970
+ CLICKHOUSE_USER: clickhouse
1971
+ CLICKHOUSE_PASSWORD: clickhouse
1972
+ volumes:
1973
+ - langfuse-clickhouse-data:/var/lib/clickhouse
1974
+ - langfuse-clickhouse-logs:/var/log/clickhouse-server
1975
+ healthcheck:
1976
+ test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1"]
1977
+ interval: 5s
1978
+ timeout: 5s
1979
+ retries: 10
1980
+
1981
+ minio:
1982
+ image: ${formatContainerImage(supportedStack.containers.minio)}
1983
+ profiles:
1984
+ - observability
1985
+ entrypoint: sh
1986
+ command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
1987
+ environment:
1988
+ MINIO_ROOT_USER: minio
1989
+ MINIO_ROOT_PASSWORD: miniosecret
1990
+ ports:
1991
+ - "9090:9000"
1992
+ - "127.0.0.1:9091:9001"
1993
+ volumes:
1994
+ - langfuse-minio-data:/data
1995
+ healthcheck:
1996
+ test: ["CMD", "mc", "ready", "local"]
1997
+ interval: 1s
1998
+ timeout: 5s
1999
+ retries: 5
2000
+
2001
+ langfuse-redis:
2002
+ image: ${formatContainerImage(supportedStack.containers.redis)}
2003
+ profiles:
2004
+ - observability
2005
+ command: ["redis-server", "--requirepass", "langfuse-redis", "--maxmemory-policy", "noeviction"]
2006
+ volumes:
2007
+ - langfuse-redis-data:/data
2008
+ healthcheck:
2009
+ test: ["CMD", "redis-cli", "-a", "langfuse-redis", "ping"]
2010
+ interval: 3s
2011
+ timeout: 10s
2012
+ retries: 10
2013
+ ` : ''}
2014
+ ${plan.workload === 'genai' ? `volumes:
2015
+ langfuse-clickhouse-data:
2016
+ langfuse-clickhouse-logs:
2017
+ langfuse-minio-data:
2018
+ langfuse-redis-data:
1918
2019
  ` : ''}
1919
- `;
1920
- }
1921
- function renderTofuVersions() {
1922
- return `terraform {
1923
- required_version = ">= 1.6.0"
1924
- required_providers {
1925
- azurerm = {
1926
- source = "hashicorp/azurerm"
1927
- version = "~> 3.110"
1928
- }
1929
- }
1930
- }
1931
2020
  `;
1932
2021
  }
1933
2022
  function renderTofuProviders() {
@@ -1940,7 +2029,7 @@ function renderTofuVariables(plan) {
1940
2029
  const frontendVariables = plan.includeFrontend ? `
1941
2030
  variable "frontend_image" {
1942
2031
  type = string
1943
- default = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"
2032
+ default = "${formatContainerImage(supportedStack.containers['container-apps-bootstrap'])}"
1944
2033
  description = "Frontend image. Replace the bootstrap image with the generated frontend image after pushing it to ACR."
1945
2034
  }
1946
2035
  ` : '';
@@ -1953,7 +2042,7 @@ variable "function_worker_queue_name" {
1953
2042
 
1954
2043
  variable "functions_python_version" {
1955
2044
  type = string
1956
- default = "3.12"
2045
+ default = "${supportedStack.runtimes.python.releaseLine}"
1957
2046
  description = "Python runtime version for the generated Azure Functions worker."
1958
2047
  }
1959
2048
  ` : '';
@@ -1980,7 +2069,7 @@ variable "resource_suffix" {
1980
2069
 
1981
2070
  variable "backend_image" {
1982
2071
  type = string
1983
- default = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"
2072
+ default = "${formatContainerImage(supportedStack.containers['container-apps-bootstrap'])}"
1984
2073
  description = "Backend image. Replace the bootstrap image with the generated backend image after pushing it to ACR."
1985
2074
  }
1986
2075
 
@@ -2281,7 +2370,7 @@ resource "azurerm_storage_account" "main" {
2281
2370
 
2282
2371
  resource "azurerm_storage_container" "documents" {
2283
2372
  name = "documents"
2284
- storage_account_name = azurerm_storage_account.main.name
2373
+ storage_account_id = azurerm_storage_account.main.id
2285
2374
  container_access_type = "private"
2286
2375
  }
2287
2376
 
@@ -2305,11 +2394,12 @@ resource "azurerm_communication_service" "main" {
2305
2394
  }
2306
2395
 
2307
2396
  resource "azurerm_key_vault" "main" {
2308
- name = "${names.keyVault}"
2309
- location = azurerm_resource_group.main.location
2310
- resource_group_name = azurerm_resource_group.main.name
2311
- tenant_id = data.azurerm_client_config.current.tenant_id
2312
- sku_name = "standard"
2397
+ name = "${names.keyVault}"
2398
+ location = azurerm_resource_group.main.location
2399
+ resource_group_name = azurerm_resource_group.main.name
2400
+ tenant_id = data.azurerm_client_config.current.tenant_id
2401
+ sku_name = "standard"
2402
+ rbac_authorization_enabled = true
2313
2403
  }
2314
2404
 
2315
2405
  data "azurerm_client_config" "current" {}
@@ -2383,17 +2473,18 @@ Build the generated backend in ACR, then replace the bootstrap image:
2383
2473
 
2384
2474
  \`\`\`bash
2385
2475
  ACR_NAME="$(tofu output -raw container_registry_name)"
2386
- az acr build --registry "$ACR_NAME" --image ${plan.safeProjectName}-backend:latest ../../..
2476
+ SOURCE_SHA="$(git rev-parse HEAD)"
2477
+ az acr build --registry "$ACR_NAME" --image ${plan.safeProjectName}-backend:"$SOURCE_SHA" ../../..
2387
2478
  ${plan.includeFrontend ? `BACKEND_URL="https://$(tofu output -raw backend_url)"
2388
- az acr build --registry "$ACR_NAME" --image ${plan.safeProjectName}-frontend:latest --build-arg VITE_API_BASE_URL="$BACKEND_URL" ../../../frontend
2479
+ az acr build --registry "$ACR_NAME" --image ${plan.safeProjectName}-frontend:"$SOURCE_SHA" --build-arg VITE_API_BASE_URL="$BACKEND_URL" ../../../frontend
2389
2480
  ` : ''}\`\`\`
2390
2481
 
2391
2482
  Persist the deployed images in \`environments/${env}.tfvars\` so future applies do not restore the bootstrap image:
2392
2483
 
2393
2484
  \`\`\`hcl
2394
- backend_image = "<login-server>/${plan.safeProjectName}-backend:latest"
2485
+ backend_image = "<login-server>/${plan.safeProjectName}-backend@sha256:<manifest-digest>"
2395
2486
  backend_target_port = 8000
2396
- ${plan.includeFrontend ? `frontend_image = "<login-server>/${plan.safeProjectName}-frontend:latest"
2487
+ ${plan.includeFrontend ? `frontend_image = "<login-server>/${plan.safeProjectName}-frontend@sha256:<manifest-digest>"
2397
2488
  ` : ''}\`\`\`
2398
2489
 
2399
2490
  \`\`\`bash
@@ -2414,15 +2505,15 @@ function renderTofuTfvars(plan, environment) {
2414
2505
  ['environment', JSON.stringify(environment)],
2415
2506
  ['location', JSON.stringify(plan.region.slug)],
2416
2507
  ['resource_suffix', JSON.stringify(stableResourceSuffix(plan, environment))],
2417
- ['backend_image', JSON.stringify('mcr.microsoft.com/azuredocs/containerapps-helloworld:latest')],
2508
+ ['backend_image', JSON.stringify(formatContainerImage(supportedStack.containers['container-apps-bootstrap']))],
2418
2509
  ['backend_target_port', '80'],
2419
2510
  ['enable_private_networking', 'false']
2420
2511
  ];
2421
2512
  if (plan.includeFrontend) {
2422
- values.push(['frontend_image', JSON.stringify('mcr.microsoft.com/azuredocs/containerapps-helloworld:latest')]);
2513
+ values.push(['frontend_image', JSON.stringify(formatContainerImage(supportedStack.containers['container-apps-bootstrap']))]);
2423
2514
  }
2424
2515
  if (hasFunctionWorker(plan)) {
2425
- values.push(['function_worker_queue_name', JSON.stringify(DEFAULT_FUNCTION_WORKER_QUEUE_NAME)], ['functions_python_version', JSON.stringify('3.12')]);
2516
+ values.push(['function_worker_queue_name', JSON.stringify(DEFAULT_FUNCTION_WORKER_QUEUE_NAME)], ['functions_python_version', JSON.stringify(supportedStack.runtimes.python.releaseLine)]);
2426
2517
  }
2427
2518
  const width = Math.max(...values.map(([key]) => key.length));
2428
2519
  return values.map(([key, value]) => `${key.padEnd(width)} = ${value}`).join('\n');
@@ -2804,16 +2895,15 @@ async function submit(): Promise<void> {
2804
2895
  `;
2805
2896
  }
2806
2897
  function renderFrontendStyles() {
2807
- return `@tailwind base;
2808
- @tailwind components;
2809
- @tailwind utilities;
2898
+ return `@import "tailwindcss";
2810
2899
  `;
2811
2900
  }
2812
2901
  function renderFrontendViteConfig() {
2813
2902
  return `import { defineConfig } from 'vite';
2814
- import vue from '@vitejs/plugin-vue';
2903
+ import tailwindcss from '@tailwindcss/vite';
2904
+ import vue from '@vitejs/plugin-vue';
2815
2905
 
2816
- export default defineConfig({ plugins: [vue()] });
2906
+ export default defineConfig({ plugins: [vue(), tailwindcss()] });
2817
2907
  `;
2818
2908
  }
2819
2909
  function renderFrontendTailwindConfig() {
@@ -2827,16 +2917,17 @@ export default {
2827
2917
  `;
2828
2918
  }
2829
2919
  function renderFrontendDockerfile() {
2830
- return `FROM node:20-alpine AS build
2920
+ return `FROM ${formatContainerImage(supportedStack.containers['node-runtime'])} AS build
2831
2921
  WORKDIR /app
2922
+ ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org
2832
2923
  COPY package.json package-lock.json* ./
2833
- RUN npm install
2924
+ RUN npm ci --ignore-scripts --no-audit --no-fund --registry="$NPM_CONFIG_REGISTRY"
2834
2925
  COPY . .
2835
2926
  ARG VITE_API_BASE_URL=http://localhost:8000
2836
2927
  ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
2837
2928
  RUN npm run build
2838
2929
 
2839
- FROM nginx:1.27-alpine
2930
+ FROM ${formatContainerImage(supportedStack.containers['nginx-runtime'])}
2840
2931
  COPY --from=build /app/dist /usr/share/nginx/html
2841
2932
  `;
2842
2933
  }