Skip to content

Commit 5363d00

Browse files
Merge pull request #1872 from codatio/exp-2127-pr-preview-deployments
EXP-2127: Add per-PR preview deployments via GitHub Pages
2 parents e6f9e23 + bdb23e5 commit 5363d00

13 files changed

Lines changed: 176 additions & 25 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ ZENDESK_KEY=<zendesk key>
22
GTM_ID=<google tag manager id>
33

44
BRANCH=optional (sets the edit path)
5+
BASE_URL=optional (subpath the site is served from, no trailing slash; used by PR previews)

.github/workflows/pr.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ name: PR build
22

33
on:
44
pull_request:
5+
# `closed` is included so the preview deployment is torn down when the PR
6+
# closes; the build/link-check job skips that event
7+
types: [opened, reopened, synchronize, closed]
58
workflow_dispatch: # Allows manual triggering from the GitHub UI
69

710
permissions:
@@ -11,6 +14,7 @@ permissions:
1114

1215
jobs:
1316
build:
17+
if: github.event.action != 'closed'
1418
runs-on: ubuntu-latest
1519
timeout-minutes: 30
1620
steps:
@@ -116,3 +120,60 @@ jobs:
116120
if (filtered.length > 0) {
117121
core.setFailed("There are broken links in the documentation.");
118122
}
123+
124+
# Deploys the built site to GitHub Pages under pr-preview/pr-<number>/ and
125+
# posts a sticky comment on the PR with the preview URL; the preview is
126+
# removed when the PR closes. Builds separately from the job above because
127+
# the preview needs a different baseUrl than the link check. Fork PRs are
128+
# skipped: they get neither secrets nor a writable GITHUB_TOKEN.
129+
deploy-preview:
130+
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
131+
runs-on: ubuntu-latest
132+
timeout-minutes: 30
133+
# Concurrent runs for the same PR would race on pushing to gh-pages
134+
concurrency: pr-preview-${{ github.ref }}
135+
permissions:
136+
contents: write
137+
pull-requests: write
138+
steps:
139+
- uses: actions/checkout@v7
140+
with:
141+
# Images are Git LFS-tracked; skip fetching them on teardown, where
142+
# the checkout is only needed so the action can push to gh-pages
143+
lfs: ${{ github.event.action != 'closed' }}
144+
145+
- uses: actions/setup-node@v6
146+
if: github.event.action != 'closed'
147+
with:
148+
node-version: 24
149+
cache: npm
150+
151+
# The ADO registry requires auth even to install; the repo .npmrc is
152+
# credential-less so creds go into ~/.npmrc here
153+
- name: Authenticate to codat-npm feed
154+
if: github.event.action != 'closed'
155+
run: |
156+
{
157+
echo "//pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/registry/:username=codat"
158+
echo "//pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/registry/:_password=${ADO_NPM_FEED_TOKEN}"
159+
echo "//pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/registry/:email=npm-requires-email@example.com"
160+
} >> ~/.npmrc
161+
env:
162+
ADO_NPM_FEED_TOKEN: ${{ secrets.ADO_NPM_FEED_TOKEN }}
163+
164+
- name: Install dependencies
165+
if: github.event.action != 'closed'
166+
run: npm ci
167+
168+
- name: Build site for preview
169+
if: github.event.action != 'closed'
170+
run: npm run build
171+
env:
172+
GTM_ID: ${{ vars.GTM_ID }}
173+
# GitHub Pages serves the preview from a subpath (no trailing slash)
174+
BASE_URL: /codat-docs/pr-preview/pr-${{ github.event.number }}
175+
176+
- name: Deploy preview
177+
uses: rossjrw/pr-preview-action@v1
178+
with:
179+
source-dir: ./build

docusaurus.config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,11 @@ import navbar from "./nav.config";
8080
import redirects from "./redirects.config";
8181

8282
import { generateAPISitemaps } from "./src/utils/oas-sitemap.js";
83+
import rehypeImageBaseUrl from "./src/utils/rehype-image-base-url.js";
8384

84-
const BASE_URL = "";
85+
// PR preview deploys serve the site from a subpath on GitHub Pages, so the
86+
// preview workflow overrides this. No trailing slash — baseUrl appends one.
87+
const BASE_URL = process.env.BASE_URL ?? "";
8588

8689
require("dotenv").config();
8790

