diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 0059b385..28b23cf6 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -1,8 +1,6 @@ name: Deploy Docusaurus to GitHub Pages on: - # Trigger the workflow on pull requests and pushes to specific branches - pull_request: push: branches: - main @@ -10,34 +8,18 @@ on: workflow_dispatch: # Concurrency configuration to manage parallel workflow runs -# -# Group composition: pages-- -# - event_type: 'pull_request', 'push', or 'workflow_dispatch' -# - unique_identifier: PR number for PRs, branch ref for pushes/manual runs -# -# Examples of group names: -# - PR #123: "pages-pull_request-123" -# - Push to main: "pages-push-refs/heads/main" -# - Manual run on main: "pages-workflow_dispatch-refs/heads/main" -# -# Behavior: -# - PRs: New commits cancel previous runs (cancel-in-progress: true) -# - Main branch: Runs complete without cancellation (cancel-in-progress: false) -# - Manual dispatch: Runs complete without cancellation (cancel-in-progress: false) concurrency: - group: pages-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + group: production-deploy + cancel-in-progress: true jobs: build: name: Build Docusaurus runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 + - uses: actions/checkout@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: '22' cache: 'npm' @@ -81,8 +63,6 @@ jobs: needs: build name: Deploy to GitHub Pages runs-on: ubuntu-latest - # Only deploy on push to main or manual trigger, not on PRs - if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' permissions: pages: write diff --git a/.github/workflows/pr-preview.yaml b/.github/workflows/pr-preview.yaml new file mode 100644 index 00000000..0bd3303c --- /dev/null +++ b/.github/workflows/pr-preview.yaml @@ -0,0 +1,203 @@ +name: Deploy PR Preview + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + +# Cancel previous runs when new commits are pushed to the PR +concurrency: + group: pr-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + deploy-preview: + name: Deploy PR Preview + runs-on: ubuntu-latest + if: github.event.action != 'closed' + permissions: + pull-requests: write + deployments: write + contents: read + + steps: + - name: Checkout PR code + uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: 'package-lock.json' + + - name: Install dependencies + run: | + echo "Installing dependencies..." + npm ci + + - name: Build Docusaurus site + env: + DOCUSAURUS_BASE_URL: /pr-${{ github.event.pull_request.number }} + run: | + echo "Building site with base URL: $DOCUSAURUS_BASE_URL" + npm run build + + - name: Upload build artifact for local testing + uses: actions/upload-artifact@v4 + with: + name: pr-${{ github.event.pull_request.number }}-build + path: build/ + retention-days: 30 + + - name: Prepare deployment directory + run: | + PR_DIR="pr-${{ github.event.pull_request.number }}" + echo "Creating directory: $PR_DIR" + mkdir -p "$PR_DIR" + + # Create config.yaml + cat > "$PR_DIR/config.yaml" << 'EOF' + static: + files: 'build/**' + urlPath: 'pr-${{ github.event.pull_request.number }}' + extensions: ['html'] + index: true + EOF + + # Copy build directory + cp -r build "$PR_DIR/" + + echo "Contents of $PR_DIR:" + ls -la "$PR_DIR" + + - name: Install HarperDB CLI + run: | + npm install -g harperdb + + - name: Deploy to HarperDB + env: + HARPER_PREVIEW_TARGET: ${{ secrets.HARPER_PREVIEW_TARGET }} + HARPER_PREVIEW_USERNAME: ${{ secrets.HARPER_PREVIEW_USERNAME }} + HARPER_PREVIEW_PASSWORD: ${{ secrets.HARPER_PREVIEW_PASSWORD }} + run: | + cd "pr-${{ github.event.pull_request.number }}" + # Add your additional arguments here + harper deploy \ + target=${HARPER_PREVIEW_TARGET}:9925 \ + username=$HARPER_PREVIEW_USERNAME \ + password=$HARPER_PREVIEW_PASSWORD \ + project=pr-${{ github.event.pull_request.number }} \ + restart=true \ + replicated=true + + - name: Create deployment + env: + HARPER_PREVIEW_TARGET: ${{ secrets.HARPER_PREVIEW_TARGET }} + uses: actions/github-script@v8 + with: + script: | + const prNumber = context.payload.pull_request.number; + const target = process.env.HARPER_PREVIEW_TARGET; + const deploymentUrl = `${target}/pr-${prNumber}`; + + // Create deployment + const deployment = await github.rest.repos.createDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: context.payload.pull_request.head.sha, + environment: `pr-${prNumber}`, + description: `Preview deployment for PR #${prNumber}`, + auto_merge: false, + required_contexts: [] + }); + + // Create deployment status + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.data.id, + state: 'success', + environment_url: deploymentUrl, + description: 'Preview deployment successful' + }); + + // Also add a comment to the PR + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + 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.` + }); + + cleanup-preview: + name: Cleanup PR Preview + runs-on: ubuntu-latest + if: github.event.action == 'closed' + permissions: + pull-requests: write + deployments: write + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: 'package-lock.json' + + - name: Install HarperDB CLI + run: | + npm install -g harperdb + + - name: Remove preview deployment + env: + HARPER_PREVIEW_TARGET: ${{ secrets.HARPER_PREVIEW_TARGET }} + HARPER_PREVIEW_USERNAME: ${{ secrets.HARPER_PREVIEW_USERNAME }} + HARPER_PREVIEW_PASSWORD: ${{ secrets.HARPER_PREVIEW_PASSWORD }} + run: | + # Add your cleanup command here (e.g., harper undeploy or similar) + harper drop_component \ + target=${HARPER_PREVIEW_TARGET}:9925 \ + username=$HARPER_PREVIEW_USERNAME \ + password=$HARPER_PREVIEW_PASSWORD \ + project=pr-${{ github.event.pull_request.number }} \ + replicated=true \ + restart=true + echo "Cleaning up preview for PR #${{ github.event.pull_request.number }}" + + - name: Update deployment status + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.pull_request.number; + + // Mark deployment as inactive + const deployments = await github.rest.repos.listDeployments({ + owner: context.repo.owner, + repo: context.repo.repo, + environment: `pr-${prNumber}` + }); + + for (const deployment of deployments.data) { + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id, + state: 'inactive', + description: 'Preview deployment removed' + }); + } + + // Add cleanup comment to PR + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: `## ๐Ÿงน Preview Cleanup\n\nThe preview deployment for this PR has been removed.` + }); diff --git a/docs/developers/applications/index.md b/docs/developers/applications/index.md index 5122f875..8ac979ab 100644 --- a/docs/developers/applications/index.md +++ b/docs/developers/applications/index.md @@ -81,7 +81,7 @@ This guide is going to walk you through building a basic Harper application usin ## Custom Functionality with JavaScript -[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. +[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. 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: @@ -117,8 +117,8 @@ type Breed @table { 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: 1. Automatic tracking of most recently updated timestamps across resources for caching purposes -1. Sharing of contextual metadata (like user who requested the data) -1. Transactional atomicity for any writes (not needed in this get operation, but important for other operations) +2. Sharing of contextual metadata (like user who requested the data) +3. Transactional atomicity for any writes (not needed in this get operation, but important for other operations) 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: diff --git a/docs/index.mdx b/docs/index.mdx index 97b2b16c..b7943200 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -19,7 +19,7 @@ Here, you'll find all things Harper, and everything you need to get started, tro ## Getting Started -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. +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. ## Building with Harper diff --git a/fabric/index.md b/fabric/index.md index 573557ec..5afd0c30 100644 --- a/fabric/index.md +++ b/fabric/index.md @@ -8,7 +8,7 @@ Fabric Studio is the web-based GUI for Harper. Studio enables you to administer, [Sign up for free!](https://fabric.harper.fast/#/sign-up) -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/). +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/). --- diff --git a/scripts/preview-pr.mjs b/scripts/preview-pr.mjs index 48adef74..8e9b3f6a 100644 --- a/scripts/preview-pr.mjs +++ b/scripts/preview-pr.mjs @@ -98,7 +98,7 @@ async function main() { // Get the workflow run for this PR (using sanitized branch name) const runs = JSON.parse( execSync( - `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]'`, + `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]'`, { encoding: 'utf-8' } ) ); @@ -121,15 +121,16 @@ async function main() { // Get the artifacts for this run const artifacts = JSON.parse( - execSync(`gh api repos/HarperDB/documentation/actions/runs/${runs.id}/artifacts --jq '.artifacts'`, { + execSync(`gh api repos/HarperFast/documentation/actions/runs/${runs.id}/artifacts --jq '.artifacts'`, { encoding: 'utf-8', }) ); - const artifact = artifacts.find((a) => a.name === 'github-pages'); + const artifactName = `pr-${PR_NUMBER}-build`; + const artifact = artifacts.find((a) => a.name === artifactName); if (!artifact) { - console.error(`โŒ No 'github-pages' artifact found for this PR`); + console.error(`โŒ No '${artifactName}' artifact found for this PR`); process.exit(1); } @@ -161,7 +162,7 @@ async function main() { // Download the artifact console.log('โฌ‡๏ธ Downloading artifact...'); const artifactZip = join(PR_DIR, 'artifact.zip'); - execSync(`gh api repos/HarperDB/documentation/actions/artifacts/${artifact.id}/zip > "${artifactZip}"`, { + execSync(`gh api repos/HarperFast/documentation/actions/artifacts/${artifact.id}/zip > "${artifactZip}"`, { stdio: 'inherit', }); @@ -170,18 +171,10 @@ async function main() { throw new Error('Downloaded artifact file not found'); } - // Extract the artifact (it's a tar.gz inside a zip) + // Extract the artifact (it's a direct zip of the build directory) console.log('๐Ÿ“‚ Extracting artifact...'); - execSync(`unzip -q "${artifactZip}" -d "${PR_DIR}"`, { stdio: 'inherit' }); - - // The github-pages artifact contains a tar.gz file - const tarFile = join(PR_DIR, 'artifact.tar'); - if (existsSync(tarFile)) { - mkdirSync(BUILD_DIR, { recursive: true }); - execSync(`tar -xzf "${tarFile}" -C "${BUILD_DIR}"`, { stdio: 'inherit' }); - } else { - throw new Error('Expected artifact.tar not found in artifact'); - } + mkdirSync(BUILD_DIR, { recursive: true }); + execSync(`unzip -q "${artifactZip}" -d "${BUILD_DIR}"`, { stdio: 'inherit' }); // Verify extracted files are within expected directory const resolvedBuildDir = join(BUILD_DIR); @@ -189,9 +182,8 @@ async function main() { throw new Error('Security violation: extracted files outside preview directory'); } - // Clean up compressed files + // Clean up zip file rmSync(artifactZip, { force: true }); - rmSync(tarFile, { force: true }); console.log('\nโœ… Preview ready!\n'); console.log(`๐Ÿ“ Build location: ${BUILD_DIR}`); @@ -208,7 +200,10 @@ async function main() { console.log(`\n๐Ÿš€ Starting preview server...\n`); // Start the server with quoted path to prevent injection - execSync(`npm run serve -- --dir "${BUILD_DIR}"`, { stdio: 'inherit' }); + execSync(`npm run serve -- --dir "${BUILD_DIR}"`, { + stdio: 'inherit', + env: { ...process.env, DOCUSAURUS_BASE_URL: `pr-${PR_NUMBER}` }, + }); } catch (error) { console.error('\nโŒ Error:', error.message); process.exit(1); diff --git a/versioned_docs/version-4.5/index.mdx b/versioned_docs/version-4.5/index.mdx index 8feac69a..d697bec7 100644 --- a/versioned_docs/version-4.5/index.mdx +++ b/versioned_docs/version-4.5/index.mdx @@ -17,7 +17,7 @@ Welcome to the Harper Documentation! Here, you'll find all things Harper, and ev ## Getting Started -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. +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. ## Building with Harper diff --git a/versioned_docs/version-4.6/index.mdx b/versioned_docs/version-4.6/index.mdx index 7143837b..ee58c9ac 100644 --- a/versioned_docs/version-4.6/index.mdx +++ b/versioned_docs/version-4.6/index.mdx @@ -19,7 +19,7 @@ Here, you'll find all things Harper, and everything you need to get started, tro ## Getting Started -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. +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. ## Building with Harper diff --git a/versioned_docs/version-4.7/index.mdx b/versioned_docs/version-4.7/index.mdx index 7143837b..ee58c9ac 100644 --- a/versioned_docs/version-4.7/index.mdx +++ b/versioned_docs/version-4.7/index.mdx @@ -19,7 +19,7 @@ Here, you'll find all things Harper, and everything you need to get started, tro ## Getting Started -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. +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. ## Building with Harper