Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions .github/workflows/deploy-pr-previews.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,36 @@ jobs:
--head HEAD \
--pr "${{ github.event.pull_request.number }}"

- name: Run Replay QA on deployed previews
env:
REPLAY_QA_API_TOKEN: ${{ secrets.REPLAY_QA_API_TOKEN }}
REPLAY_QA_MAX_WAIT_MS: 1200000
REPLAY_QA_POLL_INTERVAL_MS: 15000
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
node tools/replay-qa/run-preview-qa.mjs \
--deployments netlify-deployments.json \
--pr "${{ github.event.pull_request.number }}"

- name: Comment preview links
if: always()
uses: actions/github-script@v8
with:
script: |
const fs = require("fs")
const marker = "<!-- netlify-preview-links -->"
const path = "netlify-deployments.md"
const summary = fs.existsSync(path)
? fs.readFileSync(path, "utf8")
: "Netlify preview deployment did not produce a summary."
const body = `${marker}\n${summary}`
const sections = []
if (fs.existsSync("netlify-deployments.md")) {
sections.push(fs.readFileSync("netlify-deployments.md", "utf8"))
} else {
sections.push("Netlify preview deployment did not produce a summary.")
}
if (fs.existsSync("replay-qa-results.md")) {
sections.push(fs.readFileSync("replay-qa-results.md", "utf8"))
} else {
sections.push("Replay QA did not produce a summary.")
}
const body = `${marker}\n${sections.join("\n")}`
const { owner, repo } = context.repo
const issue_number = context.payload.pull_request.number
const comments = await github.rest.issues.listComments({
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ dist/
coverage/
*.tsbuildinfo
netlify-deployments.md
netlify-deployments.json
replay-qa-results.md
replay-qa-results.json
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,14 @@ node tools/netlify/deploy-affected.mjs --mode preview --base origin/main --head
```

The GitHub Actions workflows require a repository secret named
`NETLIFY_AUTH_TOKEN`.
`NETLIFY_AUTH_TOKEN`. PR preview QA also requires a Replay QA bearer token in
`REPLAY_QA_API_TOKEN`. Generate it from the Replay QA settings page; valid
tokens start with `lqa_`.

- `Deploy Main` runs on pushes to `main`, builds affected app projects, uploads
draft deploys, and publishes them to the configured production Netlify sites.
- `Deploy PR Previews` runs on same-repository pull requests, deploys affected
apps to stable `deploy-preview-<pr>` Netlify aliases, and updates a PR comment
with the preview links.
with the preview links. After Netlify deploys, the workflow creates Replay QA
projects for the deployed preview URLs, polls until QA completes or times out,
lists open bugs, and appends the QA summary to the same PR comment.
2 changes: 1 addition & 1 deletion apps/todoist-clone/src/components/task-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function DetailBody({ task }: { task: Task }) {
const meta = PRIORITY_META[task.priority]

// Description preview shown in the header summary.
const preview = (task.description as string).slice(0, 240)
const preview = (task.description ?? "").slice(0, 240)

return (
<div className="flex flex-col gap-4 p-5">
Expand Down
28 changes: 26 additions & 2 deletions tools/netlify/deploy-affected.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,29 @@ function touchesGlobalBuildSurface(files) {
file === "package.json" ||
file === "package-lock.json" ||
file === "nx.json" ||
file.startsWith("tools/") ||
file.startsWith(".github/workflows/")
file === "tools/netlify/sites.json"
)
}

function directlyChangedProjects(files) {
const projectSet = new Set()
for (const file of files) {
const match = /^apps\/([^/]+)\//.exec(file)
if (match && sites[match[1]]) projectSet.add(match[1])
}
return orderedProjects.filter((project) => projectSet.has(project))
}

function affectedProjects() {
if (forceAll || !base || !head || isZeroSha(base)) return orderedProjects

const files = changedFiles()
if (!files || touchesGlobalBuildSurface(files)) return orderedProjects

const directProjects = directlyChangedProjects(files)
if (directProjects.length > 0) return directProjects
if (!files.some((file) => file.startsWith("apps/"))) return []

const nxArgs = [
"nx",
"show",
Expand Down Expand Up @@ -242,7 +254,18 @@ for (const project of projects) {
}

const markdown = markdownFor(results, projects)
const json = {
mode,
base,
head,
prNumber,
dryRun,
createdAt: new Date().toISOString(),
projects,
results,
}
writeFileSync(path.join(workspaceRoot, "netlify-deployments.md"), markdown)
writeFileSync(path.join(workspaceRoot, "netlify-deployments.json"), `${JSON.stringify(json, null, 2)}\n`)
console.log(markdown)

if (process.env.GITHUB_STEP_SUMMARY) {
Expand All @@ -251,4 +274,5 @@ if (process.env.GITHUB_STEP_SUMMARY) {

if (process.env.GITHUB_OUTPUT) {
appendFileSync(process.env.GITHUB_OUTPUT, "markdown_path=netlify-deployments.md\n")
appendFileSync(process.env.GITHUB_OUTPUT, "json_path=netlify-deployments.json\n")
}
Loading
Loading