Skip to content

Commit 03fbd21

Browse files
Test estate content (forecode scripts/dev/create_test_estate.py)
1 parent 0262b2d commit 03fbd21

12 files changed

Lines changed: 217 additions & 1 deletion

File tree

‎.github/CODEOWNERS‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @forecode-ai/api

‎.github/simulate.sh‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Simulated deploys for the ForeCode test estate. Source it before deploy commands:
2+
#
3+
# source .github/simulate.sh
4+
# helm upgrade --install web ./chart ...
5+
#
6+
# The workflow keeps real deploy commands, so ForeCode detects them exactly as it would in a customer's pipeline,
7+
# but while SIMULATE is "true" (the default) the deploy tools only print what they would do. Nothing is deployed and
8+
# no cloud credentials are needed.
9+
#
10+
# SIMULATE_FAIL_EVERY=N makes every Nth run (by GITHUB_RUN_NUMBER) fail its deploy, to give a pipeline a known change
11+
# failure rate. 0 (the default) never fails.
12+
13+
SIMULATE="${SIMULATE:-true}"
14+
SIMULATE_FAIL_EVERY="${SIMULATE_FAIL_EVERY:-0}"
15+
16+
_simulated() {
17+
echo "[simulated] $*"
18+
}
19+
20+
# Deploy tools can fail on purpose (SIMULATE_FAIL_EVERY); building and publishing artifacts never do.
21+
_simulated_deploy() {
22+
_simulated "$@"
23+
if [ "$SIMULATE_FAIL_EVERY" -gt 0 ] && [ $(( ${GITHUB_RUN_NUMBER:-1} % SIMULATE_FAIL_EVERY )) -eq 0 ]; then
24+
echo "::error::simulated deploy failure (run ${GITHUB_RUN_NUMBER:-1}, every ${SIMULATE_FAIL_EVERY}th run fails)"
25+
return 1
26+
fi
27+
}
28+
29+
if [ "$SIMULATE" = "true" ]; then
30+
helm() { _simulated_deploy helm "$@"; }
31+
kubectl() { _simulated_deploy kubectl "$@"; }
32+
aws() { _simulated_deploy aws "$@"; }
33+
sam() { _simulated_deploy sam "$@"; }
34+
ssh() { _simulated_deploy ssh "$@"; }
35+
scp() { _simulated scp "$@"; }
36+
docker() {
37+
case "$1" in
38+
push|login) _simulated docker "$@" ;;
39+
*) command docker "$@" ;;
40+
esac
41+
}
42+
npm() {
43+
case "$1" in
44+
publish) _simulated npm "$@" ;;
45+
*) command npm "$@" ;;
46+
esac
47+
}
48+
terraform() {
49+
case "$1" in
50+
plan) _simulated terraform "$@" ;;
51+
apply|destroy|import) _simulated_deploy terraform "$@" ;;
52+
init) command terraform init -backend=false "${@:2}" ;;
53+
*) command terraform "$@" ;;
54+
esac
55+
}
56+
echo "Deploys are simulated (SIMULATE=true): deploy tools print instead of acting."
57+
fi

‎.github/workflows/deploy-stage.yml‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: deploy-stage
2+
3+
# Reusable: build and deploy the functions to one stage.
4+
on:
5+
workflow_call:
6+
inputs:
7+
stage:
8+
type: string
9+
required: true
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
deploy:
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 15
18+
environment: ${{ inputs.stage }}
19+
permissions:
20+
contents: read
21+
id-token: write
22+
env:
23+
SIMULATE: ${{ vars.LIVE_DEPLOYS != 'true' }}
24+
steps:
25+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
26+
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
27+
if: vars.AWS_ROLE_ARN != ''
28+
with:
29+
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
30+
aws-region: ap-southeast-2
31+
- name: Deploy with SAM
32+
run: |
33+
source .github/simulate.sh
34+
sam build --use-container
35+
sam deploy --config-env "${{ inputs.stage }}" --no-confirm-changeset --no-fail-on-empty-changeset

