Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ AWS_HOSTED_ZONE_ID=Z0123456789ABCDEFGHIJ

# Environment
NODE_ENV=development
ENVIRONMENT=development
109 changes: 109 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: Deploy to AWS

on:
push:
branches:
- dev # Triggers the workflow on push events to the dev branch
pull_request:
types: [closed]
branches:
- main # Triggers on PR close to main

jobs:
deploy_dev:
name: Deploy to Development
if: github.event_name == 'push' && github.ref == 'refs/heads/dev' # Ensure this job only runs for pushes to dev
runs-on: ubuntu-latest
# environment: development # Optional: configure a GitHub environment named "development"
permissions:
id-token: write # Required for OIDC if you choose to use it
contents: read

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20' # Specify your Node.js version

- name: Install dependencies
run: npm install

- name: Run tests
run: npm test

- name: Build Vite application
run: npm run build

- name: Compile CDK TypeScript
run: npx tsc

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }} # e.g., us-east-1

- name: Deploy CDK Stack (Development)
run: npm run cdk -- deploy --all -c environment=development --require-approval never
env:
# DEV_DOMAIN_NAME: ${{ secrets.DEV_DOMAIN_NAME }}
# DEV_BASE_DOMAIN_NAME: ${{ secrets.DEV_BASE_DOMAIN_NAME }}
# DEV_HOSTED_ZONE_ID: ${{ secrets.DEV_HOSTED_ZONE_ID }}
CI: true

deploy_prod:
name: Deploy to Production
runs-on: ubuntu-latest
# environment: production # Optional: configure a GitHub environment named "production"
if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true && github.ref == 'refs/heads/main'
# The condition above ensures this job only runs when a PR is merged into the main branch.
# Note: github.ref in the context of a pull_request event for 'closed' points to the base branch (e.g., refs/heads/main).

permissions:
id-token: write # Required for OIDC if you choose to use it
contents: read

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: main # Ensure we check out the main branch after merge

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20' # Specify your Node.js version

- name: Install dependencies
run: npm install

- name: Run tests
run: npm test # It's good practice to run tests before deploying to production

- name: Build Vite application
run: npm run build

- name: Compile CDK TypeScript
run: npx tsc

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }} # Ensure this is your production region or use a specific secret like secrets.PROD_AWS_REGION

- name: Deploy CDK Stack (Production)
run: npm run cdk -- deploy --all -c environment=production --require-approval never
env:
# Ensure your config/index.ts can resolve values for 'production'
# or pass them via -c context or environment variables if needed, e.g.,
# PROD_DOMAIN_NAME: ${{ secrets.PROD_DOMAIN_NAME }}
# PROD_BASE_DOMAIN_NAME: ${{ secrets.PROD_BASE_DOMAIN_NAME }}
# PROD_HOSTED_ZONE_ID: ${{ secrets.PROD_HOSTED_ZONE_ID }}
# Ensure these secrets are configured in your GitHub repository settings if they are required by your CDK stack for production.
CI: true
2 changes: 2 additions & 0 deletions app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const environment = app.node.tryGetContext('environment') || 'development'; // R
const stackId = `ClockWebsiteStack-${environment}`; // Create a unique stack ID based on environment