@@ -129,6 +132,7 @@ const config = {
129132
routeBasePath: "/",
130133
sidebarPath: "./sidebars.js",
131134
editUrl: `https://github.com/codatio/codat-docs/edit/${process.env?.BRANCH || "main"}/`,
135+
rehypePlugins: [[rehypeImageBaseUrl, { baseUrl: `${BASE_URL}/` }]],
132136
exclude: ["README.md"],
133137
lastVersion: "current",
134138
versions: {
@@ -140,6 +144,7 @@ const config = {
140144
},
141145
blog: {
142146
showReadingTime: true,
147+
rehypePlugins: [[rehypeImageBaseUrl, { baseUrl: `${BASE_URL}/` }]],
143148
blogTitle: "Codat updates",
144149
blogDescription: "Engineering and product updates from Codat.",
145150
postsPerPage: 10,

src/components/Api/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useState, Suspense } from "react";
22
import { Helmet } from "react-helmet";
33

44
import BrowserOnly from "@docusaurus/BrowserOnly";
5+
import useBaseUrl from "@docusaurus/useBaseUrl";
56

67
import Layout from "@theme/Layout";
78
import Navbar from "@theme/Navbar";
@@ -21,6 +22,9 @@ const Api = ({
2122
socialBanner = "https://docs.codat.io/img/meta/codat-bg.png",
2223
}) => {
2324
const [menuOpen, setMenuOpen] = useState(false);
25+
// The OAS specs live in static/, so the fetch URL must respect baseUrl for
26+
// deploys served from a subpath (e.g. PR previews)
27+
const specUrl = useBaseUrl(url);
2428

2529
return (
2630
<Layout title={title}>
@@ -48,7 +52,7 @@ const Api = ({
4852
<BrowserOnly>
4953
{() => (
5054
<Suspense fallback={Fallback}>
51-
<LazyStoplight apiDescriptionUrl={url} />
55+
<LazyStoplight apiDescriptionUrl={specUrl} />
5256
</Suspense>
5357
)}
5458
</BrowserOnly>

src/components/Cards/index.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1+
import { useBaseUrlUtils } from "@docusaurus/useBaseUrl";
2+
3+
// Callers pass absolute image/link paths (e.g. /img/...), which bypass
4+
// baseUrl and break deploys served from a subpath (e.g. PR previews);
5+
// external URLs pass through withBaseUrl untouched
16
const Card = (props) => {
27
const { image, icon: Icon, title, children, className } = props;
8+
const { withBaseUrl } = useBaseUrlUtils();
39

410
return (
511
<li className={`card ${className}`}>
612
<div className="header">
7-
{Icon ? <Icon /> : <img src={image} className="mini-icon" />}
13+
{Icon ? (
14+
<Icon />
15+
) : (
16+
<img src={withBaseUrl(image)} className="mini-icon" />
17+
)}
818

919
<h3>{title}</h3>
1020
</div>
@@ -16,40 +26,42 @@ const Card = (props) => {
1626

1727
const CardTwo = (props) => {
1828
const { image, title, link, linkText, children, className } = props;
29+
const { withBaseUrl } = useBaseUrlUtils();
1930

2031
return (
2132
<li className={`card two ${className}`}>
2233
<div className="header">
23-
<img src={image} className="mini-icon" />
34+
<img src={withBaseUrl(image)} className="mini-icon" />
2435

2536
<h3>{title}</h3>
2637
</div>
2738

2839
{children}
2940

3041
<p>
31-
<a href={link}>{linkText}</a>
42+
<a href={withBaseUrl(link)}>{linkText}</a>
3243
</p>
3344
</li>
3445
);
3546
};
3647

3748
const MiniCard = (props) => {
3849
const { image, title, subtitle, link, children, className } = props;
50+
const { withBaseUrl } = useBaseUrlUtils();
3951

4052
return (
4153
<li className={`card mini ${className}`}>
4254
<div className="card-row">
4355
<div className="header">
44-
<a href={link}>
45-
<img src={image} className="icon usecase" />
56+
<a href={withBaseUrl(link)}>
57+
<img src={withBaseUrl(image)} className="icon usecase" />
4658
</a>
4759
</div>
4860

4961
<div className="content">
5062
<h4>{title}</h4>
5163
<p>
52-
<a href={link}>{subtitle}</a>
64+
<a href={withBaseUrl(link)}>{subtitle}</a>
5365
</p>
5466
</div>
5567
</div>

src/components/ClientLibraries/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from "react";
2+
import { useBaseUrlUtils } from "@docusaurus/useBaseUrl";
23

34
const repoBaseUrl = "https://github.com/codatio/client-sdk-";
45
const repoProductTMP = "/tree/main/bank-feeds";
@@ -93,6 +94,9 @@ const getShieldUrl = (productName, language) => {
9394

9495
const ClientLibraries = ({ productName }) => {
9596
const productUrl = !productName ? "" : "/tree/main/" + productName;
97+
// The language icons are absolute /img/ paths, which bypass baseUrl and
98+
// break deploys served from a subpath (e.g. PR previews)
99+
const { withBaseUrl } = useBaseUrlUtils();
96100
return (
97101
<ul className="card-container mini">
98102
{languages.map((language, i) => {
@@ -104,7 +108,10 @@ const ClientLibraries = ({ productName }) => {
104108
href={repoBaseUrl + language.name + productUrl}
105109
target="_blank"
106110
>
107-
<img src={language.icon} className="icon usecase" />
111+
<img
112+
src={withBaseUrl(language.icon)}
113+
className="icon usecase"
114+
/>
108115
</a>
109116
</div>
110117

src/components/Clients/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import React from "react";
2+
import { useBaseUrlUtils } from "@docusaurus/useBaseUrl";
23

34
import styles from "./styles.module.scss";
45

56
const Client = (props) => {
67
const { path, name, scale } = props;
8+
// Callers pass absolute /img/ paths, which bypass baseUrl and break
9+
// deploys served from a subpath (e.g. PR previews)
10+
const { withBaseUrl } = useBaseUrlUtils();
711

812
return (
913
<div className={styles.client}>
1014
<img
11-
src={path}
15+
src={withBaseUrl(path)}
1216
alt={`${name} logo`}
1317
style={
1418
scale

src/components/PageHeader/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from "react";
22
import clsx from "clsx";
33
import { useColorMode } from "@docusaurus/theme-common";
4+
import { useBaseUrlUtils } from "@docusaurus/useBaseUrl";
45

56
import { ModalController } from "../Modal";
67

@@ -35,7 +36,12 @@ const PageHeader = ({
3536
videoText,
3637
}) => {
3738
const { colorMode } = useColorMode();
38-
const resolvedIcon = iconDark && colorMode === "dark" ? iconDark : icon;
39+
// icon/img arrive as absolute paths from frontmatter (banner_image etc.),
40+
// which bypass baseUrl and break deploys served from a subpath
41+
const { withBaseUrl } = useBaseUrlUtils();
42+
const resolvedIcon = withBaseUrl(
43+
iconDark && colorMode === "dark" ? iconDark : icon,
44+
);
3945

4046
return (
4147
<div className={clsx(styles.wrapper, className)}>
@@ -62,7 +68,7 @@ const PageHeader = ({
6268
{videoUrl && <BannerVideo text={videoText} url={videoUrl} />}
6369
</div>
6470

65-
{img && <img src={img} className={styles.heroImg} alt="" />}
71+
{img && <img src={withBaseUrl(img)} className={styles.heroImg} alt="" />}
6672
</div>
6773
);
6874
};

src/components/Products/index.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from "react";
22
import { useColorMode } from "@docusaurus/theme-common";
3+
import { useBaseUrlUtils } from "@docusaurus/useBaseUrl";
34

45
const other = [
56
{
@@ -104,6 +105,9 @@ const allProducts = [
104105

105106
const Products = ({ mini, products, verbose }) => {
106107
const { colorMode } = useColorMode();
108+
// The logo/link paths above are absolute, which bypasses baseUrl and
109+
// breaks deploys served from a subpath (e.g. PR previews)
110+
const { withBaseUrl } = useBaseUrlUtils();
107111
const validProducts = !products
108112
? allProducts
109113
: products
@@ -130,15 +134,15 @@ const Products = ({ mini, products, verbose }) => {
130134
<div className="card-row">
131135
<div className="header">
132136
<a
133-
href={product.link}
137+
href={withBaseUrl(product.link)}
134138
className={`icon-wrapper product animated ${product.slug}`}
135139
>
136140
<img
137-
src={
141+
src={withBaseUrl(
138142
colorMode === "dark" && product.logoDark
139143
? product.logoDark
140-
: product.logo
141-
}
144+
: product.logo,
145+
)}
142146
className="icon product"
143147
/>
144148
</a>
@@ -147,7 +151,7 @@ const Products = ({ mini, products, verbose }) => {
147151
<div className="content">
148152
<h4>{product.name}</h4>
149153
<p>
150-
<a href={product.link}>Explore solution →</a>
154+
<a href={withBaseUrl(product.link)}>Explore solution →</a>
151155
</p>
152156
</div>
153157
</div>
@@ -167,23 +171,23 @@ const Products = ({ mini, products, verbose }) => {
167171
<li key={i} className="card">
168172
<div className="header">
169173
<a
170-
href={product.link}
174+
href={withBaseUrl(product.link)}
171175
className={`icon-wrapper product animated ${product.slug}`}
172176
>
173177
<img
174-
src={
178+
src={withBaseUrl(
175179
colorMode === "dark" && product.logoDark
176180
? product.logoDark
177-
: product.logo
178-
}
181+
: product.logo,
182+
)}
179183
className="icon product"
180184
/>
181185
</a>
182186
</div>
183187
<h3>{product.name}</h3>
184188
<p>{product.description}</p>
185189
<p>
186-
<a href={product.link}>{product.linkText}</a>
190+
<a href={withBaseUrl(product.link)}>{product.linkText}</a>
187191
</p>
188192
</li>
189193
);

src/pages/support/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from "react";
2+
import Link from "@docusaurus/Link";
23
import GearFinanceDownIcon from "@components/GearFinanceDownIcon";
34
import FeatureBullet from "@components/FeatureBullet";
45
import Layout from "@theme/Layout";
@@ -24,7 +25,7 @@ const RaiseSupportTicket = () => {
2425
</div>
2526
<p>
2627
Raise a technical issue with our support team{" "}
27-
<a href="/raise-support-ticket">here</a>.
28+
<Link to="/raise-support-ticket">here</Link>.
2829
</p>
2930
</li>
3031
<li className="card">

0 commit comments

Comments
 (0)