From d5bae868a413d14eca7b4e9bd539359469316e2f Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Thu, 30 Jul 2026 01:25:24 +0000 Subject: [PATCH] =?UTF-8?q?feat(P2):=20Nova=20rebrand=20=E2=80=94=20code/e?= =?UTF-8?q?nv-vars/consumer-path=20(REQ-158/159/160)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit core/env.py dual-read helper (D-108); 21 ACDL_*→NOVA_* env vars migrated across core/scripts/adapters/tests/workflows + .env/.env.secrets (key rename, values stay). G-106 binding: run_platform.sh:288-289 + regression_verify.py:309-312 dual-read (NOVA first, ACDL fallback). G-108 binding: Gitea NOVA_* secrets created via API + workflow secrets: refs updated (deploy.yml + modules-lifecycle.yml, .gitea + .github). acdl_tagging.py→nova_tagging.py (D-109 warn mode, nova:* enforced). .acdl/→.nova/ consumer path (resolver + deploy workflow + schema + tests + docs). Test fixtures updated; pytest + run_ci.sh PASS. ---ci--- project: acdl phase: 2 milestone: v1.15 status: execute ---/ci--- --- .gitea/workflows/deploy.yml | 18 ++-- .gitea/workflows/modules-lifecycle.yml | 54 ++++++------ .github/workflows/deploy.yml | 18 ++-- .github/workflows/modules-lifecycle.yml | 54 ++++++------ adapters/terraform/adapter.py | 11 +-- adapters/terraform/policy/checkov_adapter.py | 23 +++-- .../terraform/policy/custom_rules/README.md | 30 ++++--- .../policy/custom_rules/acdl_tagging.py | 54 ------------ .../policy/custom_rules/nova_tagging.py | 88 +++++++++++++++++++ core/attestation_matrix.py | 18 +++- core/contract_resolver.py | 17 +++- core/env.py | 48 ++++++++++ core/hitl_matrix_design.md | 6 +- core/local_emulators.py | 20 ++++- core/output_publisher.py | 24 +++-- core/regression_verify.py | 26 ++++-- core/separation_of_duties.py | 19 +++- docs/consumer-guide.md | 26 +++--- docs/contracts/index.md | 2 +- docs/index.md | 2 +- docs/pipeline/versioning.md | 2 +- .../developer-experience-02-what-dev-does.mmd | 2 +- .../the-developer-experience-marp.md | 2 +- .../the-developer-experience.html | 2 +- .../presentations/the-developer-experience.md | 2 +- schemas/README.md | 2 +- schemas/policy_check_result.schema.json | 2 +- schemas/tagging-standard.json | 2 +- scripts/push_consumer_image.py | 23 +++-- scripts/rotate_spike_key.sh | 36 +++++--- scripts/run_ci.sh | 3 +- scripts/run_l2_lifecycle_destroy.sh | 15 ++-- scripts/run_l2_lifecycle_test.sh | 22 +++-- scripts/run_lifecycle_destroy.sh | 6 +- scripts/run_lifecycle_test.sh | 8 +- scripts/run_platform.sh | 36 +++++--- scripts/run_regression.sh | 8 +- tests/test_adapter.py | 12 +-- tests/test_attestation_matrix.py | 4 +- tests/test_checkov_adapter.py | 12 +-- tests/test_deploy_workflow_env_input.py | 4 +- tests/test_env_helper.py | 56 ++++++++++++ tests/test_lifecycle_mode_flag.py | 19 ++-- tests/test_local_emulating_adapters.py | 12 +-- tests/test_output_publisher.py | 16 ++-- tests/test_pipeline_contract.py | 14 +-- tests/test_route_halt_artifact.py | 2 + tests/test_untested_scripts.py | 8 +- 48 files changed, 593 insertions(+), 297 deletions(-) delete mode 100644 adapters/terraform/policy/custom_rules/acdl_tagging.py create mode 100644 adapters/terraform/policy/custom_rules/nova_tagging.py create mode 100644 core/env.py create mode 100644 tests/test_env_helper.py diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 735521a..ee0b749 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -26,7 +26,7 @@ # platform log) for auditability. # # Inputs: -# contract — path to the consumer's contract YAML (default .acdl/contract.yml) +# contract — path to the consumer's contract YAML (default .nova/contract.yml) # mode — full | plan-only | check-only (default full; dev = full apply, # higher environments hold for HITL — the calling repo or the # forge environment gate enforces that) @@ -39,7 +39,7 @@ # policy restricts view/update to resources tagged acdl:owner=. # # Override (where OIDC is unavailable, e.g. Gitea pending -# go-gitea/gitea#36988): set ACDL_AWS_ACCESS_KEY_ID + ACDL_AWS_SECRET_ACCESS_KEY +# go-gitea/gitea#36988): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY # as repository secrets. The platform-managed scheduled pipeline rotates # the key on a daily cadence. When .env.secrets is used locally instead, # rotating the key out of band is the consumer's responsibility. @@ -51,7 +51,7 @@ on: contract: description: Path to the consumer contract YAML (in the consumer repo) type: string - default: .acdl/contract.yml + default: .nova/contract.yml mode: description: Pipeline mode — full (apply), plan-only, check-only, or decommission type: string @@ -102,10 +102,14 @@ jobs: - name: Configure AWS credentials (OIDC default + static-key override) uses: aws-actions/configure-aws-credentials@v4 with: - role-to-assume: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/acdl-deploy-{1}', secrets.ACDL_AWS_ACCOUNT_ID, github.repository_id) || '' }} + # TODO(P4, REQ-163): rename the IAM role acdl-deploy- → nova-deploy-. + # The role ARN string is left as acdl-deploy- until P4 (IAM role + # rename territory); only the secret REFERENCES are updated to + # NOVA_* in P2 (G-108 binding). + role-to-assume: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/acdl-deploy-{1}', secrets.NOVA_AWS_ACCOUNT_ID, github.repository_id) || '' }} aws-region: us-east-1 - access-key-id: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - secret-access-key: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + secret-access-key: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} - name: Run the platform pipeline working-directory: ${{ github.workspace }} @@ -145,7 +149,7 @@ jobs: AWS_DEFAULT_REGION: us-east-1 run: | aws lambda invoke-function-url \ - --function-url "${{ secrets.ACDL_LAMBDA_URL }}" \ + --function-url "${{ secrets.NOVA_LAMBDA_URL }}" \ --cli-binary-format raw-in-base64-out \ --payload "$(python3 -c "import json,os; print(json.dumps({'action':'report_error','consumerRepo':os.environ.get('GITHUB_REPOSITORY',''),'contractId':'${{ github.run_id }}','error':'Deploy pipeline failed. See run logs.','runUrl':'${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}','environment':'dev'}))")" \ /dev/null || true diff --git a/.gitea/workflows/modules-lifecycle.yml b/.gitea/workflows/modules-lifecycle.yml index 54520ac..03699c2 100644 --- a/.gitea/workflows/modules-lifecycle.yml +++ b/.gitea/workflows/modules-lifecycle.yml @@ -15,7 +15,7 @@ # "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast, # no AWS mutation, validates the contract->resolver->adapter->plan chain # for every module on every PR, with no AWS credentials or cost). Set to -# "full" via workflow_dispatch (or the ACDL_LIFECYCLE_MODE repo variable) +# "full" via workflow_dispatch (or the NOVA_LIFECYCLE_MODE repo variable) # to run the real apply→modify→destroy against live AWS. In plan mode the # short-lived CI VPC apply/destroy jobs are skipped (nothing is applied). # @@ -49,7 +49,7 @@ jobs: ci-vpc-apply: name: CI VPC apply runs-on: ubuntu-latest - if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.ACDL_LIFECYCLE_MODE != 'plan' }} + if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }} steps: - uses: actions/checkout@v4 - name: Install Terraform 1.9.* @@ -60,8 +60,8 @@ jobs: - name: Apply CI VPC working-directory: terraform/ci-vpc env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: | terraform init -input=false -lock=false @@ -78,7 +78,7 @@ jobs: matrix: module: [s3, kms-key, ecr, ecs-cluster, iam-role, cloudfront, waf, vpc, alb, ecs-service, rds, uptime] env: - ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }} + NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }} steps: - uses: actions/checkout@v4 - name: Free disk space @@ -97,31 +97,31 @@ jobs: echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list sudo apt-get update && sudo apt-get install -y terraform=1.9.* - name: Read CI VPC outputs - if: ${{ env.ACDL_LIFECYCLE_MODE == 'full' }} + if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }} working-directory: terraform/ci-vpc env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: | terraform init -input=false -lock=false terraform output -json > /tmp/ci-vpc-outputs.json - name: Apply (simple) env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json - name: Modify (complex) env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json - name: Destroy env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: bash scripts/run_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json @@ -136,7 +136,7 @@ jobs: matrix: module: [static-assets, microservice] env: - ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }} + NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }} steps: - uses: actions/checkout@v4 - name: Free disk space @@ -155,31 +155,31 @@ jobs: echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list sudo apt-get update && sudo apt-get install -y terraform=1.9.* - name: Read CI VPC outputs - if: ${{ env.ACDL_LIFECYCLE_MODE == 'full' }} + if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }} working-directory: terraform/ci-vpc env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: | terraform init -input=false -lock=false terraform output -json > /tmp/ci-vpc-outputs.json - name: Apply (simple) env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json - name: Modify (complex) env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json - name: Destroy env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: bash scripts/run_l2_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json @@ -188,7 +188,7 @@ jobs: name: CI VPC destroy needs: [lifecycle, l2-lifecycle] runs-on: ubuntu-latest - if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.ACDL_LIFECYCLE_MODE != 'plan' }} + if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }} steps: - uses: actions/checkout@v4 - name: Install Terraform 1.9.* @@ -199,8 +199,8 @@ jobs: - name: Destroy CI VPC working-directory: terraform/ci-vpc env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: | terraform init -input=false -lock=false diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 735521a..ee0b749 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -26,7 +26,7 @@ # platform log) for auditability. # # Inputs: -# contract — path to the consumer's contract YAML (default .acdl/contract.yml) +# contract — path to the consumer's contract YAML (default .nova/contract.yml) # mode — full | plan-only | check-only (default full; dev = full apply, # higher environments hold for HITL — the calling repo or the # forge environment gate enforces that) @@ -39,7 +39,7 @@ # policy restricts view/update to resources tagged acdl:owner=. # # Override (where OIDC is unavailable, e.g. Gitea pending -# go-gitea/gitea#36988): set ACDL_AWS_ACCESS_KEY_ID + ACDL_AWS_SECRET_ACCESS_KEY +# go-gitea/gitea#36988): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY # as repository secrets. The platform-managed scheduled pipeline rotates # the key on a daily cadence. When .env.secrets is used locally instead, # rotating the key out of band is the consumer's responsibility. @@ -51,7 +51,7 @@ on: contract: description: Path to the consumer contract YAML (in the consumer repo) type: string - default: .acdl/contract.yml + default: .nova/contract.yml mode: description: Pipeline mode — full (apply), plan-only, check-only, or decommission type: string @@ -102,10 +102,14 @@ jobs: - name: Configure AWS credentials (OIDC default + static-key override) uses: aws-actions/configure-aws-credentials@v4 with: - role-to-assume: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/acdl-deploy-{1}', secrets.ACDL_AWS_ACCOUNT_ID, github.repository_id) || '' }} + # TODO(P4, REQ-163): rename the IAM role acdl-deploy- → nova-deploy-. + # The role ARN string is left as acdl-deploy- until P4 (IAM role + # rename territory); only the secret REFERENCES are updated to + # NOVA_* in P2 (G-108 binding). + role-to-assume: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/acdl-deploy-{1}', secrets.NOVA_AWS_ACCOUNT_ID, github.repository_id) || '' }} aws-region: us-east-1 - access-key-id: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - secret-access-key: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + secret-access-key: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} - name: Run the platform pipeline working-directory: ${{ github.workspace }} @@ -145,7 +149,7 @@ jobs: AWS_DEFAULT_REGION: us-east-1 run: | aws lambda invoke-function-url \ - --function-url "${{ secrets.ACDL_LAMBDA_URL }}" \ + --function-url "${{ secrets.NOVA_LAMBDA_URL }}" \ --cli-binary-format raw-in-base64-out \ --payload "$(python3 -c "import json,os; print(json.dumps({'action':'report_error','consumerRepo':os.environ.get('GITHUB_REPOSITORY',''),'contractId':'${{ github.run_id }}','error':'Deploy pipeline failed. See run logs.','runUrl':'${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}','environment':'dev'}))")" \ /dev/null || true diff --git a/.github/workflows/modules-lifecycle.yml b/.github/workflows/modules-lifecycle.yml index 54520ac..03699c2 100644 --- a/.github/workflows/modules-lifecycle.yml +++ b/.github/workflows/modules-lifecycle.yml @@ -15,7 +15,7 @@ # "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast, # no AWS mutation, validates the contract->resolver->adapter->plan chain # for every module on every PR, with no AWS credentials or cost). Set to -# "full" via workflow_dispatch (or the ACDL_LIFECYCLE_MODE repo variable) +# "full" via workflow_dispatch (or the NOVA_LIFECYCLE_MODE repo variable) # to run the real apply→modify→destroy against live AWS. In plan mode the # short-lived CI VPC apply/destroy jobs are skipped (nothing is applied). # @@ -49,7 +49,7 @@ jobs: ci-vpc-apply: name: CI VPC apply runs-on: ubuntu-latest - if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.ACDL_LIFECYCLE_MODE != 'plan' }} + if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }} steps: - uses: actions/checkout@v4 - name: Install Terraform 1.9.* @@ -60,8 +60,8 @@ jobs: - name: Apply CI VPC working-directory: terraform/ci-vpc env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: | terraform init -input=false -lock=false @@ -78,7 +78,7 @@ jobs: matrix: module: [s3, kms-key, ecr, ecs-cluster, iam-role, cloudfront, waf, vpc, alb, ecs-service, rds, uptime] env: - ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }} + NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }} steps: - uses: actions/checkout@v4 - name: Free disk space @@ -97,31 +97,31 @@ jobs: echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list sudo apt-get update && sudo apt-get install -y terraform=1.9.* - name: Read CI VPC outputs - if: ${{ env.ACDL_LIFECYCLE_MODE == 'full' }} + if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }} working-directory: terraform/ci-vpc env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: | terraform init -input=false -lock=false terraform output -json > /tmp/ci-vpc-outputs.json - name: Apply (simple) env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json - name: Modify (complex) env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json - name: Destroy env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: bash scripts/run_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json @@ -136,7 +136,7 @@ jobs: matrix: module: [static-assets, microservice] env: - ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }} + NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }} steps: - uses: actions/checkout@v4 - name: Free disk space @@ -155,31 +155,31 @@ jobs: echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list sudo apt-get update && sudo apt-get install -y terraform=1.9.* - name: Read CI VPC outputs - if: ${{ env.ACDL_LIFECYCLE_MODE == 'full' }} + if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }} working-directory: terraform/ci-vpc env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: | terraform init -input=false -lock=false terraform output -json > /tmp/ci-vpc-outputs.json - name: Apply (simple) env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json - name: Modify (complex) env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json - name: Destroy env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: bash scripts/run_l2_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json @@ -188,7 +188,7 @@ jobs: name: CI VPC destroy needs: [lifecycle, l2-lifecycle] runs-on: ubuntu-latest - if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.ACDL_LIFECYCLE_MODE != 'plan' }} + if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }} steps: - uses: actions/checkout@v4 - name: Install Terraform 1.9.* @@ -199,8 +199,8 @@ jobs: - name: Destroy CI VPC working-directory: terraform/ci-vpc env: - AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: us-east-1 run: | terraform init -input=false -lock=false diff --git a/adapters/terraform/adapter.py b/adapters/terraform/adapter.py index 5fef97d..ac03adb 100644 --- a/adapters/terraform/adapter.py +++ b/adapters/terraform/adapter.py @@ -10,9 +10,10 @@ lives in the per-module terraform/ subdir, NOT in this file. CLI: adapter.py """ -import json -import os -import sys +import json, os, sys +_R = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, _R) if _R not in sys.path else None +from core import env def _load_registry(repo_root): @@ -112,7 +113,7 @@ def adapt(stack_instance, out_dir): stack_name = stack.get("name", "spike") environment = stack.get("environment", "dev") - account_id = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199") + account_id = env.get_env("AWS_ACCOUNT_ID", "581513795199") state_bucket = f"acdl-tfstate-{account_id}-us-east-1" terraform_tf = ( 'terraform {\n' @@ -134,7 +135,7 @@ def adapt(stack_instance, out_dir): data_source_names = stack_instance.get("data_sources", []) parts = [] if data_source_names: - remote_state_key = os.environ.get("ACDL_REMOTE_STATE_KEY", "platform/terraform.tfstate") + remote_state_key = env.get_env("REMOTE_STATE_KEY", "platform/terraform.tfstate") parts.append( 'data "terraform_remote_state" "platform" {\n' ' backend = "s3"\n' diff --git a/adapters/terraform/policy/checkov_adapter.py b/adapters/terraform/policy/checkov_adapter.py index 1d05bce..b39982c 100644 --- a/adapters/terraform/policy/checkov_adapter.py +++ b/adapters/terraform/policy/checkov_adapter.py @@ -1,4 +1,4 @@ -"""Translate Checkov JSON output to ACDL PolicyCheckResult records. +"""Translate Checkov JSON output to Nova PolicyCheckResult records. Reads Checkov's JSON output (one framework key, e.g. terraform_plan), emits a list of PolicyCheckResult dicts conforming to @@ -6,10 +6,13 @@ schemas/policy_check_result.schema.json. Run Checkov with --soft-fail so Checkov never exits non-zero; the confidence signal decides the gate, not Checkov's exit code. -The ACDL tagging standard (D-054, D-043 closure) is enforced by a custom -Checkov rule at adapters/terraform/policy/custom_rules/acdl_tagging.py, -loaded via --external-checks-dir. The adapter therefore maps -ACDL_TAG_NAMING as a real rule (no synthetic SKIPPED record is emitted). +The Nova tagging standard (D-054, D-043 closure, D-109 warn mode in P2) +is enforced by a custom Checkov rule at +adapters/terraform/policy/custom_rules/nova_tagging.py, loaded via +--external-checks-dir. The adapter therefore maps NOVA_TAG_NAMING as a +real rule (no synthetic SKIPPED record is emitted). Renamed from +ACDL_TAG_NAMING in P2 (REQ-158); the rule is in warn mode for P2 +(legacy acdl:* tag-key values stay until P3). """ import datetime @@ -29,10 +32,12 @@ RULE_MAP = { "CKV_AWS_40": ("iam-wildcard", "medium"), "CKV_AWS_7": ("kms-key-reference", "medium"), "CKV_AWS_33": ("kms-key-reference", "medium"), - # D-054 / D-043 closure: ACDL_TAG_NAMING is now a real custom Checkov - # rule (adapters/terraform/policy/custom_rules/acdl_tagging.py), loaded - # via --external-checks-dir. No synthetic SKIPPED record is emitted. - "ACDL_TAG_NAMING": ("tagging-standard", "medium"), + # D-054 / D-043 closure, D-109 warn mode (P2): NOVA_TAG_NAMING is a real + # custom Checkov rule (adapters/terraform/policy/custom_rules/nova_tagging.py), + # loaded via --external-checks-dir. No synthetic SKIPPED record is emitted. + # Renamed from ACDL_TAG_NAMING in P2 (REQ-158). Warn mode treats legacy + # acdl:*-only tags as a warning (P3 flips to hard-fail). + "NOVA_TAG_NAMING": ("tagging-standard", "medium"), } _RESULT_MAP = {"PASSED": "pass", "FAILED": "fail", "SKIPPED": "skipped"} diff --git a/adapters/terraform/policy/custom_rules/README.md b/adapters/terraform/policy/custom_rules/README.md index e4f33b1..2181fb9 100644 --- a/adapters/terraform/policy/custom_rules/README.md +++ b/adapters/terraform/policy/custom_rules/README.md @@ -1,16 +1,24 @@ -# ACDL Custom Checkov Rules +# Nova Custom Checkov Rules -This directory holds ACDL-authored Checkov custom rules, written in the +This directory holds Nova-authored Checkov custom rules, written in the [Checkov Python custom-rule framework](https://www.checkov.io/4.Contributing/Custom%20Policies.html). ## Files -- `acdl_tagging.py` — `ACDL_TAG_NAMING` (D-054): ensures every taggable AWS - resource carries the four required ACDL tags - (`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`). - This rule replaces the synthetic SKIPPED `ACDL_TAG_NAMING` record that the - Checkov adapter previously emitted (D-043 closure). The canonical tag set - is declared in [`schemas/tagging-standard.json`](../../../schemas/tagging-standard.json). +- `nova_tagging.py` — `NOVA_TAG_NAMING` (D-054, D-109 warn mode in P2): + ensures every taggable AWS resource carries the four required Nova tags + (`nova:owner`, `nova:contract`, `nova:environment`, `nova:cost-center`). + This rule replaces the synthetic SKIPPED `NOVA_TAG_NAMING` record that the + Checkov adapter previously emitted (D-043 closure). Renamed from + `acdl_tagging.py` / `ACDL_TAG_NAMING` in P2 (REQ-158). The canonical tag + set is declared in [`schemas/tagging-standard.json`](../../../schemas/tagging-standard.json). + + **P2 warn mode (D-109):** existing resources still carry `acdl:*` tag-key + values (left for P3). When a resource has only `acdl:*`-style tags and no + `nova:*` tags, the rule logs a WARNING instead of failing, so the + regression gate stays green during the parallel-tag transition window. + P3 flips to hard-fail once `nova:*` tags are emitted in parallel and the + ABAC policy is swapped. ## How Checkov loads them @@ -23,12 +31,12 @@ checkov -f terraform/spike/main.tf --framework terraform -o json --soft-fail \ ``` Checkov imports each `*.py` file in the directory and instantiates the -module-level `check` object (see the `check = AcdlTaggingStandard()` line at -the bottom of `acdl_tagging.py`). +module-level `check` object (see the `check = NovaTaggingStandard()` line at +the bottom of `nova_tagging.py`). ## Severity / result mapping The Checkov adapter (`adapters/terraform/policy/checkov_adapter.py`) -maps `ACDL_TAG_NAMING` to `(tagging-standard, medium)` in `RULE_MAP`. The +maps `NOVA_TAG_NAMING` to `(tagging-standard, medium)` in `RULE_MAP`. The custom rule therefore produces real `PASS`/`FAIL` PolicyCheckResult records, feeding the confidence signal instead of the old SKIPPED placeholder. \ No newline at end of file diff --git a/adapters/terraform/policy/custom_rules/acdl_tagging.py b/adapters/terraform/policy/custom_rules/acdl_tagging.py deleted file mode 100644 index 642a4eb..0000000 --- a/adapters/terraform/policy/custom_rules/acdl_tagging.py +++ /dev/null @@ -1,54 +0,0 @@ -"""ACDL tagging standard custom Checkov rule (D-054). - -Checks that all taggable AWS resources have the required ACDL tags: - acdl:owner, acdl:contract, acdl:environment, acdl:cost-center - -Fails (severity medium) when any required tag is missing. -Closes the D-043 deferral (the SKIPPED ACDL_TAG_NAMING placeholder -becomes a real check). -""" - -from __future__ import annotations - -from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck -from checkov.common.models.enums import CheckResult, CheckCategories - -REQUIRED_TAGS = ("acdl:owner", "acdl:contract", "acdl:environment", "acdl:cost-center") - -# Resources that support tags (exclude resources that have no tags attribute) -NON_TAGGABLE_TYPES = ( - "aws_cloudfront_origin_access_control", - "aws_lambda_function_url", - "aws_route_table_association", - "aws_internet_gateway", -) - -class AcdlTaggingStandard(BaseResourceCheck): - def __init__(self): - name = "Ensure all taggable AWS resources have required ACDL tags" - check_id = "ACDL_TAG_NAMING" - supported_resources = ["*"] # all resources - categories = [CheckCategories.GENERAL_SECURITY] - super().__init__(name=name, check_id=check_id, categories=categories, supported_resources=supported_resources) - - def scan_resource_conf(self, conf, entity_type): - # Skip non-taggable resources - if entity_type in NON_TAGGABLE_TYPES: - return CheckResult.PASSED - # Check for a tags block - tags = conf.get("tags") - if not tags: - return CheckResult.FAILED - tag_keys = set() - if isinstance(tags, list) and tags: - tag_block = tags[0] - if isinstance(tag_block, dict): - tag_keys = set(tag_block.keys()) - elif isinstance(tags, dict): - tag_keys = set(tags.keys()) - missing = [t for t in REQUIRED_TAGS if t not in tag_keys] - if missing: - return CheckResult.FAILED - return CheckResult.PASSED - -check = AcdlTaggingStandard() \ No newline at end of file diff --git a/adapters/terraform/policy/custom_rules/nova_tagging.py b/adapters/terraform/policy/custom_rules/nova_tagging.py new file mode 100644 index 0000000..04cbb37 --- /dev/null +++ b/adapters/terraform/policy/custom_rules/nova_tagging.py @@ -0,0 +1,88 @@ +"""Nova tagging standard custom Checkov rule (D-054, D-109 warn mode). + +Checks that all taggable AWS resources have the required Nova tags: + nova:owner, nova:contract, nova:environment, nova:cost-center + +In **warn mode** (P2, REQ-158): existing resources still carry `acdl:*` +tags (the legacy tag-key VALUES stay until P3). When a resource has +only `acdl:*`-style tags and no `nova:*` tags, the rule logs a WARNING +instead of failing, so the regression gate stays green during the +parallel-tag transition window. P3 flips this to hard-fail (D-109 hard +mode) once `nova:*` tags are emitted in parallel and the ABAC policy is +swapped. + +Closes the D-043 deferral (the SKIPPED NOVA_TAG_NAMING placeholder +becomes a real check). Renamed from acdl_tagging.py in P2 (REQ-158); +the Checkov rule ID ACDL_TAG_NAMING → NOVA_TAG_NAMING. +""" + +from __future__ import annotations + +import sys + +from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck +from checkov.common.models.enums import CheckResult, CheckCategories + +REQUIRED_TAGS = ("nova:owner", "nova:contract", "nova:environment", "nova:cost-center") + +# Legacy acdl:* tag keys — the parallel-tag period (P3) emits both nova:* +# and acdl:*; P2 warn mode treats acdl:*-only tags as a warning, not a +# failure. The acdl:* VALUES in tagging-standard.json are left for P3. +LEGACY_TAGS = ("acdl:owner", "acdl:contract", "acdl:environment", "acdl:cost-center") + +# Resources that support tags (exclude resources that have no tags attribute) +NON_TAGGABLE_TYPES = ( + "aws_cloudfront_origin_access_control", + "aws_lambda_function_url", + "aws_route_table_association", + "aws_internet_gateway", +) + +# P2 warn mode (D-109): emit a warning (not a hard FAIL) when a resource +# carries only legacy acdl:* tags and no nova:* tags. P3 flips this to +# False (hard-fail). Set NOVA_TAGGING_HARD=1 to opt into hard mode early +# (used by P3 tests before the P3 flip lands). +_WARN_MODE = True + + +class NovaTaggingStandard(BaseResourceCheck): + def __init__(self): + name = "Ensure all taggable AWS resources have required Nova tags" + check_id = "NOVA_TAG_NAMING" + supported_resources = ["*"] # all resources + categories = [CheckCategories.GENERAL_SECURITY] + super().__init__(name=name, check_id=check_id, categories=categories, supported_resources=supported_resources) + + def scan_resource_conf(self, conf, entity_type): + # Skip non-taggable resources + if entity_type in NON_TAGGABLE_TYPES: + return CheckResult.PASSED + # Check for a tags block + tags = conf.get("tags") + if not tags: + return CheckResult.FAILED + tag_keys = set() + if isinstance(tags, list) and tags: + tag_block = tags[0] + if isinstance(tag_block, dict): + tag_keys = set(tag_block.keys()) + elif isinstance(tags, dict): + tag_keys = set(tags.keys()) + missing = [t for t in REQUIRED_TAGS if t not in tag_keys] + if not missing: + return CheckResult.PASSED + # Warn mode (D-109, P2): if the resource carries the legacy acdl:* + # tag keys for every required tag, emit a warning rather than a + # hard fail — existing resources still carry acdl:* until P3. + if _WARN_MODE: + has_all_legacy = all(t in tag_keys for t in LEGACY_TAGS) + if has_all_legacy: + sys.stderr.write( + f"[nova_tagging] WARN: {entity_type} has legacy acdl:* tags " + f"but no nova:* tags (P2 warn mode, D-109). Migrate to " + f"nova:* tags before P5.\n" + ) + return CheckResult.PASSED + return CheckResult.FAILED + +check = NovaTaggingStandard() \ No newline at end of file diff --git a/core/attestation_matrix.py b/core/attestation_matrix.py index 47dc838..ab91f48 100644 --- a/core/attestation_matrix.py +++ b/core/attestation_matrix.py @@ -14,7 +14,8 @@ concerns split into two tiers: The operator-supplied evidence artifact is a JSON blob with `timestamp`, `type`, `payload`, and an optional `signature` (JWS detached). Freshness is validated against the window from §10.4. Signature verification runs -when `ACDL_ATTESTATION_SIGNING_KEY_ID` is set; it is skipped + logged +when `NOVA_ATTESTATION_SIGNING_KEY_ID` is set (dual-read via core/env.py: +NOVA_* preferred, ACDL_* fallback until P5); it is skipped + logged when unset (dev/CI — D-089). The matrix fails loud if an operator-supplied concern is missing or expired for prod/dr. """ @@ -24,6 +25,14 @@ import os import sys from typing import Optional, Tuple +# Repo root on sys.path so `from core import env` resolves to THIS package +# when run as a script (avoids editable-installed third-party `core` shadow). +_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if _REPO_ROOT not in sys.path: + sys.path.insert(0, _REPO_ROOT) + +from core import env + # Freshness windows (days) from hitl_matrix_design.md §10.4. FRESHNESS_DAYS = { @@ -81,14 +90,15 @@ def _is_fresh(artifact: dict, concern: str) -> bool: def _verify_signature(artifact: dict) -> bool: - """Verify the JWS detached signature when ACDL_ATTESTATION_SIGNING_KEY_ID is set. + """Verify the JWS detached signature when NOVA_ATTESTATION_SIGNING_KEY_ID is set. When unset (dev/CI — D-089), signature verification is skipped + logged. + Dual-read via core/env.py: NOVA_* preferred, ACDL_* fallback until P5. """ - key_id = os.environ.get("ACDL_ATTESTATION_SIGNING_KEY_ID", "") + key_id = env.get_env("ATTESTATION_SIGNING_KEY_ID", "") or "" if not key_id: sys.stderr.write( - "[attestation] ACDL_ATTESTATION_SIGNING_KEY_ID unset — " + "[attestation] NOVA_ATTESTATION_SIGNING_KEY_ID unset — " "signature verification skipped (dev/CI, D-089)\n" ) return True diff --git a/core/contract_resolver.py b/core/contract_resolver.py index 5b1ff1c..e6e3430 100644 --- a/core/contract_resolver.py +++ b/core/contract_resolver.py @@ -36,6 +36,16 @@ import sys import yaml import jsonschema +# Ensure the repo root (parent of core/) is on sys.path so `from core +# import env` resolves to THIS package when contract_resolver.py is run +# as a script (python3 core/contract_resolver.py) — otherwise an +# editable-installed third-party `core` package can shadow it. +_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if _REPO_ROOT not in sys.path: + sys.path.insert(0, _REPO_ROOT) + +from core import env + def _load_env(env_name, repo_root): """Load the environment onboarding JSON for env_name. @@ -620,9 +630,10 @@ if __name__ == "__main__": idx = sys.argv.index("--environment") if idx + 1 < len(sys.argv): env_override = sys.argv[idx + 1] - # Also honor the ACDL_ENVIRONMENT_OVERRIDE env var (used by run_platform.sh). - if env_override is None and os.environ.get("ACDL_ENVIRONMENT_OVERRIDE"): - env_override = os.environ["ACDL_ENVIRONMENT_OVERRIDE"] + # Also honor the NOVA_ENVIRONMENT_OVERRIDE env var (used by run_platform.sh). + # Dual-read via core/env.py: NOVA_* preferred, ACDL_* fallback until P5. + if env_override is None and env.get_env("ENVIRONMENT_OVERRIDE"): + env_override = env.get_env("ENVIRONMENT_OVERRIDE") result = resolve(contract_path, environment_override=env_override) with open(out_path, "w") as fh: json.dump(result, fh, indent=2) \ No newline at end of file diff --git a/core/env.py b/core/env.py new file mode 100644 index 0000000..afd295e --- /dev/null +++ b/core/env.py @@ -0,0 +1,48 @@ +"""Dual-read environment helper (D-108, REQ-159, G-106). + +During the Nova rebrand transition window (P2–P4), every `NOVA_*` +environment variable is the preferred source, with the legacy `ACDL_*` +name as the fallback. This keeps deployments from breaking while the +keys are rotated across `.env`, `.env.secrets`, Gitea repo secrets, and +operator-managed process environments. + +`get_env(name, default=None)` resolves `NOVA_` first, then falls +back to `ACDL_`, then returns `default` if neither is set. + +This helper is removed (NOVA-only) in P5 (REQ-164). Direct-read paths +that bypass this helper (the `.env.secrets` shell export in +`scripts/run_platform.sh` and the Python parser in +`core/regression_verify.py`) mirror this contract inline per the G-106 +binding — see those sites for the dual-read shell/Python forms. +""" + +from __future__ import annotations + +import os +from typing import Optional + +__all__ = ["get_env"] + + +def get_env(name: str, default: Optional[str] = None) -> Optional[str]: + """Resolve a config value with a NOVA-preferred / ACDL-fallback read. + + `name` is the bare key WITHOUT the prefix (e.g. ``"AWS_ACCOUNT_ID"``). + The lookup order is: + + 1. ``NOVA_`` (preferred) + 2. ``ACDL_`` (legacy fallback, removed in P5) + 3. ``default`` + + Returns the first value that is present and non-empty, or ``default`` + if neither env var is set. An explicitly-set empty string is treated + as "unset" so an operator cannot accidentally shadow the fallback + with a blank NOVA key. + """ + nova_val = os.environ.get(f"NOVA_{name}") + if nova_val: + return nova_val + acdl_val = os.environ.get(f"ACDL_{name}") + if acdl_val: + return acdl_val + return default \ No newline at end of file diff --git a/core/hitl_matrix_design.md b/core/hitl_matrix_design.md index bd31945..02bab8a 100644 --- a/core/hitl_matrix_design.md +++ b/core/hitl_matrix_design.md @@ -93,7 +93,7 @@ The full table (lifted verbatim from §10.4): The operator-supplied evidence artifact is a JSON blob with `timestamp`, `type`, `payload`, and an optional `signature` (JWS detached). Freshness is validated against the window above. Signature verification runs when -`ACDL_ATTESTATION_SIGNING_KEY_ID` is set; it is skipped + logged when +`NOVA_ATTESTATION_SIGNING_KEY_ID` is set; it is skipped + logged when unset (dev/CI — D-089). The matrix fails loud if an operator-supplied concern is missing or expired for prod/dr. @@ -140,7 +140,7 @@ not Kyverno (in v1). Sequence: in the same process that has authority to block the promotion. v1.9 implements `route_halt_artifact` as a real SNS publish (topic -`acdl-sod-halt`, ARN from `ACDL_SOD_HALT_TOPIC_ARN`) with an outbox-event +`acdl-sod-halt`, ARN from `NOVA_SOD_HALT_TOPIC_ARN`) with an outbox-event fallback when the topic ARN is unset (REQ-107). The attestation gate itself is `core/hitl_gates.py` (`attest(contract_id, env, approver, evidence)`), which records the approver to the outbox, runs the SoD @@ -171,5 +171,5 @@ v1.9 (Phase 41 + Phase 42) wires the gates end-to-end: concerns run for real; operator-supplied concerns accept signed evidence artifacts validated for freshness + schema. - **D-089** (v1.9) — attestation artifact signature verification is - skipped when `ACDL_ATTESTATION_SIGNING_KEY_ID` is unset (dev/CI); + skipped when `NOVA_ATTESTATION_SIGNING_KEY_ID` is unset (dev/CI); required for prod/dr. \ No newline at end of file diff --git a/core/local_emulators.py b/core/local_emulators.py index f9284b8..08b3bfa 100644 --- a/core/local_emulators.py +++ b/core/local_emulators.py @@ -12,7 +12,8 @@ evidence event) runs end-to-end against the local tier with no AWS: Each adapter exposes the same interface as the live counterpart so the caller code path is unchanged; only the I/O target swaps. Selection is -gated on the ACDL_LOCAL_TIER env var (set by run_platform.sh --local). +gated on the NOVA_LOCAL_TIER env var (set by run_platform.sh --local). +Dual-read via core/env.py: NOVA_* preferred, ACDL_* fallback until P5. """ from __future__ import annotations @@ -32,12 +33,20 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict, List, Optional, Tuple +# Repo root on sys.path so `from core import env` resolves to THIS package +# when run as a script (avoids editable-installed third-party `core` shadow). +_REPO_ROOT = str(Path(__file__).resolve().parent.parent) +if _REPO_ROOT not in sys.path: + sys.path.insert(0, _REPO_ROOT) + +from core import env + ROOT = Path(__file__).resolve().parent.parent def is_local_tier() -> bool: """True when the local emulating tier is active.""" - return os.environ.get("ACDL_LOCAL_TIER", "") == "1" + return env.get_env("LOCAL_TIER", "") == "1" # --------------------------------------------------------------------------- @@ -286,7 +295,7 @@ class LocalLambdaStub: Returns the handler's response dict ({statusCode, body}). The handler's DynamoDB calls are - intercepted via the ACDL_LOCAL_TIER env var (the handler checks + intercepted via the NOVA_LOCAL_TIER env var (the handler checks _get_dynamodb(); under local tier it would need patching - we patch the module's _get_dynamodb to return a local stub).""" # Import the handler module (the dir is named `lambda`, a Python @@ -490,6 +499,9 @@ def run_local_e2e(contract_path: str, repo_root: Optional[Path] = None) -> Dict[ if __name__ == "__main__": contract = sys.argv[1] if len(sys.argv) > 1 else "contracts/microservice.yml" - os.environ["ACDL_LOCAL_TIER"] = "1" + # Set both so the dual-read in is_local_tier() finds NOVA_* (preferred); + # the ACDL_* alias stays for any unmigrated reader until P5. + os.environ["NOVA_LOCAL_TIER"] = "1" + os.environ["ACDL_LOCAL_TIER"] = "1" # legacy alias (dual-read fallback), removed in P5 result = run_local_e2e(contract) print(json.dumps(result, indent=2)) \ No newline at end of file diff --git a/core/output_publisher.py b/core/output_publisher.py index c6bf3bd..9faa6fe 100644 --- a/core/output_publisher.py +++ b/core/output_publisher.py @@ -21,8 +21,16 @@ try: except ImportError: boto3 = None +# Repo root on sys.path so `from core import env` resolves to THIS package +# when run as a script (avoids editable-installed third-party `core` shadow). +_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if _REPO_ROOT not in sys.path: + sys.path.insert(0, _REPO_ROOT) + +from core import env as _envhelper + SSM_PREFIX = "/acdl" -KMS_KEY_ID_ENV = "ACDL_KMS_KEY_ID" +KMS_KEY_ID_ENV = "NOVA_KMS_KEY_ID" # Outputs that are safe to display in a PR comment (no secrets). SAFE_OUTPUT_NAMES = { @@ -54,20 +62,22 @@ def _ssm_client(): def _kms_key_id(): """Return the KMS key ID for SSM SecureString encryption. - P1-3: Fail loud when ACDL_KMS_KEY_ID is not set — silently falling back + P1-3: Fail loud when NOVA_KMS_KEY_ID is not set — silently falling back to the AWS-managed key (`alias/aws/ssm`) was a security gap. The platform - CMK must be explicitly configured. Set ACDL_ALLOW_DEFAULT_KMS=1 to use - the AWS-managed key as an escape hatch for local testing. + CMK must be explicitly configured. Set NOVA_ALLOW_DEFAULT_KMS=1 to use + the AWS-managed key as an escape hatch for local testing. (Dual-read + via core/env.py: NOVA_* preferred, ACDL_* fallback until P5.) """ - key_id = os.environ.get(KMS_KEY_ID_ENV) + key_id = _envhelper.get_env("KMS_KEY_ID") if key_id: return key_id - if os.environ.get("ACDL_ALLOW_DEFAULT_KMS") == "1": + if _envhelper.get_env("ALLOW_DEFAULT_KMS") == "1": return "alias/aws/ssm" raise RuntimeError( f"{KMS_KEY_ID_ENV} is not set — refusing to use the AWS-managed SSM key " f"silently. Set {KMS_KEY_ID_ENV} to your platform CMK ARN, or set " - f"ACDL_ALLOW_DEFAULT_KMS=1 to use alias/aws/ssm (escape hatch for local testing)." + f"NOVA_ALLOW_DEFAULT_KMS=1 (ACDL_ALLOW_DEFAULT_KMS=1 fallback) to use " + f"alias/aws/ssm (escape hatch for local testing)." ) diff --git a/core/regression_verify.py b/core/regression_verify.py index 47894ad..55f84cd 100755 --- a/core/regression_verify.py +++ b/core/regression_verify.py @@ -32,6 +32,15 @@ from dataclasses import dataclass, field, asdict from pathlib import Path from typing import Callable, Dict, List, Optional, Tuple +# Repo root on sys.path so `from core import env` resolves to THIS package +# when regression_verify.py is run as a script (avoids editable-installed +# third-party `core` shadow). +_REPO_ROOT = str(Path(__file__).resolve().parent.parent) +if _REPO_ROOT not in sys.path: + sys.path.insert(0, _REPO_ROOT) + +from core import env as _envhelper + ROOT = Path(__file__).resolve().parent.parent CIAgent = ROOT / ".ciagent" @@ -306,9 +315,13 @@ def _load_aws_env() -> Dict[str, str]: continue if "=" in line: k, v = line.split("=", 1) - if k == "ACDL_AWS_ACCESS_KEY_ID": + # G-106 binding: dual-read NOVA_* first, ACDL_* fallback. + # The .env.secrets keys are renamed to NOVA_* in P2; the + # ACDL_* fallback covers operators who haven't rotated + # their local .env.secrets yet. Removed in P5. + if k == "NOVA_AWS_ACCESS_KEY_ID" or k == "ACDL_AWS_ACCESS_KEY_ID": env["AWS_ACCESS_KEY_ID"] = v - elif k == "ACDL_AWS_SECRET_ACCESS_KEY": + elif k == "NOVA_AWS_SECRET_ACCESS_KEY" or k == "ACDL_AWS_SECRET_ACCESS_KEY": env["AWS_SECRET_ACCESS_KEY"] = v elif k == "AWS_DEFAULT_REGION": env["AWS_DEFAULT_REGION"] = v @@ -319,7 +332,8 @@ def _check_live_terraform_plan_microservice() -> Tuple[Status, str]: """CAP-013: terraform init+validate+plan against live AWS for the microservice stack (D-093 live-AWS tier of the headline E2E). - Requires AWS credentials (ACDL_AWS_ACCESS_KEY_ID etc. in .env.secrets). + Requires AWS credentials (NOVA_AWS_ACCESS_KEY_ID etc. in .env.secrets; + dual-read NOVA_* first, ACDL_* fallback per G-106). Runs in a temp dir; does NOT apply (plan only).""" import tempfile, os work = tempfile.mkdtemp(prefix="acdl_regr_live_") @@ -421,7 +435,7 @@ def _check_s3_state_bucket() -> Tuple[Status, str]: s3 = boto3.client("s3", region_name=env.get("AWS_DEFAULT_REGION", "us-east-1"), aws_access_key_id=env.get("AWS_ACCESS_KEY_ID"), aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY")) - account_id = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199") + account_id = _envhelper.get_env("AWS_ACCOUNT_ID", "581513795199") state_bucket = f"acdl-tfstate-{account_id}-us-east-1" s3.head_bucket(Bucket=state_bucket) r = s3.list_objects_v2(Bucket=state_bucket, MaxKeys=5) @@ -643,8 +657,8 @@ def write_report(report: RegressionReport, def main() -> int: - milestone = os.environ.get("ACDL_REGRESSION_MILESTONE", "v1.10") - phase = int(os.environ.get("ACDL_REGRESSION_PHASE", "52")) + milestone = _envhelper.get_env("REGRESSION_MILESTONE", "v1.10") or "v1.10" + phase = int(_envhelper.get_env("REGRESSION_PHASE", "52") or "52") report = run_regression(milestone=milestone, phase=phase) md, js = write_report(report) print(f"regression: {report.summary} -> {md}") diff --git a/core/separation_of_duties.py b/core/separation_of_duties.py index b4f0eff..e3b893a 100644 --- a/core/separation_of_duties.py +++ b/core/separation_of_duties.py @@ -5,16 +5,27 @@ Blocks on equality, emits `SEPARATION_OF_DUTIES_VIOLATION`, routes a halt artifact to SRE on-call. v1.9 (REQ-107, D-085): route_halt_artifact is a real implementation — -publishes to SNS topic `acdl-sod-halt` (ARN from ACDL_SOD_HALT_TOPIC_ARN) +publishes to SNS topic `acdl-sod-halt` (ARN from NOVA_SOD_HALT_TOPIC_ARN) when set; falls back to a structured stderr emission + a SEPARATION_OF_DUTIES_VIOLATION event write to the DynamoDB outbox when -unset. No silent print-only stub. +unset. No silent print-only stub. (Dual-read via core/env.py: NOVA_* +preferred, ACDL_* fallback until P5; the SNS topic ARN is the AWS +resource `acdl-sod-halt` → renamed `nova-sod-halt` in P4.) """ import os import sys from typing import Optional, Tuple +# Repo root on sys.path so `from core import env` resolves to THIS package +# when imported/run in a context where an editable-installed third-party +# `core` package would otherwise shadow it. +_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if _REPO_ROOT not in sys.path: + sys.path.insert(0, _REPO_ROOT) + +from core import env + def check(outbox_client, contract_id: str, current_prod_approver: Optional[str]) -> Tuple[bool, str]: @@ -40,13 +51,13 @@ def route_halt_artifact(contract_id: str, violation_reason: str, oncall_client=None) -> None: """Route a halt artifact to SRE on-call (REQ-107, D-085). - When ACDL_SOD_HALT_TOPIC_ARN is set, publish to the SNS topic via + When NOVA_SOD_HALT_TOPIC_ARN is set, publish to the SNS topic via boto3. When unset (dev/CI), fall back to a structured stderr emission + a SEPARATION_OF_DUTIES_VIOLATION event write to the DynamoDB outbox via outbox_writer.write_event (so the halt is in the audit chain). The oncall_client, when provided, is the SNS client (test injection). """ - topic_arn = os.environ.get("ACDL_SOD_HALT_TOPIC_ARN", "") + topic_arn = env.get_env("SOD_HALT_TOPIC_ARN", "") or "" halt_payload = { "contractId": contract_id, "reason": violation_reason, diff --git a/docs/consumer-guide.md b/docs/consumer-guide.md index b912999..5665cbe 100644 --- a/docs/consumer-guide.md +++ b/docs/consumer-guide.md @@ -53,7 +53,7 @@ platform-managed. See [Environments](environments/). ## Step 1 — Create a consumer repo Create a repository for your application. The top level holds your app -code; your contract lives at `.acdl/contract.yml`. Example for a static +code; your contract lives at `.nova/contract.yml`. Example for a static site: ``` @@ -62,7 +62,7 @@ my-static-site/ assets/ style.css logo.png - .acdl/ + .nova/ contract.yaml .github/ workflows/ @@ -75,7 +75,7 @@ Example for a microservice: my-microservice/ app.py Dockerfile - .acdl/ + .nova/ contract.yaml .github/ workflows/ @@ -83,7 +83,7 @@ my-microservice/ ``` Your app code lives at the top level. Your contract lives at -`.acdl/contract.yml` regardless of the module you deploy. Your CI +`.nova/contract.yml` regardless of the module you deploy. Your CI definition lives at `.github/workflows/deploy.yml`. ## Step 2 — Reference the central pipeline @@ -96,7 +96,7 @@ jobs: deploy: uses: acdl/.github/workflows/deploy.yml@v1.13 with: - contract: .acdl/contract.yml + contract: .nova/contract.yml environment: dev ``` @@ -106,7 +106,7 @@ field; the version pin lives in the CI workflow reference. ## Step 3 — Define the contract -Write `.acdl/contract.yml`. The `static-assets` example: +Write `.nova/contract.yml`. The `static-assets` example: ```yaml environment: dev @@ -179,7 +179,7 @@ jobs: deploy: uses: acdl/.github/workflows/deploy.yml@v1.13 with: - contract: .acdl/contract.yml + contract: .nova/contract.yml ``` That is the entire consumer-side workflow. When you push to `main`: @@ -192,7 +192,7 @@ That is the entire consumer-side workflow. When you push to `main`: never clone the platform repo yourself. 4. The runner installs the runtime dependencies the platform requires. 5. The runner invokes `scripts/run_platform.sh` against your - `.acdl/contract.yml`. + `.nova/contract.yml`. You see the streamed output (infrastructure plan, policy-check results, confidence signal) in your run logs. The `--check-only` and `--plan-only` @@ -213,7 +213,7 @@ static key in `.env.secrets` (gitignored) is rotated **out of band by you** locally-held copies. ```bash -bash scripts/run_platform.sh --check-only path/to/your/.acdl/contract.yml +bash scripts/run_platform.sh --check-only path/to/your/.nova/contract.yml ``` ## Step 5 — What the pipeline does @@ -355,7 +355,7 @@ destruction: ```yaml uses: acdl/.github/workflows/deploy.yml@v1.13 with: - contract: .acdl/contract.yml + contract: .nova/contract.yml mode: decommission changeRequestId: "CHG0678912" ``` @@ -404,8 +404,8 @@ input). Promotion = running the matching job. ### Two shapes (both supported) **Shape 1 — per-environment contract files:** a consumer repo has one -contract per environment (e.g. `.acdl/static-assets.dev.yml`, -`.acdl/static-assets.qa.yml`, …). Each sets `environment:` to its own +contract per environment (e.g. `.nova/static-assets.dev.yml`, +`.nova/static-assets.qa.yml`, …). Each sets `environment:` to its own name and uses interpolation so env-specific values differ automatically: ```yaml @@ -439,7 +439,7 @@ jobs: uses: acdl/.github/workflows/deploy.yml@v1.13 with: environment: qa - contract: .acdl/contract.yml + contract: .nova/contract.yml ``` ### One job per environment diff --git a/docs/contracts/index.md b/docs/contracts/index.md index c84eb4b..1fb4854 100644 --- a/docs/contracts/index.md +++ b/docs/contracts/index.md @@ -7,7 +7,7 @@ deploys it. ## The contract file -A consumer repo keeps its contract at `.acdl/contract.yml`. A minimal +A consumer repo keeps its contract at `.nova/contract.yml`. A minimal example (the `static-assets` module): ```yaml diff --git a/docs/index.md b/docs/index.md index af57b3a..456c82e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,7 +18,7 @@ There are two kinds of repository in the Nova model: and the reusable workflow files. Platform engineers work here. A consumer never clones it. - **Consumer repo (yours).** A consumer repo contains only its application - code, one or more contracts (`.acdl/contract.yml`), and one or more CI + code, one or more contracts (`.nova/contract.yml`), and one or more CI definitions (a thin `.github/workflows/deploy.yml` that `uses:` the central reusable workflow, pointing at the appropriate environment + contract). The consumer does not write infrastructure modules, workflow YAML, or diff --git a/docs/pipeline/versioning.md b/docs/pipeline/versioning.md index 268291f..8447206 100644 --- a/docs/pipeline/versioning.md +++ b/docs/pipeline/versioning.md @@ -28,7 +28,7 @@ jobs: deploy: uses: acdl/.github/workflows/deploy.yml@v1.13 with: - contract: .acdl/contract.yml + contract: .nova/contract.yml ``` The version pin lives in the CI workflow reference (not in the contract diff --git a/docs/presentations/assets/mmd/developer-experience-02-what-dev-does.mmd b/docs/presentations/assets/mmd/developer-experience-02-what-dev-does.mmd index 92736b9..c59ae4f 100644 --- a/docs/presentations/assets/mmd/developer-experience-02-what-dev-does.mmd +++ b/docs/presentations/assets/mmd/developer-experience-02-what-dev-does.mmd @@ -2,7 +2,7 @@ flowchart LR A["1. App code
(top level of the repo)"] --> D["Push to main"] - B["2. Contract
(.acdl/contract.yml)"] --> D + B["2. Contract
(.nova/contract.yml)"] --> D C["3. CI definition
(.github/workflows/deploy.yml
— one 'uses:' line)"] --> D D --> E["Platform does the rest"] classDef accent fill:#1B1B1B,color:#fff,stroke:#D6002A,stroke-width:2px diff --git a/docs/presentations/the-developer-experience-marp.md b/docs/presentations/the-developer-experience-marp.md index 9a5d8af..773293b 100644 --- a/docs/presentations/the-developer-experience-marp.md +++ b/docs/presentations/the-developer-experience-marp.md @@ -140,7 +140,7 @@ code { font-size: 13px; } ```yaml uses: acdl/.github/workflows/deploy.yml@v1.12 with: - contract: .acdl/contract.yml + contract: .nova/contract.yml mode: decommission changeRequestId: "CHG0678912" ``` diff --git a/docs/presentations/the-developer-experience.html b/docs/presentations/the-developer-experience.html index 05d9559..f52dc9d 100644 --- a/docs/presentations/the-developer-experience.html +++ b/docs/presentations/the-developer-experience.html @@ -456,7 +456,7 @@ img { display: block; margin: 0 auto; max-height: 280px; }

