diff --git a/.github/workflows/build-push-greenhouse-pr-preview.yaml b/.github/workflows/build-push-greenhouse-pr-preview.yaml new file mode 100644 index 0000000000..a7e2a1092f --- /dev/null +++ b/.github/workflows/build-push-greenhouse-pr-preview.yaml @@ -0,0 +1,245 @@ +# Workflow Flow: +# Initial Setup: +# 1. Developer sets 'greenhouse-pr-build' label on PR +# 2. Action builds Docker image with tag pr-{number}-{sha} +# 3. Action sets 'greenhouse-pr-preview' label (ArgoCD deploys) +# +# On subsequent commits: +# 1. Developer pushes new commit +# 2. PR-Build label already exists +# 3. Action removes PR-Preview label (ArgoCD deletes the app) +# 4. Action builds new Docker image with new tag pr-{number}-{new-sha} +# 5. Action sets PR-Preview label back (ArgoCD redeploys after 2-3 mins) +# +# On PR close/merge: +# 1. PR is closed or merged +# 2. Action deletes all Docker images matching pr-{number}-* +# 3. Action removes PR-Preview label + +name: Build Greenhouse PR Preview 🔬 + +on: + pull_request: + types: [ labeled, synchronize, opened, reopened, closed ] + +# Ensure only one workflow runs per PR at a time +concurrency: + group: greenhouse-pr-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + IMAGE_NAME: "juno-app-greenhouse-pr-preview" + PACKAGE_PATH: "apps/greenhouse" + PR_BUILD_LABEL: "greenhouse-pr-build" + PR_PREVIEW_LABEL: "greenhouse-pr-preview" + +jobs: + build-and-push: + name: Build and Push PR Preview Image + # Skip if PR is closed or from a forked repository + # For labeled events, only build when the added label is the PR build label + if: | + github.event.action != 'closed' && + github.event.pull_request.head.repo.full_name == github.repository && + ( + github.event.action != 'labeled' || + github.event.label.name == env.PR_BUILD_LABEL + ) + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + issues: write + steps: + - name: Check for PR Build label + id: check-label + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const labels = context.payload.pull_request.labels.map(l => l.name); + const hasPrBuildLabel = labels.includes('${{ env.PR_BUILD_LABEL }}'); + const hasPrPreviewLabel = labels.includes('${{ env.PR_PREVIEW_LABEL }}'); + + console.log(`PR Build Label present: ${hasPrBuildLabel}`); + console.log(`PR Preview Label present: ${hasPrPreviewLabel}`); + console.log(`Event action: ${context.payload.action}`); + + core.setOutput('should-build', hasPrBuildLabel.toString()); + core.setOutput('has-preview-label', hasPrPreviewLabel.toString()); + + - name: Exit if PR Build label not present + if: steps.check-label.outputs.should-build != 'true' + run: | + echo "Skipping build - '${{ env.PR_BUILD_LABEL }}' label not present" + exit 0 + + - name: Remove PR Preview label on new commit + if: steps.check-label.outputs.should-build == 'true' && + steps.check-label.outputs.has-preview-label == 'true' && + github.event.action == 'synchronize' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + console.log('Removing PR Preview label - new commit detected'); + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + name: '${{ env.PR_PREVIEW_LABEL }}' + }); + console.log('PR Preview label removed successfully'); + } catch (error) { + if (error.status === 404) { + console.log('Label already removed or does not exist'); + } else { + throw error; + } + } + + - name: Checkout repository + if: steps.check-label.outputs.should-build == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Generate PR version tag + if: steps.check-label.outputs.should-build == 'true' + id: pr-version + run: | + PR_NUMBER=${{ github.event.pull_request.number }} + SHORT_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7) + PR_VERSION="pr-${PR_NUMBER}-${SHORT_SHA}" + echo "PR_VERSION=${PR_VERSION}" >> $GITHUB_OUTPUT + echo "Building version: ${PR_VERSION}" + + - name: Log into registry ${{ env.REGISTRY }} + if: steps.check-label.outputs.should-build == 'true' + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate Docker metadata + if: steps.check-label.outputs.should-build == 'true' + id: meta + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + with: + images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=${{ steps.pr-version.outputs.PR_VERSION }} + labels: | + org.opencontainers.image.description=PR Preview for Greenhouse Dashboard + org.opencontainers.image.title=Greenhouse-UI-PR-${{ github.event.pull_request.number }} + + - name: Build and push Docker image + if: steps.check-label.outputs.should-build == 'true' + id: build-image + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + file: ${{ env.PACKAGE_PATH }}/docker/Dockerfile + + - name: Add PR Preview label + if: steps.check-label.outputs.should-build == 'true' && success() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: ['${{ env.PR_PREVIEW_LABEL }}'] + }); + + cleanup: + name: Cleanup PR Preview Image + # Skip if PR is not closed or from a forked repository + if: github.event.action == 'closed' && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + issues: write + steps: + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Install crane + run: | + VERSION="v0.21.5" + OS="Linux" + ARCH="x86_64" + EXPECTED_SHA256="9f823ae5ee25803161110f957b5fd4538f714d40cdf25dacb4914fefafd246bf" + + curl -fsSL "https://github.com/google/go-containerregistry/releases/download/${VERSION}/go-containerregistry_${OS}_${ARCH}.tar.gz" -o crane.tar.gz + echo "${EXPECTED_SHA256} crane.tar.gz" | sha256sum -c - + tar -xzf crane.tar.gz crane + sudo install -m 0755 crane /usr/local/bin/crane + rm crane.tar.gz + + crane version + + - name: Delete PR preview images + run: | + PR_NUMBER=${{ github.event.pull_request.number }} + IMAGE_BASE="${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}" + + echo "Looking for images matching pr-${PR_NUMBER}-*" + + # List all tags and delete matching ones. Cleanup should be tolerant when + # no repository exists yet or when there are no matching PR tags. + TAGS="$(crane ls "${IMAGE_BASE}" 2>/dev/null || true)" + MATCHING_TAGS="$(printf '%s\n' "${TAGS}" | grep "^pr-${PR_NUMBER}-" || true)" + + if [ -z "${MATCHING_TAGS}" ]; then + echo "No preview images found for PR ${PR_NUMBER}" + else + DELETE_FAILED=0 + + while IFS= read -r tag; do + echo "Deleting ${IMAGE_BASE}:${tag}" + if ! crane delete "${IMAGE_BASE}:${tag}"; then + echo "Failed to delete ${IMAGE_BASE}:${tag}" + DELETE_FAILED=1 + fi + done <<< "${MATCHING_TAGS}" + + if [ "${DELETE_FAILED}" -ne 0 ]; then + echo "Cleanup failed for PR ${PR_NUMBER}: one or more preview images could not be deleted" + exit 1 + fi + fi + + echo "Cleanup completed for PR ${PR_NUMBER}" + + - name: Remove PR Preview label + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + name: '${{ env.PR_PREVIEW_LABEL }}' + }); + console.log('PR Preview label removed successfully'); + } catch (error) { + if (error.status === 404) { + console.log('Label does not exist or already removed'); + } else { + console.error('Error removing label:', error); + throw error; + } + } diff --git a/docs/greenhouse-pr-preview-workflow.md b/docs/greenhouse-pr-preview-workflow.md new file mode 100644 index 0000000000..8230fe9e1a --- /dev/null +++ b/docs/greenhouse-pr-preview-workflow.md @@ -0,0 +1,344 @@ +# Greenhouse PR Preview Workflow + +This document describes the GitHub Actions workflow for building and deploying PR preview environments for the Greenhouse application. + +## Overview + +The PR preview workflow automatically builds Docker images for pull requests and manages their deployment via ArgoCD label-based triggers. It provides ephemeral preview environments that are automatically created, updated, and cleaned up based on PR lifecycle events. + +## Workflow File + +`.github/workflows/build-push-greenhouse-pr-preview.yaml` + +## How It Works + +### Labels + +The workflow uses two labels to manage the preview lifecycle: + +- **`greenhouse-pr-build`**: Manual label added by developers to enable preview builds +- **`greenhouse-pr-preview`**: Automatically managed label that signals ArgoCD to deploy/undeploy + +### Workflow Flow + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ PR PREVIEW WORKFLOW │ +└─────────────────────────────────────────────────────────────────────────┘ + +INITIAL SETUP +───────────── +Developer Workflow ArgoCD + │ │ │ + ├──► Add "pr-build" │ │ + │ label │ │ + │ │ │ + │ ┌───▼────────────────────┐ │ + │ │ Build Docker image │ │ + │ │ pr-{number}-{sha} │ │ + │ └───┬────────────────────┘ │ + │ │ │ + │ ┌───▼────────────────────┐ │ + │ │ Push to registry │ │ + │ └───┬────────────────────┘ │ + │ │ │ + │ ┌───▼────────────────────┐ │ + │ │ Add "pr-preview" │ │ + │ │ label │ │ + │ └───┬────────────────────┘ │ + │ │ │ + │ ├──────────────────────► │ + │ │ Label detected ▼ + │ │ ┌──────────┐ + │ │ │ Deploy │ + │ │ │ Preview │ + │ │ └──────────┘ + + +NEW COMMIT (synchronize) +──────────────────────── +Developer Workflow ArgoCD + │ │ │ + ├──► Push commit │ │ + │ │ │ + │ ┌───▼────────────────────┐ │ + │ │ Check "pr-build" │ │ + │ │ label exists? │ │ + │ └───┬────────────────────┘ │ + │ │ │ + │ ┌───────┴────────┐ │ + │ │ │ │ + │ ✓ YES ✗ NO │ + │ │ │ │ + │ ┌───────────▼───────┐ ┌────▼────────┐ │ + │ │ Remove │ │ Skip build │ │ + │ │ "pr-preview" │ │ Exit │ │ + │ │ label │ └─────────────┘ │ + │ └───────────┬───────┘ │ │ + │ │ │ │ + │ ├─────────────────┼────────────► │ + │ │ Label removed │ No changes + │ │ │ (preview + │ ▼ │ stays) + │ ┌──────────┐ │ │ + │ │ Delete │ │ │ + │ │ Preview │ │ │ + │ └────┬─────┘ │ │ + │ │ │ │ + │ ┌──────────▼────────┐ │ │ + │ │ Build new image │ │ │ + │ │ pr-{number}- │ │ │ + │ │ {new-sha} │ │ │ + │ └──────────┬────────┘ │ │ + │ │ │ │ + │ ┌──────────▼────────┐ │ │ + │ │ Push to registry │ │ │ + │ └──────────┬────────┘ │ │ + │ │ │ │ + │ ┌──────────▼────────┐ │ │ + │ │ Add "pr-preview" │ │ │ + │ │ label back │ │ │ + │ └──────────┬────────┘ │ │ + │ │ │ │ + │ ├──────────────────┼────────────► │ + │ │ Label detected │ ▼ + │ │ │ ┌──────────┐ + │ │ (2-3 mins) │ │ Redeploy │ + │ │ │ │ Preview │ + │ │ │ └──────────┘ + + +PR CLOSE/MERGE +────────────── +Developer Workflow ArgoCD + │ │ │ + ├──► Close/Merge PR │ │ + │ │ │ + │ ┌───▼────────────────────┐ │ + │ │ Install crane │ │ + │ │ (with SHA256 check) │ │ + │ └───┬────────────────────┘ │ + │ │ │ + │ ┌───▼────────────────────┐ │ + │ │ Delete all images │ │ + │ │ pr-{number}-* │ │ + │ └───┬────────────────────┘ │ + │ │ │ + │ ┌───▼────────────────────┐ │ + │ │ Remove "pr-preview" │ │ + │ │ label (always runs) │ │ + │ └───┬────────────────────┘ │ + │ │ │ + │ ├──────────────────────► │ + │ │ Label removed ▼ + │ │ ┌──────────┐ + │ │ │ Delete │ + │ │ │ Preview │ + │ │ └──────────┘ +``` + +## Usage + +### Enabling PR Preview + +1. Create a pull request +2. Add the `greenhouse-pr-build` label to the PR +3. The workflow will automatically: + - Build a Docker image tagged as `pr-{number}-{sha}` + - Push the image to `ghcr.io/cloudoperators/juno-app-greenhouse-pr-preview` + - Add the `greenhouse-pr-preview` label + - ArgoCD will detect the label and deploy the preview + +**Note**: The workflow triggers on any PR regardless of which files are changed. The `greenhouse-pr-build` label is the only control for enabling preview builds. This allows you to create previews for changes to Greenhouse itself or its dependent plugins (heureka, doop, supernova) or shared packages. + +### Updating PR Preview + +1. Push new commits to the PR +2. The workflow will automatically: + - Remove the `greenhouse-pr-preview` label (ArgoCD deletes the preview) + - Build a new Docker image with the new commit SHA + - Push the new image to the registry + - Re-add the `greenhouse-pr-preview` label (ArgoCD redeploys after 2-3 minutes) + +### Disabling PR Preview + +To stop building previews without closing the PR: + +1. Remove the `greenhouse-pr-build` label +2. Future commits will skip the build +3. The existing preview deployment will remain active (the `greenhouse-pr-preview` label stays) + +### Cleaning Up + +When the PR is closed or merged: + +1. The workflow automatically: + - Deletes all Docker images matching `pr-{number}-*` + - Removes the `greenhouse-pr-preview` label (even if image deletion fails) + - ArgoCD detects the label removal and deletes the preview deployment + +## Docker Images + +- **Registry**: `ghcr.io` +- **Image Name**: `juno-app-greenhouse-pr-preview` +- **Tag Format**: `pr-{PR_NUMBER}-{SHORT_SHA}` +- **Example**: `ghcr.io/cloudoperators/juno-app-greenhouse-pr-preview:pr-123-a1b2c3d` + +## Trigger Events + +The workflow triggers on the following pull request events: + +- `labeled` - When any label is added +- `synchronize` - When new commits are pushed +- `opened` - When a PR is created +- `reopened` - When a closed PR is reopened +- `closed` - When a PR is closed or merged + +## Jobs + +### build-and-push + +Builds and pushes the Docker image when the `greenhouse-pr-build` label is present. + +**Conditions:** + +- PR is not closed +- PR is from the same repository (not a fork) +- Event is not adding the `greenhouse-pr-preview` label (avoids self-triggering) + +**Steps:** + +1. Check for `greenhouse-pr-build` label +2. Remove `greenhouse-pr-preview` label (if new commit and label exists) +3. Checkout code at PR HEAD SHA +4. Generate version tag `pr-{number}-{sha}` +5. Login to GitHub Container Registry +6. Generate Docker metadata +7. Build and push Docker image +8. Add `greenhouse-pr-preview` label on success + +### cleanup + +Deletes Docker images and removes labels when the PR is closed. + +**Conditions:** + +- PR action is `closed` +- PR is from the same repository (not a fork) + +**Steps:** + +1. Login to GitHub Container Registry +2. Install crane (with SHA256 checksum verification) +3. List all tags and delete matching `pr-{number}-*` images +4. Remove `greenhouse-pr-preview` label (runs even if deletion fails) + +## Safety Features + +### Fork Protection + +The workflow skips execution for pull requests from forked repositories. This prevents: + +- Permission errors (forks don't have `packages:write` or `issues:write`) +- Wasted CI minutes +- Potential security issues + +### Concurrency Control + +Only one workflow run executes per PR at a time: + +```yaml +concurrency: + group: greenhouse-pr-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true +``` + +This prevents: + +- Race conditions on label removal/addition +- Multiple simultaneous builds for the same PR +- Incorrect deployment states + +### Self-Trigger Prevention + +The workflow skips execution when the `greenhouse-pr-preview` label is added to prevent triggering itself in a loop. + +### Supply Chain Security + +The crane binary is installed with SHA256 checksum verification: + +```bash +EXPECTED_SHA256="9f823ae5ee25803161110f957b5fd4538f714d40cdf25dacb4914fefafd246bf" +curl -fsSL "https://github.com/google/go-containerregistry/releases/download/v0.21.5/go-containerregistry_Linux_x86_64.tar.gz" -o crane.tar.gz +echo "${EXPECTED_SHA256} crane.tar.gz" | sha256sum -c - +``` + +### Resilient Cleanup + +The label removal step runs with `if: always()` to ensure the preview is torn down even if image deletion fails. This prevents orphaned ArgoCD deployments. + +## Permissions + +Both jobs require the following permissions: + +- `contents: read` - Read repository code +- `packages: write` - Push/delete container images +- `issues: write` - Add/remove labels on PRs + +## Configuration + +### Environment Variables + +```yaml +REGISTRY: ghcr.io +IMAGE_NAME: "juno-app-greenhouse-pr-preview" +PACKAGE_PATH: "apps/greenhouse" +PR_BUILD_LABEL: "greenhouse-pr-build" +PR_PREVIEW_LABEL: "greenhouse-pr-preview" +``` + +### Path Filters + +The workflow only triggers when files under `apps/greenhouse/**` are modified. + +## Troubleshooting + +### Preview Not Deploying + +1. Check that the `greenhouse-pr-preview` label is present on the PR +2. Verify the Docker image was pushed successfully to the registry +3. Check ArgoCD for deployment status + +### Build Failing + +1. Check workflow logs in the Actions tab +2. Verify the `greenhouse-pr-build` label is present +3. Ensure the PR is not from a forked repository + +### Images Not Cleaning Up + +1. Check the cleanup job logs +2. Verify the crane installation succeeded +3. Check that the `greenhouse-pr-preview` label was removed +4. The workflow will still remove the label even if image deletion fails + +### Stale Preview + +If the `greenhouse-pr-build` label is removed but the preview stays: + +- The preview will remain deployed (by design) +- No new builds will occur on future commits +- Close the PR to trigger cleanup + +## Best Practices + +1. **Add the label early**: Add `greenhouse-pr-build` when you open the PR if you want immediate preview +2. **Remove when not needed**: Remove `greenhouse-pr-build` to save CI resources if you don't need updates +3. **Wait for redeployment**: After pushing new commits, allow 2-3 minutes for ArgoCD to redeploy +4. **Check workflow status**: Monitor the Actions tab for build/deployment status + +## Related Documentation + +- [ArgoCD Label-Based Deployment](https://argo-cd.readthedocs.io/) +- [GitHub Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry) +- [Crane CLI](https://github.com/google/go-containerregistry/blob/main/cmd/crane/doc/crane.md)