new ClockWebsiteStack(app, stackId, {
environment: environment,
envVars: process.env, // Pass process.env to the stack
/* If you don't specify 'env', this stack will be environment-agnostic.
* Account/Region-dependent features and context lookups will not work,
* but you can deploy to any account and region. */
Expand Down
37 changes: 18 additions & 19 deletions config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,24 @@ export type DeploymentConfig = {
};
};

const configs: Record<string, DeploymentConfig> = {
development: {
environment: 'development',
aws: {
domainName: process.env.AWS_DOMAIN_NAME || 'dev-clock.taylormadetech.net',
baseDomainName: process.env.AWS_BASE_DOMAIN || 'taylormadetech.net',
hostedZoneId: process.env.AWS_HOSTED_ZONE_ID || 'Z08476952AAJG5D55EAB6'
export function getConfig(env: string = 'development', envVars: Record<string, string | undefined>): DeploymentConfig {
const configs: Record<string, DeploymentConfig> = {
development: {
environment: 'development',
aws: {
domainName: envVars.VITE_AWS_DOMAIN_NAME || envVars.AWS_DOMAIN_NAME || 'dev-clock.taylormadetech.net',
baseDomainName: envVars.VITE_AWS_BASE_DOMAIN || envVars.AWS_BASE_DOMAIN || 'taylormadetech.net',
hostedZoneId: envVars.VITE_AWS_HOSTED_ZONE_ID || envVars.AWS_HOSTED_ZONE_ID || 'Z08476952AAJG5D55EAB6'
}
},
production: {
environment: 'production',
aws: {
domainName: envVars.VITE_AWS_DOMAIN_NAME || envVars.AWS_DOMAIN_NAME || 'clock.taylormadetech.net',
baseDomainName: envVars.VITE_AWS_BASE_DOMAIN || envVars.AWS_BASE_DOMAIN || 'taylormadetech.net',
hostedZoneId: envVars.VITE_AWS_HOSTED_ZONE_ID || envVars.AWS_HOSTED_ZONE_ID || 'Z08476952AAJG5D55EAB6'
}
}
},
production: {
environment: 'production',
aws: {
domainName: process.env.AWS_DOMAIN_NAME || 'clock.taylormadetech.net',
baseDomainName: process.env.AWS_BASE_DOMAIN || 'taylormadetech.net',
hostedZoneId: process.env.AWS_HOSTED_ZONE_ID || 'Z08476952AAJG5D55EAB6'
}
}
};

export function getConfig(env: string = 'development'): DeploymentConfig {
};
return configs[env] || configs.development;
}
93 changes: 93 additions & 0 deletions github-actions-iam-user.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: >-
Creates an IAM User, Policy, and Access Key for GitHub Actions to deploy AWS CDK stacks.
WARNING: Outputs AccessKeyId and SecretAccessKey. Secure these credentials immediately
and consider more secure authentication methods like OIDC.

Parameters:
GitHubDeployUserName:
Type: String
Default: GitHubActionsDeployUser
Description: Name for the IAM user.

Resources:
GitHubActionsUser:
Type: AWS::IAM::User
Properties:
UserName: !Ref GitHubDeployUserName
Policies:
- PolicyName: GitHubActionsDeployPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow # Permissions for CDK Toolkit and CloudFormation
Action:
- cloudformation:*
- s3:GetBucketLocation # Required by CDK for S3 assets
- s3:ListAllMyBuckets # Required by CDK for some operations
- ssm:GetParameter # For CDK context lookups from SSM
- iam:GetUser # To check current user identity
- iam:ListAccountAliases # For display purposes in CDK
- sts:GetCallerIdentity # For display purposes in CDK
- ecr:DescribeRepositories # If using ECR assets with CDK
Resource: "*"
- Effect: Allow # Permissions for CDK Bootstrap resources
Action:
- s3:*
Resource:
- !Sub "arn:aws:s3:::cdk-*-assets-${AWS::AccountId}-${AWS::Region}"
- !Sub "arn:aws:s3:::cdk-*-assets-${AWS::AccountId}-${AWS::Region}/*"
- Effect: Allow # Permissions for IAM roles created/managed by CDK
Action:
- iam:GetRole
- iam:GetInstanceProfile
- iam:CreateRole
- iam:DeleteRole
- iam:PutRolePolicy
- iam:AttachRolePolicy
- iam:DetachRolePolicy
- iam:DeleteRolePolicy
- iam:PassRole
- iam:TagRole
- iam:ListAttachedRolePolicies
- iam:ListRolePolicies
- iam:ListRoleTags
Resource: !Sub "arn:aws:iam::${AWS::AccountId}:role/cdk-*"
- Effect: Allow # Permissions for application-specific resources (ClockWebsiteStack)
Action:
- s3:*
Resource:
- !Sub "arn:aws:s3:::simple-clock-website-*-${AWS::AccountId}-${AWS::Region}"
- !Sub "arn:aws:s3:::simple-clock-website-*-${AWS::AccountId}-${AWS::Region}/*"
- Effect: Allow
Action:
- cloudfront:*
- route53:*
- acm:*
- logs:CreateLogGroup # For Lambda@Edge if used by CloudFront
- logs:CreateLogStream
- logs:PutLogEvents
- iam:CreateServiceLinkedRole # For services like CloudFront, Route53, ACM, etc.
Resource: "*" # These services often require broad permissions or create resources with unpredictable names.
# For stricter security, scope these down if possible after initial deployment.

GitHubActionsUserAccessKey:
Type: AWS::IAM::AccessKey
Properties:
UserName: !Ref GitHubActionsUser
# WARNING: The SecretAccessKey will be visible in the AWS CloudFormation console
# and in API calls like DescribeStacks. Secure it immediately after creation.

Outputs:
AccessKeyId:
Description: Access Key ID for the GitHub Actions IAM user.
Value: !Ref GitHubActionsUserAccessKey
SecretAccessKey:
Description: >-
Secret Access Key for the GitHub Actions IAM user.
IMPORTANT: Store this securely in GitHub Secrets and then consider removing this output
or the AccessKey resource from the template for enhanced security.
Value: !GetAtt GitHubActionsUserAccessKey.SecretAccessKey
IAMUserArn:
Description: ARN of the created IAM user.
Value: !GetAtt GitHubActionsUser.Arn
File renamed without changes.
9 changes: 7 additions & 2 deletions lib/clock-website-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ import * as route53Targets from 'aws-cdk-lib/aws-route53-targets';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import { getConfig } from '../config/index.js';

interface ClockWebsiteStackProps extends cdk.StackProps {
environment: string;
envVars: Record<string, string | undefined>;
}

export class ClockWebsiteStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
constructor(scope: Construct, id: string, props: ClockWebsiteStackProps) {
super(scope, id, props);

// Get configuration based on environment context parameter
const config = getConfig(this.node.tryGetContext('environment'));
const config = getConfig(props.environment, props.envVars);

// Replace hardcoded values with config values
const domainName = config.aws.domainName;
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "1.0.0",
"main": "dist/app.js",
"scripts": {
"start": "vite dev",
"dev": "vite dev",
"build": "vite build",
"cdk": "cdk",
"deploy": "npm run build && npx tsc && cdk deploy",
Expand All @@ -24,6 +24,7 @@
"@aws-cdk/aws-s3": "^1.203.0",
"@aws-cdk/aws-s3-deployment": "^1.203.0",
"@types/jest": "^29.5.14",
"aws-cdk": "^2.195.0",
"aws-cdk-lib": "^2.195.0",
"constructs": "^10.4.2",
"jest": "^29.7.0",
Expand Down
6 changes: 5 additions & 1 deletion src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div id="clock"></div>
<div id="clock-container">
<div id="clock" role="timer" tabindex="0"></div>
<div id="timezone"></div>
</div>
<div id="environment-marker"></div>

<script type="module" src="./script.ts"></script>
</body>
Expand Down
23 changes: 21 additions & 2 deletions src/script.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
import './style.css'; // Import the CSS file

const clockElement = document.getElementById('clock');
const timezoneElement = document.getElementById('timezone');
const environmentMarker = document.getElementById('environment-marker');

function updateClock(): void {
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
let hours24 = now.getHours();
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const ampm = hours24 >= 12 ? 'PM' : 'AM';

let hours12 = hours24 % 12;
hours12 = hours12 ? hours12 : 12; // Convert hour '0' (midnight) to '12'

const hoursStr = String(hours12); // Hour without leading zero
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

if (clockElement) {
clockElement.textContent = `${hours}:${minutes}:${seconds}`;
clockElement.textContent = `${hoursStr}:${minutes}:${seconds} ${ampm}`;
}

if (timezoneElement) {
timezoneElement.textContent = timeZone.replace('_', ' ');
}

if (environmentMarker && import.meta.env.MODE === 'development') {
environmentMarker.textContent = 'DEV';
} else if (environmentMarker) {
environmentMarker.textContent = ''; // Clear for other modes
}
}

Expand Down
30 changes: 29 additions & 1 deletion src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,45 @@ body {
overflow: hidden; /* Prevent scrollbars */
}

#clock-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

#clock {
font-size: 8em; /* Large font size */
text-align: center;
text-shadow: 0 0 10px #0f0; /* Optional glow effect */
font-weight: bold; /* Added for better readability */
/* text-shadow: 0 0 10px #0f0; */ /* Removed for crispness */
}

#timezone {
font-size: 2em; /* Smaller than the clock */
text-align: center;
margin-top: 10px; /* Space below the clock */
color: #0f0; /* Consistent with clock color */
}

/* Basic responsiveness for smaller screens */
@media (max-width: 600px) {
#clock {
font-size: 4em;
}
#timezone {
font-size: 1em; /* Adjust for smaller screens */
}
}

#environment-marker {
position: fixed;
bottom: 20px;
right: 20px;
font-size: 2em; /* Semi-large lettering */
color: #0f0; /* Similar to clock color */
text-shadow: 0 0 10px #0f0; /* Similar glow effect */
z-index: 1000;
}

/* Added comment to trigger potential Vite update */
Loading