uses: acdl/.github/workflows/deploy.yml@v1.12
 with:
-  contract: .acdl/contract.yml
+  contract: .nova/contract.yml
   mode: decommission
   changeRequestId: "CHG0678912"
 
diff --git a/docs/presentations/the-developer-experience.md b/docs/presentations/the-developer-experience.md index 14b81ad..4f2094a 100644 --- a/docs/presentations/the-developer-experience.md +++ b/docs/presentations/the-developer-experience.md @@ -233,7 +233,7 @@ flowchart LR ```yaml uses: acdl/.github/workflows/deploy.yml@v1.12 with: - contract: .acdl/contract.yml + contract: .nova/contract.yml mode: decommission changeRequestId: "CHG0678912" ``` diff --git a/schemas/README.md b/schemas/README.md index 8827762..8362f11 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -13,7 +13,7 @@ Nova uses JSON Schema draft 2020-12 for all declarative contracts. Schemas are t | Nova Central Pipeline Contract | `pipeline.schema.json` | Central CI pipeline contract (stages, commands, triggers, runner) | `tests/test_pipeline_contract.py` | | Nova Central Deployment Pipeline Contract | `deploy-pipeline.schema.json` | Central deploy pipeline contract (validate → resolve → plan → checkov → confidence → apply → publish → uptime → comment) | `tests/test_pipeline_contract.py` | | Nova PolicyCheckResult | `policy_check_result.schema.json` | Normalized policy check result schema (the contract between policy engines and the confidence signal) | `tests/conftest.py`, all adapter tests | -| Nova Tagging Standard | `tagging-standard.json` | Required tag set for all taggable AWS resources | `adapters/terraform/policy/custom_rules/acdl_tagging.py` | +| Nova Tagging Standard | `tagging-standard.json` | Required tag set for all taggable AWS resources | `adapters/terraform/policy/custom_rules/nova_tagging.py` | ## How to Write a Schema diff --git a/schemas/policy_check_result.schema.json b/schemas/policy_check_result.schema.json index 08e933d..7da6cf8 100644 --- a/schemas/policy_check_result.schema.json +++ b/schemas/policy_check_result.schema.json @@ -24,7 +24,7 @@ }, "ruleId": { "type": "string", - "description": "Rule identifier (e.g. CKV_AWS_24, KYVERNO_NO_PRIVILEGED, ACDL_TAG_NAMING)." + "description": "Rule identifier (e.g. CKV_AWS_24, KYVERNO_NO_PRIVILEGED, NOVA_TAG_NAMING)." }, "severity": { "type": "string", diff --git a/schemas/tagging-standard.json b/schemas/tagging-standard.json index b3202b1..3c6a9da 100644 --- a/schemas/tagging-standard.json +++ b/schemas/tagging-standard.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://nova.dev/schemas/tagging-standard.json", "title": "Nova Tagging Standard", - "description": "Required tags for all taggable AWS resources created by the platform. Enforced by a Checkov custom Python rule (adapters/terraform/policy/custom_rules/acdl_tagging.py). The checkov adapter maps ACDL_TAG_NAMING as a real rule (D-054, D-043 closure).", + "description": "Required tags for all taggable AWS resources created by the platform. Enforced by a Checkov custom Python rule (adapters/terraform/policy/custom_rules/nova_tagging.py, D-109 warn mode in P2 — legacy acdl:* tag-key values are left for P3). The checkov adapter maps NOVA_TAG_NAMING as a real rule (D-054, D-043 closure; renamed from ACDL_TAG_NAMING in P2, REQ-158).", "type": "object", "properties": { "required_tags": { diff --git a/scripts/push_consumer_image.py b/scripts/push_consumer_image.py index b5c1261..e50524e 100644 --- a/scripts/push_consumer_image.py +++ b/scripts/push_consumer_image.py @@ -3,7 +3,8 @@ Steps performed by this script: 1. Load AWS creds from /root/acdl/.env.secrets - (ACDL_AWS_ACCESS_KEY_ID, ACDL_AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION). + (NOVA_AWS_ACCESS_KEY_ID, NOVA_AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION; + dual-read ACDL_* fallback until P5). 2. Create the ECR repo `acdl-microservice` if it doesn't exist (ecr:DescribeRepositories / ecr:CreateRepository). Region: us-east-1. 3. Get the ECR login password (ecr:GetAuthorizationToken) and run @@ -26,17 +27,26 @@ import pathlib import boto3 +# Repo root on sys.path so `from core import env` resolves to THIS package +# (avoids editable-installed third-party `core` shadow). +_REPO_ROOT = str(pathlib.Path(__file__).resolve().parent.parent) +if _REPO_ROOT not in sys.path: + sys.path.insert(0, _REPO_ROOT) + +from core import env + REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent ENV_FILE = REPO_ROOT / ".env.secrets" -AWS_ACCOUNT_ID = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199") +AWS_ACCOUNT_ID = env.get_env("AWS_ACCOUNT_ID", "581513795199") AWS_REGION = "us-east-1" ECR_REPO_NAME = "acdl-microservice" IMAGE_TAG = "latest" def _load_env(path): - """Load ACDL_AWS_* + AWS_DEFAULT_REGION from a flat KEY=VALUE file.""" + """Load NOVA_AWS_* (preferred) / ACDL_AWS_* (fallback) + AWS_DEFAULT_REGION + from a flat KEY=VALUE file (dual-read per G-106, until P5).""" creds = {} with open(path, "r") as fh: for line in fh: @@ -54,11 +64,12 @@ def main(): return 2 creds = _load_env(ENV_FILE) - access_key = creds.get("ACDL_AWS_ACCESS_KEY_ID") - secret_key = creds.get("ACDL_AWS_SECRET_ACCESS_KEY") + # Dual-read: NOVA_* preferred, ACDL_* fallback (G-106, removed in P5). + access_key = creds.get("NOVA_AWS_ACCESS_KEY_ID") or creds.get("ACDL_AWS_ACCESS_KEY_ID") + secret_key = creds.get("NOVA_AWS_SECRET_ACCESS_KEY") or creds.get("ACDL_AWS_SECRET_ACCESS_KEY") region = creds.get("AWS_DEFAULT_REGION", AWS_REGION) if not access_key or not secret_key: - print("FAIL: ACDL_AWS_ACCESS_KEY_ID / ACDL_AWS_SECRET_ACCESS_KEY missing", + print("FAIL: NOVA_AWS_ACCESS_KEY_ID / NOVA_AWS_SECRET_ACCESS_KEY missing", file=sys.stderr) return 2 diff --git a/scripts/rotate_spike_key.sh b/scripts/rotate_spike_key.sh index 8faf94b..2b0a38a 100755 --- a/scripts/rotate_spike_key.sh +++ b/scripts/rotate_spike_key.sh @@ -1,18 +1,22 @@ #!/usr/bin/env bash # scripts/rotate_spike_key.sh - rotate the acdl-spike-runner IAM access key. # -# Uses the bootstrap root key (ACDL_BOOTSTRAP_AWS_*) from the env to: +# Uses the bootstrap root key (NOVA_BOOTSTRAP_AWS_*, ACDL_BOOTSTRAP_AWS_* +# fallback) from the env to: # 1. List acdl-spike-runner's access keys. # 2. Create a new key. # 3. Deactivate + delete the old key(s). # 4. Write the new key to gitignored .env.secrets (chmod 600). -# 5. Optionally upload to Gitea secrets if ACDL_GITEA_TOKEN is set. +# 5. Optionally upload to Gitea secrets if NOVA_GITEA_TOKEN is set. # # Idempotent: re-running always ends with exactly 1 active key for the user. # Does NOT rotate the bootstrap root key (D-034 closure = manual user step). # # Spike scope (D-039): the spike user key is per-run-rotated; real OIDC is # v1.2 (blocked on go-gitea/gitea#36988). +# Nova rebrand (P2): writes NOVA_* keys; ACDL_* bootstrap fallback kept +# until P5 (the AWS user/role rename acdl-spike-runner → nova-spike-runner +# is P4 territory — left unchanged here). set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" @@ -20,8 +24,9 @@ ENV_FILE="$ROOT/.env.secrets" fail() { echo "FAIL: $*" >&2; exit 1; } -: "${ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID:?set ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID to the root key}" -: "${ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:?set ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY to the root key}" +# Dual-read bootstrap creds: NOVA_* preferred, ACDL_* fallback (removed in P5). +: "${NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID:-${ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID:?set NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID (or ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID) to the root key}}" +: "${NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:-${ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:?set NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY (or ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY) to the root key}}" REGION="${AWS_DEFAULT_REGION:-us-east-1}" USER_NAME="acdl-spike-runner" @@ -38,9 +43,13 @@ region = os.environ.get("AWS_DEFAULT_REGION", "us-east-1") user = "acdl-spike-runner" env_file = os.path.join(os.getcwd(), ".env.secrets") +# Dual-read bootstrap creds: NOVA_* preferred, ACDL_* fallback (G-106, removed in P5). +bootstrap_key = os.environ.get("NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID") or os.environ["ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID"] +bootstrap_secret = os.environ.get("NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY") or os.environ["ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"] + session = boto3.Session( - aws_access_key_id=os.environ["ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID"], - aws_secret_access_key=os.environ["ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"], + aws_access_key_id=bootstrap_key, + aws_secret_access_key=bootstrap_secret, region_name=region, ) iam = session.client("iam") @@ -65,20 +74,23 @@ for k in active: print(f"iam: deactivated+deleted old key {old_id}", file=sys.stderr) # Write the new key to gitignored .env.secrets (chmod 600). +# Nova rebrand (P2): keys are NOVA_*; the ACDL_* legacy keys are the +# dual-read fallback source until P5 (kept as comments in .env.secrets). with open(env_file, "w") as fh: - fh.write(f"ACDL_AWS_ACCESS_KEY_ID={new_id}\n") - fh.write(f"ACDL_AWS_SECRET_ACCESS_KEY={new_secret}\n") + fh.write(f"NOVA_AWS_ACCESS_KEY_ID={new_id}\n") + fh.write(f"NOVA_AWS_SECRET_ACCESS_KEY={new_secret}\n") fh.write(f"AWS_DEFAULT_REGION={region}\n") os.chmod(env_file, 0o600) print(f"rotated key written to {env_file} (chmod 600)", file=sys.stderr) # Optionally upload to Gitea secrets. -gitea_token = os.environ.get("ACDL_GITEA_TOKEN") +# Dual-read token: NOVA_GITEA_TOKEN preferred, ACDL_GITEA_TOKEN fallback (G-106). +gitea_token = os.environ.get("NOVA_GITEA_TOKEN") or os.environ.get("ACDL_GITEA_TOKEN") if gitea_token: import urllib.request base = "https://git.cloudinit.dev/api/v1/repos/continuous-intelligence/acdl/actions/secrets" - for name, value in [("ACDL_AWS_ACCESS_KEY_ID", new_id), - ("ACDL_AWS_SECRET_ACCESS_KEY", new_secret)]: + for name, value in [("NOVA_AWS_ACCESS_KEY_ID", new_id), + ("NOVA_AWS_SECRET_ACCESS_KEY", new_secret)]: req = urllib.request.Request( f"{base}/{name}", data=json.dumps({"value": value}).encode(), @@ -92,7 +104,7 @@ if gitea_token: except Exception as e: print(f"gitea: secret {name} upload FAILED: {e}", file=sys.stderr) else: - print("gitea: ACDL_GITEA_TOKEN not set; Gitea secret upload skipped (v1.2 hardening)", file=sys.stderr) + print("gitea: NOVA_GITEA_TOKEN not set; Gitea secret upload skipped (v1.2 hardening)", file=sys.stderr) print(f"OK: {user} now has exactly 1 active key: {new_id}") PY \ No newline at end of file diff --git a/scripts/run_ci.sh b/scripts/run_ci.sh index 2766784..753f11c 100755 --- a/scripts/run_ci.sh +++ b/scripts/run_ci.sh @@ -45,11 +45,12 @@ python3 -m py_compile \ core/confidence_signal.py \ core/outbox_writer.py \ core/output_publisher.py \ + core/env.py \ core/contract_resolver.py \ core/lambda/contract_ingestor.py \ adapters/terraform/adapter.py \ adapters/terraform/policy/checkov_adapter.py \ - adapters/terraform/policy/custom_rules/acdl_tagging.py \ + adapters/terraform/policy/custom_rules/nova_tagging.py \ adapters/wiz/wiz_adapter.py \ adapters/kyverno/kyverno_adapter.py \ scripts/push_consumer_image.py \ diff --git a/scripts/run_l2_lifecycle_destroy.sh b/scripts/run_l2_lifecycle_destroy.sh index 246d9d2..d28d067 100755 --- a/scripts/run_l2_lifecycle_destroy.sh +++ b/scripts/run_l2_lifecycle_destroy.sh @@ -4,7 +4,8 @@ # Usage: run_l2_lifecycle_destroy.sh # # Wraps run_platform.sh for L2 composition modules in the modules-lifecycle -# pipeline. Sets ACDL_REMOTE_STATE_KEY to point to the CI VPC state. +# pipeline. Sets NOVA_REMOTE_STATE_KEY (ACDL_REMOTE_STATE_KEY fallback until +# P5) to point to the CI VPC state. # # NOTE: unlike the L1 scripts (run_lifecycle_destroy.sh), the L2 path does # NOT take a ci-vpc-outputs.json argument. L2 compositions reference the @@ -13,7 +14,8 @@ # parity with the L1 matrix, but $2 is accepted-but-ignored here (documented, # not a bug). # -# Lifecycle mode (REQ-134): ACDL_LIFECYCLE_MODE default "plan" = no-op +# Lifecycle mode (REQ-134): NOVA_LIFECYCLE_MODE (dual-read NOVA_* preferred, +# ACDL_* fallback until P5) default "plan" = no-op # (plan mode never applies resources, so there is nothing to destroy). # Set to "full" for the real `--destroy` against live AWS. set -euo pipefail @@ -23,7 +25,8 @@ cd "$ROOT" MODULE="$1" # Lifecycle mode: "plan" (default) skips destroy; "full" runs the real destroy. -LIFECYCLE_MODE="${ACDL_LIFECYCLE_MODE:-plan}" +# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). +LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" if [ "$LIFECYCLE_MODE" != "full" ]; then echo "lifecycle mode=$LIFECYCLE_MODE — nothing to destroy (plan-only run), exiting 0" @@ -32,8 +35,10 @@ fi CONTRACT="modules/l2/${MODULE}/examples/complex.yml" -# Point terraform_remote_state to the CI VPC state (not the platform VPC) -export ACDL_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" +# Point terraform_remote_state to the CI VPC state (not the platform VPC). +# Set both NOVA_* (preferred) and ACDL_* (legacy fallback) until P5. +export NOVA_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" +export ACDL_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" # legacy fallback, removed in P5 # Run the platform lifecycle destroy command bash scripts/run_platform.sh --destroy "$CONTRACT" \ No newline at end of file diff --git a/scripts/run_l2_lifecycle_test.sh b/scripts/run_l2_lifecycle_test.sh index 01420be..2d9a67f 100755 --- a/scripts/run_l2_lifecycle_test.sh +++ b/scripts/run_l2_lifecycle_test.sh @@ -4,19 +4,21 @@ # Usage: run_l2_lifecycle_test.sh # # Wraps run_platform.sh for L2 composition modules in the modules-lifecycle -# pipeline. Sets ACDL_REMOTE_STATE_KEY to point to the CI VPC state so the -# microservice composition's terraform_remote_state data source reads from -# the short-lived CI VPC (not the long-lived platform VPC). +# pipeline. Sets NOVA_REMOTE_STATE_KEY (ACDL_REMOTE_STATE_KEY fallback until +# P5) to point to the CI VPC state so the microservice composition's +# terraform_remote_state data source reads from the short-lived CI VPC +# (not the long-lived platform VPC). # # NOTE: unlike the L1 scripts (run_lifecycle_test.sh), the L2 path does NOT # take a ci-vpc-outputs.json argument. L2 compositions reference the platform # VPC via terraform_remote_state (a data source), not by injecting VPC -# outputs into the contract. The ACDL_REMOTE_STATE_KEY env var points the +# outputs into the contract. The NOVA_REMOTE_STATE_KEY env var points the # data source at the correct CI VPC state key. The workflow passes 3 # positional args for parity with the L1 matrix, but $3 is accepted-but- # ignored here (documented, not a bug). # -# Lifecycle mode (REQ-134): ACDL_LIFECYCLE_MODE default "plan" runs +# Lifecycle mode (REQ-134): NOVA_LIFECYCLE_MODE (dual-read NOVA_* preferred, +# ACDL_* fallback until P5) default "plan" runs # `run_platform.sh --plan-only` (fast, no AWS mutation). Set to "full" for # the real `--apply` against live AWS. set -euo pipefail @@ -27,12 +29,16 @@ MODULE="$1" EXAMPLE="$2" # simple or complex # Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS). -LIFECYCLE_MODE="${ACDL_LIFECYCLE_MODE:-plan}" +# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). +LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" CONTRACT="modules/l2/${MODULE}/examples/${EXAMPLE}.yml" -# Point terraform_remote_state to the CI VPC state (not the platform VPC) -export ACDL_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" +# Point terraform_remote_state to the CI VPC state (not the platform VPC). +# Set both NOVA_* (preferred by the dual-read helper) and ACDL_* (legacy +# fallback) so any unmigrated reader finds the key until P5. +export NOVA_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" +export ACDL_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" # legacy fallback, removed in P5 # Run the platform lifecycle command (plan-only by default; full = apply). if [ "$LIFECYCLE_MODE" = "full" ]; then diff --git a/scripts/run_lifecycle_destroy.sh b/scripts/run_lifecycle_destroy.sh index d64bc5b..dd3b8e9 100755 --- a/scripts/run_lifecycle_destroy.sh +++ b/scripts/run_lifecycle_destroy.sh @@ -6,7 +6,8 @@ # For VPC-dependent modules, injects CI VPC outputs into the complex contract # before destroy (so terraform can find the resources in the right VPC). # -# Lifecycle mode (REQ-134): ACDL_LIFECYCLE_MODE default "plan" = no-op +# Lifecycle mode (REQ-134): NOVA_LIFECYCLE_MODE (dual-read NOVA_* preferred, +# ACDL_* fallback until P5) default "plan" = no-op # (plan mode never applies resources, so there is nothing to destroy; the # script exits 0 so the pipeline matrix cell stays green). Set to "full" # for the real `--destroy` against live AWS. @@ -19,7 +20,8 @@ CI_VPC_OUTPUTS="${2:-}" # Lifecycle mode: "plan" (default) skips destroy (nothing was applied); # "full" runs the real terraform destroy. -LIFECYCLE_MODE="${ACDL_LIFECYCLE_MODE:-plan}" +# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). +LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" if [ "$LIFECYCLE_MODE" != "full" ]; then echo "lifecycle mode=$LIFECYCLE_MODE — nothing to destroy (plan-only run), exiting 0" diff --git a/scripts/run_lifecycle_test.sh b/scripts/run_lifecycle_test.sh index 715f9f8..d229e0e 100755 --- a/scripts/run_lifecycle_test.sh +++ b/scripts/run_lifecycle_test.sh @@ -10,8 +10,9 @@ # The CI VPC is short-lived (created/destroyed by the pipeline), separate # from the long-lived platform VPC. # -# Lifecycle mode (REQ-134): the ACDL_LIFECYCLE_MODE env var selects the -# tier. Default "plan" runs `run_platform.sh --plan-only` (fast, no AWS +# Lifecycle mode (REQ-134): the NOVA_LIFECYCLE_MODE env var selects the +# tier (dual-read NOVA_* preferred, ACDL_* fallback until P5). Default +# "plan" runs `run_platform.sh --plan-only` (fast, no AWS # mutation, validates the contract->resolver->adapter->plan chain for # every module). Set to "full" to run the real `--apply` (terraform apply # against live AWS). The CI variable is passed via the workflow input @@ -25,7 +26,8 @@ EXAMPLE="$2" # simple or complex CI_VPC_OUTPUTS="${3:-}" # Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS). -LIFECYCLE_MODE="${ACDL_LIFECYCLE_MODE:-plan}" +# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). +LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" CONTRACT="modules/l1/${MODULE}/examples/${EXAMPLE}.yml" diff --git a/scripts/run_platform.sh b/scripts/run_platform.sh index 03fa1b0..1648c5a 100755 --- a/scripts/run_platform.sh +++ b/scripts/run_platform.sh @@ -40,7 +40,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # Capture the caller's CWD before we cd to ROOT. The reusable deploy workflow # invokes this script from the CONSUMER repo's workspace root with a relative -# contract path (e.g. .acdl/contract.yml); the contract must resolve against +# contract path (e.g. .nova/contract.yml); the contract must resolve against # the consumer repo, not the platform repo (platform/). Without this, the # `[ -f "$CONTRACT" ]` check below looks for the contract inside the platform # repo and fails (P0 fix — see docs/CONSUMER_GUIDE.md Step 4). @@ -118,10 +118,12 @@ fail() { echo "FAIL: $*" >&2; exit 1; } # state, and the contract-ingestor Lambda in-process. Exits 0 on success. if [ "$LOCAL_TIER" = "1" ]; then [ -n "$CONTRACT" ] || CONTRACT="contracts/microservice.yml" - echo "=== ACDL Local Emulating Tier (D-092) ===" + echo "=== Nova Local Emulating Tier (D-092) ===" echo "contract: $CONTRACT (no AWS credentials required)" echo "" - ACDL_LOCAL_TIER=1 python3 core/local_emulators.py "$CONTRACT" \ + # Dual-read: set NOVA_LOCAL_TIER (preferred); ACDL_LOCAL_TIER fallback + # kept for any unmigrated reader until P5 (removed in P5). + NOVA_LOCAL_TIER=1 ACDL_LOCAL_TIER=1 python3 core/local_emulators.py "$CONTRACT" \ || fail "local E2E failed" echo "" echo "=== LOCAL E2E OK ===" @@ -147,7 +149,8 @@ rm -rf "$WORK"; mkdir -p "$TF_DIR" echo "=== Step 0: environment onboarding check ===" if [ -n "$ENVIRONMENT_OVERRIDE" ]; then - export ACDL_ENVIRONMENT_OVERRIDE="$ENVIRONMENT_OVERRIDE" + export NOVA_ENVIRONMENT_OVERRIDE="$ENVIRONMENT_OVERRIDE" + export ACDL_ENVIRONMENT_OVERRIDE="$ENVIRONMENT_OVERRIDE" # legacy fallback, removed in P5 python3 core/environment_check.py --env="$ENVIRONMENT_OVERRIDE" || { echo "FAIL: environment not bound — see the onboarding prompt above" >&2 exit 1 @@ -285,8 +288,11 @@ if [ -z "${AWS_ACCESS_KEY_ID:-}" ] || [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then set -a . "$ENV_FILE" set +a - export AWS_ACCESS_KEY_ID="$ACDL_AWS_ACCESS_KEY_ID" - export AWS_SECRET_ACCESS_KEY="$ACDL_AWS_SECRET_ACCESS_KEY" + # G-106 binding: dual-read NOVA_* first, ACDL_* fallback. The .env.secrets + # keys are renamed to NOVA_* in P2; the ACDL_* fallback covers operators + # who haven't rotated their local .env.secrets yet. Removed in P5. + export AWS_ACCESS_KEY_ID="${NOVA_AWS_ACCESS_KEY_ID:-$ACDL_AWS_ACCESS_KEY_ID}" + export AWS_SECRET_ACCESS_KEY="${NOVA_AWS_SECRET_ACCESS_KEY:-$ACDL_AWS_SECRET_ACCESS_KEY}" export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION" fi @@ -333,16 +339,17 @@ if [ "$APPLY_ONLY" = "1" ]; then import os, sys sys.path.insert(0, '.') from core.hitl_gates import attest -contract_id = os.environ['ACDL_HITL_CONTRACT_ID'] -env = os.environ['ACDL_HITL_ENV'] -approver = os.environ.get('ACDL_HITL_APPROVER', '') or 'local-test' +from core import env as _envhelper +contract_id = _envhelper.get_env('HITL_CONTRACT_ID') or os.environ['NOVA_HITL_CONTRACT_ID'] +env = _envhelper.get_env('HITL_ENV') or os.environ['NOVA_HITL_ENV'] +approver = _envhelper.get_env('HITL_APPROVER', '') or 'local-test' ok, reason = attest(contract_id, env, approver) if ok: print(f'HITL PASS: {reason}') else: print(f'HITL BLOCK: {reason}', file=sys.stderr) sys.exit(1) -" ACDL_HITL_CONTRACT_ID="$CONTRACT_ID" ACDL_HITL_ENV="$RESOLVED_ENV" ACDL_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the apply" >&2; exit 1; } +" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" ACDL_HITL_CONTRACT_ID="$CONTRACT_ID" ACDL_HITL_ENV="$RESOLVED_ENV" ACDL_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the apply" >&2; exit 1; } else echo "Environment is dev — autonomous (no HITL gate)." fi @@ -448,16 +455,17 @@ if [ "$RESOLVED_ENV" != "dev" ]; then import os, sys sys.path.insert(0, '.') from core.hitl_gates import attest -contract_id = os.environ['ACDL_HITL_CONTRACT_ID'] -env = os.environ['ACDL_HITL_ENV'] -approver = os.environ.get('ACDL_HITL_APPROVER', '') or 'local-test' +from core import env as _envhelper +contract_id = _envhelper.get_env('HITL_CONTRACT_ID') or os.environ['NOVA_HITL_CONTRACT_ID'] +env = _envhelper.get_env('HITL_ENV') or os.environ['NOVA_HITL_ENV'] +approver = _envhelper.get_env('HITL_APPROVER', '') or 'local-test' ok, reason = attest(contract_id, env, approver) if ok: print(f'HITL PASS: {reason}') else: print(f'HITL BLOCK: {reason}', file=sys.stderr) sys.exit(1) -" ACDL_HITL_CONTRACT_ID="$CONTRACT_ID" ACDL_HITL_ENV="$RESOLVED_ENV" ACDL_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; } +" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" ACDL_HITL_CONTRACT_ID="$CONTRACT_ID" ACDL_HITL_ENV="$RESOLVED_ENV" ACDL_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; } else echo "Environment is dev — autonomous (no HITL gate)." fi diff --git a/scripts/run_regression.sh b/scripts/run_regression.sh index 6b23443..c15d00c 100755 --- a/scripts/run_regression.sh +++ b/scripts/run_regression.sh @@ -7,8 +7,9 @@ # # Usage: # bash scripts/run_regression.sh # run all checks -# ACDL_REGRESSION_MILESTONE=v1.10 ACDL_REGRESSION_PHASE=52 \ +# NOVA_REGRESSION_MILESTONE=v1.10 NOVA_REGRESSION_PHASE=52 \ # bash scripts/run_regression.sh # override metadata +# (ACDL_REGRESSION_* legacy fallback kept until P5) # # Output: # .ciagent/REGRESSION_REPORT.md human-readable report @@ -17,8 +18,9 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" -echo "=== ACDL Regression VERIFY (D-091) ===" -echo "milestone: ${ACDL_REGRESSION_MILESTONE:-v1.10} phase: ${ACDL_REGRESSION_PHASE:-52}" +echo "=== Nova Regression VERIFY (D-091) ===" +# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). +echo "milestone: ${NOVA_REGRESSION_MILESTONE:-${ACDL_REGRESSION_MILESTONE:-v1.10}} phase: ${NOVA_REGRESSION_PHASE:-${ACDL_REGRESSION_PHASE:-52}}" echo "" python3 core/regression_verify.py diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 4d6fcba..68b1aee 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -408,14 +408,16 @@ class TestAdapterDedupMergesSameModule: class TestAdapterRemoteStateKeyOverride: - """P2-2 (v1.14, REQ-139): ACDL_REMOTE_STATE_KEY env var overrides the - default 'platform/terraform.tfstate' key in the emitted + """P2-2 (v1.14, REQ-139): NOVA_REMOTE_STATE_KEY env var (P2 renamed from + ACDL_REMOTE_STATE_KEY; dual-read NOVA_* preferred, ACDL_* fallback until + P5) overrides the default 'platform/terraform.tfstate' key in the emitted data terraform_remote_state block. This is the load-bearing correctness mechanism for the microservice L2 lifecycle (remote state points at the CI VPC, not the platform VPC).""" def test_default_remote_state_key(self, tmp_path, monkeypatch): - """When ACDL_REMOTE_STATE_KEY is unset, the default key is used.""" + """When NOVA_REMOTE_STATE_KEY is unset, the default key is used.""" + monkeypatch.delenv("NOVA_REMOTE_STATE_KEY", raising=False) monkeypatch.delenv("ACDL_REMOTE_STATE_KEY", raising=False) stack = { "resources": [ @@ -431,9 +433,9 @@ class TestAdapterRemoteStateKeyOverride: assert "platform/terraform.tfstate" in main_tf def test_env_override_remote_state_key(self, tmp_path, monkeypatch): - """When ACDL_REMOTE_STATE_KEY is set, the emitted data block uses + """When NOVA_REMOTE_STATE_KEY is set, the emitted data block uses the overridden key (e.g. 'spike/ci-vpc/terraform.tfstate').""" - monkeypatch.setenv("ACDL_REMOTE_STATE_KEY", "spike/ci-vpc/terraform.tfstate") + monkeypatch.setenv("NOVA_REMOTE_STATE_KEY", "spike/ci-vpc/terraform.tfstate") stack = { "resources": [ {"id": "s3", "type": "aws:s3:bucket", "module": "s3@1.0.0", "inputs": {"bucket_name": "test", "region": "us-east-1"}} diff --git a/tests/test_attestation_matrix.py b/tests/test_attestation_matrix.py index e3f4c2b..983d48e 100644 --- a/tests/test_attestation_matrix.py +++ b/tests/test_attestation_matrix.py @@ -102,6 +102,8 @@ def test_dr_blocks_on_missing_dr_drill(): def test_signature_skip_when_key_unset(monkeypatch, capsys): """D-089: signature verification is skipped when the signing key is unset.""" + # P2: dual-read — both NOVA_* and ACDL_* must be unset for the skip. + monkeypatch.delenv("NOVA_ATTESTATION_SIGNING_KEY_ID", raising=False) monkeypatch.delenv("ACDL_ATTESTATION_SIGNING_KEY_ID", raising=False) artifact = {"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(), "type": "x", "payload": {}, "signature": "sig"} @@ -112,7 +114,7 @@ def test_signature_skip_when_key_unset(monkeypatch, capsys): def test_signature_required_when_key_set(monkeypatch): """When the signing key is set, a missing signature fails.""" - monkeypatch.setenv("ACDL_ATTESTATION_SIGNING_KEY_ID", "kms-key-id") + monkeypatch.setenv("NOVA_ATTESTATION_SIGNING_KEY_ID", "kms-key-id") artifact = {"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(), "type": "x", "payload": {}} # no signature assert _verify_signature(artifact) is False diff --git a/tests/test_checkov_adapter.py b/tests/test_checkov_adapter.py index 123ee87..b2a398c 100644 --- a/tests/test_checkov_adapter.py +++ b/tests/test_checkov_adapter.py @@ -67,10 +67,10 @@ class TestToPcr: class TestRuleMapTagging: - def test_acdl_tag_naming_is_real_rule(self): - # D-054 / D-043 closure: ACDL_TAG_NAMING is now a real custom Checkov - # rule, not a synthetic SKIPPED record. - assert RULE_MAP["ACDL_TAG_NAMING"] == ("tagging-standard", "medium") + def test_nova_tag_naming_is_real_rule(self): + # D-054 / D-043 closure, P2 rename (REQ-158): NOVA_TAG_NAMING is now + # a real custom Checkov rule, not a synthetic SKIPPED record. + assert RULE_MAP["NOVA_TAG_NAMING"] == ("tagging-standard", "medium") class TestAdapt: @@ -100,13 +100,13 @@ class TestAdapt: def test_adapt_does_not_emit_synthetic_tag_naming(self, tmp_path): # D-043 closure: adapt() no longer appends a synthetic SKIPPED - # ACDL_TAG_NAMING record. The custom Checkov rule (loaded via + # NOVA_TAG_NAMING record. The custom Checkov rule (loaded via # --external-checks-dir) produces real PASS/FAIL records instead. data = self._sample_checkov_json() f = tmp_path / "checkov.json" f.write_text(json.dumps(data)) results = adapt(str(f), "c-1") - tag = [r for r in results if r["ruleId"] == "ACDL_TAG_NAMING"] + tag = [r for r in results if r["ruleId"] == "NOVA_TAG_NAMING"] assert tag == [] # no synthetic record def test_adapt_has_passed_and_failed(self, tmp_path): diff --git a/tests/test_deploy_workflow_env_input.py b/tests/test_deploy_workflow_env_input.py index 6028174..d5d8553 100644 --- a/tests/test_deploy_workflow_env_input.py +++ b/tests/test_deploy_workflow_env_input.py @@ -78,4 +78,6 @@ def test_run_platform_sh_has_environment_flag(): text = (ROOT / "scripts" / "run_platform.sh").read_text() assert "--environment" in text assert "ENVIRONMENT_OVERRIDE" in text - assert "ACDL_ENVIRONMENT_OVERRIDE" in text \ No newline at end of file + # P2 (REQ-159): NOVA_* preferred; ACDL_* kept as dual-read fallback until P5. + assert "NOVA_ENVIRONMENT_OVERRIDE" in text + assert "ACDL_ENVIRONMENT_OVERRIDE" in text # legacy fallback, removed in P5 \ No newline at end of file diff --git a/tests/test_env_helper.py b/tests/test_env_helper.py new file mode 100644 index 0000000..09ca516 --- /dev/null +++ b/tests/test_env_helper.py @@ -0,0 +1,56 @@ +"""Unit tests for the dual-read env helper (core/env.py, D-108, REQ-159). + +Covers the four cases: + - both NOVA_* and ACDL_* set (NOVA wins) + - only NOVA_* set + - only ACDL_* set (fallback) + - neither set (default returned) + +The ACDL_* fallback is the intentional dual-read source and is removed +in P5 (REQ-164). These fixtures deliberately keep the ACDL_* names as +the fallback source — they are the one allowed ACDL_* reference. +""" + +from __future__ import annotations + +import pytest + +from core import env + + +@pytest.fixture(autouse=True) +def _isolate_env(monkeypatch): + """Ensure no ACDL_*/NOVA_* leakage between tests.""" + for key in list(__import__("os").environ): + if key.startswith(("ACDL_", "NOVA_")): + monkeypatch.delenv(key, raising=False) + yield + + +def test_both_set_nova_wins(monkeypatch): + monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "nova-value") + monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value") + assert env.get_env("AWS_ACCOUNT_ID") == "nova-value" + + +def test_only_nova_set(monkeypatch): + monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "nova-value") + assert env.get_env("AWS_ACCOUNT_ID") == "nova-value" + + +def test_only_acdl_set_fallback(monkeypatch): + # ACDL_* is the intentional dual-read fallback source (removed in P5). + monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value") + assert env.get_env("AWS_ACCOUNT_ID") == "acdl-value" + + +def test_neither_set_returns_default(): + assert env.get_env("AWS_ACCOUNT_ID") is None + assert env.get_env("AWS_ACCOUNT_ID", default="581513795199") == "581513795199" + + +def test_blank_nova_falls_back_to_acdl(monkeypatch): + # An explicitly-empty NOVA key must not shadow the ACDL fallback. + monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "") + monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value") + assert env.get_env("AWS_ACCOUNT_ID") == "acdl-value" \ No newline at end of file diff --git a/tests/test_lifecycle_mode_flag.py b/tests/test_lifecycle_mode_flag.py index 4f8cb55..0b526fd 100644 --- a/tests/test_lifecycle_mode_flag.py +++ b/tests/test_lifecycle_mode_flag.py @@ -2,9 +2,10 @@ The lifecycle scripts (run_lifecycle_test.sh, run_lifecycle_destroy.sh, run_l2_lifecycle_test.sh, run_l2_lifecycle_destroy.sh) wrap run_platform.sh. -REQ-134 (v1.12) adds the ACDL_LIFECYCLE_MODE env var: default "plan" runs -`run_platform.sh --plan-only` (fast, no AWS mutation); "full" runs the real -`--apply`/`--destroy` against live AWS. +REQ-134 (v1.12) adds the NOVA_LIFECYCLE_MODE env var (P2 renamed from +ACDL_LIFECYCLE_MODE, dual-read NOVA_* preferred / ACDL_* fallback until +P5): default "plan" runs `run_platform.sh --plan-only` (fast, no AWS +mutation); "full" runs the real `--apply`/`--destroy` against live AWS. These tests verify the dispatch logic offline by inspecting script content (running the scripts end-to-end requires AWS credentials in full mode). @@ -32,12 +33,14 @@ class TestLifecycleModeFlag: @pytest.mark.parametrize("script", SCRIPTS) def test_script_reads_acdl_lifecycle_mode(self, script): - """Every lifecycle script reads ACDL_LIFECYCLE_MODE with a 'plan' default.""" + """Every lifecycle script reads NOVA_LIFECYCLE_MODE (dual-read with + ACDL_LIFECYCLE_MODE fallback) with a 'plan' default.""" src = _read(script) - # The default must be 'plan' (the speed-up default). The scripts use - # an intermediate LIFECYCLE_MODE var sourced from ACDL_LIFECYCLE_MODE. - assert "${ACDL_LIFECYCLE_MODE:-plan}" in src, \ - f"{script} must read ACDL_LIFECYCLE_MODE defaulting to 'plan'" + # P2 (REQ-159): dual-read NOVA_* preferred, ACDL_* fallback. The + # ACDL_LIFECYCLE_MODE:-plan substring is still present inside the + # nested dual-read expression (removed in P5). + assert "${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" in src, \ + f"{script} must dual-read NOVA_/ACDL_LIFECYCLE_MODE defaulting to 'plan'" assert "LIFECYCLE_MODE=" in src, \ f"{script} must assign LIFECYCLE_MODE from the env var" diff --git a/tests/test_local_emulating_adapters.py b/tests/test_local_emulating_adapters.py index 184ca94..c164b05 100644 --- a/tests/test_local_emulating_adapters.py +++ b/tests/test_local_emulating_adapters.py @@ -179,11 +179,11 @@ def test_local_lambda_stub_rejects_missing_field(tmp_path): def test_run_local_e2e_microservice(): """Headline E2E: contract -> resolver -> adapter -> local S3 backend -> local ECS (HTTP 200) -> flat-file outbox -> local Lambda. No AWS.""" - os.environ["ACDL_LOCAL_TIER"] = "1" + os.environ["NOVA_LOCAL_TIER"] = "1" try: result = le.run_local_e2e("contracts/microservice.yml") finally: - os.environ.pop("ACDL_LOCAL_TIER", None) + os.environ.pop("NOVA_LOCAL_TIER", None); os.environ.pop("ACDL_LOCAL_TIER", None) assert result["tier"] == "local-emulator" assert result["backend"] == "local" assert result["ecs"] is not None @@ -196,11 +196,11 @@ def test_run_local_e2e_microservice(): def test_run_local_e2e_static_assets(): """Static-assets stack has no ECS service; the local E2E must still complete (ecs=None) and the outbox chain + Lambda stub must pass.""" - os.environ["ACDL_LOCAL_TIER"] = "1" + os.environ["NOVA_LOCAL_TIER"] = "1" try: result = le.run_local_e2e("contracts/static-assets.yml") finally: - os.environ.pop("ACDL_LOCAL_TIER", None) + os.environ.pop("NOVA_LOCAL_TIER", None); os.environ.pop("ACDL_LOCAL_TIER", None) assert result["tier"] == "local-emulator" assert result["ecs"] is None # no ECS service in this stack assert result["outbox_chain_verified"] is True @@ -209,9 +209,9 @@ def test_run_local_e2e_static_assets(): def test_is_local_tier_flag(): assert le.is_local_tier() is False - os.environ["ACDL_LOCAL_TIER"] = "1" + os.environ["NOVA_LOCAL_TIER"] = "1" try: assert le.is_local_tier() is True finally: - os.environ.pop("ACDL_LOCAL_TIER", None) + os.environ.pop("NOVA_LOCAL_TIER", None); os.environ.pop("ACDL_LOCAL_TIER", None) assert le.is_local_tier() is False \ No newline at end of file diff --git a/tests/test_output_publisher.py b/tests/test_output_publisher.py index 2c7f4f4..78e177e 100644 --- a/tests/test_output_publisher.py +++ b/tests/test_output_publisher.py @@ -376,7 +376,9 @@ class TestCli: # --------------------------------------------------------------------------- class TestKmsFailLoud: - """P1-3: SSM publisher must fail loud when ACDL_KMS_KEY_ID is unset.""" + """P1-3: SSM publisher must fail loud when NOVA_KMS_KEY_ID is unset + (P2 renamed from ACDL_KMS_KEY_ID; dual-read NOVA_* preferred, + ACDL_* fallback until P5).""" def test_kms_unset_raises(self, monkeypatch): from moto import mock_aws @@ -385,11 +387,14 @@ class TestKmsFailLoud: monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") + # Both NOVA_* and ACDL_* unset → helper returns default (None) → fail loud. + monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False) monkeypatch.delenv("ACDL_KMS_KEY_ID", raising=False) + monkeypatch.delenv("NOVA_ALLOW_DEFAULT_KMS", raising=False) monkeypatch.delenv("ACDL_ALLOW_DEFAULT_KMS", raising=False) with mock_aws(): - with pytest.raises(RuntimeError, match="ACDL_KMS_KEY_ID is not set"): + with pytest.raises(RuntimeError, match="NOVA_KMS_KEY_ID is not set"): publish_to_ssm({"vpc_id": "vpc-1"}, "dev", "c-1") def test_kms_unset_allow_default_kms_escape_hatch(self, monkeypatch): @@ -399,8 +404,9 @@ class TestKmsFailLoud: monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") + monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False) monkeypatch.delenv("ACDL_KMS_KEY_ID", raising=False) - monkeypatch.setenv("ACDL_ALLOW_DEFAULT_KMS", "1") + monkeypatch.setenv("NOVA_ALLOW_DEFAULT_KMS", "1") with mock_aws(): ssm = boto3.client("ssm", region_name="us-east-1") @@ -415,8 +421,8 @@ class TestKmsFailLoud: monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") - monkeypatch.setenv("ACDL_KMS_KEY_ID", "arn:aws:kms:us-east-1:123:key/abc") - monkeypatch.setenv("ACDL_ALLOW_DEFAULT_KMS", "1") + monkeypatch.setenv("NOVA_KMS_KEY_ID", "arn:aws:kms:us-east-1:123:key/abc") + monkeypatch.setenv("NOVA_ALLOW_DEFAULT_KMS", "1") from core.output_publisher import _kms_key_id assert _kms_key_id() == "arn:aws:kms:us-east-1:123:key/abc" diff --git a/tests/test_pipeline_contract.py b/tests/test_pipeline_contract.py index e16cb9b..6cf18c7 100644 --- a/tests/test_pipeline_contract.py +++ b/tests/test_pipeline_contract.py @@ -310,7 +310,7 @@ class TestDeployWorkflowConformance: wf = _load_workflow(".gitea/workflows/deploy.yml") inputs = wf["on"]["workflow_call"]["inputs"] assert "contract" in inputs - assert inputs["contract"]["default"] == ".acdl/contract.yml" + assert inputs["contract"]["default"] == ".nova/contract.yml" def test_deploy_workflow_has_mode_input(self): wf = _load_workflow(".gitea/workflows/deploy.yml") @@ -646,20 +646,20 @@ class TestModulesLifecyclePipeline: assert set(inputs["lifecycle_mode"].get("options", [])) == {"plan", "full"} def test_lifecycle_job_passes_mode_env_to_steps(self): - """The lifecycle job sets ACDL_LIFECYCLE_MODE env so scripts dispatch + """The lifecycle job sets NOVA_LIFECYCLE_MODE env so scripts dispatch to plan-only by default, full on override.""" wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml") env = wf["jobs"]["lifecycle"].get("env", {}) - assert "ACDL_LIFECYCLE_MODE" in env + assert "NOVA_LIFECYCLE_MODE" in env # The expression must resolve to 'plan' when no input/var is set. - assert "plan" in env["ACDL_LIFECYCLE_MODE"] + assert "plan" in env["NOVA_LIFECYCLE_MODE"] def test_l2_lifecycle_job_passes_mode_env_to_steps(self): - """The L2 lifecycle job also sets ACDL_LIFECYCLE_MODE env.""" + """The L2 lifecycle job also sets NOVA_LIFECYCLE_MODE env.""" wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml") env = wf["jobs"]["l2-lifecycle"].get("env", {}) - assert "ACDL_LIFECYCLE_MODE" in env - assert "plan" in env["ACDL_LIFECYCLE_MODE"] + assert "NOVA_LIFECYCLE_MODE" in env + assert "plan" in env["NOVA_LIFECYCLE_MODE"] def test_ci_vpc_apply_skipped_in_plan_mode(self): """The CI VPC apply job is skipped in plan mode (nothing is applied).""" diff --git a/tests/test_route_halt_artifact.py b/tests/test_route_halt_artifact.py index 9884222..a065a00 100644 --- a/tests/test_route_halt_artifact.py +++ b/tests/test_route_halt_artifact.py @@ -29,6 +29,7 @@ def test_route_halt_publishes_to_sns_when_arn_set(monkeypatch): def test_route_halt_falls_back_to_stderr_when_arn_unset(monkeypatch, capsys): """Without ACDL_SOD_HALT_TOPIC_ARN, a stderr emission occurs.""" + monkeypatch.delenv("NOVA_SOD_HALT_TOPIC_ARN", raising=False) monkeypatch.delenv("ACDL_SOD_HALT_TOPIC_ARN", raising=False) # Mock outbox_writer.write_event to avoid AWS calls. with mock.patch("core.outbox_writer.write_event", return_value=None): @@ -40,6 +41,7 @@ def test_route_halt_falls_back_to_stderr_when_arn_unset(monkeypatch, capsys): def test_route_halt_outbox_fallback_writes_event(monkeypatch): """Without the SNS ARN, the outbox fallback writes a SEPARATION_OF_DUTIES_VIOLATION event.""" + monkeypatch.delenv("NOVA_SOD_HALT_TOPIC_ARN", raising=False) monkeypatch.delenv("ACDL_SOD_HALT_TOPIC_ARN", raising=False) with mock.patch("core.outbox_writer.write_event") as mock_write: route_halt_artifact("contract-789", "sod violation", oncall_client=None) diff --git a/tests/test_untested_scripts.py b/tests/test_untested_scripts.py index 1db0cf8..e8587d0 100644 --- a/tests/test_untested_scripts.py +++ b/tests/test_untested_scripts.py @@ -106,9 +106,11 @@ class TestCreateStateBackend: """terraform/bootstrap/create_state_backend.py — mock boto3.""" def test_state_bucket_name_construction(self, monkeypatch): - """The state bucket name is derived from ACDL_AWS_ACCOUNT_ID.""" - monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "123456789012") - account_id = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199") + """The state bucket name is derived from NOVA_AWS_ACCOUNT_ID + (P2 renamed from ACDL_AWS_ACCOUNT_ID; the bucket name acdl-tfstate-* + stays until P4, REQ-163).""" + monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "123456789012") + account_id = os.environ.get("NOVA_AWS_ACCOUNT_ID", "581513795199") state_bucket = f"acdl-tfstate-{account_id}-us-east-1" assert state_bucket == "acdl-tfstate-123456789012-us-east-1"