Merge pull request #90 from V35HR4J/patch-1 #492
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: CI | |
on: [push, workflow_dispatch] | |
jobs: | |
build: | |
name: Build & test | |
runs-on: ubuntu-latest | |
steps: | |
- uses: actions/checkout@v4 | |
- uses: actions/setup-node@v4 | |
with: | |
node-version: 20.11.1 | |
cache: 'npm' | |
# Install & build & test: | |
- run: npm ci | |
# Build without secrets for previews, in non-push cases: | |
- name: Build for preview | |
if: github.event_name != 'push' | |
run: npm run build | |
env: | |
NODE_ENV: development | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
# Build with secrets for production, on push only: | |
- name: Build for production | |
if: github.event_name == 'push' | |
run: npm run build | |
env: | |
NODE_ENV: production | |
NEXT_PUBLIC_POSTHOG_KEY: ${{ vars.POSTHOG_KEY }} | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
- uses: actions/upload-artifact@v4 | |
with: | |
name: out | |
path: out/* | |
if-no-files-found: error | |
- uses: actions/upload-artifact@v4 | |
with: | |
name: rss | |
path: out/rss.xml | |
if-no-files-found: error | |
check-blog-changes: | |
name: Check for new blog posts to announce | |
if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
runs-on: ubuntu-latest | |
needs: build | |
outputs: | |
new-blog-post: ${{ steps.detect-changes.outputs.new-blog-post }} | |
steps: | |
- uses: actions/checkout@v4 | |
- uses: actions/download-artifact@v4 | |
with: | |
name: rss | |
path: local-rss | |
- id: detect-changes | |
name: 'Check for RSS differences' | |
shell: bash | |
run: | | |
curl -f https://httptoolkit.com/rss.xml > remote-rss.xml | |
sudo apt-get update && sudo apt-get install xmlstarlet | |
LOCAL_POSTS=$(xmlstarlet sel -t -v '//rss/channel/item/link' local-rss/rss.xml) | |
REMOTE_POSTS=$(xmlstarlet sel -t -v '//rss/channel/item/link' remote-rss.xml) | |
# Matches remote posts against each local post, excludes those matches, | |
# and returns the remaining lines (or nothing, if there are none): | |
NEW_POSTS=$(grep -vFxf <(echo "$REMOTE_POSTS") <(echo "$LOCAL_POSTS") || true) | |
if [[ $(echo "$NEW_POSTS" | wc -l) -gt 1 ]]; then | |
echo "More than one new post found - something odd is happening, failing the job" | |
exit 1 | |
fi | |
# Grab the slug (the last path chunk) from each URL: | |
NEW_POST_SLUGS=$(echo $NEW_POSTS | sed -E 's|.*/([^/]+)/?$|\1|') | |
# Ensure the output is exactly an empty string, if not a post (trim newlines etc): | |
if [[ ! -z "$(echo "$NEW_POST_SLUGS" | tr -d '\n')" ]]; then | |
echo "New blog post: $NEW_POST_SLUGS" | |
echo "new-blog-post=$(echo $NEW_POST_SLUGS)" >> "$GITHUB_OUTPUT" | |
else | |
echo "No new blog posts" | |
echo "new-blog-post=" >> "$GITHUB_OUTPUT" | |
fi | |
publish-docker: | |
name: Build & publish container to Docker Hub | |
if: github.event_name == 'push' && !startsWith(github.ref, 'refs/heads/dependabot/') | |
runs-on: ubuntu-latest | |
needs: build | |
steps: | |
- uses: actions/checkout@v4 | |
- uses: actions/download-artifact@v4 | |
with: | |
name: out | |
path: out | |
- uses: docker/setup-buildx-action@v3 | |
- name: Login to DockerHub | |
uses: docker/login-action@v3 | |
with: | |
username: ${{ secrets.DOCKERHUB_USERNAME }} | |
password: ${{ secrets.DOCKERHUB_TOKEN }} | |
- name: Extract Docker metadata | |
id: meta | |
uses: docker/metadata-action@v5 | |
with: | |
github-token: ${{ secrets.GITHUB_TOKEN }} | |
images: httptoolkit/website | |
tags: | | |
type=raw,value=prod,enable=${{ github.ref == 'refs/heads/main' }} | |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} | |
type=raw,value=staging,enable=${{ github.ref == 'refs/heads/next' }} | |
type=sha | |
- name: Publish to Docker Hub | |
uses: docker/build-push-action@v5 | |
with: | |
context: . | |
push: ${{ github.event_name != 'pull_request' }} | |
tags: ${{ steps.meta.outputs.tags }} | |
labels: ${{ steps.meta.outputs.labels }} | |
publish-scaleway: | |
name: Deploy to Scaleway | |
environment: ${{ github.ref == 'refs/heads/main' && 'production' || github.ref == 'refs/heads/next' && 'staging' || 'no-environment-deploy-skipped' }} | |
runs-on: ubuntu-latest | |
needs: publish-docker | |
steps: | |
- name: Redeploy container | |
uses: httptoolkit/deploy-scaleway-serverless-container-action@v1 | |
with: | |
container_id: ${{ vars.SCW_API_CONTAINER_ID }} | |
region: ${{ vars.SCW_API_CONTAINER_REGION }} | |
secret_key: ${{ secrets.SCW_SECRET_KEY }} | |
registry_image_url: "registry.hub.docker.com/httptoolkit/website:${{ vars.DOCKER_IMAGE_TAG }}" | |
- name: Redeploy failover container | |
uses: httptoolkit/deploy-scaleway-serverless-container-action@v1 | |
if: ${{ vars.SCW_FAILOVER_API_CONTAINER_ID }} | |
with: | |
container_id: ${{ vars.SCW_FAILOVER_API_CONTAINER_ID }} | |
region: ${{ vars.SCW_FAILOVER_API_CONTAINER_REGION }} | |
secret_key: ${{ secrets.SCW_SECRET_KEY }} | |
registry_image_url: "registry.hub.docker.com/httptoolkit/website:${{ vars.DOCKER_IMAGE_TAG }}" | |
- name: Flush CDN cache | |
if: ${{ vars.BUNNY_PULL_ZONE_ID }} | |
run: | | |
# Clear CDN cache to re-request content: | |
curl -f --request POST \ | |
--url https://api.bunny.net/pullzone/$PULL_ZONE_ID/purgeCache \ | |
--header "AccessKey: $BUNNY_SITE_API_KEY" | |
env: | |
PULL_ZONE_ID: ${{ vars.BUNNY_PULL_ZONE_ID }} | |
BUNNY_SITE_API_KEY: ${{ secrets.BUNNY_SITE_API_KEY }} | |
announce-blog-changes: | |
name: Announce new blog posts | |
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.check-blog-changes.outputs.new-blog-post != '' | |
runs-on: ubuntu-latest | |
needs: | |
- check-blog-changes | |
- publish-scaleway | |
steps: | |
- uses: actions/checkout@v4 | |
- name: Send an email about the new blog post with Mailcoach | |
run: | | |
SLUG="${{ needs.check-blog-changes.outputs.new-blog-post }}" | |
URL="https://httptoolkit.com/blog/$SLUG/" | |
MARKDOWN=$(cat "src/content/posts/$SLUG.mdx") | |
FRONTMATTER=$(echo "$MARKDOWN" | sed -n '/^---$/,/^---$/p') | |
# Get the title - stripping any surrounding quotes if required | |
TITLE=$(echo "$FRONTMATTER" | | |
sed -n 's/^title: //p' | | |
sed 's/^"//; s/"$//' | | |
sed "s/^'//; s/'$//" | |
) | |
# Get the post content, stripping out frontmatter: | |
CONTENT=$(echo "$MARKDOWN" | sed '0,/^---$/d; 0,/^---$/d') | |
# Use awk to extract the first few paragraphs | |
EXCERPT=$(echo "$CONTENT" | awk -v RS='' -v ORS='\n\n' 'NR == 1, NR == 4 { print $0 }') | |
# Stop before any HTML: | |
EXCERPT=$(echo "$EXCERPT" | sed '/\s*<[^ ]\+.*/Q') | |
EXCERPT=$(echo "$EXCERPT" | sed '/\s*!\[\](.*/Q') | |
# Try to avoid the excerpt ending with a heading: | |
LAST_LINE=$(echo "$EXCERPT" | tail -n 1) | |
if [[ $LAST_LINE =~ ^\#\#.* ]]; then | |
EXCERPT=$(echo "$EXCERPT" | head -n -1) | |
fi | |
API_ENDPOINT="https://http-toolkit.mailcoach.app/api/campaigns" | |
# Create a Mailcoach campaign for this blog post email, scheduled to send | |
# in 3 hours from now: | |
CAMPAIGN_ID=$( | |
curl \ | |
-f -X POST $API_ENDPOINT \ | |
-H "Authorization: Bearer $MAILCOACH_TOKEN" \ | |
-H 'Accept: application/json' \ | |
-H 'Content-Type: application/json' \ | |
-d "{ | |
\"name\": \"Blog post: $TITLE\", | |
\"subject\": \"$TITLE\", | |
\"schedule_at\":\"$(date -u -d "3 hours" +"%Y-%m-%d %H:%M:%S")\", | |
\"email_list_uuid\": \"$MAILING_LIST_UUID\", | |
\"template_uuid\": \"$EMAIL_TEMPLATE_ID\", | |
\"fields\": { | |
\"url\": \"$URL\", | |
\"title\": \"$TITLE\", | |
\"content\": $(echo "$EXCERPT" | jq -R -r -s @json) | |
} | |
}" \ | |
| jq -r .data.uuid | |
) | |
# Send a test email with this content immediately: | |
curl -f -X POST "$API_ENDPOINT/$CAMPAIGN_ID/send-test" \ | |
-H "Authorization: Bearer $MAILCOACH_TOKEN" \ | |
-H 'Accept: application/json' \ | |
-H 'Content-Type: application/json' \ | |
-d '{"email":"'$TEST_EMAIL'"}' | |
env: | |
MAILCOACH_TOKEN: ${{ secrets.MAILCOACH_TOKEN }} | |
EMAIL_TEMPLATE_ID: ${{ vars.BLOG_POST_EMAIL_TEMPLATE_ID }} | |
MAILING_LIST_UUID: ${{ vars.BLOG_POST_MAILING_LIST_UUID }} | |
TEST_EMAIL: ${{ vars.BLOG_POST_TEST_EMAIL }} |