‎.github/workflows/release.yml‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: release
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
schedule:
8+
- cron: "25 23 * * 1-5"
9+
workflow_dispatch:
10+
inputs:
11+
production:
12+
description: Also deploy to production (needs approval)
13+
type: boolean
14+
default: false
15+
16+
permissions:
17+
contents: read
18+
19+
jobs:
20+
test:
21+
runs-on: ubuntu-latest
22+
timeout-minutes: 10
23+
steps:
24+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
25+
- name: Unit tests
26+
run: python3 -m unittest discover -s tests -t .
27+
28+
staging:
29+
if: github.event_name != 'pull_request'
30+
needs: test
31+
permissions:
32+
contents: read
33+
id-token: write
34+
uses: ./.github/workflows/deploy-stage.yml
35+
with:
36+
stage: staging
37+
38+
production:
39+
if: github.event_name == 'workflow_dispatch' && inputs.production
40+
needs: staging
41+
permissions:
42+
contents: read
43+
id-token: write
44+
uses: ./.github/workflows/deploy-stage.yml
45+
with:
46+
stage: production

‎README.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
11
# sample-functions-lambda
2-
ForeCode test estate: Python functions deployed to AWS Lambda with SAM
2+
3+
Part of the ForeCode test estate. Not a real product.
4+
5+
Python functions deployed to AWS Lambda with the AWS SAM CLI: to `staging` on every change to `main` and on a
6+
schedule, and to `production` when run manually (the `production` environment needs an approval). Both stages use one
7+
reusable deploy job, called twice.
8+
9+
Deploys are simulated: `.github/simulate.sh` makes the deploy tools print what they would do.

‎functions/__init__.py‎

Whitespace-only changes.

‎functions/refunds/__init__.py‎

Whitespace-only changes.

‎functions/refunds/handler.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Refund eligibility: called by the payments API through API Gateway."""
2+
3+
import json
4+
5+
REFUND_WINDOW_DAYS = 30
6+
7+
8+
def eligible(days_since_purchase: int, used: bool) -> bool:
9+
return days_since_purchase <= REFUND_WINDOW_DAYS and not used
10+
11+
12+
def handler(event: dict, _context: object) -> dict:
13+
body = json.loads(event.get("body") or "{}")
14+
ok = eligible(int(body.get("days", 0)), bool(body.get("used", False)))
15+
return {"statusCode": 200, "body": json.dumps({"eligible": ok})}

‎samconfig.toml‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
version = 0.1
2+
3+
[staging.deploy.parameters]
4+
stack_name = "refunds-staging"
5+
region = "ap-southeast-2"
6+
capabilities = "CAPABILITY_IAM"
7+
parameter_overrides = "Stage=staging"
8+
9+
[production.deploy.parameters]
10+
stack_name = "refunds-production"
11+
region = "ap-southeast-2"
12+
capabilities = "CAPABILITY_IAM"
13+
parameter_overrides = "Stage=production"

‎template.yaml‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
AWSTemplateFormatVersion: "2010-09-09"
2+
Transform: AWS::Serverless-2016-10-31
3+
Description: Refund eligibility functions
4+
5+
Parameters:
6+
Stage:
7+
Type: String
8+
AllowedValues: [staging, production]
9+
10+
Resources:
11+
RefundsFunction:
12+
Type: AWS::Serverless::Function
13+
Properties:
14+
FunctionName: !Sub "refunds-${Stage}"
15+
CodeUri: functions/
16+
Handler: refunds.handler.handler
17+
Runtime: python3.13
18+
MemorySize: 128
19+
Timeout: 5
20+
Events:
21+
Check:
22+
Type: Api
23+
Properties: {Path: /refunds/eligible, Method: post}

0 commit comments

Comments
 (0)