Skip to content

Commit be035a6

Browse files
authored
Merge pull request #155 from co-cddo/fixmystreet-scenario
feat: Add FixMyStreet scenario
2 parents df8e091 + 2fa5f67 commit be035a6

37 files changed

Lines changed: 8327 additions & 1 deletion

.github/workflows/deploy-blueprints.yml

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ on:
1111
- 'cloudformation/scenarios/simply-readable/cdk/**'
1212
- 'cloudformation/scenarios/simply-readable/scripts/**'
1313
- 'cloudformation/scenarios/minute/cdk/**'
14+
- 'cloudformation/scenarios/fixmystreet/cdk/**'
1415
- 'cloudformation/isb-hub/**'
1516
workflow_dispatch:
1617

@@ -292,8 +293,64 @@ jobs:
292293
path: cloudformation/scenarios/minute/template.yaml
293294
retention-days: 1
294295

296+
synth-fixmystreet:
297+
runs-on: ubuntu-latest
298+
steps:
299+
- uses: actions/checkout@v6
300+
301+
- uses: actions/setup-node@v6
302+
with:
303+
node-version: '22'
304+
cache: 'npm'
305+
cache-dependency-path: cloudformation/scenarios/fixmystreet/cdk/package-lock.json
306+
307+
- name: Build and synth fixmystreet CDK
308+
working-directory: cloudformation/scenarios/fixmystreet/cdk
309+
run: |
310+
npm ci
311+
npm run build
312+
npx cdk synth
313+
314+
- name: Strip bootstrap cruft and validate template
315+
working-directory: cloudformation/scenarios/fixmystreet/cdk
316+
run: |
317+
node -e "
318+
const fs = require('fs');
319+
const t = JSON.parse(fs.readFileSync('../cdk.out/FixMyStreetStack.template.json', 'utf8'));
320+
delete t.Parameters?.BootstrapVersion;
321+
delete t.Resources?.CDKMetadata;
322+
delete t.Rules?.CheckBootstrapVersion;
323+
const str = JSON.stringify(t);
324+
const errors = [];
325+
if (str.includes('AssetParameters') || str.includes('cdk-bootstrap')) {
326+
errors.push('Template contains CDK bootstrap/asset dependencies');
327+
}
328+
const deletionPolicies = str.match(/\"DeletionPolicy\":\s*\"(Snapshot|Retain)\"/g);
329+
if (deletionPolicies) {
330+
errors.push('Template contains non-DESTROY deletion policies: ' + deletionPolicies.join(', '));
331+
}
332+
const size = Buffer.byteLength(JSON.stringify(t, null, 2));
333+
if (size > 400000) {
334+
errors.push('Template size ' + size + ' bytes approaching CloudFormation S3 limit (460,800)');
335+
}
336+
if (errors.length > 0) {
337+
errors.forEach(e => console.error('ERROR: ' + e));
338+
process.exit(1);
339+
}
340+
// Write JSON content to .yaml — CloudFormation accepts both formats regardless of extension
341+
const content = '# Auto-generated from CDK synthesis. Do not edit.\n' + JSON.stringify(t, null, 2);
342+
fs.writeFileSync('../template.yaml', content);
343+
console.log('Wrote template.yaml (' + size + ' bytes, ' + Object.keys(t.Resources || {}).length + ' resources)');
344+
"
345+
346+
- uses: actions/upload-artifact@v7
347+
with:
348+
name: fixmystreet-template
349+
path: cloudformation/scenarios/fixmystreet/template.yaml
350+
retention-days: 1
351+
295352
deploy:
296-
needs: [synth-localgov-drupal, synth-localgov-ims, synth-simply-readable, synth-minute]
353+
needs: [synth-localgov-drupal, synth-localgov-ims, synth-simply-readable, synth-minute, synth-fixmystreet]
297354
runs-on: ubuntu-latest
298355
steps:
299356
- uses: actions/checkout@v6
@@ -313,6 +370,11 @@ jobs:
313370
name: minute-template
314371
path: cloudformation/scenarios/minute
315372

373+
- uses: actions/download-artifact@v8
374+
with:
375+
name: fixmystreet-template
376+
path: cloudformation/scenarios/fixmystreet
377+
316378
- uses: actions/download-artifact@v8
317379
with:
318380
name: simply-readable-template
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Build and publish FixMyStreet container image to ghcr.io
2+
#
3+
# Triggers on:
4+
# - Push to main branch affecting docker files
5+
# - All pull requests (skips build if no relevant files changed)
6+
# - Manual workflow_dispatch
7+
8+
name: Build FixMyStreet Container
9+
10+
on:
11+
push:
12+
branches: [main]
13+
paths:
14+
- 'cloudformation/scenarios/fixmystreet/docker/**'
15+
- '.github/workflows/docker-build-fixmystreet.yml'
16+
pull_request:
17+
workflow_dispatch:
18+
inputs:
19+
push_image:
20+
description: 'Push image to registry'
21+
required: false
22+
default: true
23+
type: boolean
24+
25+
env:
26+
REGISTRY: ghcr.io
27+
IMAGE_NAME: co-cddo/ndx_try_aws_scenarios-fixmystreet
28+
29+
jobs:
30+
changes:
31+
name: Check for Docker changes
32+
runs-on: ubuntu-latest
33+
if: github.event_name == 'pull_request'
34+
outputs:
35+
docker: ${{ steps.filter.outputs.docker }}
36+
steps:
37+
- uses: actions/checkout@v6
38+
- uses: dorny/paths-filter@v4
39+
id: filter
40+
with:
41+
filters: |
42+
docker:
43+
- 'cloudformation/scenarios/fixmystreet/docker/**'
44+
- '.github/workflows/docker-build-fixmystreet.yml'
45+
46+
build:
47+
name: Build and Push Container
48+
runs-on: ubuntu-latest
49+
needs: [changes]
50+
# Run if: push/dispatch (changes job skipped), OR PR with docker changes
51+
if: |
52+
always() &&
53+
(needs.changes.result == 'skipped' || needs.changes.outputs.docker == 'true')
54+
permissions:
55+
contents: read
56+
packages: write
57+
58+
steps:
59+
- name: Checkout repository
60+
uses: actions/checkout@v6
61+
62+
- name: Set up Docker Buildx
63+
uses: docker/setup-buildx-action@v4
64+
65+
- name: Log in to GitHub Container Registry
66+
uses: docker/login-action@v4
67+
with:
68+
registry: ${{ env.REGISTRY }}
69+
username: ${{ github.actor }}
70+
password: ${{ secrets.GITHUB_TOKEN }}
71+
72+
- name: Extract metadata for Docker
73+
id: meta
74+
uses: docker/metadata-action@v6
75+
with:
76+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
77+
tags: |
78+
type=sha,prefix=sha-
79+
type=raw,value=latest,enable={{is_default_branch}}
80+
type=ref,event=branch,enable=${{ github.ref != 'refs/heads/main' }}
81+
82+
- name: Build and push Docker image
83+
uses: docker/build-push-action@v7
84+
with:
85+
context: cloudformation/scenarios/fixmystreet/docker
86+
file: cloudformation/scenarios/fixmystreet/docker/Dockerfile
87+
push: ${{ (github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_image != 'false')) }}
88+
tags: ${{ steps.meta.outputs.tags }}
89+
labels: ${{ steps.meta.outputs.labels }}
90+
cache-from: type=gha
91+
cache-to: type=gha,mode=max
92+
platforms: linux/amd64
93+
94+
- name: Output image details
95+
run: |
96+
echo "## Docker Image Built" >> $GITHUB_STEP_SUMMARY
97+
echo "" >> $GITHUB_STEP_SUMMARY
98+
echo "**Registry:** ${{ env.REGISTRY }}" >> $GITHUB_STEP_SUMMARY
99+
echo "**Image:** ${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY
100+
echo "**Tags:**" >> $GITHUB_STEP_SUMMARY
101+
echo '```' >> $GITHUB_STEP_SUMMARY
102+
echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
103+
echo '```' >> $GITHUB_STEP_SUMMARY

