feat(editor): Resolve $parameter[...] in UI-only context & add Docker build workflow
#17
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "Test & (optionally) Build Docker Image" | ||
| on: | ||
| pull_request: | ||
| branches: [ master ] | ||
| types: [opened, synchronize, reopened, closed] | ||
| workflow_dispatch: | ||
| inputs: | ||
| build_type: | ||
| description: 'Type of build' | ||
| required: true | ||
| default: 'test' | ||
| type: choice | ||
| options: [ test, full ] | ||
| schedule: | ||
| - cron: "0 3 * * 0" # Weekly at 03:00 UTC | ||
| permissions: | ||
| contents: read | ||
| packages: delete | ||
| jobs: | ||
| test: | ||
| if: github.event.action != 'closed' | ||
| name: Lint, Typecheck & Unit Tests | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 60 | ||
| env: | ||
| CI: true | ||
| PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # actions/[email protected] | ||
| - name: Setup pnpm | ||
| uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # pnpm/action-setup@v4 | ||
| with: | ||
| version: 10.12.1 | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # actions/[email protected] | ||
| with: | ||
| node-version: '22.x' | ||
| cache: pnpm | ||
| - name: Install dependencies | ||
| run: pnpm install --frozen-lockfile | ||
| - name: Lint | ||
| run: pnpm -w lint | ||
| - name: Typecheck | ||
| run: pnpm -w typecheck | ||
| - name: Show workspace packages | ||
| run: pnpm -w list --depth -1 | ||
| - name: Run editor-ui unit tests | ||
| run: pnpm -w test --filter n8n-editor-ui -- --run --reporter=dot | ||
| build-image: | ||
| name: Build & Push Docker Image | ||
| runs-on: ubuntu-latest | ||
| needs: test | ||
| if: > | ||
| (github.event_name != 'pull_request' || github.event.action != 'closed') && | ||
| (github.event_name == 'pull_request' || github.event.inputs.build_type == 'full') | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # actions/[email protected] | ||
| - name: Set up pnpm | ||
| uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # pnpm/action-setup@v4 | ||
| with: | ||
| version: 10.12.1 | ||
| - name: Set up Node.js | ||
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # actions/[email protected] | ||
| with: | ||
| node-version: '22.x' | ||
| cache: pnpm | ||
| - name: Install dependencies | ||
| run: pnpm install --frozen-lockfile | ||
| - name: Build n8n | ||
| run: pnpm build:n8n | ||
| - name: Set up Docker Buildx | ||
| uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # docker/[email protected] | ||
| - name: Log in to GitHub Container Registry | ||
| uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # docker/[email protected] | ||
| with: | ||
| registry: ghcr.io | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
| - name: Extract metadata (unique tags incl. PR/branch) | ||
| id: meta | ||
| uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # docker/[email protected] | ||
| with: | ||
| images: ghcr.io/${{ github.repository_owner }}/n8n | ||
| tags: | | ||
| type=raw,value=test-build | ||
| type=raw,value=latest,enable={{is_default_branch}} | ||
| type=ref,event=pr,prefix=pr- | ||
| type=ref,event=branch | ||
| type=sha,format=short,prefix={{branch}}- | ||
| - name: Compute primary test tag | ||
| id: picktag | ||
| run: | | ||
| if [ "${{ github.event_name }}" = "pull_request" ]; then | ||
| echo "tag=pr-${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "tag=test-build" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| - name: Build and push Docker image | ||
| uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # docker/[email protected] | ||
| with: | ||
| context: . | ||
| file: ./docker/images/n8n/Dockerfile | ||
| tags: ${{ steps.meta.outputs.tags }} | ||
| labels: ${{ steps.meta.outputs.labels }} | ||
| push: true | ||
| load: false | ||
| cache-from: type=gha | ||
| cache-to: type=gha,mode=max | ||
| provenance: false | ||
| - name: Smoke-test container (strict health check + logs) | ||
| shell: bash | ||
| run: | | ||
| set -Eeuo pipefail | ||
| IMAGE="ghcr.io/${{ github.repository_owner }}/n8n:${{ steps.picktag.outputs.tag }}" | ||
| CNAME="n8n-test-${GITHUB_RUN_ID}" | ||
| echo "::group::Pull image" | ||
| docker pull "$IMAGE" | ||
| echo "::endgroup::" | ||
| echo "::group::Sanity: version/help" | ||
| docker run --rm "$IMAGE" --version | ||
| docker run --rm "$IMAGE" --help >/dev/null | ||
| echo "::endgroup::" | ||
| echo "Starting $CNAME..." | ||
| docker run -d --name "$CNAME" -p 5678:5678 "$IMAGE" > /dev/null | ||
| # Always try to clean up; never fail the job because cleanup failed | ||
| cleanup() { docker rm -f "$CNAME" >/dev/null 2>&1 || true; } | ||
| trap cleanup EXIT | ||
| echo "Waiting up to 120s for /healthz..." | ||
| for i in {1..60}; do | ||
| if curl -fsS http://localhost:5678/healthz >/dev/null; then | ||
| echo "✅ Healthy after $((i*2))s" | ||
| break | ||
| fi | ||
| sleep 2 | ||
| done | ||
| # Hard fail if still not healthy; show logs to make it actionable | ||
| if ! curl -fsS http://localhost:5678/healthz >/dev/null; then | ||
| echo "❌ Health check failed. Recent logs:" | ||
| docker logs --tail 200 --timestamps "$CNAME" || true | ||
| echo "Inspect state:" | ||
| docker inspect "$CNAME" || true | ||
| exit 1 | ||
| fi | ||
| echo "✅ Container passed health check." | ||
| - name: Show image info and pull instructions | ||
| run: | | ||
| echo "🎉 Docker image successfully built and pushed!" | ||
| echo "" | ||
| echo "📦 Useful tags:" | ||
| echo " - ghcr.io/${{ github.repository_owner }}/n8n:${{ steps.picktag.outputs.tag }}" | ||
| echo " - ghcr.io/${{ github.repository_owner }}/n8n:test-build" | ||
| echo " - ghcr.io/${{ github.repository_owner }}/n8n:latest" | ||
| echo " - ghcr.io/${{ github.repository_owner }}/n8n:master-${{ github.sha }}" | ||
| echo "" | ||
| echo "🚀 Pull & run:" | ||
| echo " docker pull ghcr.io/${{ github.repository_owner }}/n8n:${{ steps.picktag.outputs.tag }}" | ||
| echo " docker run -p 5678:5678 ghcr.io/${{ github.repository_owner }}/n8n:${{ steps.picktag.outputs.tag }}" | ||
| cleanup-pr: | ||
| name: Cleanup PR Image | ||
| runs-on: ubuntu-latest | ||
| if: github.event_name == 'pull_request' && github.event.action == 'closed' | ||
| steps: | ||
| - name: Delete GHCR image tag for PR | ||
| env: | ||
| GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| TAG="pr-${{ github.event.pull_request.number }}" | ||
| IMAGE="${{ github.repository_owner }}/n8n" | ||
| echo "Fetching manifest digest for $IMAGE:$TAG from GHCR..." | ||
| DIGEST=$(curl -sI \ | ||
| -H "Accept: application/vnd.oci.image.manifest.v1+json" \ | ||
| -H "Authorization: Bearer $GHCR_TOKEN" \ | ||
| "https://ghcr.io/v2/${IMAGE}/manifests/${TAG}" \ | ||
| | grep -i 'docker-content-digest:' | awk '{print $2}' | tr -d $'\r') | ||
| if [ -z "$DIGEST" ] || [ "$DIGEST" = "null" ]; then | ||
| echo "❌ Could not find digest for tag $TAG. Tag may not exist." | ||
| exit 0 | ||
| fi | ||
| echo "Found digest: $DIGEST" | ||
| echo "Deleting $IMAGE@$DIGEST..." | ||
| curl -s -X DELETE \ | ||
| -H "Authorization: Bearer $GHCR_TOKEN" \ | ||
| "https://ghcr.io/v2/${IMAGE}/manifests/${DIGEST}" && \ | ||
| echo "✅ Deleted $IMAGE:$TAG successfully." | ||
| cleanup-orphans: | ||
| name: Scheduled Cleanup of Orphaned PR Images | ||
| runs-on: ubuntu-latest | ||
| if: github.event_name == 'schedule' | ||
| steps: | ||
| - name: List and delete orphaned PR tags from GHCR | ||
| env: | ||
| GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| IMAGE="${{ github.repository_owner }}/n8n" | ||
| echo "Fetching tags for $IMAGE..." | ||
| TAGS=$(curl -s \ | ||
| -H "Authorization: Bearer $GHCR_TOKEN" \ | ||
| "https://ghcr.io/v2/${IMAGE}/tags/list" | jq -r '.tags[]' | grep '^pr-' || true) | ||
| if [ -z "$TAGS" ]; then | ||
| echo "No orphaned pr-* tags found." | ||
| exit 0 | ||
| fi | ||
| echo "Found tags:" | ||
| echo "$TAGS" | ||
| for TAG in $TAGS; do | ||
| echo "Fetching digest for $TAG..." | ||
| DIGEST=$(curl -sI \ | ||
| -H "Accept: application/vnd.oci.image.manifest.v1+json" \ | ||
| -H "Authorization: Bearer $GHCR_TOKEN" \ | ||
| "https://ghcr.io/v2/${IMAGE}/manifests/${TAG}" \ | ||
| | grep -i 'docker-content-digest:' | awk '{print $2}' | tr -d $'\r') | ||
| if [ -n "$DIGEST" ] && [ "$DIGEST" != "null" ]; then | ||
| echo "Deleting $IMAGE@$DIGEST..." | ||
| curl -s -X DELETE \ | ||
| -H "Authorization: Bearer $GHCR_TOKEN" \ | ||
| "https://ghcr.io/v2/${IMAGE}/manifests/${DIGEST}" && \ | ||
| echo "✅ Deleted $IMAGE:$TAG" | ||
| else | ||
| echo "Skipping $TAG — digest not found." | ||
| fi | ||
| done | ||