@zaaxch/tailframe 4.0.1 → 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 +344 -118
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
|
@@ -243,8 +243,7 @@ add(`${svc}/package.json`, JSON.stringify({
|
|
|
243
243
|
"schema:apply": "node dist/app/cli/applySchema.js", "schema:apply:dev": "ts-node -r tsconfig-paths/register src/app/cli/applySchema.ts",
|
|
244
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",
|
|
245
245
|
"docker:dev": "docker compose -f docker-compose.dev.yml up",
|
|
246
|
-
"docker:prod": "docker compose -f docker-compose.yml up -d"
|
|
247
|
-
"docker:push": "bash scripts/build_and_push.sh"
|
|
246
|
+
"docker:prod": "docker compose -f docker-compose.yml up -d"
|
|
248
247
|
}, dependencies: svcDeps, devDependencies: svcDevDeps
|
|
249
248
|
}, null, "\t"));
|
|
250
249
|
add(`${svc}/tsconfig.json`, JSON.stringify({
|
|
@@ -552,10 +551,10 @@ ${options.ui ? "- In production, serve the compiled UI from `dist/public` with a
|
|
|
552
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.` : ""}
|
|
553
552
|
|
|
554
553
|
## Production deployment
|
|
555
|
-
|
|
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\`.
|
|
556
555
|
|
|
557
556
|
## Tests and validation
|
|
558
|
-
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.
|
|
559
558
|
`);
|
|
560
559
|
|
|
561
560
|
if (options.ui) {
|
|
@@ -757,7 +756,7 @@ set -euo pipefail
|
|
|
757
756
|
|
|
758
757
|
SERVICE_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
|
|
759
758
|
PRODUCT_ROOT="$(cd "\${SERVICE_DIR}/../.." && pwd)"
|
|
760
|
-
PRODUCT_SHA="$(git -C "\${PRODUCT_ROOT}" rev-parse
|
|
759
|
+
PRODUCT_SHA="$(git -C "\${PRODUCT_ROOT}" rev-parse HEAD)"
|
|
761
760
|
ECR_REPOSITORY="\${ECR_REPOSITORY:-${options.name}}"
|
|
762
761
|
IMAGE_TAG="\${IMAGE_TAG:-\${PRODUCT_SHA}}"
|
|
763
762
|
REGISTRY="\${AWS_ACCOUNT_ID}.dkr.ecr.\${AWS_REGION}.amazonaws.com"
|
|
@@ -765,6 +764,11 @@ IMAGE="\${REGISTRY}/\${ECR_REPOSITORY}:\${IMAGE_TAG}"
|
|
|
765
764
|
DOCKERFILE="\${SERVICE_DIR}/Dockerfile"
|
|
766
765
|
${options.ui && options.auth === "firebase" ? `UI_ENV_FILE="\${UI_ENV_FILE:-\${PRODUCT_ROOT}/apps/ui/.env.production}"` : ""}
|
|
767
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
|
+
|
|
768
772
|
aws ecr get-login-password --region "\${AWS_REGION}" |
|
|
769
773
|
docker login --username AWS --password-stdin "\${REGISTRY}"
|
|
770
774
|
|
|
@@ -773,106 +777,305 @@ docker push "\${IMAGE}"
|
|
|
773
777
|
|
|
774
778
|
echo "Pushed \${IMAGE}"
|
|
775
779
|
`);
|
|
776
|
-
const workflowUiPath = options.ui ? '
|
|
780
|
+
const workflowUiPath = options.ui ? ' - "apps/ui/**"\n' : "";
|
|
777
781
|
const workflowUiEnvironment = options.ui && options.auth === "firebase" ? `
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
|
783
789
|
` : "";
|
|
784
|
-
|
|
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
|
|
785
794
|
|
|
786
795
|
on:
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
${workflowUiPath}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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"
|
|
797
816
|
|
|
798
817
|
permissions:
|
|
799
|
-
|
|
800
|
-
id-token: write
|
|
818
|
+
contents: read
|
|
801
819
|
|
|
802
820
|
concurrency:
|
|
803
|
-
|
|
804
|
-
|
|
821
|
+
group: ${options.name}-release-\${{ github.ref }}
|
|
822
|
+
cancel-in-progress: true
|
|
805
823
|
|
|
806
824
|
jobs:
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
|
838
883
|
env:
|
|
839
|
-
|
|
840
|
-
|
|
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
|
|
841
966
|
|
|
842
|
-
|
|
967
|
+
permissions:
|
|
968
|
+
contents: read
|
|
969
|
+
id-token: write
|
|
970
|
+
|
|
971
|
+
concurrency:
|
|
972
|
+
group: ${options.name}-production
|
|
973
|
+
cancel-in-progress: false
|
|
974
|
+
|
|
975
|
+
jobs:
|
|
976
|
+
deploy:
|
|
977
|
+
name: Deploy production
|
|
978
|
+
if: github.ref == 'refs/heads/main'
|
|
979
|
+
runs-on: ubuntu-latest
|
|
980
|
+
environment: production
|
|
843
981
|
env:
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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"
|
|
876
1079
|
`);
|
|
877
1080
|
add(`${svc}/scripts/deploy_remote.sh`, `#!/usr/bin/env bash
|
|
878
1081
|
set -euo pipefail
|
|
@@ -925,9 +1128,20 @@ add(`${svc}/docs/production-deployment.md`, `# Production deployment
|
|
|
925
1128
|
|
|
926
1129
|
## Delivery model
|
|
927
1130
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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.
|
|
931
1145
|
|
|
932
1146
|
The host separates replaceable artifacts from persistent state:
|
|
933
1147
|
|
|
@@ -936,14 +1150,15 @@ The host separates replaceable artifacts from persistent state:
|
|
|
936
1150
|
├── current/
|
|
937
1151
|
│ ├── docker-compose.yml
|
|
938
1152
|
│ ├── deploy_remote.sh
|
|
939
|
-
│ └── deploy
|
|
1153
|
+
│ └── deploy/${deploymentArtifactDirectory}/${deploymentArtifactFile}
|
|
940
1154
|
└── shared/
|
|
941
1155
|
├── .env.production
|
|
942
1156
|
├── .env.infrastructure
|
|
943
1157
|
${options.auth === "firebase" ? ` ├── secrets/firebase-service-account.json
|
|
944
|
-
` : ""} ├──
|
|
1158
|
+
` : ""}${postgres ? ` ├── postgres/data/
|
|
1159
|
+
` : ` ├── mongo/config/keyfile
|
|
945
1160
|
├── mongo/data/
|
|
946
|
-
${options.redis ? ` └── redis/data/
|
|
1161
|
+
`}${options.redis ? ` └── redis/data/
|
|
947
1162
|
` : ""}\`\`\`
|
|
948
1163
|
|
|
949
1164
|
Actions may replace \`current/\` and must never replace \`shared/\`. The server never needs a source checkout.
|
|
@@ -962,42 +1177,53 @@ Do not inject \`.env.infrastructure\` into Node. Make both host environment file
|
|
|
962
1177
|
## One-time host bootstrap
|
|
963
1178
|
|
|
964
1179
|
1. Install Docker Engine and Docker Compose.
|
|
965
|
-
|
|
966
|
-
|
|
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\`` : ""}.`}
|
|
967
1184
|
3. Install \`.env.production\` and a completed \`.env.infrastructure\` under \`shared/\`.
|
|
968
|
-
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
|
|
969
1186
|
mode \`0400\`.
|
|
970
|
-
${options.auth === "firebase" ?
|
|
971
|
-
|
|
972
|
-
${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.
|
|
973
1190
|
|
|
974
|
-
Use a fresh
|
|
1191
|
+
Use a fresh ${postgres ? "PostgreSQL" : "MongoDB"} data directory. Migrating an existing unauthenticated database requires a separately reviewed plan.
|
|
975
1192
|
|
|
976
|
-
## 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}\`.
|
|
977
1197
|
|
|
978
|
-
|
|
1198
|
+
The ECR repository and AWS OIDC provider/role are provisioned separately. Workflows never store long-lived AWS keys
|
|
1199
|
+
or create cloud infrastructure.
|
|
979
1200
|
|
|
980
|
-
|
|
981
|
-
- \`DEPLOY_HOST\` and \`DEPLOY_USER\`; the deployment job remains skipped until both exist.
|
|
982
|
-
- Optional \`ECR_REPOSITORY\`; the default is \`${options.name}\`.
|
|
1201
|
+
## GitHub release environment
|
|
983
1202
|
|
|
984
|
-
Configure
|
|
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
|
|
985
1206
|
|
|
986
|
-
|
|
987
|
-
|
|
1207
|
+
Configure \`DEPLOY_HOST\` and \`DEPLOY_USER\` as production-environment variables. Configure these secrets:
|
|
1208
|
+
|
|
1209
|
+
- \`DEPLOY_SSH_PRIVATE_KEY\`: dedicated host deployment key.
|
|
988
1210
|
- \`DEPLOY_SSH_KNOWN_HOSTS\`: pinned host key.
|
|
989
1211
|
|
|
990
|
-
|
|
991
|
-
|
|
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.
|
|
992
1214
|
|
|
993
1215
|
## Deploy and rollback
|
|
994
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
|
+
|
|
995
1221
|
The remote script requires the provisioned infrastructure file, atomically replaces only \`ECR_IMAGE\` and
|
|
996
1222
|
\`IMAGE_TAG\`, validates Compose, pulls, and starts with health waiting. Roll back by selecting a previous immutable
|
|
997
|
-
|
|
998
|
-
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.
|
|
999
1225
|
|
|
1000
|
-
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,
|
|
1001
1227
|
database/cache authentication, file permissions, and off-host backup/restore before treating the deployment as
|
|
1002
1228
|
production-ready. Product-specific TLS, capacity limits, migrations, and scheduled-work grace periods must be added
|
|
1003
1229
|
from measured application requirements rather than copied from another product.
|