cloudformation/isb-hub/lib/isb-hub-stack.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const SCENARIOS: ScenarioConfig[] = [
3232
{ name: 'simply-readable', description: 'NDX:Try Simply Readable - Document Translation & Easy Read, built by Swindon Borough Council' },
3333
{ name: 'localgov-ims', description: 'NDX:Try LocalGov IMS - Income Management System with GOV.UK Pay', parameterKeys: ['GovUkPayApiKey'] },
3434
{ name: 'minute', description: 'Minute AI - Meeting transcription and AI-powered minute generation' },
35+
{ name: 'fixmystreet', description: 'NDX:Try FixMyStreet - Citizen problem reporting platform for UK councils' },
3536
{ name: 'all-demo', description: 'NDX:Try All Demo - Deploys all 7 scenarios as nested stacks' },
3637
];
3738

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env node
2+
import 'source-map-support/register';
3+
import * as cdk from 'aws-cdk-lib';
4+
import { FixMyStreetStack } from '../lib/fixmystreet-stack';
5+
6+
const app = new cdk.App();
7+
8+
new FixMyStreetStack(app, 'FixMyStreetStack', {
9+
// No env — produces environment-agnostic template using CloudFormation intrinsics
10+
// (Fn::GetAZs, Ref: AWS::Region) so the same template works in any account/region via StackSets
11+
description: 'FixMyStreet - Citizen Problem Reporting Platform for UK Councils',
12+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
3+
"watch": {
4+
"include": [
5+
"**"
6+
],
7+
"exclude": [
8+
"README.md",
9+
"cdk*.json",
10+
"**/*.d.ts",
11+
"**/*.js",
12+
"tsconfig.json",
13+
"package*.json",
14+
"yarn.lock",
15+
"node_modules",
16+
"test",
17+
"dist"
18+
]
19+
},
20+
"context": {
21+
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
22+
"@aws-cdk/core:checkSecretUsage": true,
23+
"@aws-cdk/core:target-partitions": [
24+
"aws"
25+
],
26+
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
27+
"@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
28+
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
29+
"@aws-cdk/core:enablePartitionLiterals": true,
30+
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
31+
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true
32+
},
33+
"output": "../cdk.out"
34+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
testEnvironment: 'node',
3+
roots: ['<rootDir>/test'],
4+
testMatch: ['**/*.test.ts'],
5+
transform: {
6+
'^.+\\.tsx?$': 'ts-jest'
7+
}
8+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
2+
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
3+
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
4+
import { Construct } from 'constructs';
5+
6+
export interface CloudFrontConstructProps {
7+
readonly loadBalancer: elbv2.IApplicationLoadBalancer;
8+
}
9+
10+
export class CloudFrontConstruct extends Construct {
11+
public readonly distribution: cloudfront.Distribution;
12+
public readonly domainName: string;
13+
14+
constructor(scope: Construct, id: string, props: CloudFrontConstructProps) {
15+
super(scope, id);
16+
17+
this.distribution = new cloudfront.Distribution(this, 'Distribution', {
18+
comment: 'FixMyStreet - HTTPS Termination',
19+
defaultBehavior: {
20+
origin: new origins.LoadBalancerV2Origin(props.loadBalancer, {
21+
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
22+
}),
23+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
24+
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
25+
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
26+
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER,
27+
},
28+
});
29+
30+
this.domainName = this.distribution.distributionDomainName;
31+
}
32+
}

0 commit comments

Comments
 (0)