diff --git a/.github/workflows/ci-preview.yml b/.github/workflows/ci-preview.yml new file mode 100644 index 0000000..f9f0656 --- /dev/null +++ b/.github/workflows/ci-preview.yml @@ -0,0 +1,17 @@ +name: Preview validation + +on: + pull_request: + workflow_dispatch: + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Validate required frontend files exist + run: | + test -f frontend/index.html + test -f frontend/style.css + test -f frontend/script.js + echo "Frontend files present." diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..715bd05 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,47 @@ +name: Deploy resume to S3 + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + id-token: write + contents: read + +concurrency: + group: deploy-cloud-resume + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure AWS credentials (OIDC) + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ vars.AWS_REGION }} + + - name: Upload static assets with long cache + run: | + aws s3 sync frontend/ s3://${{ vars.S3_BUCKET }} \ + --delete \ + --exclude "index.html" \ + --cache-control "public,max-age=31536000,immutable" + + - name: Upload index.html with no-cache + run: | + aws s3 cp frontend/index.html s3://${{ vars.S3_BUCKET }}/index.html \ + --content-type "text/html; charset=utf-8" \ + --cache-control "no-cache, no-store, must-revalidate" + + - name: Invalidate CloudFront cache (optional) + if: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID != '' }} + run: | + aws cloudfront create-invalidation \ + --distribution-id ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }} \ + --paths "/*" diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..8354374 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,91 @@ +# Cloud Resume Auto Deploy (GitHub Actions → S3) + +This repo auto-deploys `frontend/` to S3 on every push to `main`. + +## 1) GitHub repo settings + +Add these in **Settings → Secrets and variables → Actions**. + +### Secrets +- `AWS_ROLE_TO_ASSUME` = IAM role ARN used by GitHub OIDC + +### Variables +- `AWS_REGION` (example: `us-east-1`) +- `S3_BUCKET` (bucket name only) +- `CLOUDFRONT_DISTRIBUTION_ID` (optional; leave empty if not using CloudFront) + +## 2) Create IAM role for GitHub OIDC + +### Trust policy (replace ACCOUNT_ID and repo name if needed) + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" + }, + "StringLike": { + "token.actions.githubusercontent.com:sub": "repo:CameronMB/cloud-resume:ref:refs/heads/main" + } + } + } + ] +} +``` + +### Permissions policy (replace BUCKET_NAME and distribution ARN if used) + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ListBucket", + "Effect": "Allow", + "Action": ["s3:ListBucket"], + "Resource": "arn:aws:s3:::BUCKET_NAME" + }, + { + "Sid": "ReadWriteObjects", + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject" + ], + "Resource": "arn:aws:s3:::BUCKET_NAME/*" + }, + { + "Sid": "CloudFrontInvalidateOptional", + "Effect": "Allow", + "Action": ["cloudfront:CreateInvalidation"], + "Resource": "*" + } + ] +} +``` + +If you do not use CloudFront invalidations, remove the final statement. + +## 3) Deployment behavior + +Workflow file: `.github/workflows/deploy.yml` + +On push to `main`: +1. Assume AWS role via OIDC +2. Sync all files from `frontend/` to S3 (deletes removed files) +3. Set long cache headers for static assets +4. Upload `index.html` with no-cache headers +5. Optionally invalidate CloudFront + +## 4) Manual deploy + +You can run manually from the GitHub Actions tab via **workflow_dispatch**.