Skip to content

Commit 4e12356

Browse files
Preview PRs using Harper Fabric ✨ (#408)
* first pass * fix workflow maybe * try again * beep boop * - to _ * fix preview-pr script * cancel-in-progress for deploy * fix fetch depth and global install harper
1 parent 53704dc commit 4e12356

File tree

9 files changed

+229
-51
lines changed

9 files changed

+229
-51
lines changed

.github/workflows/deploy.yaml

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,25 @@
11
name: Deploy Docusaurus to GitHub Pages
22

33
on:
4-
# Trigger the workflow on pull requests and pushes to specific branches
5-
pull_request:
64
push:
75
branches:
86
- main
97
# Allows you to run this workflow manually from the Actions tab
108
workflow_dispatch:
119

1210
# Concurrency configuration to manage parallel workflow runs
13-
#
14-
# Group composition: pages-<event_type>-<unique_identifier>
15-
# - event_type: 'pull_request', 'push', or 'workflow_dispatch'
16-
# - unique_identifier: PR number for PRs, branch ref for pushes/manual runs
17-
#
18-
# Examples of group names:
19-
# - PR #123: "pages-pull_request-123"
20-
# - Push to main: "pages-push-refs/heads/main"
21-
# - Manual run on main: "pages-workflow_dispatch-refs/heads/main"
22-
#
23-
# Behavior:
24-
# - PRs: New commits cancel previous runs (cancel-in-progress: true)
25-
# - Main branch: Runs complete without cancellation (cancel-in-progress: false)
26-
# - Manual dispatch: Runs complete without cancellation (cancel-in-progress: false)
2711
concurrency:
28-
group: pages-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}
29-
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
12+
group: production-deploy
13+
cancel-in-progress: true
3014

3115
jobs:
3216
build:
3317
name: Build Docusaurus
3418
runs-on: ubuntu-latest
3519
steps:
36-
- uses: actions/checkout@v4
37-
with:
38-
fetch-depth: 0
20+
- uses: actions/checkout@v6
3921

40-
- uses: actions/setup-node@v4
22+
- uses: actions/setup-node@v6
4123
with:
4224
node-version: '22'
4325
cache: 'npm'
@@ -81,8 +63,6 @@ jobs:
8163
needs: build
8264
name: Deploy to GitHub Pages
8365
runs-on: ubuntu-latest
84-
# Only deploy on push to main or manual trigger, not on PRs
85-
if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch'
8666

8767
permissions:
8868
pages: write

