Skip to content

Fix console auth CloudFormation validation #413

Fix console auth CloudFormation validation

Fix console auth CloudFormation validation #413

Workflow file for this run

name: CI
on:
pull_request:
push:
branches:
- main
permissions:
contents: read
jobs:
azure-container-apps-pack:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
terraform_version: 1.15.8
terraform_wrapper: false
- uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1
with:
version: v0.72.0
cache: true
- name: Validate the Azure Container Apps deployment pack
working-directory: deploy/azure-container-apps
run: |
set -euo pipefail
terraform fmt -check -recursive
terraform init -backend=false -input=false -lockfile=readonly
terraform validate
terraform test -no-color | tee "$RUNNER_TEMP/azure-terraform-test.txt"
shellcheck \
deploy.sh \
verify.sh \
verify-acs-prerequisites.sh \
rollback.sh \
cleanup.sh
test "$(< .containerapp-extension-version)" = "1.3.0b4"
test "$(< .communication-control-plane-api-version)" = \
"2026-03-18"
node --check event-grid.mjs
node --check event-grid-ensure.mjs
node --check event-grid-verify.mjs
node --check event-grid-delete.mjs
node --check ../../scripts/azure-terminal-delivery.mjs
trivy config \
--exit-code 1 \
--severity HIGH,CRITICAL \
--skip-version-check \
.
terraform providers schema -json \
> "$RUNNER_TEMP/azure-provider-schema.json"
jq --exit-status '
.provider_schemas[
"registry.terraform.io/hashicorp/azurerm"
].resource_schemas as $resources
| $resources.azurerm_key_vault_secret.block.attributes.value_wo.write_only
and $resources.azurerm_postgresql_flexible_server.block.attributes.administrator_password_wo.write_only
' "$RUNNER_TEMP/azure-provider-schema.json" >/dev/null
if grep --recursive --fixed-strings --quiet \
'resource "azurerm_eventgrid_event_subscription"' .; then
echo "Event Grid secret delivery must remain outside Terraform state."
exit 1
fi
if grep --fixed-strings --quiet \
'event-grid-test-test-test-test-test' \
"$RUNNER_TEMP/azure-terraform-test.txt"; then
echo "Terraform test output exposed the Event Grid test secret."
exit 1
fi
cloud-run-pack:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
terraform_version: 1.15.8
terraform_wrapper: false
- uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1
with:
version: v0.72.0
cache: true
- name: Validate the Cloud Run deployment pack
working-directory: deploy/cloud-run
env:
GOOGLE_OAUTH_ACCESS_TOKEN: invalid-ci-token
TF_VAR_api_key: re_DO_NOT_PERSIST_API_KEY_ABCDEFGHIJKL
TF_VAR_database_password: DO_NOT_PERSIST_DATABASE_PASSWORD_1234
TF_VAR_image: >-
ghcr.io/haya-inc/hayasend@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
TF_VAR_project_id: hayasend-test-project
run: |
set -euo pipefail
terraform fmt -check -recursive
terraform init -backend=false -input=false -lockfile=readonly
terraform validate
terraform test -no-color
shellcheck deploy.sh cleanup.sh verify-zero-residue.sh
trivy config \
--exit-code 1 \
--severity HIGH,CRITICAL \
--skip-version-check \
.
TF_VAR_enable_pubsub_wakeup=true terraform plan \
-input=false \
-lock=false \
-no-color \
-refresh=false \
-out="$RUNNER_TEMP/cloud-run.tfplan" \
> "$RUNNER_TEMP/cloud-run-plan.txt"
grep --fixed-strings --quiet \
'connector_enforcement = "REQUIRED"' \
"$RUNNER_TEMP/cloud-run-plan.txt"
grep --fixed-strings --quiet \
'secret_data_wo = (write-only attribute)' \
"$RUNNER_TEMP/cloud-run-plan.txt"
grep --fixed-strings --quiet \
'resource "google_pubsub_topic" "wakeup"' \
"$RUNNER_TEMP/cloud-run-plan.txt"
grep --fixed-strings --quiet \
'role = "roles/pubsub.publisher"' \
"$RUNNER_TEMP/cloud-run-plan.txt"
grep --fixed-strings --quiet \
'role = "roles/pubsub.subscriber"' \
"$RUNNER_TEMP/cloud-run-plan.txt"
if grep --fixed-strings --quiet \
"$TF_VAR_api_key" "$RUNNER_TEMP/cloud-run-plan.txt"; then
echo "Terraform plan exposed the HayaSend API key."
exit 1
fi
if grep --fixed-strings --quiet \
"$TF_VAR_database_password" "$RUNNER_TEMP/cloud-run-plan.txt"; then
echo "Terraform plan exposed the database password."
exit 1
fi
terraform show -json "$RUNNER_TEMP/cloud-run.tfplan" \
> "$RUNNER_TEMP/cloud-run-plan.json"
if grep --fixed-strings --quiet \
"$TF_VAR_api_key" "$RUNNER_TEMP/cloud-run-plan.json"; then
echo "The saved Terraform plan exposed the HayaSend API key."
exit 1
fi
if grep --fixed-strings --quiet \
"$TF_VAR_database_password" \
"$RUNNER_TEMP/cloud-run-plan.json"; then
echo "The saved Terraform plan exposed the database password."
exit 1
fi
render-pack:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Verify the pinned Render CLI and deployment scripts
env:
RENDER_CLI_SHA256: 6cdcd11897b7bd7e673317e6f4aaf041b654d818444f3b1efec7240a835f79ec
RENDER_CLI_VERSION: 2.22.0
run: |
set -euo pipefail
cli_directory="$RUNNER_TEMP/render-cli"
mkdir -p "$cli_directory"
curl --fail --location --silent --show-error \
--output "$cli_directory/render.zip" \
"https://github.com/render-oss/cli/releases/download/v${RENDER_CLI_VERSION}/cli_${RENDER_CLI_VERSION}_linux_amd64.zip"
printf '%s %s\n' \
"$RENDER_CLI_SHA256" \
"$cli_directory/render.zip" \
| sha256sum --check
unzip -q "$cli_directory/render.zip" -d "$cli_directory"
render_cli="$cli_directory/cli_v${RENDER_CLI_VERSION}"
test "$("$render_cli" --version | sed -n '1p')" = \
"render v${RENDER_CLI_VERSION}"
"$render_cli" blueprints validate --help > /dev/null
shellcheck \
deploy/render/deploy.sh \
deploy/render/verify.sh \
deploy/render/proof.sh \
deploy/render/verify-project.sh \
deploy/render/cleanup.sh \
deploy/render/cleanup-project.sh
ruby -ryaml -e \
'YAML.safe_load(File.read(ARGV.fetch(0)), permitted_classes: [], permitted_symbols: [], aliases: false)' \
deploy/render/render.yaml
railway-pack:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.18.1
cache: npm
- name: Install npm
run: npm install --global --ignore-scripts npm@12.0.2
- name: Verify the pinned Railway CLI and deployment pack
env:
HAYASEND_API_KEY: re_RAILWAY_CI_VALIDATION_DO_NOT_USE
RAILWAY_CLI_SHA256: c6169e27e87d95d73fb7a30ec2c6b6c767c042f8a3ba9aa98c92deccda8db10c
RAILWAY_CLI_VERSION: 5.30.1
run: |
set -euo pipefail
cli_directory="$RUNNER_TEMP/railway-cli"
mkdir -p "$cli_directory"
curl --fail --location --silent --show-error \
--output "$cli_directory/railway.tar.gz" \
"https://github.com/railwayapp/cli/releases/download/v${RAILWAY_CLI_VERSION}/railway-v${RAILWAY_CLI_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
printf '%s %s\n' \
"$RAILWAY_CLI_SHA256" \
"$cli_directory/railway.tar.gz" \
| sha256sum --check
tar -xzf "$cli_directory/railway.tar.gz" -C "$cli_directory"
test "$("$cli_directory/railway" --version)" = \
"railway ${RAILWAY_CLI_VERSION}"
"$cli_directory/railway" config plan --help > /dev/null
test "$(< deploy/railway/.railway-cli-version)" = \
"$RAILWAY_CLI_VERSION"
test "$(< deploy/railway/.railway-cli-linux-x86_64-sha256)" = \
"$RAILWAY_CLI_SHA256"
npm ci
npm run validate:railway
shellcheck \
deploy/railway/deploy.sh \
deploy/railway/verify.sh \
deploy/railway/cleanup.sh
flyio-pack:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Verify the pinned flyctl and deployment pack
env:
FLYCTL_SHA256: 000edf4711de7b1a854a11f8148912dc63b30fba99ae52d3bbce98627015ec26
FLYCTL_VERSION: 0.4.75
run: |
set -euo pipefail
cli_directory="$RUNNER_TEMP/flyctl"
mkdir -p "$cli_directory"
curl --fail --location --silent --show-error \
--output "$cli_directory/flyctl.tar.gz" \
"https://github.com/superfly/flyctl/releases/download/v${FLYCTL_VERSION}/flyctl_${FLYCTL_VERSION}_Linux_x86_64.tar.gz"
printf '%s %s\n' \
"$FLYCTL_SHA256" \
"$cli_directory/flyctl.tar.gz" \
| sha256sum --check
tar -xzf "$cli_directory/flyctl.tar.gz" -C "$cli_directory"
test "$("$cli_directory/flyctl" version | sed -n '1s/ .*//p')" = \
"flyctl"
test "$(
"$cli_directory/flyctl" version |
sed -n '1s/^flyctl v\([^ ]*\).*/\1/p'
)" = "$FLYCTL_VERSION"
"$cli_directory/flyctl" config validate --help > /dev/null
"$cli_directory/flyctl" mpg create --help > /dev/null
"$cli_directory/flyctl" storage create --help > /dev/null
test "$(< deploy/flyio/.flyctl-version)" = "$FLYCTL_VERSION"
test "$(< deploy/flyio/.flyctl-linux-x86_64-sha256)" = \
"$FLYCTL_SHA256"
image="$(
sed -n 's/^ image = "\(.*\)"/\1/p' \
deploy/flyio/fly.toml
)"
machine_digest="$(
docker buildx imagetools inspect --raw "$image" |
jq --raw-output '
[
.manifests[] |
select(
.platform.os == "linux" and
.platform.architecture == "amd64"
)
] |
if length == 1 then
.[0].digest
else
error("ambiguous Linux/amd64 manifest")
end
'
)"
test "$(< deploy/flyio/.image-linux-amd64-sha256)" = \
"$machine_digest"
node scripts/validate-flyio-config.mjs
shellcheck -x \
deploy/flyio/lib.sh \
deploy/flyio/provision.sh \
deploy/flyio/deploy.sh \
deploy/flyio/verify.sh \
deploy/flyio/proof.sh \
deploy/flyio/verify-bucket-empty.sh \
deploy/flyio/cleanup-proof-machine.sh \
deploy/flyio/rollback.sh \
deploy/flyio/cleanup.sh
vercel-pack:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.18.1
cache: npm
- name: Install npm
run: npm install --global --ignore-scripts npm@12.0.2
- run: npm ci
- name: Verify the pinned Vercel SDKs, CLI, and deployment pack
run: |
set -euo pipefail
npm run validate:vercel
shellcheck -x \
deploy/vercel/lib.sh \
deploy/vercel/deploy.sh \
deploy/vercel/verify.sh \
deploy/vercel/rollback.sh \
deploy/vercel/cleanup.sh
for script in \
deploy/vercel/blob-count.mjs \
deploy/vercel/blob-store-assert-empty.mjs \
deploy/vercel/blob-store-create.mjs \
deploy/vercel/blob-store-delete.mjs \
deploy/vercel/blob-store-lifecycle.mjs \
deploy/vercel/blob-store-verify.mjs \
deploy/vercel/extract-production-blob-token.mjs \
deploy/vercel/neon-branch-create.mjs \
deploy/vercel/neon-branch-delete.mjs \
deploy/vercel/neon-branch-verify.mjs \
deploy/vercel/neon-branch.mjs \
deploy/vercel/project-create.mjs \
deploy/vercel/project-delete.mjs \
deploy/vercel/project-lifecycle.mjs \
deploy/vercel/project-verify.mjs \
scripts/vercel-blob-hosted-proof.mjs; do
node --check "$script"
done
version="$(< deploy/vercel/.vercel-cli-version)"
expected_integrity="$(< deploy/vercel/.vercel-cli-integrity)"
metadata="$(npm view "vercel@$version" version dist.integrity --json)"
test "$(
jq --raw-output \
'(if type == "array" then .[0] else . end).version' \
<<<"$metadata"
)" = "$version"
test "$(
jq --raw-output \
'(if type == "array" then .[0] else . end).["dist.integrity"]' \
<<<"$metadata"
)" = \
"$expected_integrity"
postgres-substrate:
runs-on: ubuntu-latest
timeout-minutes: 25
services:
postgres:
image: postgres:18-alpine@sha256:9a8afca54e7861fd90fab5fdf4c42477a6b1cb7d293595148e674e0a3181de15
env:
POSTGRES_DB: hayasend_test
POSTGRES_PASSWORD: hayasend-test
POSTGRES_USER: hayasend
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready --username=hayasend --dbname=hayasend_test"
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.18.1
cache: npm
- name: Install npm
run: npm install --global --ignore-scripts npm@12.0.2
- run: npm ci
- name: Exercise PostgreSQL 18 store, jobs, and portable runtime
env:
HAYASEND_POSTGRES_TEST_URL: >-
postgres://hayasend:hayasend-test@127.0.0.1:5432/hayasend_test
run: npm run test:postgres
- name: Smoke-test production console proof in the built image
env:
HAYASEND_POSTGRES_TEST_URL: >-
postgres://hayasend:hayasend-test@127.0.0.1:5432/hayasend_test
run: |
set -euo pipefail
docker build \
--build-arg VERSION=ci \
--build-arg REVISION=${{ github.sha }} \
--tag hayasend:portable-ci \
.
if docker run --rm \
--network host \
--env HAYASEND_MODE=portable \
--env HAYASEND_DATABASE_URL="$HAYASEND_POSTGRES_TEST_URL" \
--env HAYASEND_API_KEY=re_portable_container_ci \
--env HAYASEND_TRANSPORT=console \
hayasend:portable-ci \
node dist/portable/migrate.js \
> "$RUNNER_TEMP/portable-console-without-guard.log" 2>&1; then
echo "Production console mode started without its proof guard."
exit 1
fi
grep --fixed-strings --quiet \
'"error_type":"validation_error"' \
"$RUNNER_TEMP/portable-console-without-guard.log"
docker run --rm \
--network host \
--env HAYASEND_MODE=portable \
--env HAYASEND_DATABASE_URL="$HAYASEND_POSTGRES_TEST_URL" \
--env HAYASEND_API_KEY=re_portable_container_ci \
--env HAYASEND_TRANSPORT=console \
--env HAYASEND_CONSOLE_PROOF_CONFIRM=isolated-non-sending \
hayasend:portable-ci \
node dist/portable/migrate.js
docker run --detach \
--name hayasend-portable-ci \
--network host \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges \
--env HAYASEND_MODE=portable \
--env HAYASEND_DATABASE_URL="$HAYASEND_POSTGRES_TEST_URL" \
--env HAYASEND_API_KEY=re_portable_container_ci \
--env HAYASEND_TRANSPORT=console \
--env HAYASEND_CONSOLE_PROOF_CONFIRM=isolated-non-sending \
--env HAYASEND_PORT=8790 \
hayasend:portable-ci
for attempt in $(seq 1 20); do
if curl --fail --silent http://127.0.0.1:8790/readyz > /dev/null; then
break
fi
if [ "$attempt" = 20 ]; then
docker logs hayasend-portable-ci
exit 1
fi
sleep 1
done
test "$(
docker inspect --format '{{.Config.User}}' hayasend-portable-ci
)" = "node"
- name: Stop portable container
if: always()
run: docker rm --force hayasend-portable-ci || true
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.18.1
cache: npm
- uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2.0.5
with:
deno-version: v2.9.4
cache: true
- uses: aws-actions/setup-sam@89ddb14d60e682855e3fea4be85b3c56485de310 # v3
with:
version: 1.165.0
- name: Install npm
run: npm install --global --ignore-scripts npm@12.0.2
- run: |
npm ci
echo "$GITHUB_WORKSPACE/node_modules/.bin" >> "$GITHUB_PATH"
- run: npm run check
- run: npm run check:supabase-hook
- run: npm run check:workers
- run: npm run check:conformance
- run: npm run validate:flyio
- run: npm run validate:railway
- run: npm run validate:render
- run: npm run validate:vercel
- run: npm test
- run: npm run build
- name: Validate CloudFormation
run: |
python -m pip install --disable-pip-version-check cfn-lint==1.53.3
cfn-lint template.yaml
- name: Validate OpenAPI
run: npm run lint:openapi
- name: Build and verify static site
run: |
npm run site:build
npm run site:verify
- name: Build and load Lambda bundles
run: |
sam build
node scripts/verify-lambda-bundles.mjs
- name: Build local container
run: |
docker build --check .
docker build \
--build-arg VERSION=ci \
--build-arg REVISION=${{ github.sha }} \
--tag hayasend:ci \
.
- name: Smoke-test hardened local container
run: |
smoke_api_key="re_""hayasend_dev"
docker run --detach \
--name hayasend-ci \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges \
--publish 127.0.0.1:8787:8787 \
hayasend:ci
for attempt in $(seq 1 20); do
if curl --fail --silent http://127.0.0.1:8787/healthz > /dev/null; then
break
fi
if [ "$attempt" = 20 ]; then
docker logs hayasend-ci
exit 1
fi
sleep 1
done
test "$(docker inspect --format '{{.Config.User}}' hayasend-ci)" = "node"
curl --fail --silent http://127.0.0.1:8787/emails \
--header "Authorization: Bearer ${smoke_api_key}" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: container-smoke' \
--data '{"from":"Smoke <sender@example.com>","to":"recipient@example.net","subject":"Container smoke test","text":"Read-only local mode works."}' \
| grep --quiet '"id":"email_'
- name: Stop local container
if: always()
run: docker rm --force hayasend-ci || true
python-sdk:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.18.1
cache: npm
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.14.6"
cache: pip
cache-dependency-path: tests/sdk/python-requirements.txt
- name: Install npm
run: npm install --global --ignore-scripts npm@12.0.2
- run: npm ci
- name: Install the official Resend Python SDK
run: >
python -m pip install
--disable-pip-version-check
--only-binary=:all:
--require-hashes
--requirement tests/sdk/python-requirements.txt
- name: Exercise Python SDK compatibility
env:
HAYASEND_API_KEY: re_hayasend_dev
HAYASEND_BASE_URL: http://127.0.0.1:8787
run: |
set -euo pipefail
if HAYASEND_BASE_URL=http://api.resend.com:80 \
python tests/sdk/python_resend_compat.py \
2> "$RUNNER_TEMP/python-sdk-endpoint-guard.log"; then
echo "The SDK compatibility test accepted a production endpoint."
exit 1
fi
grep --quiet "loopback" "$RUNNER_TEMP/python-sdk-endpoint-guard.log"
npm run dev > "$RUNNER_TEMP/hayasend-python-sdk.log" 2>&1 &
hayasend_server_pid=$!
trap 'kill "$hayasend_server_pid" 2>/dev/null || true' EXIT
for attempt in $(seq 1 20); do
if curl --fail --silent "$HAYASEND_BASE_URL/healthz" > /dev/null; then
break
fi
if [ "$attempt" = 20 ]; then
cat "$RUNNER_TEMP/hayasend-python-sdk.log"
exit 1
fi
sleep 1
done
python tests/sdk/python_resend_compat.py
openapi-contract:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24.18.1
cache: npm
- name: Install npm
run: npm install --global --ignore-scripts npm@12.0.2
- run: npm ci
- name: Exercise the published OpenAPI contract
env:
HAYASEND_CONTRACT_BASE_URL: http://127.0.0.1:8787
SCHEMATHESIS_VERSION: "4.24.3"
SCHEMATHESIS_IMAGE: >-
ghcr.io/schemathesis/schemathesis@sha256:dd1ebf7519958c34c276a65c20f9f2f808dbefb06c86163eb284ff5674c6a9f3
run: |
set -euo pipefail
if HAYASEND_CONTRACT_BASE_URL=https://api.resend.com \
node scripts/openapi-contract-safety.mjs \
2> "$RUNNER_TEMP/openapi-contract-endpoint-guard.log"; then
echo "The OpenAPI contract test accepted a production endpoint."
exit 1
fi
grep --quiet "loopback" \
"$RUNNER_TEMP/openapi-contract-endpoint-guard.log"
contract_base_url="$(
node scripts/openapi-contract-safety.mjs
)"
contract_api_key="re_""hayasend_dev"
test "$(
docker run --rm \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges \
"$SCHEMATHESIS_IMAGE" \
--version
)" = "schemathesis, version $SCHEMATHESIS_VERSION"
npm run dev > "$RUNNER_TEMP/hayasend-openapi-contract.log" 2>&1 &
hayasend_server_pid=$!
trap 'kill "$hayasend_server_pid" 2>/dev/null || true' EXIT
for attempt in $(seq 1 20); do
if curl --fail --silent \
"$contract_base_url/healthz" > /dev/null; then
break
fi
if [ "$attempt" = 20 ]; then
cat "$RUNNER_TEMP/hayasend-openapi-contract.log"
exit 1
fi
sleep 1
done
batch_response="$(
curl --fail-with-body --silent --show-error \
--request POST \
--header "Authorization: Bearer ${contract_api_key}" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: openapi-contract/batch" \
--data '[
{
"from": "Contract <contract@example.test>",
"to": ["recipient@example.test"],
"subject": "Contract batch",
"text": "Synthetic OpenAPI batch contract."
}
]' \
"$contract_base_url/emails/batch"
)"
jq --exit-status '
(.data | type == "array" and length == 1) and
(.data[0].id | test("^email_[a-f0-9]{32}$"))
' <<< "$batch_response"
docker run --rm \
--network host \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges \
--env SCHEMATHESIS_COVERAGE=false \
--env SCHEMATHESIS_HOOKS=/work/schemathesis_hooks.py \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--workdir /tmp \
--mount \
"type=bind,source=$GITHUB_WORKSPACE/openapi.yaml,target=/work/openapi.yaml,readonly" \
--mount \
"type=bind,source=$GITHUB_WORKSPACE/tests/contract/schemathesis_hooks.py,target=/work/schemathesis_hooks.py,readonly" \
"$SCHEMATHESIS_IMAGE" \
run /work/openapi.yaml \
--url "$contract_base_url" \
--header "Authorization: Bearer ${contract_api_key}" \
--phases fuzzing \
--mode all \
--checks \
not_a_server_error,status_code_conformance,content_type_conformance,response_schema_conformance \
--max-examples 10 \
--generation-deterministic \
--workers 1 \
--max-failures 20 \
--request-timeout 2 \
--suppress-health-check filter_too_much \
--no-color
npm-package:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24.18.1
package-manager-cache: false
- name: Install npm
run: npm install --global --ignore-scripts npm@12.0.2
- uses: aws-actions/setup-sam@89ddb14d60e682855e3fea4be85b3c56485de310 # v3
with:
version: 1.165.0
- run: npm ci
- name: Build and exercise the distributable CLI
run: |
set -euo pipefail
mkdir -p dist
printf 'must not ship\n' > dist/stale-cross-branch-artifact.txt
npm pack --json --pack-destination "$RUNNER_TEMP" \
> "$RUNNER_TEMP/npm-pack.json"
jq '
if type == "array" then
.[0]
else
.["@haya-inc/hayasend"]
end
' "$RUNNER_TEMP/npm-pack.json" \
> "$RUNNER_TEMP/npm-pack-metadata.json"
version="$(node --print "require('./package.json').version")"
expected_archive="haya-inc-hayasend-${version}.tgz"
archive="$(jq --raw-output '.filename' \
"$RUNNER_TEMP/npm-pack-metadata.json")"
test "$archive" = "$expected_archive"
test -f "$RUNNER_TEMP/$archive"
test "$(
node scripts/npm-package-integrity.mjs < "$RUNNER_TEMP/$archive"
)" = "$(
jq --raw-output '.integrity' \
"$RUNNER_TEMP/npm-pack-metadata.json"
)"
jq '{entryCount, size, unpackedSize, filename}' \
"$RUNNER_TEMP/npm-pack-metadata.json"
jq --exit-status '
.entryCount <= 639 and
.size < 900000 and
.unpackedSize < 4600000 and
([.files[].path] | index("dist/cli.js") != null) and
([.files[].path] |
index("dist/cli-resend-migration.js") != null) and
([.files[].path] |
index("dist/cli-aws-bootstrap.js") != null) and
([.files[].path] |
index("dist/operator-console.js") != null) and
([.files[].path] |
index("src/operator-console.ts") != null) and
([.files[].path] |
index("dist/operator-console-view.js") != null) and
([.files[].path] |
index("src/operator-console-view.tsx") != null) and
([.files[].path] |
index("dist/console-auth.js") != null) and
([.files[].path] |
index("src/console-auth.ts") != null) and
([.files[].path] |
index("deploy/aws-cloudformation-bootstrap.yaml") != null) and
([.files[].path] | index("NOTICE") != null) and
([.files[].path] |
index("src/cli-resend-migration.ts") != null) and
([.files[].path] |
index("migrations/0003_cloudflare_suppressions.sql") != null) and
([.files[].path] |
index("migrations/postgres/0002_application_store.sql") != null) and
([.files[].path] |
index("migrations/postgres/0003_jobs.sql") != null) and
([.files[].path] |
index("migrations/postgres/0004_provider_message_correlation.sql") != null) and
([.files[].path] |
index("dist/portable/migrate.js") != null) and
([.files[].path] |
index("dist/portable/worker.js") != null) and
([.files[].path] |
index("dist/portable/hosted-proof.js") != null) and
([.files[].path] |
index("dist/portable/backup-restore-proof.js") != null) and
([.files[].path] |
index("dist/portable/backup-restore-seed.js") != null) and
([.files[].path] |
index("dist/portable/backup-restore-verify.js") != null) and
([.files[].path] |
index("scripts/cloudflare-cost-model.mjs") != null) and
([.files[].path] | index("src/aws/api.ts") != null) and
([.files[].path] | index("src/workers/index.ts") != null) and
([.files[].path] |
index("schemas/runtime-capabilities.v1.schema.json") != null) and
([.files[].path] |
index("schemas/deployment-capabilities.v1.schema.json") != null) and
([.files[].path] |
index("conformance/deployments/aws-ses.v1.json") != null) and
([.files[].path] |
index("conformance/deployments/azure-container-apps-acs.v1.json") != null) and
([.files[].path] |
index("conformance/runtimes/portable-postgres.v1.json") != null) and
([.files[].path] |
index("conformance/providers/azure-communication-services.v1.json") != null) and
([.files[].path] |
index("conformance/providers/sendgrid.v1.json") != null) and
([.files[].path] |
index("dist/provider-capability-registry.js") != null) and
([.files[].path] |
index("dist/runtime-capability-registry.js") != null) and
([.files[].path] |
index("dist/deployment-capability-registry.js") != null) and
([.files[].path] |
index("dist/capability-profiles.js") != null) and
([.files[].path] |
index("dist/adapters/portable-attachment-storage.js") != null) and
([.files[].path] |
index("dist/adapters/azure/acs-email-transport.js") != null) and
([.files[].path] |
index("dist/adapters/azure/acs-email-events.js") != null) and
([.files[].path] |
index("dist/adapters/sendgrid/sendgrid-email-transport.js") != null) and
([.files[].path] |
index("dist/adapters/sendgrid/sendgrid-email-events.js") != null) and
([.files[].path] |
index("dist/adapters/google-pubsub-wakeup.js") != null) and
([.files[].path] |
index("dist/adapters/wakeup-job-queue.js") != null) and
([.files[].path] |
index("dist/ports/job-wakeup.js") != null) and
([.files[].path] |
index("dist/adapters/vercel-blob-attachment-storage.js") != null) and
([.files[].path] |
index("dist/vercel/functions.js") != null) and
([.files[].path] | index("app.ts") != null) and
([.files[].path] | index("api/queue.ts") != null) and
([.files[].path] | index("api/reconcile.ts") != null) and
([.files[].path] | index("vercel.json") != null) and
([.files[].path] |
index("deploy/vercel/deploy.sh") != null) and
([.files[].path] |
index("deploy/vercel/.vercel-cli-version") != null) and
([.files[].path] |
index("conformance/runtimes/vercel-serverless.v1.json") != null) and
([.files[].path] |
index("deploy/cloud-run/main.tf") != null) and
([.files[].path] |
index("deploy/cloud-run/.terraform.lock.hcl") != null) and
([.files[].path] |
index("deploy/cloud-run/.terraform-version") != null) and
([.files[].path] |
index("deploy/cloud-run/backend.tf.example") != null) and
([.files[].path] |
index("deploy/cloud-run/rollback.sh") != null) and
([.files[].path] |
index("deploy/azure-container-apps/main.tf") != null) and
([.files[].path] |
index("deploy/azure-container-apps/.terraform.lock.hcl") != null) and
([.files[].path] |
index("deploy/azure-container-apps/.terraform-version") != null) and
([.files[].path] |
index("deploy/azure-container-apps/.azure-cli-version") != null) and
([.files[].path] |
index("deploy/azure-container-apps/.containerapp-extension-version") != null) and
([.files[].path] |
index("deploy/azure-container-apps/.communication-control-plane-api-version") != null) and
([.files[].path] |
index("deploy/azure-container-apps/verify-acs-prerequisites.sh") != null) and
([.files[].path] |
index("deploy/azure-container-apps/event-grid.mjs") != null) and
([.files[].path] |
index("deploy/azure-container-apps/event-grid-ensure.mjs") != null) and
([.files[].path] |
index("deploy/azure-container-apps/event-grid-verify.mjs") != null) and
([.files[].path] |
index("deploy/azure-container-apps/event-grid-delete.mjs") != null) and
([.files[].path] |
index("deploy/render/render.yaml") != null) and
([.files[].path] |
index("deploy/render/.render-cli-version") != null) and
([.files[].path] |
index("deploy/render/.render-schema-sha256") != null) and
([.files[].path] |
index("deploy/render/rollback.sh") != null) and
([.files[].path] |
index("deploy/railway/.railway/railway.ts") != null) and
([.files[].path] |
index("deploy/railway/.railway-cli-version") != null) and
([.files[].path] |
index("deploy/railway/.railway-cli-linux-x86_64-sha256") != null) and
([.files[].path] |
index("deploy/railway/rollback.sh") != null) and
([.files[].path] |
index("deploy/flyio/fly.toml") != null) and
([.files[].path] |
index("deploy/flyio/.flyctl-version") != null) and
([.files[].path] |
index("deploy/flyio/.flyctl-linux-x86_64-sha256") != null) and
([.files[].path] |
index("deploy/flyio/.image-linux-amd64-sha256") != null) and
([.files[].path] |
index("conformance/readiness.v1.json") != null) and
([.files[].path] |
index("scripts/azure-terminal-delivery.mjs") != null) and
([.files[].path] |
index("scripts/extract-portable-hosted-proof.mjs") != null) and
([.files[].path] |
index("scripts/vercel-blob-hosted-proof.mjs") != null) and
([.files[].path] |
index("schemas/readiness-matrix.v1.schema.json") != null) and
([.files[].path] | index("template.yaml") != null) and
([.files[].path] |
index("dist/stale-cross-branch-artifact.txt") == null)
' "$RUNNER_TEMP/npm-pack-metadata.json" > /dev/null
if tar --list --gzip --file "$RUNNER_TEMP/$archive" |
grep --extended-regexp --invert-match --quiet \
'^package/(LICENSE|NOTICE|README\.md|package\.json|api/.*|app\.ts|conformance/.*|deploy/.*|dist/.*|migrations/.*|schemas/.*|scripts/(azure-terminal-delivery|clean-dist|cloudflare-cost-model|extract-portable-hosted-proof|npm-sam-compat|validate-vercel-config|vercel-blob-hosted-proof)\.mjs|src/.*|template\.yaml|vercel\.json)$'; then
echo "The npm package contains an unexpected path."
tar --list --gzip --file "$RUNNER_TEMP/$archive"
exit 1
fi
consumer="$RUNNER_TEMP/hayasend-consumer"
project="$RUNNER_TEMP/hayasend-project"
npm install \
--ignore-scripts \
--no-audit \
--no-fund \
--prefix "$consumer" \
"$RUNNER_TEMP/$archive"
"$consumer/node_modules/.bin/hayasend" help \
> "$RUNNER_TEMP/hayasend-help.txt"
grep --quiet "HayaSend CLI" "$RUNNER_TEMP/hayasend-help.txt"
npm exec \
--yes \
--ignore-scripts \
--package="$RUNNER_TEMP/$archive" \
-- hayasend help \
> "$RUNNER_TEMP/hayasend-npx-help.txt"
grep --quiet "HayaSend CLI" "$RUNNER_TEMP/hayasend-npx-help.txt"
package_root="$consumer/node_modules/@haya-inc/hayasend"
test -f "$package_root/src/workers/index.ts"
test -f \
"$package_root/migrations/0003_cloudflare_suppressions.sql"
test -f \
"$package_root/migrations/postgres/0002_application_store.sql"
test -f \
"$package_root/migrations/postgres/0003_jobs.sql"
test -f \
"$package_root/migrations/postgres/0004_provider_message_correlation.sql"
test -f "$package_root/dist/portable/migrate.js"
test -f "$package_root/dist/portable/worker.js"
test -f \
"$package_root/dist/portable/backup-restore-proof.js"
test -f \
"$package_root/dist/portable/backup-restore-seed.js"
test -f \
"$package_root/dist/portable/backup-restore-verify.js"
test -f "$package_root/scripts/cloudflare-cost-model.mjs"
test -f \
"$package_root/schemas/runtime-capabilities.v1.schema.json"
test -f \
"$package_root/schemas/deployment-capabilities.v1.schema.json"
test -f \
"$package_root/conformance/deployments/aws-ses.v1.json"
test -f \
"$package_root/conformance/deployments/azure-container-apps-acs.v1.json"
test -f \
"$package_root/conformance/runtimes/portable-postgres.v1.json"
test -f \
"$package_root/conformance/providers/azure-communication-services.v1.json"
test -f \
"$package_root/dist/provider-capability-registry.js"
test -f \
"$package_root/dist/runtime-capability-registry.js"
test -f \
"$package_root/dist/deployment-capability-registry.js"
test -f "$package_root/dist/capability-profiles.js"
test -f \
"$package_root/dist/adapters/portable-attachment-storage.js"
test -f \
"$package_root/dist/adapters/azure/acs-email-transport.js"
test -f \
"$package_root/dist/adapters/azure/acs-email-events.js"
test -f "$package_root/deploy/cloud-run/main.tf"
test -f "$package_root/deploy/cloud-run/.terraform.lock.hcl"
test -f "$package_root/deploy/cloud-run/.terraform-version"
test -f "$package_root/deploy/cloud-run/backend.tf.example"
test -x "$package_root/deploy/cloud-run/deploy.sh"
test -x "$package_root/deploy/cloud-run/rollback.sh"
test -x "$package_root/deploy/cloud-run/cleanup.sh"
test -f "$package_root/deploy/azure-container-apps/main.tf"
test -f \
"$package_root/deploy/azure-container-apps/.terraform.lock.hcl"
test -f \
"$package_root/deploy/azure-container-apps/.terraform-version"
test -f \
"$package_root/deploy/azure-container-apps/.azure-cli-version"
test -f \
"$package_root/deploy/azure-container-apps/.containerapp-extension-version"
test -f \
"$package_root/deploy/azure-container-apps/.communication-control-plane-api-version"
test -x "$package_root/deploy/azure-container-apps/deploy.sh"
test -x "$package_root/deploy/azure-container-apps/verify.sh"
test -x \
"$package_root/deploy/azure-container-apps/verify-acs-prerequisites.sh"
test -x "$package_root/deploy/azure-container-apps/rollback.sh"
test -x "$package_root/deploy/azure-container-apps/cleanup.sh"
test -x "$package_root/deploy/azure-container-apps/event-grid.mjs"
test -x \
"$package_root/deploy/azure-container-apps/event-grid-ensure.mjs"
test -x \
"$package_root/deploy/azure-container-apps/event-grid-verify.mjs"
test -x \
"$package_root/deploy/azure-container-apps/event-grid-delete.mjs"
test -f "$package_root/deploy/render/render.yaml"
test -f "$package_root/deploy/render/.render-cli-version"
test -f "$package_root/deploy/render/.render-schema-sha256"
test -x "$package_root/deploy/render/deploy.sh"
test -x "$package_root/deploy/render/verify.sh"
test -x "$package_root/deploy/render/proof.sh"
test -x "$package_root/deploy/render/verify-project.sh"
test -x "$package_root/deploy/render/rollback.sh"
test -x "$package_root/deploy/render/cleanup.sh"
test -x "$package_root/deploy/render/cleanup-project.sh"
test -f \
"$package_root/deploy/railway/.railway/railway.ts"
test -f "$package_root/deploy/railway/.railway-cli-version"
test -f \
"$package_root/deploy/railway/.railway-cli-linux-x86_64-sha256"
test -x "$package_root/deploy/railway/deploy.sh"
test -x "$package_root/deploy/railway/verify.sh"
test -x "$package_root/deploy/railway/rollback.sh"
test -x "$package_root/deploy/railway/cleanup.sh"
test -f "$package_root/deploy/flyio/fly.toml"
test -f "$package_root/deploy/flyio/.flyctl-version"
test -f \
"$package_root/deploy/flyio/.flyctl-linux-x86_64-sha256"
test -f \
"$package_root/deploy/flyio/.image-linux-amd64-sha256"
test -x "$package_root/deploy/flyio/provision.sh"
test -x "$package_root/deploy/flyio/deploy.sh"
test -x "$package_root/deploy/flyio/verify.sh"
test -x "$package_root/deploy/flyio/proof.sh"
test -x \
"$package_root/deploy/flyio/verify-bucket-empty.sh"
test -x \
"$package_root/deploy/flyio/cleanup-proof-machine.sh"
test -x "$package_root/deploy/flyio/rollback.sh"
test -x "$package_root/deploy/flyio/cleanup.sh"
test -f \
"$package_root/scripts/azure-terminal-delivery.mjs"
test -f \
"$package_root/scripts/extract-portable-hosted-proof.mjs"
test -f \
"$package_root/scripts/vercel-blob-hosted-proof.mjs"
test -x "$consumer/node_modules/.bin/railway-iac-ts"
(
cd "$consumer"
HAYASEND_API_KEY=re_RAILWAY_PACKAGE_VALIDATION_DO_NOT_USE \
SENDGRID_API_KEY=SG.RAILWAY_PACKAGE_VALIDATION_DO_NOT_USE_000000 \
SENDGRID_EVENT_WEBHOOK_PUBLIC_KEY=RAILWAY_PACKAGE_VALIDATION_PUBLIC_KEY_DO_NOT_USE_0000000000000000 \
node --input-type=module --eval '
import { evaluateRailwayFile } from "railway/iac";
const result = await evaluateRailwayFile(process.argv[1]);
if (result.graph.project.name !== "hayasend-railway") {
process.exit(1);
}
' "$package_root/deploy/railway/.railway/railway.ts"
)
test -f "$package_root/conformance/readiness.v1.json"
test -f \
"$package_root/schemas/readiness-matrix.v1.schema.json"
test -x "$consumer/node_modules/.bin/esbuild"
npm_root="$(npm root --global)"
npm_compat="$RUNNER_TEMP/npm-sam-compat"
mkdir "$npm_compat"
cp "$package_root/scripts/npm-sam-compat.mjs" "$npm_compat/npm"
chmod 700 "$npm_compat/npm"
sam validate \
--lint \
--template-file "$package_root/template.yaml"
HAYASEND_REAL_NPM_CLI="$npm_root/npm/bin/npm-cli.js" \
PATH="$npm_compat:$consumer/node_modules/.bin:$PATH" sam build \
--parallel \
--template-file "$package_root/template.yaml" \
--base-dir "$package_root" \
--build-dir "$RUNNER_TEMP/hayasend-sam-build"
test -f "$RUNNER_TEMP/hayasend-sam-build/template.yaml"
mkdir "$project"
(
cd "$project"
"$consumer/node_modules/.bin/hayasend" init > init.json
)
jq --exit-status '
.ok == true and
.created == [
"compose.hayasend.yaml",
".env.hayasend.example"
]
' "$project/init.json" > /dev/null
grep --fixed-strings --quiet \
"image: ghcr.io/haya-inc/hayasend:${version}" \
"$project/compose.hayasend.yaml"