Mcp over mqtt #9776
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: Create pre-staging environment | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: 'Pull request number' | |
| required: true | |
| driver_k8s_branch: | |
| description: 'flowfuse/driver-k8s branch name' | |
| required: true | |
| default: 'main' | |
| nr_launcher_branch: | |
| description: 'flowfuse/nr-launcher branch name' | |
| required: true | |
| default: 'main' | |
| nr_project_nodes_branch: | |
| description: 'flowfuse/nr-project-nodes branch name' | |
| required: true | |
| default: 'main' | |
| nr_file_nodes_branch: | |
| description: 'flowfuse/nr-file-nodes branch name' | |
| required: true | |
| default: 'main' | |
| nr_assistant_branch: | |
| description: 'flowfuse/nr-assistant branch name' | |
| required: true | |
| default: 'main' | |
| nr_tables_nodes_branch: | |
| description: 'flowfuse/nr-tables-nodes branch name' | |
| required: true | |
| default: 'main' | |
| nr_mqtt_nodes_branch: | |
| description: 'flowfuse/nr-mqtt-nodes branch name' | |
| required: true | |
| default: 'main' | |
| nr_subflow_export_branch: | |
| description: 'flowfuse/nr-subflow-export branch name' | |
| required: true | |
| default: 'main' | |
| pull_request: | |
| types: | |
| - opened | |
| - synchronize | |
| - reopened | |
| - closed | |
| paths-ignore: | |
| - 'docs/**' | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.number || inputs.pr_number }} | |
| cancel-in-progress: false | |
| env: | |
| AWS_ECR_PUSH_ROLE: 'ECR_push_pull_images' | |
| AWS_EKS_DEPLOY_ROLE: 'EKSDeploymentRole' | |
| jobs: | |
| # This job validates if the user, who triggered the workflow, is a member of the organization | |
| # Note: Any workflow re-runs will use the privileges of github.actor, even if the actor initiating the re-run (github.triggering_actor) has different privileges. | |
| validate-user: | |
| name: Validate trigger author | |
| runs-on: ubuntu-latest | |
| if: | | |
| (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && | |
| github.actor != 'dependabot[bot]' && | |
| !startsWith(github.head_ref, 'release-') | |
| outputs: | |
| is_org_member: ${{ steps.validate.outputs.is_member }} | |
| steps: | |
| - name: Generate a token | |
| id: generate_token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| app-id: ${{ secrets.GH_BOT_APP_ID }} | |
| private-key: ${{ secrets.GH_BOT_APP_KEY }} | |
| - name: Validate | |
| id: validate | |
| run: | | |
| if [ "${{ github.actor }}" == 'dependabot[bot]' ]; then | |
| echo "is_member=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| member_status=$(gh api orgs/flowfuse/memberships/${{ github.actor }} -q '.state') | |
| if [ "${member_status}" == "active" ]; then | |
| echo "is_member=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "is_member=false" >> $GITHUB_OUTPUT | |
| fi | |
| env: | |
| GH_TOKEN: ${{ steps.generate_token.outputs.token }} | |
| publish_k8s_driver: | |
| name: Build and publish kubernetes driver | |
| needs: validate-user | |
| if: | | |
| needs.validate-user.outputs.is_org_member == 'true' && | |
| github.event_name == 'workflow_dispatch' && | |
| inputs.driver_k8s_branch != 'main' | |
| uses: 'flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml@publish_node_package/v1' | |
| with: | |
| package_name: driver-k8s | |
| publish_package: true | |
| repository_name: 'FlowFuse/driver-k8s' | |
| branch_name: ${{ inputs.driver_k8s_branch }} | |
| release_name: "pre-staging-${{ inputs.driver_k8s_branch }}" | |
| secrets: | |
| npm_registry_token: ${{ secrets.NPM_PUBLISH_TOKEN }} | |
| publish_nr_project_nodes: | |
| name: Build and publish nr-project-nodes package | |
| needs: validate-user | |
| if: | | |
| needs.validate-user.outputs.is_org_member == 'true' && | |
| github.event_name == 'workflow_dispatch' && | |
| inputs.nr_project_nodes_branch != 'main' | |
| uses: 'flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml@publish_node_package/v1' | |
| with: | |
| package_name: nr-project-nodes | |
| publish_package: true | |
| repository_name: 'FlowFuse/nr-project-nodes' | |
| branch_name: ${{ inputs.nr_project_nodes_branch }} | |
| release_name: "pre-staging-${{ inputs.nr_project_nodes_branch }}" | |
| secrets: | |
| npm_registry_token: ${{ secrets.NPM_PUBLISH_TOKEN }} | |
| publish_nr_file_nodes: | |
| name: Build and publish nr-file-nodes package | |
| needs: validate-user | |
| if: | | |
| needs.validate-user.outputs.is_org_member == 'true' && | |
| github.event_name == 'workflow_dispatch' && | |
| inputs.nr_file_nodes_branch != 'main' | |
| uses: 'flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml@publish_node_package/v1' | |
| with: | |
| package_name: nr-file-nodes | |
| publish_package: true | |
| repository_name: 'FlowFuse/nr-file-nodes' | |
| branch_name: ${{ inputs.nr_file_nodes_branch }} | |
| release_name: "pre-staging-${{ inputs.nr_file_nodes_branch }}" | |
| secrets: | |
| npm_registry_token: ${{ secrets.NPM_PUBLISH_TOKEN }} | |
| publish_nr_assistant: | |
| name: Build and publish nr-assistant package | |
| needs: validate-user | |
| if: | | |
| needs.validate-user.outputs.is_org_member == 'true' && | |
| github.event_name == 'workflow_dispatch' && | |
| inputs.nr_assistant_branch != 'main' | |
| uses: 'flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml@publish_node_package/v1' | |
| with: | |
| package_name: nr-assistant | |
| publish_package: true | |
| repository_name: 'FlowFuse/nr-assistant' | |
| branch_name: ${{ inputs.nr_assistant_branch }} | |
| release_name: "pre-staging-${{ inputs.nr_assistant_branch }}" | |
| secrets: | |
| npm_registry_token: ${{ secrets.NPM_PUBLISH_TOKEN }} | |
| publish_nr_tables_nodes: | |
| name: Build and publish nr-tables-nodes package | |
| needs: validate-user | |
| if: | | |
| needs.validate-user.outputs.is_org_member == 'true' && | |
| github.event_name == 'workflow_dispatch' && | |
| inputs.nr_tables_nodes_branch != 'main' | |
| uses: 'flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml@publish_node_package/v1' | |
| with: | |
| package_name: nr-tables-nodes | |
| publish_package: true | |
| repository_name: 'FlowFuse/nr-tables-nodes' | |
| branch_name: ${{ inputs.nr_tables_nodes_branch }} | |
| release_name: "pre-staging-${{ inputs.nr_tables_nodes_branch }}" | |
| secrets: | |
| npm_registry_token: ${{ secrets.NPM_PUBLISH_TOKEN }} | |
| publish_nr_subflow_export: | |
| name: Build and publish nr-subflow-export package | |
| needs: validate-user | |
| if: | | |
| needs.validate-user.outputs.is_org_member == 'true' && | |
| github.event_name == 'workflow_dispatch' && | |
| inputs.nr_subflow_export_branch != 'main' | |
| uses: 'flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml@publish_node_package/v1' | |
| with: | |
| package_name: nr-subflow-export | |
| publish_package: true | |
| repository_name: 'FlowFuse/nr-subflow-export' | |
| branch_name: ${{ inputs.nr_subflow_export_branch }} | |
| release_name: "pre-staging-${{ inputs.nr_subflow_export_branch }}" | |
| secrets: | |
| npm_registry_token: ${{ secrets.NPM_PUBLISH_TOKEN }} | |
| publish_nr_mqtt_nodes: | |
| name: Build and publish nr-mqtt-nodes package | |
| needs: validate-user | |
| if: | | |
| needs.validate-user.outputs.is_org_member == 'true' && | |
| github.event_name == 'workflow_dispatch' && | |
| inputs.nr_mqtt_nodes_branch != 'main' | |
| uses: 'flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml@publish_node_package/v1' | |
| with: | |
| package_name: nr-mqtt-nodes | |
| publish_package: true | |
| repository_name: 'FlowFuse/nr-mqtt-nodes' | |
| branch_name: ${{ inputs.nr_mqtt_nodes_branch }} | |
| release_name: "pre-staging-${{ inputs.nr_mqtt_nodes_branch }}" | |
| secrets: | |
| npm_registry_token: ${{ secrets.NPM_PUBLISH_TOKEN }} | |
| publish_nr_launcher: | |
| name: Build and publish nr-launcher package | |
| needs: | |
| - validate-user | |
| - publish_nr_project_nodes | |
| - publish_nr_file_nodes | |
| - publish_nr_assistant | |
| - publish_nr_tables_nodes | |
| - publish_nr_mqtt_nodes | |
| - publish_nr_subflow_export | |
| if: | | |
| needs.validate-user.outputs.is_org_member == 'true' && | |
| github.event_name == 'workflow_dispatch' && | |
| (always() && inputs.nr_launcher_branch != 'main') || | |
| needs.publish_nr_project_nodes.result == 'success' || | |
| needs.publish_nr_file_nodes.result == 'success' || | |
| needs.publish_nr_assistant.result == 'success' || | |
| needs.publish_nr_tables_nodes.result == 'success' || | |
| needs.publish_nr_mqtt_nodes.result == 'success' || | |
| needs.publish_nr_subflow_export.result == 'success' | |
| uses: 'flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml@publish_node_package/v1' | |
| with: | |
| package_name: flowfuse-nr-launcher | |
| publish_package: true | |
| repository_name: 'FlowFuse/nr-launcher' | |
| branch_name: ${{ inputs.nr_launcher_branch }} | |
| release_name: "pre-staging-${{ inputs.nr_launcher_branch == 'main' && github.sha || inputs.nr_launcher_branch }}" | |
| package_dependencies: | | |
| @flowfuse/nr-project-nodes=${{ inputs.nr_project_nodes_branch != 'main' && needs.publish_nr_project_nodes.outputs.release_name || 'nightly' }} | |
| @flowfuse/nr-file-nodes=${{ inputs.nr_file_nodes_branch != 'main' && needs.publish_nr_file_nodes.outputs.release_name || 'nightly' }} | |
| @flowfuse/nr-assistant=${{ inputs.nr_assistant_branch != 'main' && needs.publish_nr_assistant.outputs.release_name || 'nightly' }} | |
| @flowfuse/nr-tables-nodes=${{ inputs.nr_tables_nodes_branch != 'main' && needs.publish_nr_tables_nodes.outputs.release_name || 'nightly' }} | |
| @flowfuse/nr-mqtt-nodes=${{ inputs.nr_mqtt_nodes_branch != 'main' && needs.publish_nr_mqtt_nodes.outputs.release_name || 'nightly' }} | |
| @flowfuse/nr-subflow-export=${{ inputs.nr_subflow_export_branch != 'main' && needs.publish_nr_subflow_export.outputs.release_name || 'nightly' }} | |
| secrets: | |
| npm_registry_token: ${{ secrets.NPM_PUBLISH_TOKEN }} | |
| build-node-red: | |
| name: Build Node-RED 4.1.x container images | |
| needs: | |
| - validate-user | |
| - publish_nr_launcher | |
| if: | | |
| needs.validate-user.outputs.is_org_member == 'true' && | |
| github.event_name == 'workflow_dispatch' && | |
| (always() && needs.publish_nr_launcher.result == 'success') | |
| uses: flowfuse/github-actions-workflows/.github/workflows/build_container_image.yml@build_container_image/v1 | |
| with: | |
| image_name: 'node-red' | |
| dockerfile_path: Dockerfile | |
| image_tag_prefix: '4.1.x-' | |
| build_context: './ci/node-red' | |
| build_arguments: | | |
| BUILD_TAG=${{ needs.publish_nr_launcher.outputs.release_name }} | |
| build_platform: "linux/arm64" | |
| npm_registry_url: ${{ vars.PUBLIC_NPM_REGISTRY_URL }} | |
| secrets: | |
| temporary_registry_token: ${{ secrets.GITHUB_TOKEN }} | |
| upload-node-red: | |
| name: Publish Node-RED 4.1.x container images | |
| needs: | |
| - validate-user | |
| - build-node-red | |
| if: | | |
| needs.validate-user.outputs.is_org_member == 'true' && | |
| github.event_name == 'workflow_dispatch' && | |
| (always() && needs.build-node-red.result == 'success') | |
| runs-on: ubuntu-latest | |
| environment: staging | |
| env: | |
| IMAGE_NAME: 'node-red' | |
| PR_NUMBER: ${{ github.event.number == '' && inputs.pr_number || github.event.number }} | |
| outputs: | |
| nr_custom_image_tag: ${{ steps.set_outputs.outputs.nr_image_tag }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| packages: read | |
| steps: | |
| - name: Set variables | |
| run: | | |
| echo "tagged_image=${{ env.IMAGE_NAME }}:4.1.x-pr-${{ env.PR_NUMBER }}" >> $GITHUB_ENV | |
| echo "timestamp=$(date +%s)" >> $GITHUB_ENV | |
| - name: Setup Docker buildx | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| - name: Configure AWS credentials for ECR interaction | |
| id: aws-config | |
| uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1 | |
| with: | |
| role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ env.AWS_ECR_PUSH_ROLE }} | |
| role-session-name: GithubActionsRoleSession | |
| role-duration-seconds: 900 | |
| aws-region: eu-west-1 | |
| mask-aws-account-id: true | |
| - name: Login to AWS ECR | |
| id: login-ecr | |
| uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2.1.6 | |
| with: | |
| mask-password: true | |
| - name: Login to GHCR | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Push to ECR | |
| id: image-push | |
| run: | | |
| docker pull ${{ needs.build-node-red.outputs.image }} | |
| docker tag ${{ needs.build-node-red.outputs.image }} ${{ steps.aws-config.outputs.aws-account-id }}.dkr.ecr.eu-west-1.amazonaws.com/flowforge/${{ env.tagged_image }}-${{ env.timestamp }} | |
| docker push ${{ steps.aws-config.outputs.aws-account-id }}.dkr.ecr.eu-west-1.amazonaws.com/flowforge/${{ env.tagged_image }}-${{ env.timestamp }} | |
| - name: Set outputs | |
| id: set_outputs | |
| run: | | |
| echo "nr_image_tag=${{ env.tagged_image }}-${{ env.timestamp }}" >> $GITHUB_OUTPUT | |
| build: | |
| name: Build and contenerize | |
| needs: | |
| - validate-user | |
| - publish_k8s_driver | |
| if: | | |
| needs.validate-user.outputs.is_org_member == 'true' && | |
| (always() && needs.publish_k8s_driver.result != 'failure') && | |
| (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') && | |
| github.event.action != 'closed' | |
| runs-on: ubuntu-latest | |
| env: | |
| IMAGE_NAME: 'forge-k8s' | |
| PR_NUMBER: ${{ github.event.number == '' && inputs.pr_number || github.event.number }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Set variables | |
| run: | | |
| echo "tagged_image=${{ env.IMAGE_NAME }}:pr-${{ env.PR_NUMBER}}" >> $GITHUB_ENV | |
| - name: Setup QEMU | |
| uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 | |
| - name: Setup Docker buildx | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| - name: Set build-args if branch is not main | |
| id: set-build-args | |
| run: | | |
| { | |
| echo "BUILD_ARGS<<EOF" | |
| # CACHEBUST forces a fresh fetch of the device-cache script (helm main branch) | |
| echo "CACHEBUST=${{ github.run_id }}-${{ github.run_attempt }}" | |
| if [ "${{ inputs.driver_k8s_branch }}" != "" ]; then | |
| echo "KUBERNETES_DRIVER_TAG=pre-staging-${{ inputs.driver_k8s_branch }}" | |
| fi | |
| echo "EOF" | |
| } >> $GITHUB_ENV | |
| - name: Build container image | |
| id: build | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: . | |
| file: "./ci/Dockerfile" | |
| tags: ${{ env.tagged_image }} | |
| push: false | |
| outputs: type=docker,dest=/tmp/k8s-forge.tar | |
| build-args: ${{ env.BUILD_ARGS }} | |
| env: | |
| DOCKER_BUILD_SUMMARY: false | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: k8s-forge | |
| path: /tmp/k8s-forge.tar | |
| retention-days: 7 | |
| deploy: | |
| name: Deploy application | |
| if: | | |
| always() && | |
| needs.build.result == 'success' && | |
| (needs.publish_k8s_driver.result != 'failure' || needs.upload-node-red.result != 'failure' ) && | |
| (( github.event_name == 'pull_request' && github.event.action != 'closed' ) || | |
| ( github.event_name == 'workflow_dispatch' && github.event.action != 'closed' )) | |
| runs-on: ubuntu-latest | |
| environment: staging | |
| needs: | |
| - validate-user | |
| - publish_k8s_driver | |
| - upload-node-red | |
| - build | |
| env: | |
| IMAGE_NAME: 'forge-k8s' | |
| PR_NUMBER: ${{ github.event.number == '' && inputs.pr_number || github.event.number }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Set variables | |
| run: | | |
| echo "tagged_image=${{ env.IMAGE_NAME }}:pr-${{ env.PR_NUMBER}}" >> $GITHUB_ENV | |
| echo "timestamp=$(date +%s)" >> $GITHUB_ENV | |
| - name: Download artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: k8s-forge | |
| path: /tmp | |
| - name: Load image | |
| run: | | |
| docker load --input /tmp/k8s-forge.tar | |
| docker image ls -a | |
| - name: Delete artifact | |
| uses: geekyeggo/delete-artifact@176a747ab7e287e3ff4787bf8a148716375ca118 # v6.0.0 | |
| with: | |
| name: k8s-forge | |
| failOnError: false | |
| - name: Configure AWS credentials for ECR interaction | |
| id: aws-config | |
| uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1 | |
| with: | |
| role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ env.AWS_ECR_PUSH_ROLE }} | |
| role-session-name: GithubActionsRoleSession | |
| role-duration-seconds: 900 | |
| aws-region: eu-west-1 | |
| mask-aws-account-id: true | |
| - name: Login to AWS ECR | |
| id: login-ecr | |
| uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2.1.6 | |
| with: | |
| mask-password: true | |
| - name: Push to ECR | |
| run: | | |
| docker tag ${{ env.tagged_image }} ${{ steps.aws-config.outputs.aws-account-id }}.dkr.ecr.eu-west-1.amazonaws.com/flowforge/${{ env.tagged_image }}-${{ env.timestamp }} | |
| docker push ${{ steps.aws-config.outputs.aws-account-id }}.dkr.ecr.eu-west-1.amazonaws.com/flowforge/${{ env.tagged_image }}-${{ env.timestamp }} | |
| - name: Configure AWS credentials for EKS interaction | |
| uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1 | |
| with: | |
| role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ env.AWS_EKS_DEPLOY_ROLE }} | |
| role-session-name: GithubActionsRoleSessio | |
| role-duration-seconds: 900 | |
| aws-region: eu-west-1 | |
| - name: Configure kubeconfig | |
| run: | | |
| aws eks update-kubeconfig --region eu-west-1 --name ${{ secrets.EKS_CLUSTER_NAME }} | |
| - name: Install 1Password CLI | |
| uses: 1password/install-cli-action@a5215d3a7f75c1629216c465ea9ab3ab399c4b71 # v4.0.0 | |
| with: | |
| version: 2.25.0 | |
| - name: Check out FlowFuse/helm repository (to access latest helm chart) | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| repository: 'FlowFuse/helm' | |
| ref: 'main' | |
| path: 'helm-repo' | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Check if deployment exists | |
| id: check-initial-setup | |
| run: | | |
| if helm status --namespace "pr-${{ env.PR_NUMBER }}" flowfuse-pr-${{ env.PR_NUMBER }} &> /dev/null; then | |
| echo "initialSetup=false" >> $GITHUB_ENV | |
| else | |
| echo "initialSetup=true" >> $GITHUB_ENV | |
| fi | |
| - name: Deploy | |
| env: | |
| OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} | |
| run: | | |
| helm upgrade --install \ | |
| --create-namespace \ | |
| --namespace "pr-${{ env.PR_NUMBER }}" \ | |
| --timeout 300s \ | |
| --wait \ | |
| --atomic \ | |
| --values ci/ci-values.yaml \ | |
| --set forge.image=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.eu-west-1.amazonaws.com/flowforge/${{ env.tagged_image }}-${{ env.timestamp }} \ | |
| --set forge.entryPoint=${{ env.PR_NUMBER }}.flowfuse.dev \ | |
| --set forge.broker.hostname=${{ env.PR_NUMBER }}-mqtt.flowfuse.dev \ | |
| --set forge.projectNamespace=pr-${{ env.PR_NUMBER }}-projects \ | |
| --set forge.clusterRole.name=pr-${{ env.PR_NUMBER }}-clusterrole \ | |
| --set forge.license=${{ secrets.PRE_STAGING_LICENSE }} \ | |
| --set forge.aws.IAMRole=arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/flowforge_service_account_role \ | |
| --set forge.email.ses.sourceArn=arn:aws:ses:eu-west-1:${{ secrets.AWS_ACCOUNT_ID }}:identity/flowfuse.com \ | |
| --set forge.assistant.service.url=$(op read op://ci/staging_flowfuse/assistant_url) \ | |
| --set forge.assistant.service.token=$(op read op://ci/staging_flowfuse/assistant_token) \ | |
| --set forge.expert.service.url=$(op read op://ci/prestaging_flowfuse/expert_url) \ | |
| --set forge.expert.service.token=$(op read op://ci/prestaging_flowfuse/expert_token) \ | |
| --set forge.tables.driver.options.database.user=$(op read op://ci/prestaging_flowfuse/local_postgres_admin_username) \ | |
| --set forge.tables.driver.options.database.password=$(op read op://ci/prestaging_flowfuse/local_postgres_admin_password) \ | |
| --set forge.tables.driver.options.database.host=flowfuse-pr-${{ env.PR_NUMBER }}-postgresql.pr-${{ env.PR_NUMBER }}.svc.cluster.local \ | |
| flowfuse-pr-${{ env.PR_NUMBER }} ./helm-repo/helm/flowfuse | |
| - name: Initial setup | |
| if: ${{ env.initialSetup == 'true' }} | |
| run: | | |
| ./.github/scripts/initial-setup.sh ${{ env.PR_NUMBER }} ${{ secrets.INIT_CONFIG_PASSWORD_HASH }} ${{ secrets.INIT_CONFIG_ACCESS_TOKEN_HASH }} ${{ secrets.INIT_CONFIG_ACCESS_TOKEN }} ${{ secrets.INIT_CONFIG_PASSWORD }} ${{ secrets.PRE_STAGING_FLOWFUSE_NODES_TOKEN }} | |
| - name: Summary | |
| run: | | |
| echo "### :rocket: Deployment succeeded" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Deployed commit SHA:** ${{ github.event.pull_request.head.sha }}" >> $GITHUB_STEP_SUMMARY | |
| echo "**Deployed to:** [https://${{ env.PR_NUMBER }}.flowfuse.dev](https://${{ env.PR_NUMBER }}.flowfuse.dev)" >> $GITHUB_STEP_SUMMARY | |
| create-custom-stack: | |
| name: Create stack with custom Node-RED image | |
| needs: [upload-node-red, deploy] | |
| if: | | |
| always() && | |
| github.event_name == 'workflow_dispatch' && | |
| needs.deploy.result == 'success' && | |
| needs.upload-node-red.result == 'success' | |
| runs-on: ubuntu-latest | |
| environment: staging | |
| env: | |
| PR_NUMBER: ${{ github.event.number == '' && inputs.pr_number || github.event.number }} | |
| FLOWFUSE_DOMAIN: 'flowfuse.dev' | |
| steps: | |
| - name: Create/update stack | |
| run: | | |
| customStackId=$(curl -ks -XGET -H "Authorization: Bearer ${{ secrets.INIT_CONFIG_ACCESS_TOKEN }}" https://$PR_NUMBER.$FLOWFUSE_DOMAIN/api/v1/stacks/ | jq -r '.stacks[] | select(.name == "NR-41-Custom") | .id') | |
| if [ -n "$customStackId" ]; then | |
| echo "Stack already exists, updating..." | |
| curl -ks -X PUT \ | |
| --fail-with-body \ | |
| -H "Authorization: Bearer ${{ secrets.INIT_CONFIG_ACCESS_TOKEN }}" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"properties": {"container": "'"${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.eu-west-1.amazonaws.com/flowforge/${{ needs.upload-node-red.outputs.nr_custom_image_tag }}"'"}}' \ | |
| https://$PR_NUMBER.$FLOWFUSE_DOMAIN/api/v1/stacks/$customStackId | |
| else | |
| echo "Stack does not exists, creating..." | |
| projectTypeId=$(curl -ks -XGET -H "Authorization: Bearer ${{ secrets.INIT_CONFIG_ACCESS_TOKEN }}" https://$PR_NUMBER.$FLOWFUSE_DOMAIN/api/v1/project-types/ | jq -r '.types[].id') | |
| curl -ks -w "\n" -XPOST \ | |
| --fail-with-body \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer ${{ secrets.INIT_CONFIG_ACCESS_TOKEN }}" \ | |
| -d '{"name":"'"NR-41-Custom"'","label":"'"4.1.x-custom"'", "projectType":"'"$projectTypeId"'","properties":{ "cpu":'"30"',"memory":'"256"',"container":"'"${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.eu-west-1.amazonaws.com/flowforge/${{ needs.upload-node-red.outputs.nr_custom_image_tag }}"'"}}' \ | |
| https://$PR_NUMBER.$FLOWFUSE_DOMAIN/api/v1/stacks/ | |
| fi | |
| notify-slack: | |
| name: Notify about pre-staging deployment | |
| needs: | |
| - deploy | |
| - create-custom-stack | |
| if: | | |
| always() && | |
| (needs.deploy.result != 'skipped' || needs.create-custom-stack.result != 'skipped') | |
| runs-on: ubuntu-latest | |
| env: | |
| PR_NUMBER: ${{ github.event.number == '' && inputs.pr_number || github.event.number }} | |
| steps: | |
| # Builds the recipient list (PR author + requested reviewers, or github.actor on workflow_dispatch) | |
| # and resolves each GitHub username to a Slack user ID by scanning workspace profiles and picking the value of custom profile field (GitHub). | |
| # Outputs `recipients` as JSON: [{ slack_id, github, role }, ...]. Unresolved users are skipped with a warning. | |
| - name: Resolve Slack users for recipients | |
| id: resolve | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| SLACK_BOT_TOKEN: ${{ secrets.SLACK_GHBOT_TOKEN }} | |
| SLACK_GITHUB_FIELD_ID: "Xf0A2BPU8U77" | |
| PR_AUTHOR: ${{ github.event.pull_request.user.login }} | |
| REQUESTED_REVIEWERS: ${{ toJSON(github.event.pull_request.requested_reviewers) }} | |
| GITHUB_ACTOR: ${{ github.actor }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| with: | |
| script: | | |
| const token = process.env.SLACK_BOT_TOKEN; | |
| const fieldId = process.env.SLACK_GITHUB_FIELD_ID; | |
| // Build recipient list with roles, deduped (first-added role wins: Author > Reviewer > Pusher). | |
| // Bot accounts (e.g. dependabot[bot]) are skipped — they have no Slack profile. | |
| const recipients = []; | |
| const seen = new Set(); | |
| const add = (login, role) => { | |
| if (!login) return; | |
| if (login.endsWith('[bot]')) return; | |
| const key = login.toLowerCase(); | |
| if (seen.has(key)) return; | |
| seen.add(key); | |
| recipients.push({ github: key, role }); | |
| }; | |
| if (process.env.EVENT_NAME === 'workflow_dispatch') { | |
| add(process.env.GITHUB_ACTOR, 'Trigger'); | |
| } else { | |
| add(process.env.PR_AUTHOR, 'Author'); | |
| const reviewers = JSON.parse(process.env.REQUESTED_REVIEWERS || '[]'); | |
| for (const r of reviewers) add(r.login, 'Reviewer'); | |
| add(process.env.GITHUB_ACTOR, 'Pusher'); | |
| } | |
| if (recipients.length === 0) { | |
| core.warning('No recipients to notify'); | |
| core.setOutput('recipients', '[]'); | |
| return; | |
| } | |
| // Build Slack username -> ID map by scanning workspace profiles | |
| const targets = new Set(recipients.map(r => r.github)); | |
| const resolved = new Map(); | |
| let cursor; | |
| outer: do { | |
| const params = new URLSearchParams({ limit: '200' }); | |
| if (cursor) params.set('cursor', cursor); | |
| const res = await fetch(`https://slack.com/api/users.list?${params}`, { | |
| headers: { Authorization: `Bearer ${token}` } | |
| }); | |
| const data = await res.json(); | |
| if (!data.ok) return core.setFailed(`Slack users.list error: ${data.error}`); | |
| for (const member of data.members) { | |
| if (member.deleted || member.is_bot) continue; | |
| const profileRes = await fetch(`https://slack.com/api/users.profile.get?user=${member.id}`, { | |
| headers: { Authorization: `Bearer ${token}` } | |
| }); | |
| const profileData = await profileRes.json(); | |
| if (!profileData.ok) continue; | |
| const ghField = profileData.profile?.fields?.[fieldId]?.value; | |
| if (!ghField) continue; | |
| const ghLower = ghField.toLowerCase(); | |
| if (targets.has(ghLower)) { | |
| resolved.set(ghLower, member.id); | |
| if (resolved.size === targets.size) break outer; | |
| } | |
| } | |
| cursor = data.response_metadata?.next_cursor; | |
| } while (cursor); | |
| const output = []; | |
| for (const r of recipients) { | |
| const slack_id = resolved.get(r.github); | |
| if (slack_id) { | |
| output.push({ slack_id, github: r.github, role: r.role }); | |
| } else { | |
| core.warning(`No Slack user found with GitHub username: ${r.github} (role: ${r.role})`); | |
| } | |
| } | |
| core.info(`Resolved ${output.length}/${recipients.length} recipients`); | |
| core.setOutput('recipients', JSON.stringify(output)); | |
| # Sends one DM per resolved recipient via Slack chat.postMessage. | |
| # Same message content for everyone, with a *Role:* field indicating Author / Reviewer / Trigger. | |
| # Step fails only if all sends fail; partial failures are logged as warnings. | |
| - name: Send Slack notifications | |
| if: steps.resolve.outputs.recipients != '' && steps.resolve.outputs.recipients != '[]' | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| SLACK_BOT_TOKEN: ${{ secrets.SLACK_GHBOT_TOKEN }} | |
| RECIPIENTS: ${{ steps.resolve.outputs.recipients }} | |
| PR_NUMBER: ${{ env.PR_NUMBER }} | |
| PR_TITLE: ${{ github.event.pull_request.title }} | |
| PR_AUTHOR: ${{ github.event.pull_request.user.login }} | |
| DEPLOY_RESULT: ${{ needs.deploy.result }} | |
| COMMIT_SHA: ${{ github.event.pull_request.head.sha }} | |
| SERVER_URL: ${{ github.server_url }} | |
| REPOSITORY: ${{ github.repository }} | |
| RUN_ID: ${{ github.run_id }} | |
| GRAFANA_URL: ${{ secrets.STAGING_GRAFANA_URL }} | |
| with: | |
| script: | | |
| const token = process.env.SLACK_BOT_TOKEN; | |
| const recipients = JSON.parse(process.env.RECIPIENTS); | |
| const prNumber = process.env.PR_NUMBER; | |
| const prTitle = process.env.PR_TITLE || `#${prNumber}`; | |
| const prAuthor = process.env.PR_AUTHOR || 'n/a'; | |
| const deployResult = process.env.DEPLOY_RESULT; | |
| const commitSha = process.env.COMMIT_SHA; | |
| const serverUrl = process.env.SERVER_URL; | |
| const repository = process.env.REPOSITORY; | |
| const runId = process.env.RUN_ID; | |
| const grafanaUrl = process.env.GRAFANA_URL; | |
| const statusText = deployResult === 'success' ? ':white_check_mark: Success' : ':x: Failure '; | |
| const logsUrl = `https://${grafanaUrl}/explore?orgId=1&left=%7B%22datasource%22:%22P8E80F9AEF21F6940%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22editorMode%22:%22code%22,%22expr%22:%22%7Bnamespace%3D%5C%22pr-${prNumber}%5C%22%7D%20%7C%3D%20%60%60%20%7C%20json%22,%22queryType%22:%22range%22%7D%5D,%22range%22:%7B%22from%22:%22now-30m%22,%22to%22:%22now%22%7D%7D`; | |
| let failures = 0; | |
| for (const r of recipients) { | |
| const blocks = [ | |
| { | |
| type: 'header', | |
| text: { type: 'plain_text', text: `Pull Request ${prNumber} pre-staging deployment`, emoji: true } | |
| }, | |
| { | |
| type: 'section', | |
| fields: [ | |
| { type: 'mrkdwn', text: `*Status:*\n${statusText}` }, | |
| { type: 'mrkdwn', text: `*Role:*\n${r.role}` }, | |
| { type: 'mrkdwn', text: `*Pull Request:*\n<https://github.com/FlowFuse/flowfuse/pull/${prNumber}|${prTitle}>` }, | |
| { type: 'mrkdwn', text: `*Workflow run:*\n<${serverUrl}/${repository}/actions/runs/${runId}|View>` } | |
| ] | |
| }, | |
| { | |
| type: 'section', | |
| fields: [ | |
| { type: 'mrkdwn', text: `*Author:*\n${prAuthor}` }, | |
| { type: 'mrkdwn', text: `*Commit SHA:*\n<${serverUrl}/${repository}/commit/${commitSha}|${commitSha}>` }, | |
| { type: 'mrkdwn', text: `*Deployed to:*\n<https://${prNumber}.flowfuse.dev|https://${prNumber}.flowfuse.dev>` }, | |
| { type: 'mrkdwn', text: `*Logs:*\n<${logsUrl}|View>` } | |
| ] | |
| } | |
| ]; | |
| const res = await fetch('https://slack.com/api/chat.postMessage', { | |
| method: 'POST', | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| 'Content-Type': 'application/json; charset=utf-8' | |
| }, | |
| body: JSON.stringify({ channel: r.slack_id, blocks }) | |
| }); | |
| const data = await res.json(); | |
| if (!data.ok) { | |
| core.warning(`Slack chat.postMessage failed for ${r.github} (${r.slack_id}): ${data.error}`); | |
| failures++; | |
| } else { | |
| core.info(`Notified ${r.github} (${r.role}) at ${r.slack_id}`); | |
| } | |
| } | |
| if (failures > 0 && failures === recipients.length) { | |
| core.setFailed(`All ${failures} Slack notifications failed`); | |
| } | |
| destroy: | |
| name: Remove application | |
| needs: [ validate-user ] | |
| runs-on: ubuntu-latest | |
| if: | | |
| needs.validate-user.outputs.is_org_member == 'true' && | |
| github.event_name == 'pull_request' && | |
| github.event.action == 'closed' | |
| environment: staging | |
| env: | |
| IMAGE_NAME: 'forge-k8s' | |
| PR_NUMBER: ${{ github.event.number == '' && inputs.pr_number || github.event.number }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Configure AWS credentials for EKS interaction | |
| uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1 | |
| with: | |
| role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ env.AWS_EKS_DEPLOY_ROLE }} | |
| role-session-name: GithubActionsRoleSession | |
| role-duration-seconds: 900 | |
| aws-region: eu-west-1 | |
| - name: Configure kubeconfig | |
| run: | | |
| aws eks update-kubeconfig --region eu-west-1 --name ${{ secrets.EKS_CLUSTER_NAME }} | |
| - name: Remove resources | |
| run: | | |
| if helm list -n "pr-${{ env.PR_NUMBER }}" --filter "^flowfuse-pr-${{ env.PR_NUMBER }}$" | grep "flowfuse-pr-${{ env.PR_NUMBER }}"; then | |
| helm uninstall --namespace "pr-${{ env.PR_NUMBER }}" flowfuse-pr-${{ env.PR_NUMBER }} | |
| sleep 15 | |
| kubectl delete namespace "pr-${{ env.PR_NUMBER }}" | |
| else | |
| echo "Release flowfuse-pr-${{ env.PR_NUMBER }} does not exist" | |
| fi |