.github/workflows/pr-preview.yaml

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
name: Deploy PR Preview
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, closed]
6+
7+
# Cancel previous runs when new commits are pushed to the PR
8+
concurrency:
9+
group: pr-preview-${{ github.event.pull_request.number }}
10+
cancel-in-progress: true
11+
12+
jobs:
13+
deploy-preview:
14+
name: Deploy PR Preview
15+
runs-on: ubuntu-latest
16+
if: github.event.action != 'closed'
17+
permissions:
18+
pull-requests: write
19+
deployments: write
20+
contents: read
21+
22+
steps:
23+
- name: Checkout PR code
24+
uses: actions/checkout@v6
25+
with:
26+
ref: ${{ github.event.pull_request.head.sha }}
27+
28+
- name: Setup Node.js
29+
uses: actions/setup-node@v6
30+
with:
31+
node-version: '22'
32+
cache: 'npm'
33+
cache-dependency-path: 'package-lock.json'
34+
35+
- name: Install dependencies
36+
run: |
37+
echo "Installing dependencies..."
38+
npm ci
39+
40+
- name: Build Docusaurus site
41+
env:
42+
DOCUSAURUS_BASE_URL: /pr-${{ github.event.pull_request.number }}
43+
run: |
44+
echo "Building site with base URL: $DOCUSAURUS_BASE_URL"
45+
npm run build
46+
47+
- name: Upload build artifact for local testing
48+
uses: actions/upload-artifact@v4
49+
with:
50+
name: pr-${{ github.event.pull_request.number }}-build
51+
path: build/
52+
retention-days: 30
53+
54+
- name: Prepare deployment directory
55+
run: |
56+
PR_DIR="pr-${{ github.event.pull_request.number }}"
57+
echo "Creating directory: $PR_DIR"
58+
mkdir -p "$PR_DIR"
59+
60+
# Create config.yaml
61+
cat > "$PR_DIR/config.yaml" << 'EOF'
62+
static:
63+
files: 'build/**'
64+
urlPath: 'pr-${{ github.event.pull_request.number }}'
65+
extensions: ['html']
66+
index: true
67+
EOF
68+
69+
# Copy build directory
70+
cp -r build "$PR_DIR/"
71+
72+
echo "Contents of $PR_DIR:"
73+
ls -la "$PR_DIR"
74+
75+
- name: Install HarperDB CLI
76+
run: |
77+
npm install -g harperdb
78+
79+
- name: Deploy to HarperDB
80+
env:
81+
HARPER_PREVIEW_TARGET: ${{ secrets.HARPER_PREVIEW_TARGET }}
82+
HARPER_PREVIEW_USERNAME: ${{ secrets.HARPER_PREVIEW_USERNAME }}
83+
HARPER_PREVIEW_PASSWORD: ${{ secrets.HARPER_PREVIEW_PASSWORD }}
84+
run: |
85+
cd "pr-${{ github.event.pull_request.number }}"
86+
# Add your additional arguments here
87+
harper deploy \
88+
target=${HARPER_PREVIEW_TARGET}:9925 \
89+
username=$HARPER_PREVIEW_USERNAME \
90+
password=$HARPER_PREVIEW_PASSWORD \
91+
project=pr-${{ github.event.pull_request.number }} \
92+
restart=true \
93+
replicated=true
94+
95+
- name: Create deployment
96+
env:
97+
HARPER_PREVIEW_TARGET: ${{ secrets.HARPER_PREVIEW_TARGET }}
98+
uses: actions/github-script@v8
99+
with:
100+
script: |
101+
const prNumber = context.payload.pull_request.number;
102+
const target = process.env.HARPER_PREVIEW_TARGET;
103+
const deploymentUrl = `${target}/pr-${prNumber}`;
104+
105+
// Create deployment
106+
const deployment = await github.rest.repos.createDeployment({
107+
owner: context.repo.owner,
108+
repo: context.repo.repo,
109+
ref: context.payload.pull_request.head.sha,
110+
environment: `pr-${prNumber}`,
111+
description: `Preview deployment for PR #${prNumber}`,
112+
auto_merge: false,
113+
required_contexts: []
114+
});
115+
116+
// Create deployment status
117+
await github.rest.repos.createDeploymentStatus({
118+
owner: context.repo.owner,
119+
repo: context.repo.repo,
120+
deployment_id: deployment.data.id,
121+
state: 'success',
122+
environment_url: deploymentUrl,
123+
description: 'Preview deployment successful'
124+
});
125+
126+
// Also add a comment to the PR
127+
await github.rest.issues.createComment({
128+
owner: context.repo.owner,
129+
repo: context.repo.repo,
130+
issue_number: prNumber,
131+
body: `## 🚀 Preview Deployment\n\nYour preview deployment is ready!\n\n🔗 **Preview URL:** ${deploymentUrl}\n\nThis preview will update automatically when you push new commits.`
132+
});
133+
134+
cleanup-preview:
135+
name: Cleanup PR Preview
136+
runs-on: ubuntu-latest
137+
if: github.event.action == 'closed'
138+
permissions:
139+
pull-requests: write
140+
deployments: write
141+
contents: read
142+
143+
steps:
144+
- name: Checkout code
145+
uses: actions/checkout@v6
146+
147+
- name: Setup Node.js
148+
uses: actions/setup-node@v6
149+
with:
150+
node-version: '22'
151+
cache: 'npm'
152+
cache-dependency-path: 'package-lock.json'
153+
154+
- name: Install HarperDB CLI
155+
run: |
156+
npm install -g harperdb
157+
158+
- name: Remove preview deployment
159+
env:
160+
HARPER_PREVIEW_TARGET: ${{ secrets.HARPER_PREVIEW_TARGET }}
161+
HARPER_PREVIEW_USERNAME: ${{ secrets.HARPER_PREVIEW_USERNAME }}
162+
HARPER_PREVIEW_PASSWORD: ${{ secrets.HARPER_PREVIEW_PASSWORD }}
163+
run: |
164+
# Add your cleanup command here (e.g., harper undeploy or similar)
165+
harper drop_component \
166+
target=${HARPER_PREVIEW_TARGET}:9925 \
167+
username=$HARPER_PREVIEW_USERNAME \
168+
password=$HARPER_PREVIEW_PASSWORD \
169+
project=pr-${{ github.event.pull_request.number }} \
170+
replicated=true \
171+
restart=true
172+
echo "Cleaning up preview for PR #${{ github.event.pull_request.number }}"
173+
174+
- name: Update deployment status
175+
uses: actions/github-script@v7
176+
with:
177+
script: |
178+
const prNumber = context.payload.pull_request.number;
179+
180+
// Mark deployment as inactive
181+
const deployments = await github.rest.repos.listDeployments({
182+
owner: context.repo.owner,
183+
repo: context.repo.repo,
184+
environment: `pr-${prNumber}`
185+
});
186+
187+
for (const deployment of deployments.data) {
188+
await github.rest.repos.createDeploymentStatus({
189+
owner: context.repo.owner,
190+
repo: context.repo.repo,
191+
deployment_id: deployment.id,
192+
state: 'inactive',
193+
description: 'Preview deployment removed'
194+
});
195+
}
196+
197+
// Add cleanup comment to PR
198+
await github.rest.issues.createComment({
199+
owner: context.repo.owner,
200+
repo: context.repo.repo,
201+
issue_number: prNumber,
202+
body: `## 🧹 Preview Cleanup\n\nThe preview deployment for this PR has been removed.`
203+
});

