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..fbb289d3423 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: @@ -20,11 +21,24 @@ on: required: false type: boolean default: false + single_pr: + description: 'Deploy a single PR only (enter PR number, e.g. 123)' + required: false + type: string + default: '' + +# 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 @@ -55,23 +69,44 @@ jobs: echo "🔍 Triggering PR: #${{ github.event.pull_request.number }}" fi - # Get all open, non-draft PRs targeting master (regardless of CI status) - PRS=$(gh pr list --base master --state open --json number,title,headRefName,isDraft --jq '.[] | select(.isDraft == false) | .number') - - if [ -z "$PRS" ]; then - echo "No open non-draft PRs found" - echo "branch=master" >> $GITHUB_OUTPUT - echo "included_prs=" >> $GITHUB_OUTPUT - echo "attempted_prs=" >> $GITHUB_OUTPUT - echo "total_prs=0" >> $GITHUB_OUTPUT - echo "has_prs=false" >> $GITHUB_OUTPUT - exit 0 + # Check if single PR mode is enabled + SINGLE_PR="${{ github.event.inputs.single_pr }}" + if [ -n "$SINGLE_PR" ] && [ "$SINGLE_PR" != "" ]; then + echo "🎯 Single PR mode: deploying only PR #$SINGLE_PR" + + # Validate PR exists and is open + if ! gh pr view "$SINGLE_PR" --json state --jq '.state' | grep -q "OPEN"; then + echo "❌ PR #$SINGLE_PR is not open or doesn't exist" + echo "branch=master" >> $GITHUB_OUTPUT + echo "included_prs=" >> $GITHUB_OUTPUT + echo "attempted_prs=$SINGLE_PR" >> $GITHUB_OUTPUT + echo "total_prs=0" >> $GITHUB_OUTPUT + echo "has_prs=false" >> $GITHUB_OUTPUT + exit 1 + fi + + PRS="$SINGLE_PR" + echo "has_prs=true" >> $GITHUB_OUTPUT + echo "total_prs=1" >> $GITHUB_OUTPUT + else + # Get all open, non-draft PRs targeting master (regardless of CI status) + PRS=$(gh pr list --base master --state open --json number,title,headRefName,isDraft --jq '.[] | select(.isDraft == false) | .number') + + if [ -z "$PRS" ]; then + echo "No open non-draft PRs found" + echo "branch=master" >> $GITHUB_OUTPUT + echo "included_prs=" >> $GITHUB_OUTPUT + echo "attempted_prs=" >> $GITHUB_OUTPUT + echo "total_prs=0" >> $GITHUB_OUTPUT + echo "has_prs=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "Found PRs: $PRS" + echo "total_prs=$(echo "$PRS" | wc -l)" >> $GITHUB_OUTPUT + echo "has_prs=true" >> $GITHUB_OUTPUT fi - echo "Found PRs: $PRS" - echo "total_prs=$(echo "$PRS" | wc -l)" >> $GITHUB_OUTPUT - echo "has_prs=true" >> $GITHUB_OUTPUT - # Create a new branch for aggregation starting from master AGGREGATE_BRANCH="staging-aggregate-$(date +%Y%m%d-%H%M%S)" git checkout master @@ -160,6 +195,7 @@ jobs: echo "✅ Aggregation completed" echo "📊 Summary:" + echo " - Mode: $( [ -n \"$SINGLE_PR\" ] && echo 'Single PR' || echo 'Aggregate' )" echo " - Total PRs found: $(echo "$PRS" | wc -l)" echo " - PRs attempted: $ATTEMPTED_PRS" echo " - PRs successfully merged: $INCLUDED_PRS" @@ -173,6 +209,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 +383,7 @@ jobs: deploy-staging: name: Deploy - Staging runs-on: ubuntu-22.04 + timeout-minutes: 10 # Prevent blocking the queue concurrency: group: staging permissions: @@ -353,7 +391,8 @@ jobs: pull-requests: write issues: write needs: [build, aggregate-prs] - if: always() && needs.build.result == 'success' + # Run on successful build OR on schedule (for monthly reports even when no PRs) + if: always() && (needs.build.result == 'success' || github.event_name == 'schedule') steps: - name: Checkout repository uses: actions/checkout@v4 @@ -361,6 +400,7 @@ jobs: token: ${{ secrets.STAGING_GITHUB_TOKEN }} - name: Download Artifact - Website + if: needs.build.result == 'success' uses: actions/download-artifact@v4 with: name: forrt-website-aggregate-${{ github.run_number }} @@ -416,6 +456,7 @@ jobs: echo "- ✅ Shows combined state of all compatible PRs" >> deployment-summary.md - name: Deploy - GitHub Pages + if: needs.build.result == 'success' uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e with: personal_token: ${{ secrets.STAGING_GITHUB_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f2cad98942..a9894e32e0f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,4 +91,66 @@ To edit it locally, you will then need to: 8. Then you can push this branch to GitHub. 9. Create a pull request to the original FORRT repo. -Please note that RStudio is not designed for website development, so you may find it easier to use the Dev Containers method described above. \ No newline at end of file +Please note that RStudio is not designed for website development, so you may find it easier to use the Dev Containers method described above. + +## 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 + +#### 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 + +#### 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