AWS Amplify PR Preview Links in 15 Minutes
Why this matters
Your team opens a frontend PR. The reviewer has to either:
- Check out the branch locally (5-10 min setup)
- Trust the screenshots in the description
- Skip visual testing entirely
None of these are good. Amplify already builds a preview deployment for every branch. The URL just doesn't show up anywhere useful.
Here's how to surface it automatically in every PR, in 15 minutes.
What you'll build
A GitHub Actions workflow that:
- Posts a comment the moment a PR is opened: "Building... expected URL:
https://pr-42.d1xxxxxxxxx.amplifyapp.com" - Polls Amplify every 30 seconds until the build completes
- Updates the same comment to ✅ with the live link (or ❌ on failure)
No third-party services or polling scripts needed. The URL is deterministic: Amplify always uses pr-{PR_NUMBER}.{APP_ID}.amplifyapp.com.
Prerequisites
- AWS Amplify Gen 1 app (classic console hosting) connected to a GitHub repo
- PR previews enabled in Amplify (Hosting → Previews → Enable)
- An IAM user with
amplify:ListJobspermission on your app's ARN
Gen 2 note: If you're using Amplify Gen 2 (CDK-based), PR previews work differently. This tutorial covers Gen 1 — the "Amplify Hosting" option in the console.
Step 1: Enable PR Previews in Amplify
AWS Console → Amplify → your app → Hosting → Previews
Toggle "Enable pull request previews" and connect your GitHub repo. This tells Amplify to spin up a branch deployment for every PR.
Step 2: Create an IAM policy
Create a policy for a CI user with this minimum permission:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["amplify:ListJobs"],
"Resource": "arn:aws:amplify:eu-central-1:ACCOUNT_ID:apps/APP_ID/branches/*"
}]
}
Generate access keys for this user (IAM → User → Security credentials → Create access key → Application running outside AWS).
Security note: Static access keys need periodic rotation. For a more secure setup, use OIDC federation to grant GitHub Actions short-lived tokens with no key management. For a quick internal setup, static keys are fine.
Step 3: Add GitHub secrets
Settings → Secrets and variables → Actions → New repository secret:
| Secret | Value |
|---|---|
AWS_ACCESS_KEY_ID | From IAM |
AWS_SECRET_ACCESS_KEY | From IAM |
AWS_REGION | e.g. eu-central-1 |
AMPLIFY_APP_ID | From Amplify App settings (starts with d) |
Step 4: Add the workflow
Create .github/workflows/amplify-preview.yml:
name: Amplify PR Preview
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
pull-requests: write
concurrency:
group: amplify-preview-${{ github.event.pull_request.number }}
cancel-in-progress: false
jobs:
preview:
runs-on: ubuntu-latest
steps:
- name: Post initial comment
uses: actions/github-script@v7
id: initial-comment
with:
script: |
const prNumber = context.payload.pull_request.number;
const appId = '${{ secrets.AMPLIFY_APP_ID }}';
const expectedUrl = `https://pr-${prNumber}.${appId}.amplifyapp.com`;
const comment = await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `🔄 **Amplify preview building...**\n\nExpected URL: ${expectedUrl}\n\n_This comment will update when the build completes._`
});
return comment.data.id;
- 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 }}
- name: Wait for Amplify build
id: wait-build
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
APP_ID="${{ secrets.AMPLIFY_APP_ID }}"
BRANCH="pr-${PR_NUMBER}"
MAX_ATTEMPTS=20
ATTEMPT=0
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
STATUS=$(aws amplify list-jobs \
--app-id "$APP_ID" \
--branch-name "$BRANCH" \
--max-results 1 \
--query 'jobSummaries[0].status' \
--output text 2>/dev/null || echo "PENDING")
[ "$STATUS" = "None" ] && STATUS="PENDING"
echo "Attempt $ATTEMPT: $STATUS"
if [ "$STATUS" = "SUCCEED" ]; then
echo "build_status=success" >> $GITHUB_OUTPUT
break
elif [ "$STATUS" = "FAILED" ]; then
echo "build_status=failed" >> $GITHUB_OUTPUT
break
fi
ATTEMPT=$((ATTEMPT + 1))
sleep 30
done
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
echo "build_status=timeout" >> $GITHUB_OUTPUT
fi
- name: Update comment with result
uses: actions/github-script@v7
with:
script: |
const prNumber = context.payload.pull_request.number;
const commentId = ${{ steps.initial-comment.outputs.result }};
const appId = '${{ secrets.AMPLIFY_APP_ID }}';
const previewUrl = `https://pr-${prNumber}.${appId}.amplifyapp.com`;
const buildStatus = '${{ steps.wait-build.outputs.build_status }}';
const messages = {
success: `✅ **Amplify preview ready**\n\n🔗 [Open preview](${previewUrl})\n\`${previewUrl}\``,
failed: `❌ **Amplify build failed**\n\nCheck the [Amplify console](https://console.aws.amazon.com/amplify) for logs.`,
timeout: `⏱️ **Build taking longer than expected**\n\nExpected URL (may not be ready yet): ${previewUrl}`
};
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId,
body: messages[buildStatus] || messages.timeout
});
How it works
The preview URL is deterministic: Amplify always names preview branches pr-{PR_NUMBER}, so the URL can be constructed before the build even starts. That's why we post the "building" comment immediately. Reviewers can bookmark it right away.
The polling loop runs every 30 seconds for up to 10 minutes (20 attempts). For most frontend builds that's more than enough. Bump MAX_ATTEMPTS if your builds regularly run longer.
What it looks like
When a PR is opened:
🔄 Amplify preview building...
Expected URL:
https://pr-42.d1abc123.amplifyapp.comThis comment will update when the build completes.
After build succeeds:
✅ Amplify preview ready
🔗 Open preview
https://pr-42.d1abc123.amplifyapp.com
Troubleshooting
PENDING for several attempts: When a PR is first opened, Amplify may take 30-60 seconds before queueing the first build job. The polling loop handles this. If it stays PENDING past 5 minutes, check that PR previews are enabled in the Amplify console (Hosting → Previews).
Comments pile up on rapid pushes: Each push to the PR triggers a new workflow run, which posts a new "building..." comment. Runs execute in sequence rather than in parallel, but you'll still see one comment thread per push. For repos with lots of commits per PR, consider replacing createComment with peter-evans/find-comment + peter-evans/create-or-update-comment to always update a single comment.
Fork PRs don't get preview links: GitHub doesn't expose repository secrets to workflows triggered by fork PRs. External contributors opening PRs from their own fork won't see preview comments. For public/OSS repos, consider Amplify webhooks and a Lambda instead.
Preview URL 404 after build succeeds: Amplify sometimes takes 30-60 seconds after marking the build SUCCEED to serve traffic. The URL is correct, just wait and refresh.
The tradeoff
This approach polls the Amplify API from GitHub Actions. An alternative is using an Amplify webhook to trigger a Lambda that posts to GitHub, but that's 3x the setup complexity for the same result. Unless you have very high PR volume, polling from GHA is the simpler call.
Wrap-up
Four steps, one YAML file. The core trick is that Amplify's preview URLs are deterministic. You know the URL before the build starts, so you can post the link immediately and update it in place when the build finishes. It works out of the box for team repos, with no external services to maintain.
The one real limitation: fork PRs from external contributors won't get preview links (GitHub secrets restriction). If you hit issues, the Amplify console build logs are your first stop. The pr-{N} branch and its job history are right there.
Written 2026-04-27 based on actual implementation in Crafthunt-App/baugpt_frontend. See TKT #747 for context.