diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
index ad8fd73455c..c6c3be878a7 100644
--- a/.github/workflows/deploy.yaml
+++ b/.github/workflows/deploy.yaml
@@ -11,18 +11,18 @@ on:
push:
branches:
- master
- pull_request:
- branches:
- - master
workflow_dispatch:
inputs:
pr_number:
- description: 'PR number to deploy (optional)'
+ description: 'PR number to deploy (optional, for manual testing)'
required: false
type: string
repository_dispatch:
types: [data-update]
+# Note: PR staging deployments are handled by staging-aggregate.yaml
+# This workflow focuses on production deployments
+
jobs:
build:
name: Build
@@ -165,14 +165,9 @@ jobs:
# =======================
- name: Build site
run: |
- if [ "$BRANCH" != 'refs/heads/master' ]; then
- hugo --gc --minify --cleanDestinationDir --destination public --baseURL https://staging.forrt.org
- else
- hugo --gc --minify --cleanDestinationDir --destination public
- fi
+ hugo --gc --minify --cleanDestinationDir --destination public
env:
- BRANCH: ${{ github.ref }}
- HUGO_ENV: ${{ github.ref != 'refs/heads/master' && 'staging' || 'production' }}
+ HUGO_ENV: production
# =======================
# Deployment Artifact
@@ -186,50 +181,17 @@ jobs:
path: public/
retention-days: 1
- #========================================
- # Deploy website to staging GitHub Pages
- #========================================
- deploy-test:
- name: Deploy - Test
- runs-on: ubuntu-22.04
- concurrency:
- group: staging
- permissions:
- contents: write
- needs: build
- steps:
- #========================================
- # Download website artifact for staging deployment
- #========================================
- - name: Download Artifact - Website
- uses: actions/download-artifact@v4
- with:
- name: forrt-website-${{ github.run_number }}
- path: ${{ github.repository }}/forrt-website
-
- #========================================
- # Deploy website to staging GitHub Pages
- #========================================
- - name: Deploy - GitHub Pages
- uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e
- with:
- personal_token: ${{ secrets.STAGING_GITHUB_TOKEN }}
- publish_dir: ${{ github.repository }}/forrt-website
- external_repository: forrtproject/webpage-staging
- publish_branch: staging
- cname: staging.forrt.org
-
#========================================
# Deploy website to production GitHub Pages
+ # Note: Staging deployments are handled by staging-aggregate.yaml
#========================================
deploy-prod:
name: Deploy - Production
runs-on: ubuntu-22.04
permissions:
contents: write
- needs:
- - deploy-test
- if: ((github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' || github.event_name == 'repository_dispatch') && github.event.repository.fork == false
+ needs: build
+ if: github.event.repository.fork == false
steps:
#========================================
# Download website artifact for production deployment
diff --git a/.github/workflows/staging-aggregate.yaml b/.github/workflows/staging-aggregate.yaml
index 4d337592880..cf0c13594f2 100644
--- a/.github/workflows/staging-aggregate.yaml
+++ b/.github/workflows/staging-aggregate.yaml
@@ -11,6 +11,7 @@ on:
pull_request:
branches:
- master
+ types: [opened, synchronize, reopened, ready_for_review]
schedule:
- cron: '0 1 1 * *' # 1 AM UTC on the 1st of each month
workflow_dispatch:
@@ -21,10 +22,18 @@ on:
type: boolean
default: false
+# Workflow-level concurrency to queue staging builds (wait instead of cancel)
+concurrency:
+ group: staging-aggregate
+ cancel-in-progress: false
+
jobs:
aggregate-prs:
name: Aggregate PRs for Staging
runs-on: ubuntu-22.04
+ timeout-minutes: 10 # Prevent blocking the queue
+ # Skip draft PRs entirely - they should not trigger staging builds
+ if: github.event.pull_request.draft != true
permissions:
contents: write
pull-requests: read
@@ -173,6 +182,7 @@ jobs:
build:
name: Build
runs-on: ubuntu-22.04
+ timeout-minutes: 20 # Prevent blocking the queue
needs: [aggregate-prs]
if: always() && (needs.aggregate-prs.outputs.has-prs == 'true' || github.event.inputs.force_deploy == 'true')
permissions:
@@ -346,6 +356,7 @@ jobs:
deploy-staging:
name: Deploy - Staging
runs-on: ubuntu-22.04
+ timeout-minutes: 10 # Prevent blocking the queue
concurrency:
group: staging
permissions:
diff --git a/README.md b/README.md
index d5137e66357..c79b00310ed 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,79 @@ Please note that it's very important to us that we maintain a positive and suppo
This is the website for the **Framework for Open and Reproducible Research Training (FORRT)**, built with [hugo](https://gohugo.io/), and deployed with [Github Actions](https://docs.github.com/en/actions). You can find the website at [forrt.org](https://forrt.org/). See CONTRIBUTING.md for more information on how to contribute.
+## Deployment & Staging
+
+The FORRT website uses a dual deployment strategy to ensure quality and enable collaborative testing:
+
+### Production Deployment
+
+- **URL**: [https://forrt.org](https://forrt.org)
+- **Workflow**: `.github/workflows/deploy.yaml`
+- **Trigger**: Pushes to `master` branch
+- **Target**: GitHub Pages (`gh-pages` branch)
+- **Purpose**: Serves the live, production website
+
+### Staging Deployment
+
+- **URL**: [https://staging.forrt.org](https://staging.forrt.org)
+- **Workflow**: `.github/workflows/staging-aggregate.yaml`
+- **Trigger**: Pull requests to `master`, monthly schedule (1st of each month), or manual dispatch
+- **Target**: External repository (`forrtproject/webpage-staging`)
+- **Purpose**: Preview combined changes from all open PRs
+- **Note**: Staging always deploys aggregated changes from all open PRs. There is currently no option to deploy a single PR in isolation to staging.
+
+#### How Staging Works
+
+The staging deployment uses an **aggregation strategy** to provide a unified preview environment:
+
+1. **Automatic Aggregation**: When any PR is opened, synchronized, or reopened, the workflow:
+ - Collects all open, non-draft PRs targeting `master`
+ - Creates a temporary aggregation branch from `master`
+ - Attempts to merge each PR in sequence
+
+2. **Conflict Handling**:
+ - PRs that merge cleanly are included in the staging build
+ - PRs with merge conflicts are skipped but logged
+ - The deployment continues with all compatible PRs
+
+3. **Deployment Comments**: Each PR receives an automated comment indicating:
+ - ✅ Successfully deployed (for PRs without conflicts)
+ - ⚠️ Skipped due to conflicts (for conflicting PRs)
+ - Deployment timestamp and staging URL
+
+4. **Queuing & Timeouts**:
+ - Workflow uses concurrency control to queue builds (not cancel)
+ - Job timeouts (10-20 minutes) prevent indefinite blocking
+ - Draft PRs are filtered out to avoid unnecessary builds
+
+5. **Branch Cleanup**:
+ - Keeps only the 5 most recent staging branches
+ - Automatically cleans up older staging-aggregate branches
+
+#### Viewing Staging Changes
+
+- Visit [https://staging.forrt.org](https://staging.forrt.org) to see the combined state of all open, compatible PRs
+- Note: Staging shows aggregated changes from **all** open PRs, not individual PR changes
+- PRs with merge conflicts won't appear in staging until conflicts are resolved
+
+#### Manual Single-PR Deployment
+
+**Currently Not Supported for Staging**: There is no option to deploy a single PR in isolation to the staging environment. All staging deployments include all compatible open PRs.
+
+**Workaround for Testing Individual PRs**:
+- The `deploy.yaml` workflow supports manual dispatch with a `pr_number` input
+- However, this deploys directly to **production** (forrt.org), not staging
+- Use with extreme caution - only for emergency fixes or when you're certain the changes are ready for production
+- For regular PR testing, rely on the aggregated staging deployment or test locally
+
+#### Monthly Reports
+
+On the 1st of each month, an automated deployment report is created as a GitHub issue with:
+- Total PRs processed
+- Successfully merged PRs
+- Skipped PRs (with conflict information)
+- Deployment statistics
+
## License

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.