docs/developers/applications/index.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ This guide is going to walk you through building a basic Harper application usin
8181
8282
## Custom Functionality with JavaScript
8383

84-
[The getting started guide](../../learn/) covers how to build an application entirely through schema configuration. However, if your application requires more custom functionality, you will probably want to employ your own JavaScript modules to implement more specific features and interactions. This gives you tremendous flexibility and control over how data is accessed and modified in Harper. Let's take a look at how we can use JavaScript to extend and define "resources" for custom functionality. In Harper, data is accessed through our [Resource API](../../reference/resources/), a standard interface to access data sources, tables, and make them available to endpoints. Database tables are `Resource` classes, and so extending the function of a table is as simple as extending their class.
84+
[The getting started guide](/learn/) covers how to build an application entirely through schema configuration. However, if your application requires more custom functionality, you will probably want to employ your own JavaScript modules to implement more specific features and interactions. This gives you tremendous flexibility and control over how data is accessed and modified in Harper. Let's take a look at how we can use JavaScript to extend and define "resources" for custom functionality. In Harper, data is accessed through our [Resource API](../../reference/resources/), a standard interface to access data sources, tables, and make them available to endpoints. Database tables are `Resource` classes, and so extending the function of a table is as simple as extending their class.
8585

8686
To define custom (JavaScript) resources as endpoints, we need to create a `resources.js` module (this goes in the root of your application folder). And then endpoints can be defined with Resource classes that `export`ed. This can be done in addition to, or in lieu of the `@export`ed types in the schema.graphql. If you are exporting and extending a table you defined in the schema make sure you remove the `@export` from the schema so that don't export the original table or resource to the same endpoint/path you are exporting with a class. Resource classes have methods that correspond to standard HTTP/REST methods, like `get`, `post`, `patch`, and `put` to implement specific handling for any of these methods (for tables they all have default implementations). Let's add a property to the dog records when they are returned, that includes their age in human years. To do this, we get the `Dog` class from the defined tables, extend it (with our custom logic), and export it:
8787

@@ -117,8 +117,8 @@ type Breed @table {
117117
We use the new table's (static) `get()` method to retrieve a breed by id. Harper will maintain the current context, ensuring that we are accessing the data atomically, in a consistent snapshot across tables. This provides:
118118

119119
1. Automatic tracking of most recently updated timestamps across resources for caching purposes
120-
1. Sharing of contextual metadata (like user who requested the data)
121-
1. Transactional atomicity for any writes (not needed in this get operation, but important for other operations)
120+
2. Sharing of contextual metadata (like user who requested the data)
121+
3. Transactional atomicity for any writes (not needed in this get operation, but important for other operations)
122122

123123
The resource methods are automatically wrapped with a transaction and will automatically commit the changes when the method finishes. This allows us to fully utilize multiple resources in our current transaction. With our own snapshot of the database for the Dog and Breed table we can then access data like this:
124124

docs/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Here, you'll find all things Harper, and everything you need to get started, tro
1919

2020
## Getting Started
2121

22-
The best way to get started using Harper is to head over to the [Learn](../learn/) section and work through the Getting Started and Developer guides.
22+
The best way to get started using Harper is to head over to the [Learn](/learn/) section and work through the Getting Started and Developer guides.
2323

2424
## Building with Harper
2525

fabric/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Fabric Studio is the web-based GUI for Harper. Studio enables you to administer,
88

99
[Sign up for free!](https://fabric.harper.fast/#/sign-up)
1010

11-
Harper includes a simplified local Studio that is packaged with all Harper installations and served directly from the cluster. It can be enabled in the [configuration file](../docs/deployments/configuration#localstudio). This section is dedicated to the hosted Studio accessed at [studio.harperdb.io](https://fabric.harper.fast/).
11+
Harper includes a simplified local Studio that is packaged with all Harper installations and served directly from the cluster. It can be enabled in the [configuration file](/docs/deployments/configuration#localstudio). This section is dedicated to the hosted Studio accessed at [studio.harperdb.io](https://fabric.harper.fast/).
1212

1313
---
1414

scripts/preview-pr.mjs

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ async function main() {
9898
// Get the workflow run for this PR (using sanitized branch name)
9999
const runs = JSON.parse(
100100
execSync(
101-
`gh api repos/HarperDB/documentation/actions/runs --paginate -X GET -f branch=${sanitizedBranch} --jq '.workflow_runs | map(select(.conclusion == "success" and .name == "Deploy Docusaurus to GitHub Pages")) | sort_by(.created_at) | reverse | .[0]'`,
101+
`gh api repos/HarperFast/documentation/actions/runs --paginate -X GET -f branch=${sanitizedBranch} --jq '.workflow_runs | map(select(.conclusion == "success" and .name == "Deploy PR Preview")) | sort_by(.created_at) | reverse | .[0]'`,
102102
{ encoding: 'utf-8' }
103103
)
104104
);
@@ -121,15 +121,16 @@ async function main() {
121121

122122
// Get the artifacts for this run
123123
const artifacts = JSON.parse(
124-
execSync(`gh api repos/HarperDB/documentation/actions/runs/${runs.id}/artifacts --jq '.artifacts'`, {
124+
execSync(`gh api repos/HarperFast/documentation/actions/runs/${runs.id}/artifacts --jq '.artifacts'`, {
125125
encoding: 'utf-8',
126126
})
127127
);
128128

129-
const artifact = artifacts.find((a) => a.name === 'github-pages');
129+
const artifactName = `pr-${PR_NUMBER}-build`;
130+
const artifact = artifacts.find((a) => a.name === artifactName);
130131

131132
if (!artifact) {
132-
console.error(`❌ No 'github-pages' artifact found for this PR`);
133+
console.error(`❌ No '${artifactName}' artifact found for this PR`);
133134
process.exit(1);
134135
}
135136

@@ -161,7 +162,7 @@ async function main() {
161162
// Download the artifact
162163
console.log('⬇️ Downloading artifact...');
163164
const artifactZip = join(PR_DIR, 'artifact.zip');
164-
execSync(`gh api repos/HarperDB/documentation/actions/artifacts/${artifact.id}/zip > "${artifactZip}"`, {
165+
execSync(`gh api repos/HarperFast/documentation/actions/artifacts/${artifact.id}/zip > "${artifactZip}"`, {
165166
stdio: 'inherit',
166167
});
167168

@@ -170,28 +171,19 @@ async function main() {
170171
throw new Error('Downloaded artifact file not found');
171172
}
172173

173-
// Extract the artifact (it's a tar.gz inside a zip)
174+
// Extract the artifact (it's a direct zip of the build directory)
174175
console.log('📂 Extracting artifact...');
175-
execSync(`unzip -q "${artifactZip}" -d "${PR_DIR}"`, { stdio: 'inherit' });
176-
177-
// The github-pages artifact contains a tar.gz file
178-
const tarFile = join(PR_DIR, 'artifact.tar');
179-
if (existsSync(tarFile)) {
180-
mkdirSync(BUILD_DIR, { recursive: true });
181-
execSync(`tar -xzf "${tarFile}" -C "${BUILD_DIR}"`, { stdio: 'inherit' });
182-
} else {
183-
throw new Error('Expected artifact.tar not found in artifact');
184-
}
176+
mkdirSync(BUILD_DIR, { recursive: true });
177+
execSync(`unzip -q "${artifactZip}" -d "${BUILD_DIR}"`, { stdio: 'inherit' });
185178

186179
// Verify extracted files are within expected directory
187180
const resolvedBuildDir = join(BUILD_DIR);
188181
if (!resolvedBuildDir.startsWith(PREVIEW_DIR)) {
189182
throw new Error('Security violation: extracted files outside preview directory');
190183
}
191184

192-
// Clean up compressed files
185+
// Clean up zip file
193186
rmSync(artifactZip, { force: true });
194-
rmSync(tarFile, { force: true });
195187

196188
console.log('\n✅ Preview ready!\n');
197189
console.log(`📁 Build location: ${BUILD_DIR}`);
@@ -208,7 +200,10 @@ async function main() {
208200
console.log(`\n🚀 Starting preview server...\n`);
209201

210202
// Start the server with quoted path to prevent injection
211-
execSync(`npm run serve -- --dir "${BUILD_DIR}"`, { stdio: 'inherit' });
203+
execSync(`npm run serve -- --dir "${BUILD_DIR}"`, {
204+
stdio: 'inherit',
205+
env: { ...process.env, DOCUSAURUS_BASE_URL: `pr-${PR_NUMBER}` },
206+
});
212207
} catch (error) {
213208
console.error('\n❌ Error:', error.message);
214209
process.exit(1);

versioned_docs/version-4.5/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Welcome to the Harper Documentation! Here, you'll find all things Harper, and ev
1717

1818
## Getting Started
1919

20-
The best way to get started using Harper is to head over to the [Learn](../../learn/) section and work through the Getting Started and Developer guides.
20+
The best way to get started using Harper is to head over to the [Learn](/learn/) section and work through the Getting Started and Developer guides.
2121

2222
## Building with Harper
2323

versioned_docs/version-4.6/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Here, you'll find all things Harper, and everything you need to get started, tro
1919

2020
## Getting Started
2121

22-
The best way to get started using Harper is to head over to the [Learn](../../learn/) section and work through the Getting Started and Developer guides.
22+
The best way to get started using Harper is to head over to the [Learn](/learn/) section and work through the Getting Started and Developer guides.
2323

2424
## Building with Harper
2525

0 commit comments

Comments
 (0)