mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 06:53:12 +00:00
fix(ci): skip duplicate verify on content CD and ignore origin timeout drift (SH-300) (#183)
* fix(terraform): ignore origin response_completion_timeout in the release plan guard (SH-300) AWS returns 0 when the timeout is unset. The provider writes null on origin_path updates, so the first real CD plan failed closed. * fix(ci): drop duplicate verify from the content CD workflow (SH-300) Frontend checks already runs verify on PRs and pushes. Removing the validate job also requires dropping needs: validate so dispatch can run. * fix(terraform): equate origin timeout 0 and null only (SH-300) Numeric timeout changes still fail closed. Rename the filter so it is not read as an after_unknown allowlist.
This commit is contained in:
parent
7716b4afc8
commit
f23c60ccc2
5 changed files with 245 additions and 55 deletions
56
.github/workflows/deploy.yml
vendored
56
.github/workflows/deploy.yml
vendored
|
|
@ -1,13 +1,14 @@
|
||||||
name: Validate and deploy
|
name: Deploy dev content
|
||||||
|
|
||||||
# Dev content CD through Terraform (SH-300). GitHub uploads an immutable
|
# Dev content CD through Terraform (SH-300). GitHub uploads an immutable
|
||||||
# releases/<sha>-<run>-<attempt>/ prefix. Terraform owns the pointer, origin
|
# releases/<sha>-<run>-<attempt>/ prefix. Terraform owns the pointer, origin
|
||||||
# group, and invalidation. Push-to-dev stays off until
|
# group, and invalidation. Push-to-dev stays off until
|
||||||
# vars.TERRAFORM_CONTENT_CD_ENABLED is the string true.
|
# vars.TERRAFORM_CONTENT_CD_ENABLED is the string true.
|
||||||
|
#
|
||||||
|
# Quality gates live in Frontend checks (`ci.yaml`). This workflow does not
|
||||||
|
# re-run those gates on pull requests, pushes, or workflow_dispatch.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
|
||||||
branches: [dev]
|
|
||||||
push:
|
push:
|
||||||
branches: [dev]
|
branches: [dev]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
|
|
@ -18,61 +19,12 @@ permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate:
|
|
||||||
name: Validate production build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
||||||
with:
|
|
||||||
node-version: "24"
|
|
||||||
cache: npm
|
|
||||||
- name: Set up Terraform
|
|
||||||
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
|
|
||||||
with:
|
|
||||||
terraform_version: "1.16.0"
|
|
||||||
terraform_wrapper: false
|
|
||||||
- name: Install actionlint
|
|
||||||
env:
|
|
||||||
ACTIONLINT_VERSION: "1.7.12"
|
|
||||||
ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
curl -fsSL -o actionlint.tar.gz \
|
|
||||||
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
|
|
||||||
echo "${ACTIONLINT_SHA256} actionlint.tar.gz" | sha256sum -c -
|
|
||||||
tar -xzf actionlint.tar.gz actionlint
|
|
||||||
sudo mv actionlint /usr/local/bin/actionlint
|
|
||||||
- name: Quality gates
|
|
||||||
run: npm ci && npm run verify
|
|
||||||
env:
|
|
||||||
GOVERNANCE_BASE: ${{ github.event.pull_request.base.sha || 'origin/dev' }}
|
|
||||||
- name: Build with pinned API URL
|
|
||||||
env:
|
|
||||||
VITE_API_URL: https://api.dev.seahaven.com/api
|
|
||||||
VITE_APP_COMMIT_SHA: ${{ github.sha }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
npm run build
|
|
||||||
if grep -Rq "api.staging.seahaven.com" dist/; then
|
|
||||||
echo "::error::Built assets contain the staging API URL." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if grep -Rq "localhost:5141" dist/; then
|
|
||||||
echo "::error::Built assets contain the Vite proxy target localhost:5141." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
grep -Rq "api.dev.seahaven.com" dist/
|
|
||||||
|
|
||||||
deploy-dev:
|
deploy-dev:
|
||||||
name: Deploy shoc-frontend-new-dev through Terraform
|
name: Deploy shoc-frontend-new-dev through Terraform
|
||||||
if: >
|
if: >
|
||||||
(github.event_name == 'push' && github.ref == 'refs/heads/dev' &&
|
(github.event_name == 'push' && github.ref == 'refs/heads/dev' &&
|
||||||
vars.TERRAFORM_CONTENT_CD_ENABLED == 'true') ||
|
vars.TERRAFORM_CONTENT_CD_ENABLED == 'true') ||
|
||||||
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev')
|
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev')
|
||||||
needs: validate
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 180
|
timeout-minutes: 180
|
||||||
permissions:
|
permissions:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
Accepts exactly:
|
Accepts exactly:
|
||||||
- an update of the release pointer (content, plus computed etag/version_id)
|
- an update of the release pointer (content, plus computed etag/version_id)
|
||||||
- an update of the distribution with only origin[*].origin_path changed
|
- an update of the distribution with only origin[*].origin_path changed
|
||||||
|
(response_completion_timeout 0, null, and a missing key are equivalent)
|
||||||
- exactly one action invocation for the CloudFront invalidation
|
- exactly one action invocation for the CloudFront invalidation
|
||||||
|
|
||||||
after origin_path values must match the expected labels. before origin_path
|
after origin_path values must match the expected labels. before origin_path
|
||||||
|
|
@ -52,6 +53,11 @@ DISTRIBUTION_UNKNOWN_ATTRIBUTES = frozenset(
|
||||||
"in_progress_validation_batches",
|
"in_progress_validation_batches",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
# Not an after_unknown allowlist. AWS returns 0 when the timeout is unset;
|
||||||
|
# the provider writes null on origin_path updates. Treat 0, null, and a
|
||||||
|
# missing key as the same. Any other value still fails closed.
|
||||||
|
ORIGIN_RESPONSE_COMPLETION_TIMEOUT = "response_completion_timeout"
|
||||||
|
ORIGIN_TIMEOUT_UNSET = frozenset({0, None})
|
||||||
REDIRECT_STATUSES = {301, 302, 303, 307, 308}
|
REDIRECT_STATUSES = {301, 302, 303, 307, 308}
|
||||||
|
|
||||||
UrlOpen = Callable[..., Any]
|
UrlOpen = Callable[..., Any]
|
||||||
|
|
@ -302,10 +308,16 @@ def _validate_pointer(
|
||||||
return violations
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
def _origin_fields_for_compare(origin: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
rest = {key: value for key, value in origin.items() if key != "origin_path"}
|
||||||
|
timeout = rest.get(ORIGIN_RESPONSE_COMPLETION_TIMEOUT)
|
||||||
|
if timeout in ORIGIN_TIMEOUT_UNSET:
|
||||||
|
rest.pop(ORIGIN_RESPONSE_COMPLETION_TIMEOUT, None)
|
||||||
|
return rest
|
||||||
|
|
||||||
|
|
||||||
def _origin_non_path_fields_changed(before: dict[str, Any], after: dict[str, Any]) -> bool:
|
def _origin_non_path_fields_changed(before: dict[str, Any], after: dict[str, Any]) -> bool:
|
||||||
before_rest = {key: value for key, value in before.items() if key != "origin_path"}
|
return _origin_fields_for_compare(before) != _origin_fields_for_compare(after)
|
||||||
after_rest = {key: value for key, value in after.items() if key != "origin_path"}
|
|
||||||
return before_rest != after_rest
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_distribution(
|
def _validate_distribution(
|
||||||
|
|
|
||||||
|
|
@ -291,12 +291,29 @@ def test_deploy_workflow_confirms_prefix_with_head_object() -> list[str]:
|
||||||
)
|
)
|
||||||
if "aws s3 ls" in workflow:
|
if "aws s3 ls" in workflow:
|
||||||
failures.append("deploy.yml must not list the prefix with aws s3 ls")
|
failures.append("deploy.yml must not list the prefix with aws s3 ls")
|
||||||
|
if any(
|
||||||
|
line.lstrip().startswith("run:") and "npm run verify" in line
|
||||||
|
for line in workflow.splitlines()
|
||||||
|
):
|
||||||
|
failures.append(
|
||||||
|
"deploy.yml must not re-run npm run verify; Frontend checks owns that gate"
|
||||||
|
)
|
||||||
|
if any(line.lstrip().startswith("pull_request:") for line in workflow.splitlines()):
|
||||||
|
failures.append(
|
||||||
|
"deploy.yml must not run on pull_request; Frontend checks owns PR verify"
|
||||||
|
)
|
||||||
return failures
|
return failures
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
cases = [
|
cases = [
|
||||||
("version-only", run_case("version-only.json"), 0),
|
("version-only", run_case("version-only.json"), 0),
|
||||||
|
(
|
||||||
|
"origin-timeout-normalization",
|
||||||
|
run_case("origin-timeout-normalization.json"),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
("origin-timeout-change", run_case("origin-timeout-change.json"), 1),
|
||||||
("wrong-label", run_case("wrong-label.json"), 1),
|
("wrong-label", run_case("wrong-label.json"), 1),
|
||||||
("wrong-before", run_case("wrong-before.json"), 1),
|
("wrong-before", run_case("wrong-before.json"), 1),
|
||||||
("extra-origin-change", run_case("extra-origin-change.json"), 1),
|
("extra-origin-change", run_case("extra-origin-change.json"), 1),
|
||||||
|
|
|
||||||
104
scripts/testdata/terraform-release-plans/origin-timeout-change.json
vendored
Normal file
104
scripts/testdata/terraform-release-plans/origin-timeout-change.json
vendored
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
{
|
||||||
|
"resource_changes": [
|
||||||
|
{
|
||||||
|
"address": "module.environment_owned.aws_iam_role_policy.github_deploy",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "aws_iam_role_policy",
|
||||||
|
"change": {
|
||||||
|
"actions": ["no-op"],
|
||||||
|
"before": {
|
||||||
|
"name": "policy"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"name": "policy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "module.environment_owned.aws_s3_object.release_pointer",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "aws_s3_object",
|
||||||
|
"change": {
|
||||||
|
"actions": ["update"],
|
||||||
|
"before": {
|
||||||
|
"content": "{\"current\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1\",\"previous\":\"0000000000000000000000000000000000000000-1-1\"}",
|
||||||
|
"key": ".release/current"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"content": "{\"current\":\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-2-1\",\"previous\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1\"}",
|
||||||
|
"key": ".release/current"
|
||||||
|
},
|
||||||
|
"after_unknown": {
|
||||||
|
"etag": true,
|
||||||
|
"version_id": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "module.environment_owned.aws_cloudfront_distribution.site",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "aws_cloudfront_distribution",
|
||||||
|
"change": {
|
||||||
|
"actions": ["update"],
|
||||||
|
"before": {
|
||||||
|
"origin": [
|
||||||
|
{
|
||||||
|
"connection_attempts": 3,
|
||||||
|
"connection_timeout": 10,
|
||||||
|
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||||
|
"origin_access_control_id": "E30VSIK87N8H64",
|
||||||
|
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1",
|
||||||
|
"origin_path": "/releases/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1",
|
||||||
|
"response_completion_timeout": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"connection_attempts": 3,
|
||||||
|
"connection_timeout": 10,
|
||||||
|
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||||
|
"origin_access_control_id": "E30VSIK87N8H64",
|
||||||
|
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1-previous",
|
||||||
|
"origin_path": "/releases/0000000000000000000000000000000000000000-1-1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"comment": "SeaHaven SHOC frontend (dev)"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"origin": [
|
||||||
|
{
|
||||||
|
"connection_attempts": 3,
|
||||||
|
"connection_timeout": 10,
|
||||||
|
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||||
|
"origin_access_control_id": "E30VSIK87N8H64",
|
||||||
|
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1",
|
||||||
|
"origin_path": "/releases/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-2-1",
|
||||||
|
"response_completion_timeout": 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"connection_attempts": 3,
|
||||||
|
"connection_timeout": 10,
|
||||||
|
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||||
|
"origin_access_control_id": "E30VSIK87N8H64",
|
||||||
|
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1-previous",
|
||||||
|
"origin_path": "/releases/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"comment": "SeaHaven SHOC frontend (dev)"
|
||||||
|
},
|
||||||
|
"after_unknown": {
|
||||||
|
"etag": true,
|
||||||
|
"last_modified_time": true,
|
||||||
|
"status": true,
|
||||||
|
"in_progress_validation_batches": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action_invocations": [
|
||||||
|
{
|
||||||
|
"address": "module.environment_owned.action.aws_cloudfront_create_invalidation.release",
|
||||||
|
"type": "aws_cloudfront_create_invalidation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
105
scripts/testdata/terraform-release-plans/origin-timeout-normalization.json
vendored
Normal file
105
scripts/testdata/terraform-release-plans/origin-timeout-normalization.json
vendored
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
{
|
||||||
|
"resource_changes": [
|
||||||
|
{
|
||||||
|
"address": "module.environment_owned.aws_iam_role_policy.github_deploy",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "aws_iam_role_policy",
|
||||||
|
"change": {
|
||||||
|
"actions": ["no-op"],
|
||||||
|
"before": {
|
||||||
|
"name": "policy"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"name": "policy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "module.environment_owned.aws_s3_object.release_pointer",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "aws_s3_object",
|
||||||
|
"change": {
|
||||||
|
"actions": ["update"],
|
||||||
|
"before": {
|
||||||
|
"content": "{\"current\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1\",\"previous\":\"0000000000000000000000000000000000000000-1-1\"}",
|
||||||
|
"key": ".release/current"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"content": "{\"current\":\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-2-1\",\"previous\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1\"}",
|
||||||
|
"key": ".release/current"
|
||||||
|
},
|
||||||
|
"after_unknown": {
|
||||||
|
"etag": true,
|
||||||
|
"version_id": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "module.environment_owned.aws_cloudfront_distribution.site",
|
||||||
|
"mode": "managed",
|
||||||
|
"type": "aws_cloudfront_distribution",
|
||||||
|
"change": {
|
||||||
|
"actions": ["update"],
|
||||||
|
"before": {
|
||||||
|
"origin": [
|
||||||
|
{
|
||||||
|
"connection_attempts": 3,
|
||||||
|
"connection_timeout": 10,
|
||||||
|
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||||
|
"origin_access_control_id": "E30VSIK87N8H64",
|
||||||
|
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1",
|
||||||
|
"origin_path": "/releases/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1",
|
||||||
|
"response_completion_timeout": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"connection_attempts": 3,
|
||||||
|
"connection_timeout": 10,
|
||||||
|
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||||
|
"origin_access_control_id": "E30VSIK87N8H64",
|
||||||
|
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1-previous",
|
||||||
|
"origin_path": "/releases/0000000000000000000000000000000000000000-1-1",
|
||||||
|
"response_completion_timeout": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"comment": "SeaHaven SHOC frontend (dev)"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"origin": [
|
||||||
|
{
|
||||||
|
"connection_attempts": 3,
|
||||||
|
"connection_timeout": 10,
|
||||||
|
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||||
|
"origin_access_control_id": "E30VSIK87N8H64",
|
||||||
|
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1",
|
||||||
|
"origin_path": "/releases/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-2-1",
|
||||||
|
"response_completion_timeout": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"connection_attempts": 3,
|
||||||
|
"connection_timeout": 10,
|
||||||
|
"domain_name": "seahaven-shoc-frontend-dev.s3.us-east-1.amazonaws.com",
|
||||||
|
"origin_access_control_id": "E30VSIK87N8H64",
|
||||||
|
"origin_id": "shocfrontenddevDistributionOrigin10CCD0EE1-previous",
|
||||||
|
"origin_path": "/releases/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"comment": "SeaHaven SHOC frontend (dev)"
|
||||||
|
},
|
||||||
|
"after_unknown": {
|
||||||
|
"etag": true,
|
||||||
|
"last_modified_time": true,
|
||||||
|
"status": true,
|
||||||
|
"in_progress_validation_batches": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action_invocations": [
|
||||||
|
{
|
||||||
|
"address": "module.environment_owned.action.aws_cloudfront_create_invalidation.release",
|
||||||
|
"type": "aws_cloudfront_create_invalidation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue