diff --git a/.github/workflows/workflow-deploy-to-s3.yml b/.github/workflows/workflow-deploy-to-s3.yml new file mode 100644 index 0000000..8b233d2 --- /dev/null +++ b/.github/workflows/workflow-deploy-to-s3.yml @@ -0,0 +1,157 @@ +name: Deploy Static Site to S3 + +on: + workflow_call: + inputs: + bucket: + description: "Destination S3 bucket name (without the s3:// prefix)." + required: true + type: string + source: + description: "Directory to sync to the S3 bucket." + required: false + default: public + type: string + aws-region: + description: "AWS region for S3/SES calls." + required: false + default: us-west-2 + type: string + delete-extra-files: + description: "When true, remove objects from the bucket that are not present locally." + required: false + default: true + type: boolean + cloudflare-zone-id: + description: "Optional Cloudflare Zone ID for cache purge." + required: false + default: "" + type: string + purge-cloudflare: + description: "Purge the entire Cloudflare cache when a zone ID and API token are provided." + required: false + default: true + type: boolean + cloudflare-api-token: + description: "Optional Cloudflare API token with purge_cache permission." + required: false + default: "" + type: string + email-subject: + description: "Subject for the SES notification email (defaults to bucket name)." + required: false + default: "" + type: string + email-body: + description: "Body for the SES notification email." + required: false + default: "" + type: string + email-from: + description: "Sender address for SES notifications." + required: false + default: "" + type: string + email-to: + description: "Recipient address for SES notifications." + required: false + default: "" + type: string + secrets: + aws_access_key_id: + description: "AWS access key for S3/SES." + required: true + aws_secret_access_key: + description: "AWS secret key for S3/SES." + required: true + aws_session_token: + description: "Optional session token for temporary credentials." + required: false + outputs: + deployed: + description: "True when the sync step completed." + value: ${{ jobs.deploy.outputs.deployed }} + +permissions: + contents: read + +jobs: + deploy: + name: Sync static assets + runs-on: ubuntu-latest + outputs: + deployed: ${{ steps.sync.outputs.deployed }} + env: + AWS_REGION: ${{ inputs.aws-region }} + BUCKET: ${{ inputs.bucket }} + SOURCE_DIR: ${{ inputs.source }} + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - 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-session-token: ${{ secrets.aws_session_token }} + aws-region: ${{ inputs.aws-region }} + + - name: Sync directory to S3 + id: sync + env: + DELETE_FLAG: ${{ inputs.delete-extra-files }} + run: | + set -euo pipefail + if [ ! -d "$SOURCE_DIR" ]; then + echo "Source directory '$SOURCE_DIR' does not exist." >&2 + exit 1 + fi + + delete_arg="" + if [ "${DELETE_FLAG,,}" = "true" ]; then + delete_arg="--delete" + fi + + aws s3 sync "$SOURCE_DIR" "s3://${BUCKET}" $delete_arg + echo "deployed=true" >> "$GITHUB_OUTPUT" + + - name: Purge Cloudflare cache + if: ${{ inputs.purge-cloudflare && inputs.cloudflare-zone-id != '' && inputs.cloudflare-api-token != '' }} + env: + CLOUDFLARE_ZONE_ID: ${{ inputs.cloudflare-zone-id }} + CLOUDFLARE_API_TOKEN: ${{ inputs.cloudflare-api-token }} + run: | + set -euo pipefail + curl -X POST "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/purge_cache" \ + -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ + -H "Content-Type: application/json" \ + --data '{"purge_everything":true}' + + - name: Send SES notification + if: ${{ inputs.email-from != '' && inputs.email-to != '' }} + env: + EMAIL_FROM: ${{ inputs.email-from }} + EMAIL_TO: ${{ inputs.email-to }} + CUSTOM_SUBJECT: ${{ inputs.email-subject }} + CUSTOM_BODY: ${{ inputs.email-body }} + run: | + set -euo pipefail + + subject="${CUSTOM_SUBJECT}" + if [ -z "$subject" ]; then + subject="Deployment to ${BUCKET}" + fi + + body="${CUSTOM_BODY}" + if [ -z "$body" ]; then + body="Static site synced to s3://${BUCKET} by ${GITHUB_ACTOR} (run ${GITHUB_RUN_ID}) in ${GITHUB_REPOSITORY}." + fi + + aws ses send-email \ + --from "$EMAIL_FROM" \ + --destination "ToAddresses=$EMAIL_TO" \ + --message "{ + \"Subject\": {\"Data\": \"${subject}\", \"Charset\": \"utf8\"}, + \"Body\": {\"Text\": {\"Data\": \"${body}\", \"Charset\": \"utf8\"}} + }" diff --git a/README.md b/README.md index f06ef25..c20d0fd 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,51 @@ jobs: gh_token: ${{ secrets.GITHUB_TOKEN }} ``` +### `workflow-deploy-to-s3.yml` +Syncs a directory to an S3 bucket with optional Cloudflare cache purge and SES notification. + +**Inputs** +- `bucket` (required): destination S3 bucket (without `s3://`). +- `source` (default `public`): local directory to sync. +- `aws-region` (default `us-west-2`): region for S3/SES calls. +- `delete-extra-files` (default `true`): remove objects not present locally. +- `cloudflare-zone-id` (optional): zone to purge after deploy. +- `purge-cloudflare` (default `true`): whether to purge the zone when credentials are provided. +- `cloudflare-api-token` (optional): Cloudflare token (pass a secret from the caller). +- `email-subject` (optional): SES email subject (defaults to the bucket name). +- `email-body` (optional): SES email body (defaults to an auto-generated message). +- `email-from` (optional): sender address for SES notifications (pass a secret from the caller). +- `email-to` (optional): recipient address for SES notifications (pass a secret from the caller). + +**Secrets** +- `aws_access_key_id` (required) +- `aws_secret_access_key` (required) +- `aws_session_token` (optional) + +**Outputs** +- `deployed`: `true` when the S3 sync completes. + +**Example** +```yaml +jobs: + deploy-static: + needs: tests + uses: vinitu-net/github-workflows/.github/workflows/workflow-deploy-to-s3.yml@vX.Y.Z + with: + bucket: www.example.com + source: public + aws-region: us-west-2 + delete-extra-files: true + cloudflare-zone-id: ${{ secrets.CLOUDFLARE_ZONE_ID }} + cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} + email-subject: "Site deployed" + email-from: ${{ secrets.EMAIL_FROM }} + email-to: ${{ secrets.EMAIL_TO }} + secrets: + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +``` + ### End-to-end usage in a caller repo ```yaml jobs: