@zaaxch/tailframe 4.0.1 → 4.0.3
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 +352 -118
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zaaxch/tailframe",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
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 with distinct AWS roles: the release role publishes, while the pull-only deployment role manually deploys from \`main\` using an already-published full SHA from \`main\` history and that commit's deployment files. Product data migrations must authenticate to the registry, pull and digest-verify that exact release image, and explicitly select the production datastore; never inherit rehearsal image or target defaults. 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_RELEASE_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_RELEASE_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
|
+
printf -- '- Image: \`%s:%s\`\\n' "$IMAGE" "$IMAGE_TAG"
|
|
954
|
+
printf -- '- Digest: \`%s\`\\n' "$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
|
|
841
974
|
|
|
842
|
-
|
|
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_DEPLOY_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
|
+
printf -- '- Image: \`%s:%s\`\\n' "$IMAGE" "$IMAGE_TAG"
|
|
1077
|
+
printf -- '- Digest: \`%s\`\\n' "$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. The release-only role may publish; the distinct deployment
|
|
1144
|
+
role is pull-only. Record the full commit SHA and ECR digest; deployment and product migrations 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,61 @@ 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
|
|
977
1194
|
|
|
978
|
-
Configure
|
|
1195
|
+
Configure \`AWS_ACCOUNT_ID\`, \`AWS_REGION\`, \`AWS_RELEASE_ROLE_ARN\`, and \`AWS_DEPLOY_ROLE_ARN\` as repository
|
|
1196
|
+
variables. Optionally set \`ECR_REPOSITORY\`; the default is \`${options.name}\`.
|
|
979
1197
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
- Optional \`ECR_REPOSITORY\`; the default is \`${options.name}\`.
|
|
1198
|
+
The ECR repository and AWS OIDC provider/roles are provisioned separately. Workflows never store long-lived AWS keys
|
|
1199
|
+
or create cloud infrastructure.
|
|
983
1200
|
|
|
984
|
-
|
|
1201
|
+
## GitHub release environment
|
|
985
1202
|
|
|
986
|
-
${options.ui && options.auth === "firebase" ?
|
|
987
|
-
` : "
|
|
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
|
|
1206
|
+
|
|
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 both environments' deployment branches to \`main\`. The release role trust accepts only the repository's
|
|
1213
|
+
\`release\` environment OIDC subject; the pull-only deployment role accepts only its \`production\` subject. Use the
|
|
1214
|
+
repository's active GitHub subject format: legacy subjects name the owner/repository, while immutable subjects use
|
|
1215
|
+
their IDs. Do not use a repository-wide wildcard. Required reviewer approval MAY add a second manual gate.
|
|
992
1216
|
|
|
993
1217
|
## Deploy and rollback
|
|
994
1218
|
|
|
1219
|
+
Run \`Deploy release image\` manually from \`main\` and enter the full SHA recorded by the release workflow. The
|
|
1220
|
+
deployment fails before contacting the host if the SHA is invalid, is not in \`main\` history, cannot be checked out,
|
|
1221
|
+
or has no matching ECR image.
|
|
1222
|
+
|
|
995
1223
|
The remote script requires the provisioned infrastructure file, atomically replaces only \`ECR_IMAGE\` and
|
|
996
1224
|
\`IMAGE_TAG\`, validates Compose, pulls, and starts with health waiting. Roll back by selecting a previous immutable
|
|
997
|
-
|
|
998
|
-
database state.
|
|
1225
|
+
full-SHA release and running the same workflow. Never use \`docker compose down -v\` in production; image rollback
|
|
1226
|
+
does not restore database state.
|
|
1227
|
+
|
|
1228
|
+
## Product data-migration handoff
|
|
1229
|
+
|
|
1230
|
+
Tailframe does not generate product-specific data-migration orchestration. Before a production data migration,
|
|
1231
|
+
authenticate to ECR, pull the selected full-SHA release image, verify its digest against the release record, and
|
|
1232
|
+
explicitly configure the production datastore target. Never inherit a rehearsal URI, local image default, or source build.
|
|
999
1233
|
|
|
1000
|
-
MongoDB and optional Redis expose no host ports and require authentication. Verify API readiness, container health,
|
|
1234
|
+
${postgres ? "PostgreSQL" : "MongoDB"} and optional Redis expose no host ports and require authentication. Verify API readiness, container health,
|
|
1001
1235
|
database/cache authentication, file permissions, and off-host backup/restore before treating the deployment as
|
|
1002
1236
|
production-ready. Product-specific TLS, capacity limits, migrations, and scheduled-work grace periods must be added
|
|
1003
1237
|
from measured application requirements rather than copied from another product.
|