@zaaxch/tailframe 4.0.0 → 4.0.2
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/package.json +1 -1
- package/src/new.mjs +364 -131
- package/src/validate.mjs +44 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zaaxch/tailframe",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"description": "Tailframe architecture toolkit: validates the Tailframe structure, import-boundary, and file-convention contracts. The package version is the contract version.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/new.mjs
CHANGED
|
@@ -131,6 +131,8 @@ add("package.json", JSON.stringify({
|
|
|
131
131
|
add("pnpm-workspace.yaml", `packages:
|
|
132
132
|
- "apps/*"
|
|
133
133
|
injectWorkspacePackages: true
|
|
134
|
+
minimumReleaseAgeExclude:
|
|
135
|
+
- "@zaaxch/tailframe@${contractVersion}"
|
|
134
136
|
allowBuilds:
|
|
135
137
|
"@firebase/util": true
|
|
136
138
|
esbuild: true
|
|
@@ -241,8 +243,7 @@ add(`${svc}/package.json`, JSON.stringify({
|
|
|
241
243
|
"schema:apply": "node dist/app/cli/applySchema.js", "schema:apply:dev": "ts-node -r tsconfig-paths/register src/app/cli/applySchema.ts",
|
|
242
244
|
"test:db:up": "docker compose -f docker-compose.test.yml up -d", "test:db:down": "docker compose -f docker-compose.test.yml down -v",
|
|
243
245
|
"docker:dev": "docker compose -f docker-compose.dev.yml up",
|
|
244
|
-
"docker:prod": "docker compose -f docker-compose.yml up -d"
|
|
245
|
-
"docker:push": "bash scripts/build_and_push.sh"
|
|
246
|
+
"docker:prod": "docker compose -f docker-compose.yml up -d"
|
|
246
247
|
}, dependencies: svcDeps, devDependencies: svcDevDeps
|
|
247
248
|
}, null, "\t"));
|
|
248
249
|
add(`${svc}/tsconfig.json`, JSON.stringify({
|
|
@@ -550,10 +551,10 @@ ${options.ui ? "- In production, serve the compiled UI from `dist/public` with a
|
|
|
550
551
|
The ECR build targets \`linux/amd64\`. ${options.ui ? "Its default immutable tag is `<product-sha>` and production Compose requires that exact tag." : "Its default immutable tag is `<product-sha>` and production Compose requires that exact tag."} The runtime image contains production dependencies only and runs as the Node user.${options.ui && options.auth === "firebase" ? ` The build requires \`${ui}/.env.production\` and mounts it as a BuildKit secret only while Vite compiles the browser bundle; it is not copied into the final image.` : ""}
|
|
551
552
|
|
|
552
553
|
## Production deployment
|
|
553
|
-
|
|
554
|
+
Pull requests into product \`main\` validate release candidates. The resulting \`main\` push validates again before GitHub Actions may publish an immutable image tagged with the full product commit SHA. Image publication and production deployment are separate workflows: deployment is manual from \`main\`, accepts only an already-published full SHA from \`main\` history, and checks out that exact commit for deployment files. GitHub Actions may replace files under \`/opt/${options.name}/current\` and MUST NOT replace persistent configuration, credentials, keyfiles, or data under \`/opt/${options.name}/shared\`. Keep Node runtime behavior in the shared service \`.env.production\`, Compose interpolation and infrastructure credentials in \`.env.infrastructure\`, and fixed topology wiring in Compose \`environment:\`. The optional UI build-time \`.env.production\` is a separate ephemeral file. Do not run deployment until the host and GitHub environments have been bootstrapped according to \`docs/production-deployment.md\`.
|
|
554
555
|
|
|
555
556
|
## Tests and validation
|
|
556
|
-
Run \`pnpm validate:architecture\` and \`pnpm format:check\` after changing files or imports. The generated service also includes unit tests and ${postgres ? "PostgreSQL-backed" : "Mongo-backed"} integration tests. Run focused service database tests through pnpm for the full service check. Test use cases, policies, ${postgres ? "PostgreSQL" : "Mongo"} repository adapters, and HTTP boundaries where behavior lives. Every public operation needs success and error-envelope coverage; authenticated operations need trusted-identity coverage; persisted capabilities need ${postgres ? "PostgreSQL" : "Mongo"} ownership/query coverage; UI-enabled services must verify that \`/api\` paths never fall through to the SPA. All AWS/ECR and Docker commands, including the
|
|
557
|
+
Run \`pnpm validate:architecture\` and \`pnpm format:check\` after changing files or imports. The generated service also includes unit tests and ${postgres ? "PostgreSQL-backed" : "Mongo-backed"} integration tests. Run focused service database tests through pnpm for the full service check. Test use cases, policies, ${postgres ? "PostgreSQL" : "Mongo"} repository adapters, and HTTP boundaries where behavior lives. Every public operation needs success and error-envelope coverage; authenticated operations need trusted-identity coverage; persisted capabilities need ${postgres ? "PostgreSQL" : "Mongo"} ownership/query coverage; UI-enabled services must verify that \`/api\` paths never fall through to the SPA. All AWS/ECR and Docker commands, including the release image script, must run outside the sandbox from the first attempt.
|
|
557
558
|
`);
|
|
558
559
|
|
|
559
560
|
if (options.ui) {
|
|
@@ -564,8 +565,8 @@ add(`${ui}/package.json`, JSON.stringify({ name: uiPackage, version: "0.1.0", pr
|
|
|
564
565
|
add(`${ui}/.prettierrc.json`, JSON.stringify(prettierConfig, null, "\t"));
|
|
565
566
|
add(`${ui}/.gitattributes`, gitAttributes);
|
|
566
567
|
add(`${ui}/tsconfig.json`, JSON.stringify({ files: [], references: [{ path: "./tsconfig.app.json" }, { path: "./tsconfig.node.json" }] }, null, "\t"));
|
|
567
|
-
add(`${ui}/tsconfig.app.json`, JSON.stringify({ extends: "@vue/tsconfig/tsconfig.dom.json", include: ["env.d.ts", "src/**/*", "src/**/*.vue"], compilerOptions: { composite: true, baseUrl: ".", paths: { "@/*": ["./src/*"] } } }, null, "\t"));
|
|
568
|
-
add(`${ui}/tsconfig.node.json`, JSON.stringify({ extends: "@tsconfig/node22/tsconfig.json", include: ["vite.config
|
|
568
|
+
add(`${ui}/tsconfig.app.json`, JSON.stringify({ extends: "@vue/tsconfig/tsconfig.dom.json", include: ["env.d.ts", "src/**/*", "src/**/*.vue"], compilerOptions: { composite: true, noEmit: true, tsBuildInfoFile: "./.tmp/tsconfig.app.tsbuildinfo", types: ["vitest/globals"], baseUrl: ".", paths: { "@/*": ["./src/*"] } } }, null, "\t"));
|
|
569
|
+
add(`${ui}/tsconfig.node.json`, JSON.stringify({ extends: "@tsconfig/node22/tsconfig.json", include: ["vite.config.ts"], compilerOptions: { composite: true, noEmit: true, tsBuildInfoFile: "./.tmp/tsconfig.node.tsbuildinfo", types: ["node"] } }, null, "\t"));
|
|
569
570
|
add(`${ui}/vite.config.ts`, `import { fileURLToPath, URL } from "node:url";\nimport { defineConfig } from "vitest/config";\nimport vue from "@vitejs/plugin-vue";\nimport tailwindcss from "@tailwindcss/vite";\nexport default defineConfig({ plugins: [vue(), tailwindcss()], resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } }, server: { proxy: { "/api": { target: "http://localhost:3000", changeOrigin: true } } }, test: { environment: "jsdom", globals: true } });`);
|
|
570
571
|
add(`${ui}/env.d.ts`, `/// <reference types="vite/client" />`);
|
|
571
572
|
add(`${ui}/index.html`, `<!doctype html>
|
|
@@ -581,7 +582,7 @@ add(`${ui}/index.html`, `<!doctype html>
|
|
|
581
582
|
</body>
|
|
582
583
|
</html>
|
|
583
584
|
`);
|
|
584
|
-
add(`${ui}/.gitignore`, `node_modules/\ndist/\n.env*\n!.env.example\ncerts/\n*.log\n`);
|
|
585
|
+
add(`${ui}/.gitignore`, `node_modules/\ndist/\n.env*\n!.env.example\ncerts/\n*.log\n.tmp/\n`);
|
|
585
586
|
if (options.auth === "firebase") add(`${ui}/.env.example`, `VITE_FIREBASE_API_KEY=\nVITE_FIREBASE_AUTH_DOMAIN=\nVITE_FIREBASE_PROJECT_ID=\nVITE_FIREBASE_APP_ID=\n`);
|
|
586
587
|
add(`${ui}/src/core/errors.ts`, uiErrorsSource);
|
|
587
588
|
add(`${ui}/src/core/rpc.ts`, uiRpcSource);
|
|
@@ -719,7 +720,7 @@ Run \`pnpm validate:architecture\` and \`pnpm format:check\`, then add focused V
|
|
|
719
720
|
}
|
|
720
721
|
|
|
721
722
|
add(`${svc}/Dockerfile`, `FROM node:22.13-bookworm AS build
|
|
722
|
-
RUN corepack enable
|
|
723
|
+
RUN npm install --global corepack@0.34.5 && corepack enable
|
|
723
724
|
WORKDIR /workspace
|
|
724
725
|
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
|
725
726
|
COPY apps/service/package.json apps/service/package.json
|
|
@@ -737,7 +738,7 @@ EXPOSE 3000
|
|
|
737
738
|
CMD ["node", "dist/server.js"]
|
|
738
739
|
`);
|
|
739
740
|
add(`${svc}/Dockerfile.dev`, `FROM node:22.13-alpine
|
|
740
|
-
RUN corepack enable
|
|
741
|
+
RUN npm install --global corepack@0.34.5 && corepack enable
|
|
741
742
|
WORKDIR /workspace
|
|
742
743
|
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
|
743
744
|
COPY apps/service/package.json apps/service/package.json
|
|
@@ -755,7 +756,7 @@ set -euo pipefail
|
|
|
755
756
|
|
|
756
757
|
SERVICE_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
|
|
757
758
|
PRODUCT_ROOT="$(cd "\${SERVICE_DIR}/../.." && pwd)"
|
|
758
|
-
PRODUCT_SHA="$(git -C "\${PRODUCT_ROOT}" rev-parse
|
|
759
|
+
PRODUCT_SHA="$(git -C "\${PRODUCT_ROOT}" rev-parse HEAD)"
|
|
759
760
|
ECR_REPOSITORY="\${ECR_REPOSITORY:-${options.name}}"
|
|
760
761
|
IMAGE_TAG="\${IMAGE_TAG:-\${PRODUCT_SHA}}"
|
|
761
762
|
REGISTRY="\${AWS_ACCOUNT_ID}.dkr.ecr.\${AWS_REGION}.amazonaws.com"
|
|
@@ -763,6 +764,11 @@ IMAGE="\${REGISTRY}/\${ECR_REPOSITORY}:\${IMAGE_TAG}"
|
|
|
763
764
|
DOCKERFILE="\${SERVICE_DIR}/Dockerfile"
|
|
764
765
|
${options.ui && options.auth === "firebase" ? `UI_ENV_FILE="\${UI_ENV_FILE:-\${PRODUCT_ROOT}/apps/ui/.env.production}"` : ""}
|
|
765
766
|
|
|
767
|
+
if [[ "\${IMAGE_TAG}" != "\${PRODUCT_SHA}" ]]; then
|
|
768
|
+
echo "IMAGE_TAG must equal the full product commit SHA (\${PRODUCT_SHA})" >&2
|
|
769
|
+
exit 1
|
|
770
|
+
fi
|
|
771
|
+
|
|
766
772
|
aws ecr get-login-password --region "\${AWS_REGION}" |
|
|
767
773
|
docker login --username AWS --password-stdin "\${REGISTRY}"
|
|
768
774
|
|
|
@@ -771,106 +777,305 @@ docker push "\${IMAGE}"
|
|
|
771
777
|
|
|
772
778
|
echo "Pushed \${IMAGE}"
|
|
773
779
|
`);
|
|
774
|
-
const workflowUiPath = options.ui ? '
|
|
780
|
+
const workflowUiPath = options.ui ? ' - "apps/ui/**"\n' : "";
|
|
775
781
|
const workflowUiEnvironment = options.ui && options.auth === "firebase" ? `
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
782
|
+
- name: Write UI production environment
|
|
783
|
+
if: steps.existing.outputs.exists != 'true'
|
|
784
|
+
env:
|
|
785
|
+
UI_ENV_PRODUCTION: \${{ secrets.UI_ENV_PRODUCTION }}
|
|
786
|
+
run: |
|
|
787
|
+
test -n "$UI_ENV_PRODUCTION"
|
|
788
|
+
printf '%s\\n' "$UI_ENV_PRODUCTION" > apps/ui/.env.production
|
|
781
789
|
` : "";
|
|
782
|
-
|
|
790
|
+
const deploymentArtifactDirectory = postgres ? "postgres" : "mongo";
|
|
791
|
+
const deploymentArtifactFile = postgres ? "10-create-application-user.sh" : "10-create-users.js";
|
|
792
|
+
const deploymentArtifactChmod = postgres ? ` /opt/${options.name}/current/deploy/postgres/10-create-application-user.sh` : "";
|
|
793
|
+
add(".github/workflows/release-image.yml", `name: Validate and build release image
|
|
783
794
|
|
|
784
795
|
on:
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
${workflowUiPath}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
796
|
+
pull_request:
|
|
797
|
+
branches:
|
|
798
|
+
- main
|
|
799
|
+
paths:
|
|
800
|
+
- "apps/service/**"
|
|
801
|
+
${workflowUiPath} - "package.json"
|
|
802
|
+
- "pnpm-workspace.yaml"
|
|
803
|
+
- "pnpm-lock.yaml"
|
|
804
|
+
- "tailframe.json"
|
|
805
|
+
- ".github/workflows/release-image.yml"
|
|
806
|
+
push:
|
|
807
|
+
branches:
|
|
808
|
+
- main
|
|
809
|
+
paths:
|
|
810
|
+
- "apps/service/**"
|
|
811
|
+
${workflowUiPath} - "package.json"
|
|
812
|
+
- "pnpm-workspace.yaml"
|
|
813
|
+
- "pnpm-lock.yaml"
|
|
814
|
+
- "tailframe.json"
|
|
815
|
+
- ".github/workflows/release-image.yml"
|
|
795
816
|
|
|
796
817
|
permissions:
|
|
797
|
-
|
|
798
|
-
id-token: write
|
|
818
|
+
contents: read
|
|
799
819
|
|
|
800
820
|
concurrency:
|
|
801
|
-
|
|
802
|
-
|
|
821
|
+
group: ${options.name}-release-\${{ github.ref }}
|
|
822
|
+
cancel-in-progress: true
|
|
803
823
|
|
|
804
824
|
jobs:
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
825
|
+
validate:
|
|
826
|
+
name: Validate release candidate
|
|
827
|
+
runs-on: ubuntu-latest
|
|
828
|
+
|
|
829
|
+
steps:
|
|
830
|
+
- name: Checkout product
|
|
831
|
+
uses: actions/checkout@v6
|
|
832
|
+
|
|
833
|
+
- name: Install pnpm
|
|
834
|
+
uses: pnpm/action-setup@v4
|
|
835
|
+
with:
|
|
836
|
+
version: 11.22.0
|
|
837
|
+
|
|
838
|
+
- name: Install Node.js
|
|
839
|
+
uses: actions/setup-node@v6
|
|
840
|
+
with:
|
|
841
|
+
node-version: 22.13.0
|
|
842
|
+
cache: pnpm
|
|
843
|
+
|
|
844
|
+
- name: Install dependencies
|
|
845
|
+
run: pnpm install --frozen-lockfile
|
|
846
|
+
|
|
847
|
+
- name: Validate architecture and formatting
|
|
848
|
+
run: |
|
|
849
|
+
pnpm sync:architecture
|
|
850
|
+
pnpm validate:architecture
|
|
851
|
+
pnpm format:check
|
|
852
|
+
|
|
853
|
+
- name: Start integration database
|
|
854
|
+
run: pnpm --filter ${servicePackage} test:db:up
|
|
855
|
+
|
|
856
|
+
- name: Run application tests
|
|
857
|
+
run: pnpm test
|
|
858
|
+
|
|
859
|
+
- name: Stop integration database
|
|
860
|
+
if: always()
|
|
861
|
+
run: pnpm --filter ${servicePackage} test:db:down
|
|
862
|
+
|
|
863
|
+
- name: Type-check and build applications
|
|
864
|
+
run: |
|
|
865
|
+
pnpm type-check
|
|
866
|
+
pnpm build
|
|
867
|
+
|
|
868
|
+
build-image:
|
|
869
|
+
name: Build immutable image
|
|
870
|
+
needs: validate
|
|
871
|
+
if: >-
|
|
872
|
+
github.event_name == 'push' &&
|
|
873
|
+
vars.AWS_ACCOUNT_ID != '' &&
|
|
874
|
+
vars.AWS_REGION != '' &&
|
|
875
|
+
vars.AWS_ROLE_ARN != ''
|
|
876
|
+
runs-on: ubuntu-latest
|
|
877
|
+
environment:
|
|
878
|
+
name: release
|
|
879
|
+
deployment: false
|
|
880
|
+
permissions:
|
|
881
|
+
contents: read
|
|
882
|
+
id-token: write
|
|
836
883
|
env:
|
|
837
|
-
|
|
838
|
-
|
|
884
|
+
AWS_ACCOUNT_ID: \${{ vars.AWS_ACCOUNT_ID }}
|
|
885
|
+
AWS_REGION: \${{ vars.AWS_REGION }}
|
|
886
|
+
ECR_REPOSITORY: \${{ vars.ECR_REPOSITORY || '${options.name}' }}
|
|
887
|
+
|
|
888
|
+
steps:
|
|
889
|
+
- name: Checkout product
|
|
890
|
+
uses: actions/checkout@v6
|
|
891
|
+
|
|
892
|
+
- name: Configure AWS credentials
|
|
893
|
+
uses: aws-actions/configure-aws-credentials@v6
|
|
894
|
+
with:
|
|
895
|
+
role-to-assume: \${{ vars.AWS_ROLE_ARN }}
|
|
896
|
+
aws-region: \${{ vars.AWS_REGION }}
|
|
897
|
+
|
|
898
|
+
- name: Resolve immutable image
|
|
899
|
+
id: image
|
|
900
|
+
run: |
|
|
901
|
+
product_sha="$(git rev-parse HEAD)"
|
|
902
|
+
registry="\${AWS_ACCOUNT_ID}.dkr.ecr.\${AWS_REGION}.amazonaws.com"
|
|
903
|
+
image="\${registry}/\${ECR_REPOSITORY}"
|
|
904
|
+
echo "tag=\${product_sha}" >> "$GITHUB_OUTPUT"
|
|
905
|
+
echo "registry=\${registry}" >> "$GITHUB_OUTPUT"
|
|
906
|
+
echo "image=\${image}" >> "$GITHUB_OUTPUT"
|
|
907
|
+
|
|
908
|
+
- name: Check for existing image
|
|
909
|
+
id: existing
|
|
910
|
+
env:
|
|
911
|
+
IMAGE_TAG: \${{ steps.image.outputs.tag }}
|
|
912
|
+
run: |
|
|
913
|
+
digest="$(aws ecr describe-images \\
|
|
914
|
+
--repository-name "$ECR_REPOSITORY" \\
|
|
915
|
+
--image-ids "imageTag=$IMAGE_TAG" \\
|
|
916
|
+
--query 'imageDetails[0].imageDigest' \\
|
|
917
|
+
--output text 2>/dev/null || true)"
|
|
918
|
+
if [[ -n "$digest" && "$digest" != "None" ]]; then
|
|
919
|
+
echo "exists=true" >> "$GITHUB_OUTPUT"
|
|
920
|
+
else
|
|
921
|
+
echo "exists=false" >> "$GITHUB_OUTPUT"
|
|
922
|
+
fi
|
|
923
|
+
${workflowUiEnvironment}
|
|
924
|
+
- name: Build and push image
|
|
925
|
+
if: steps.existing.outputs.exists != 'true'
|
|
926
|
+
env:
|
|
927
|
+
IMAGE_TAG: \${{ steps.image.outputs.tag }}
|
|
928
|
+
run: apps/service/scripts/build_and_push.sh
|
|
929
|
+
|
|
930
|
+
- name: Verify published image
|
|
931
|
+
id: published
|
|
932
|
+
env:
|
|
933
|
+
IMAGE_TAG: \${{ steps.image.outputs.tag }}
|
|
934
|
+
run: |
|
|
935
|
+
digest="$(aws ecr describe-images \\
|
|
936
|
+
--repository-name "$ECR_REPOSITORY" \\
|
|
937
|
+
--image-ids "imageTag=$IMAGE_TAG" \\
|
|
938
|
+
--query 'imageDetails[0].imageDigest' \\
|
|
939
|
+
--output text)"
|
|
940
|
+
test -n "$digest"
|
|
941
|
+
test "$digest" != "None"
|
|
942
|
+
echo "digest=\${digest}" >> "$GITHUB_OUTPUT"
|
|
943
|
+
|
|
944
|
+
- name: Record release image
|
|
945
|
+
env:
|
|
946
|
+
IMAGE: \${{ steps.image.outputs.image }}
|
|
947
|
+
IMAGE_TAG: \${{ steps.image.outputs.tag }}
|
|
948
|
+
IMAGE_DIGEST: \${{ steps.published.outputs.digest }}
|
|
949
|
+
run: |
|
|
950
|
+
{
|
|
951
|
+
echo "### ${title} release image"
|
|
952
|
+
echo
|
|
953
|
+
echo "- Image: \`\${IMAGE}:\${IMAGE_TAG}\`"
|
|
954
|
+
echo "- Digest: \`\${IMAGE_DIGEST}\`"
|
|
955
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
956
|
+
`);
|
|
957
|
+
add(".github/workflows/deploy.yml", `name: Deploy release image
|
|
958
|
+
|
|
959
|
+
on:
|
|
960
|
+
workflow_dispatch:
|
|
961
|
+
inputs:
|
|
962
|
+
image_tag:
|
|
963
|
+
description: Full 40-character product commit SHA already published to ECR
|
|
964
|
+
required: true
|
|
965
|
+
type: string
|
|
966
|
+
|
|
967
|
+
permissions:
|
|
968
|
+
contents: read
|
|
969
|
+
id-token: write
|
|
970
|
+
|
|
971
|
+
concurrency:
|
|
972
|
+
group: ${options.name}-production
|
|
973
|
+
cancel-in-progress: false
|
|
839
974
|
|
|
840
|
-
|
|
975
|
+
jobs:
|
|
976
|
+
deploy:
|
|
977
|
+
name: Deploy production
|
|
978
|
+
if: github.ref == 'refs/heads/main'
|
|
979
|
+
runs-on: ubuntu-latest
|
|
980
|
+
environment: production
|
|
841
981
|
env:
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
982
|
+
AWS_ACCOUNT_ID: \${{ vars.AWS_ACCOUNT_ID }}
|
|
983
|
+
AWS_REGION: \${{ vars.AWS_REGION }}
|
|
984
|
+
ECR_REPOSITORY: \${{ vars.ECR_REPOSITORY || '${options.name}' }}
|
|
985
|
+
DEPLOY_HOST: \${{ vars.DEPLOY_HOST }}
|
|
986
|
+
DEPLOY_USER: \${{ vars.DEPLOY_USER }}
|
|
987
|
+
IMAGE_TAG: \${{ inputs.image_tag }}
|
|
988
|
+
|
|
989
|
+
steps:
|
|
990
|
+
- name: Validate deployment input
|
|
991
|
+
run: |
|
|
992
|
+
[[ "$IMAGE_TAG" =~ ^[0-9a-f]{40}$ ]]
|
|
993
|
+
test -n "$AWS_ACCOUNT_ID"
|
|
994
|
+
test -n "$AWS_REGION"
|
|
995
|
+
test -n "$DEPLOY_HOST"
|
|
996
|
+
test -n "$DEPLOY_USER"
|
|
997
|
+
|
|
998
|
+
- name: Checkout exact product commit
|
|
999
|
+
uses: actions/checkout@v6
|
|
1000
|
+
with:
|
|
1001
|
+
ref: \${{ inputs.image_tag }}
|
|
1002
|
+
fetch-depth: 0
|
|
1003
|
+
|
|
1004
|
+
- name: Verify product release commit
|
|
1005
|
+
run: |
|
|
1006
|
+
test "$(git rev-parse HEAD)" = "$IMAGE_TAG"
|
|
1007
|
+
git merge-base --is-ancestor "$IMAGE_TAG" origin/main
|
|
1008
|
+
|
|
1009
|
+
- name: Configure AWS credentials
|
|
1010
|
+
uses: aws-actions/configure-aws-credentials@v6
|
|
1011
|
+
with:
|
|
1012
|
+
role-to-assume: \${{ vars.AWS_ROLE_ARN }}
|
|
1013
|
+
aws-region: \${{ vars.AWS_REGION }}
|
|
1014
|
+
|
|
1015
|
+
- name: Resolve and verify image
|
|
1016
|
+
id: image
|
|
1017
|
+
run: |
|
|
1018
|
+
registry="\${AWS_ACCOUNT_ID}.dkr.ecr.\${AWS_REGION}.amazonaws.com"
|
|
1019
|
+
image="\${registry}/\${ECR_REPOSITORY}"
|
|
1020
|
+
digest="$(aws ecr describe-images \\
|
|
1021
|
+
--repository-name "$ECR_REPOSITORY" \\
|
|
1022
|
+
--image-ids "imageTag=$IMAGE_TAG" \\
|
|
1023
|
+
--query 'imageDetails[0].imageDigest' \\
|
|
1024
|
+
--output text)"
|
|
1025
|
+
test -n "$digest"
|
|
1026
|
+
test "$digest" != "None"
|
|
1027
|
+
echo "registry=\${registry}" >> "$GITHUB_OUTPUT"
|
|
1028
|
+
echo "image=\${image}" >> "$GITHUB_OUTPUT"
|
|
1029
|
+
echo "digest=\${digest}" >> "$GITHUB_OUTPUT"
|
|
1030
|
+
|
|
1031
|
+
- name: Configure deployment SSH
|
|
1032
|
+
env:
|
|
1033
|
+
DEPLOY_SSH_PRIVATE_KEY: \${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
|
|
1034
|
+
DEPLOY_SSH_KNOWN_HOSTS: \${{ secrets.DEPLOY_SSH_KNOWN_HOSTS }}
|
|
1035
|
+
run: |
|
|
1036
|
+
test -n "$DEPLOY_SSH_PRIVATE_KEY"
|
|
1037
|
+
test -n "$DEPLOY_SSH_KNOWN_HOSTS"
|
|
1038
|
+
install -m 700 -d ~/.ssh
|
|
1039
|
+
printf '%s\\n' "$DEPLOY_SSH_PRIVATE_KEY" > ~/.ssh/${options.name}-deploy
|
|
1040
|
+
chmod 600 ~/.ssh/${options.name}-deploy
|
|
1041
|
+
printf '%s\\n' "$DEPLOY_SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
|
1042
|
+
|
|
1043
|
+
- name: Authenticate server with ECR
|
|
1044
|
+
run: |
|
|
1045
|
+
aws ecr get-login-password --region "$AWS_REGION" |
|
|
1046
|
+
ssh -i ~/.ssh/${options.name}-deploy "\${DEPLOY_USER}@\${DEPLOY_HOST}" \\
|
|
1047
|
+
"docker login --username AWS --password-stdin '\${{ steps.image.outputs.registry }}'"
|
|
1048
|
+
|
|
1049
|
+
- name: Install deployment files
|
|
1050
|
+
run: |
|
|
1051
|
+
ssh -i ~/.ssh/${options.name}-deploy "\${DEPLOY_USER}@\${DEPLOY_HOST}" \\
|
|
1052
|
+
"install -m 755 -d /opt/${options.name}/current/deploy/${deploymentArtifactDirectory}"
|
|
1053
|
+
scp -i ~/.ssh/${options.name}-deploy \\
|
|
1054
|
+
apps/service/docker-compose.yml \\
|
|
1055
|
+
apps/service/scripts/deploy_remote.sh \\
|
|
1056
|
+
"\${DEPLOY_USER}@\${DEPLOY_HOST}:/opt/${options.name}/current/"
|
|
1057
|
+
scp -i ~/.ssh/${options.name}-deploy \\
|
|
1058
|
+
apps/service/deploy/${deploymentArtifactDirectory}/${deploymentArtifactFile} \\
|
|
1059
|
+
"\${DEPLOY_USER}@\${DEPLOY_HOST}:/opt/${options.name}/current/deploy/${deploymentArtifactDirectory}/"
|
|
1060
|
+
ssh -i ~/.ssh/${options.name}-deploy "\${DEPLOY_USER}@\${DEPLOY_HOST}" \\
|
|
1061
|
+
"chmod 755 /opt/${options.name}/current/deploy_remote.sh${deploymentArtifactChmod}"
|
|
1062
|
+
|
|
1063
|
+
- name: Deploy image
|
|
1064
|
+
run: |
|
|
1065
|
+
ssh -i ~/.ssh/${options.name}-deploy "\${DEPLOY_USER}@\${DEPLOY_HOST}" \\
|
|
1066
|
+
"/opt/${options.name}/current/deploy_remote.sh '$IMAGE_TAG' '\${{ steps.image.outputs.image }}'"
|
|
1067
|
+
|
|
1068
|
+
- name: Record deployment
|
|
1069
|
+
env:
|
|
1070
|
+
IMAGE: \${{ steps.image.outputs.image }}
|
|
1071
|
+
IMAGE_DIGEST: \${{ steps.image.outputs.digest }}
|
|
1072
|
+
run: |
|
|
1073
|
+
{
|
|
1074
|
+
echo "### ${title} production deployment"
|
|
1075
|
+
echo
|
|
1076
|
+
echo "- Image: \`\${IMAGE}:\${IMAGE_TAG}\`"
|
|
1077
|
+
echo "- Digest: \`\${IMAGE_DIGEST}\`"
|
|
1078
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
874
1079
|
`);
|
|
875
1080
|
add(`${svc}/scripts/deploy_remote.sh`, `#!/usr/bin/env bash
|
|
876
1081
|
set -euo pipefail
|
|
@@ -923,9 +1128,20 @@ add(`${svc}/docs/production-deployment.md`, `# Production deployment
|
|
|
923
1128
|
|
|
924
1129
|
## Delivery model
|
|
925
1130
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1131
|
+
Delivery is split into two GitHub Actions workflows:
|
|
1132
|
+
|
|
1133
|
+
1. \`release-image.yml\` validates pull requests into \`main\`. After merge, the resulting \`main\` push is validated
|
|
1134
|
+
again before the workflow builds one non-root production image, tags it with the full 40-character product commit
|
|
1135
|
+
SHA, and pushes it to ECR. If that tag already exists, the workflow reuses it instead of rebuilding it.
|
|
1136
|
+
2. \`deploy.yml\` is manual-only. It runs only from product \`main\`, accepts the full SHA of an existing ECR image,
|
|
1137
|
+
verifies the commit belongs to \`main\` history, checks out that exact commit for deployment files, and updates the host.
|
|
1138
|
+
|
|
1139
|
+
Only service${options.ui ? ", UI," : ","} workspace, lockfile, architecture, and release-workflow changes trigger validation.
|
|
1140
|
+
The ECR build job runs only for the post-merge \`main\` push and remains skipped until the repository AWS variables are
|
|
1141
|
+
configured. No push or merge deploys production automatically.
|
|
1142
|
+
|
|
1143
|
+
Configure the ECR repository with immutable image tags and restrict routine push permission to the repository's
|
|
1144
|
+
release workflow. Record the full commit SHA and ECR digest; deployment and migration must use that release image.
|
|
929
1145
|
|
|
930
1146
|
The host separates replaceable artifacts from persistent state:
|
|
931
1147
|
|
|
@@ -934,14 +1150,15 @@ The host separates replaceable artifacts from persistent state:
|
|
|
934
1150
|
├── current/
|
|
935
1151
|
│ ├── docker-compose.yml
|
|
936
1152
|
│ ├── deploy_remote.sh
|
|
937
|
-
│ └── deploy
|
|
1153
|
+
│ └── deploy/${deploymentArtifactDirectory}/${deploymentArtifactFile}
|
|
938
1154
|
└── shared/
|
|
939
1155
|
├── .env.production
|
|
940
1156
|
├── .env.infrastructure
|
|
941
1157
|
${options.auth === "firebase" ? ` ├── secrets/firebase-service-account.json
|
|
942
|
-
` : ""} ├──
|
|
1158
|
+
` : ""}${postgres ? ` ├── postgres/data/
|
|
1159
|
+
` : ` ├── mongo/config/keyfile
|
|
943
1160
|
├── mongo/data/
|
|
944
|
-
${options.redis ? ` └── redis/data/
|
|
1161
|
+
`}${options.redis ? ` └── redis/data/
|
|
945
1162
|
` : ""}\`\`\`
|
|
946
1163
|
|
|
947
1164
|
Actions may replace \`current/\` and must never replace \`shared/\`. The server never needs a source checkout.
|
|
@@ -960,42 +1177,53 @@ Do not inject \`.env.infrastructure\` into Node. Make both host environment file
|
|
|
960
1177
|
## One-time host bootstrap
|
|
961
1178
|
|
|
962
1179
|
1. Install Docker Engine and Docker Compose.
|
|
963
|
-
|
|
964
|
-
|
|
1180
|
+
${postgres
|
|
1181
|
+
? `2. Create \`/opt/${options.name}/current/deploy/postgres\` and \`/opt/${options.name}/shared/postgres/data\`${options.redis ? `, plus \`/opt/${options.name}/shared/redis/data\`` : ""}.`
|
|
1182
|
+
: `2. Create \`/opt/${options.name}/current/deploy/mongo\`, \`/opt/${options.name}/shared/mongo/config\`, and
|
|
1183
|
+
\`/opt/${options.name}/shared/mongo/data\`${options.redis ? `, plus \`/opt/${options.name}/shared/redis/data\`` : ""}.`}
|
|
965
1184
|
3. Install \`.env.production\` and a completed \`.env.infrastructure\` under \`shared/\`.
|
|
966
|
-
4. Generate the MongoDB replica key with \`openssl rand -base64 756\`, make it owned by UID/GID \`999:999\`, and
|
|
1185
|
+
${postgres ? "" : `4. Generate the MongoDB replica key with \`openssl rand -base64 756\`, make it owned by UID/GID \`999:999\`, and
|
|
967
1186
|
mode \`0400\`.
|
|
968
|
-
${options.auth === "firebase" ?
|
|
969
|
-
|
|
970
|
-
${options.auth === "firebase" ? "7" : "6"}. Authorize the dedicated deployment SSH key and record the host key for GitHub Actions.
|
|
1187
|
+
`}${options.auth === "firebase" ? `${postgres ? "4" : "5"}. Install the Firebase service account at \`shared/secrets/firebase-service-account.json\`, owned by \`root:1000\` and mode \`0640\`.
|
|
1188
|
+
` : ""}${postgres ? (options.auth === "firebase" ? "5" : "4") : (options.auth === "firebase" ? "6" : "5")}. Give the deployment user Docker access and write access to \`current/\`; keep \`shared/\` and its files protected.
|
|
1189
|
+
${postgres ? (options.auth === "firebase" ? "6" : "5") : (options.auth === "firebase" ? "7" : "6")}. Authorize the dedicated deployment SSH key and record the host key for GitHub Actions.
|
|
971
1190
|
|
|
972
|
-
Use a fresh
|
|
1191
|
+
Use a fresh ${postgres ? "PostgreSQL" : "MongoDB"} data directory. Migrating an existing unauthenticated database requires a separately reviewed plan.
|
|
973
1192
|
|
|
974
|
-
## GitHub
|
|
1193
|
+
## GitHub repository variables
|
|
1194
|
+
|
|
1195
|
+
Configure \`AWS_ACCOUNT_ID\`, \`AWS_REGION\`, and \`AWS_ROLE_ARN\` as repository variables for the
|
|
1196
|
+
repository-restricted OIDC role. Optionally set \`ECR_REPOSITORY\`; the default is \`${options.name}\`.
|
|
1197
|
+
|
|
1198
|
+
The ECR repository and AWS OIDC provider/role are provisioned separately. Workflows never store long-lived AWS keys
|
|
1199
|
+
or create cloud infrastructure.
|
|
975
1200
|
|
|
976
|
-
|
|
1201
|
+
## GitHub release environment
|
|
977
1202
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1203
|
+
${options.ui && options.auth === "firebase" ? `Configure the \`UI_ENV_PRODUCTION\` secret for the Vite/Firebase build-time environment. The release environment MAY require approval before exposing production-capable browser configuration.
|
|
1204
|
+
` : "The release workflow uses this environment without creating a deployment record. It MAY require approval before publishing an image.\n"}
|
|
1205
|
+
## GitHub production environment
|
|
981
1206
|
|
|
982
|
-
Configure these secrets:
|
|
1207
|
+
Configure \`DEPLOY_HOST\` and \`DEPLOY_USER\` as production-environment variables. Configure these secrets:
|
|
983
1208
|
|
|
984
|
-
|
|
985
|
-
` : ""}- \`DEPLOY_SSH_PRIVATE_KEY\`: dedicated host deployment key.
|
|
1209
|
+
- \`DEPLOY_SSH_PRIVATE_KEY\`: dedicated host deployment key.
|
|
986
1210
|
- \`DEPLOY_SSH_KNOWN_HOSTS\`: pinned host key.
|
|
987
1211
|
|
|
988
|
-
|
|
989
|
-
|
|
1212
|
+
Restrict the production environment's deployment branches to \`main\`. Required reviewer approval MAY add a second
|
|
1213
|
+
manual gate, but the branch restriction and manual full-SHA input remain mandatory.
|
|
990
1214
|
|
|
991
1215
|
## Deploy and rollback
|
|
992
1216
|
|
|
1217
|
+
Run \`Deploy release image\` manually from \`main\` and enter the full SHA recorded by the release workflow. The
|
|
1218
|
+
deployment fails before contacting the host if the SHA is invalid, is not in \`main\` history, cannot be checked out,
|
|
1219
|
+
or has no matching ECR image.
|
|
1220
|
+
|
|
993
1221
|
The remote script requires the provisioned infrastructure file, atomically replaces only \`ECR_IMAGE\` and
|
|
994
1222
|
\`IMAGE_TAG\`, validates Compose, pulls, and starts with health waiting. Roll back by selecting a previous immutable
|
|
995
|
-
|
|
996
|
-
database state.
|
|
1223
|
+
full-SHA release and running the same workflow. Never use \`docker compose down -v\` in production; image rollback
|
|
1224
|
+
does not restore database state.
|
|
997
1225
|
|
|
998
|
-
MongoDB and optional Redis expose no host ports and require authentication. Verify API readiness, container health,
|
|
1226
|
+
${postgres ? "PostgreSQL" : "MongoDB"} and optional Redis expose no host ports and require authentication. Verify API readiness, container health,
|
|
999
1227
|
database/cache authentication, file permissions, and off-host backup/restore before treating the deployment as
|
|
1000
1228
|
production-ready. Product-specific TLS, capacity limits, migrations, and scheduled-work grace periods must be added
|
|
1001
1229
|
from measured application requirements rather than copied from another product.
|
|
@@ -1005,8 +1233,9 @@ const serviceVolumes = [
|
|
|
1005
1233
|
...(options.auth === "firebase" ? [" - ${SECRETS_DIR:?Set SECRETS_DIR}/firebase-service-account.json:/app/secrets/firebase-service-account.json:ro"] : [])
|
|
1006
1234
|
];
|
|
1007
1235
|
const devVolumes = [
|
|
1008
|
-
" - .:/
|
|
1009
|
-
" - /
|
|
1236
|
+
" - .:/workspace/apps/service",
|
|
1237
|
+
" - /workspace/node_modules",
|
|
1238
|
+
" - /workspace/apps/service/node_modules",
|
|
1010
1239
|
...(options.auth === "firebase" ? [" - ./firebase-service-account.development.json:/app/firebase-service-account.development.json:ro"] : [])
|
|
1011
1240
|
];
|
|
1012
1241
|
const productionDependencies = [
|
|
@@ -1031,9 +1260,13 @@ const productionEnvironment = [
|
|
|
1031
1260
|
...(options.redis ? [" REDIS_URL: \"redis://:\${REDIS_PASSWORD:?Set REDIS_PASSWORD}@redis:6379\""] : [])
|
|
1032
1261
|
];
|
|
1033
1262
|
const productionEnvironmentBlock = `\n environment:\n${productionEnvironment.join("\n")}`;
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1263
|
+
const developmentEnvironment = [
|
|
1264
|
+
postgres
|
|
1265
|
+
? ` POSTGRES_URI: "postgresql://${options.name}-app:development@postgres:5432/${databaseName}"`
|
|
1266
|
+
: ` MONGODB_URI: "mongodb://mongodb:27017/?replicaSet=rs0&directConnection=true"`,
|
|
1267
|
+
...(options.auth === "firebase" ? [" FIREBASE_SERVICE_ACCOUNT_PATH: /app/firebase-service-account.development.json"] : [])
|
|
1268
|
+
];
|
|
1269
|
+
const developmentEnvironmentBlock = `\n environment:\n${developmentEnvironment.join("\n")}`;
|
|
1037
1270
|
const volumeBlock = serviceVolumes.length ? `\n volumes:\n${serviceVolumes.join("\n")}` : "";
|
|
1038
1271
|
const workerService = options.worker ? `\n worker:
|
|
1039
1272
|
image: \${ECR_IMAGE:?Set ECR_IMAGE}:\${IMAGE_TAG:?Set IMAGE_TAG}
|
|
@@ -1206,7 +1439,7 @@ const devWorker = options.worker ? `\n worker:
|
|
|
1206
1439
|
dockerfile: Dockerfile.dev
|
|
1207
1440
|
env_file:
|
|
1208
1441
|
- .env.development
|
|
1209
|
-
command: ["npm", "run", "dev:worker"]${
|
|
1442
|
+
command: ["npm", "run", "dev:worker"]${developmentEnvironmentBlock}
|
|
1210
1443
|
volumes:
|
|
1211
1444
|
${devVolumes.join("\n")}${devDependsBlock}` : "";
|
|
1212
1445
|
const devDatabase = postgres ? `\n postgres:
|
|
@@ -1270,7 +1503,7 @@ services:
|
|
|
1270
1503
|
dockerfile: Dockerfile.dev
|
|
1271
1504
|
env_file:
|
|
1272
1505
|
- .env.development
|
|
1273
|
-
command: ["npm", "run", "schema:apply:dev"]${
|
|
1506
|
+
command: ["npm", "run", "schema:apply:dev"]${developmentEnvironmentBlock}
|
|
1274
1507
|
volumes:
|
|
1275
1508
|
${devVolumes.join("\n")}
|
|
1276
1509
|
depends_on:
|
|
@@ -1283,7 +1516,7 @@ ${devVolumes.join("\n")}
|
|
|
1283
1516
|
env_file:
|
|
1284
1517
|
- .env.development
|
|
1285
1518
|
ports:
|
|
1286
|
-
- "3000:3000"${
|
|
1519
|
+
- "3000:3000"${developmentEnvironmentBlock}
|
|
1287
1520
|
volumes:
|
|
1288
1521
|
${devVolumes.join("\n")}${devDependsBlock}${devWorker}${devDatabase}${devRedis}
|
|
1289
1522
|
volumes:
|
package/src/validate.mjs
CHANGED
|
@@ -65,6 +65,50 @@ function validateAppMetadata(productRoot, app) {
|
|
|
65
65
|
const errors = [];
|
|
66
66
|
const root = appRoot(productRoot, app);
|
|
67
67
|
if (!fs.existsSync(root)) return [`Missing configured application root: ${app.path}`];
|
|
68
|
+
if (app.kind === "service") {
|
|
69
|
+
const composeFile = path.join(root, "docker-compose.dev.yml");
|
|
70
|
+
if (!fs.existsSync(composeFile)) errors.push(`${app.path}/docker-compose.dev.yml is required`);
|
|
71
|
+
else {
|
|
72
|
+
const compose = fs.readFileSync(composeFile, "utf8");
|
|
73
|
+
const generatedProcessCount = app.profiles.includes("worker") ? 3 : 2;
|
|
74
|
+
const count = (term) => compose.split(term).length - 1;
|
|
75
|
+
for (const term of [
|
|
76
|
+
" - .:/workspace/apps/service",
|
|
77
|
+
" - /workspace/node_modules",
|
|
78
|
+
" - /workspace/apps/service/node_modules"
|
|
79
|
+
]) {
|
|
80
|
+
if (count(term) < generatedProcessCount) {
|
|
81
|
+
errors.push(`${app.path}/docker-compose.dev.yml must mount every generated service process at the image workspace: ${term.trim()}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (compose.includes(" - .:/app\n")) {
|
|
85
|
+
errors.push(`${app.path}/docker-compose.dev.yml uses the obsolete /app service source mount`);
|
|
86
|
+
}
|
|
87
|
+
if (app.profiles.includes("firebase")) {
|
|
88
|
+
for (const term of [
|
|
89
|
+
" FIREBASE_SERVICE_ACCOUNT_PATH: /app/firebase-service-account.development.json",
|
|
90
|
+
" - ./firebase-service-account.development.json:/app/firebase-service-account.development.json:ro"
|
|
91
|
+
]) {
|
|
92
|
+
if (count(term) < generatedProcessCount) {
|
|
93
|
+
errors.push(`${app.path}/docker-compose.dev.yml must use the canonical Firebase development credential path for every generated service process: ${term.trim()}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (app.kind === "ui") {
|
|
100
|
+
const generatedArtifacts = fs.readdirSync(root, { withFileTypes: true })
|
|
101
|
+
.filter(
|
|
102
|
+
(entry) =>
|
|
103
|
+
entry.isFile() &&
|
|
104
|
+
(entry.name.endsWith(".tsbuildinfo") || ["vite.config.js", "vite.config.d.ts"].includes(entry.name))
|
|
105
|
+
)
|
|
106
|
+
.map((entry) => entry.name)
|
|
107
|
+
.sort();
|
|
108
|
+
for (const artifact of generatedArtifacts) {
|
|
109
|
+
errors.push(`${app.path}/${artifact} is generated compiler output; keep incremental state under apps/ui/.tmp`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
68
112
|
if (fs.existsSync(path.join(root, "tailframe.json"))) {
|
|
69
113
|
errors.push(`${app.path}/tailframe.json is forbidden; the product root owns Tailframe metadata`);
|
|
70
114
|
}
|