Background
As part of setting up another static blog site project, I wanted to email my users whenever a new post was added. I used AWS S3 to host the static files and GitHub Actions for my CI/CD.
One of the first options I found with a great free tier was Resend. You get up to 100 emails a day, which met the needs of this project. However, any other provider with an API that can be called from a GitHub Action could also be used.
Resend Set Up
I used AWS Route53 DNS for this project and Terraform for IaC, so to validate my domain, I just had to add the DNS records provided when I signed up with Resend and wait for them to be confirmed.
Route53 Terraform Config for Resend…
resource "aws_route53_record" "resend_mx" { provider = aws.provider zone_id = aws_route53_zone.primary_zone.zone_id name = "send" type = "MX" ttl = 300 records = ["10 feedback-smtp.us-east-1.amazonses.com"]}
resource "aws_route53_record" "resend_txt" {provider = aws.providerzone_id = aws_route53_zone.primary_zone.zone_idname = "send"type = "TXT"ttl = 300records = ["v=spf1 include:amazonses.com ~all"]}
resource "aws_route53_record" "resend_dk_txt" {provider = aws.providerzone_id = aws_route53_zone.primary_zone.zone_idname = "resend.\_domainkey"type = "TXT"ttl = 300records = ["p=<YourValueHere>"]}
resource "aws_route53_record" "resend_dmarc_txt" {provider = aws.providerzone_id = aws_route53_zone.primary_zone.zone_idname = "\_dmarc"type = "TXT"ttl = 300records = ["v=DMARC1; p=none;"]}It looks like Resend actually uses AWS SES for its backend, which provides 3,000 emails a month free for 12 months (about 100 per day). Using their service means I don’t have to worry about email limits after the 12 months expire or go through the process of getting my AWS account production approved.
GitHub Actions
I needed to get the deployed count of posts and the new count of posts to compare. I did this using a few GitHub Action steps and then compared the counts.
After that, if the counts indicated a new post, I needed to call Resend’s API to send the emails.
I made sure to do this after my site had already deployed to avoid users receiving a notification before the new post was visible.
Old vs New Post Count
You can easily save the output of different GitHub Action steps. You just have to set an id for the steps you want to use output from and then use the following syntax:
echo "key=value" >> "$GITHUB_OUTPUT"In my static site, each blog entry gets its own directory.
So, for the old count, I can simply query S3 for the number of prefixes (PRE) using the AWS CLI:
echo "count=$(aws s3 ls s3://${{ secrets.BUCKET_NAME }}/blog/ | grep -c PRE | xargs)" >> "$GITHUB_OUTPUT"For the new count, I can simply count the directories in the newly built output:
echo "count=$(ls -d dist/blog/*/ | wc -l | xargs)" >> "$GITHUB_OUTPUT"Depending on your particular setup, how you get this count will vary, but as long as you can obtain your deployed and new numbers, you can use this pattern.
Resend API
I searched through GitHub Actions and didn’t find anything specific to Resend, so I wrote my own github.com/brenden-hogan/resend-action.
I added a dry-run flag to the action so that when it is true, it doesn’t actually send an email. I then set the dry-run flag for my Resend action to a comparison of the count values we obtained earlier:
${{ !(steps.old_blog_count.outputs.count < steps.new_blog_count.outputs.count) }}This will only set the dry-run flag to false if the count increases, not if it remains the same or decreases. See more here for information about how GitHub evaluates expressions in actions.
Putting it all together
Here is the full GitHub Actions deployment YAML file.
If you are using S3, you’ll need to configure the AWS CLI as well. I used OIDC just like I did in this previous post.
You’ll also need to include your own specific build steps for your site and adjust the count steps for your cloud provider or framework. I included the sync to S3 here, but you’ll need to replace that with your own cloud provider’s sync command if it is different.
name: Deployon: [push]
env: AWS_REGION: <BucketRegion>
permissions: id-token: write # Required for requesting the JWT contents: read # Required for actions/checkout
jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup AWS CLI uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: <role> role-session-name: GitHub_to_AWS_via_FederatedOIDC aws-region: <BucketRegion> - name: Build Static Site # Add your build commands here - name: Get current number of posts id: old_blog_count run: | echo "count=$(aws s3 ls s3://${{ secrets.BUCKET_NAME }}/blog/ | grep -c PRE | xargs)" >> "$GITHUB_OUTPUT" - name: Get new blog count id: new_blog_count run: | echo "count=$(ls -d dist/blog/*/ | wc -l | xargs)" >> "$GITHUB_OUTPUT" - name: Sync files to S3 bucket run: | aws s3 sync dist s3://${{ secrets.BUCKET_NAME }} <Command Options> - name: email-users with: api-key: '${{ secrets.RESEND_API_KEY }}' from-domain: '${{ secrets.EMAIL_DOMAIN }}' sender: 'no-reply' subject: "A new entry has been added to xxx blog" send-to-separately: 'true' scheduled-at: 'in 900 sec' delay: '1000' dry-run: ${{ !(steps.old_blog_count.outputs.count < steps.new_blog_count.outputs.count) }} html: <p>A new blog post has been added to <a href="https://${{ secrets.EMAIL_DOMAIN }}">${{ secrets.EMAIL_DOMAIN }}</a></p><br/><p>To opt out of these notifications reply with the word STOP</p>Remember to keep your secrets secure and never commit sensitive information to your repository. Adjust the placeholders (e.g., <role>, <BucketRegion>, <Command Options>, email addresses) to fit your environment.