Skip to content

Create custom pre-staging environment #68

Create custom pre-staging environment

Create custom pre-staging environment #68

name: Create custom 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'
pull_request:
types:
- opened
- synchronize
- reopened
- closed
paths-ignore:
- 'docs/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.number }}
cancel-in-progress: false
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]'
outputs:
is_org_member: ${{ steps.validate.outputs.is_member }}
steps:
- name: Generate a token
id: generate_token
uses: tibdex/github-app-token@v2
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' &&
github.event.action != 'closed' &&
github.event.inputs.driver-k8s-branch != 'main'
uses: 'flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml@feat-publish-package-from-branch'
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_launcher:
name: Build and publish Node-RED launcher package
needs: validate-user
if: |
needs.validate-user.outputs.is_org_member == 'true' &&
github.event_name == 'workflow_dispatch' &&
github.event.action != 'closed' &&
github.event.inputs.nr_launcher_branch != 'main'
runs-on: ubuntu-latest
steps:
- name: Simulate package build and push
run:
echo "Build package"
build-node-red:
name: Build Node-RED 4.0.x container images
needs:
- validate-user
- publish_nr_launcher
if: |
needs.validate-user.outputs.is_org_member == 'true' &&
github.event_name == 'workflow_dispatch' &&
github.event.action != 'closed' &&
(always() && needs.publish_nr_launcher.result != 'failure')
uses: flowfuse/github-actions-workflows/.github/workflows/build_container_image.yml@v0.30.0
with:
image_name: 'node-red'
dockerfile_path: Dockerfile
image_tag_prefix: '4.0.x-'
package_dependencies: |
@flowforge/nr-project-nodes
build_context: './ci/node-red'
build_platform: "linux/arm64"
npm_registry_url: ${{ vars.PUBLIC_NPM_REGISTRY_URL }}
secrets:
npm_registry_auth_token: ${{ secrets.NPM_PUBLISH_TOKEN }}
temporary_registry_token: ${{ secrets.GITHUB_TOKEN }}
upload-node-red:
name: Publish Node-RED 4.0.x container images
needs:
- validate-user
- build-node-red
if: |
needs.validate-user.outputs.is_org_member == 'true' &&
github.event_name == 'workflow_dispatch' &&
github.event.action != 'closed'
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 }}
steps:
- name: Set variables
run: |
echo "tagged_image=${{ env.IMAGE_NAME }}:4.0.x-pr-${{ env.PR_NUMBER }}" >> $GITHUB_ENV
echo "timestamp=$(date +%s)" >> $GITHUB_ENV
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
- name: Configure AWS credentials for ECR interaction
id: aws-config
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_ACCESS_KEY_SECRET }}
aws-region: eu-west-1
mask-aws-account-id: true
- name: Login to AWS ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
with:
mask-password: true
- name: Login to GHCR
uses: docker/login-action@v3
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
create-custom-stack:
name: Create stack with custom Node-RED image
needs:
- validate-user
- upload-node-red
- deploy
if: |
needs.validate-user.outputs.is_org_member == 'true' &&
github.event_name == 'workflow_dispatch' &&
github.event.action != 'closed'
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-40-Custom") | .id')
if [ -n "$customStackId" ]; then
echo "Stack already exists, updating..."
curl -ks -X PUT \
-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 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${{ secrets.INIT_CONFIG_ACCESS_TOKEN }}" \
-d '{"name":"'"NR-40-Custom"'","label":"'"4.0.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
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@v4
- name: Set variables
run: |
echo "tagged_image=${{ env.IMAGE_NAME }}:pr-${{ env.PR_NUMBER}}" >> $GITHUB_ENV
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
- name: Set build-args if branch is not main
id: set-build-args
run: |
if [ "${{ inputs.driver_k8s_branch }}" != "" ]; then
echo "BUILD_ARGS=KUBERNETES_DRIVER_TAG=pre-staging-${{ inputs.driver_k8s_branch }}" >> $GITHUB_ENV
else
echo "BUILD_ARGS=" >> $GITHUB_ENV
fi
- name: Build container image
id: build
uses: docker/build-push-action@v6
with:
context: .
file: "./ci/Dockerfile"
tags: ${{ env.tagged_image }}
push: false
outputs: type=docker,dest=/tmp/k8s-forge.tar
build-args: ${{ env.BUILD_ARGS }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: k8s-forge
path: /tmp/k8s-forge.tar
retention-days: 7
- name: debug
run: |
echo "Event name: ${{ github.event_name }}"
echo "Event action: ${{ github.event.action }}"
deploy:
name: Deploy application
if: |
(always() && needs.publish_k8s_driver.result != 'failure') &&
(always() && 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 }}
steps:
- name: Checkout
uses: actions/checkout@v4
- 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@v4
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@v5
with:
name: k8s-forge
failOnError: false
- name: Configure AWS credentials for ECR interaction
id: aws-config
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_ACCESS_KEY_SECRET }}
aws-region: eu-west-1
mask-aws-account-id: true
- name: Login to AWS ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
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@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_ACCESS_KEY_SECRET }}
aws-region: eu-west-1
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/K8sAdmin
role-duration-seconds: 1200
- 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@v1
with:
version: 2.25.0
- name: Check out FlowFuse/helm repository (to access latest helm chart)
uses: actions/checkout@v4
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) \
flowfuse-pr-${{ env.PR_NUMBER }} ./helm-repo/helm/flowforge
- 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 }}
- 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
# notify-slack:
# name: Notify about pre-staging deployment
# needs: [deploy]
# if: |
# ( success() || failure() ) &&
# github.actor != 'dependabot[bot]'
# runs-on: ubuntu-latest
# env:
# PR_NUMBER: ${{ github.event.number }}
# steps:
# - name: Map users
# id: map-actor-to-slack
# uses: icalia-actions/map-github-actor@v0.0.2
# with:
# actor-map: ${{ vars.SLACK_GITHUB_USERS_MAP }}
# default-mapping: C067BD0377F
# - name: Post to a Slack channel
# id: slack
# uses: slackapi/slack-github-action@v1.27.0
# with:
# channel-id: 'C067BD0377F'
# payload: |
# {
# "blocks": [
# {
# "type": "header",
# "text": {
# "type": "plain_text",
# "text": "Pull Request ${{ env.PR_NUMBER }} pre-staging deployment",
# "emoji": true
# }
# },
# {
# "type": "section",
# "fields": [
# {
# "type": "mrkdwn",
# "text": "*Status:*\n${{ needs.deploy.result == 'success' && ':white_check_mark: Success' || ':x: Failure '}}"
# },
# {
# "type": "mrkdwn",
# "text": "*Pull Request:*\n<https://github.com/FlowFuse/flowfuse/pull/${{ env.PR_NUMBER }}|${{ github.event.pull_request.title }}>"
# },
# {
# "type": "mrkdwn",
# "text": "*Workflow run:*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View>"
# }
# ]
# },
# {
# "type": "section",
# "fields": [
# {
# "type": "mrkdwn",
# "text": "*Author:*\n<@${{ steps.map-actor-to-slack.outputs.actor-mapping }}>"
# },
# {
# "type": "mrkdwn",
# "text": "*Commit SHA:*\n<${{ github.server_url }}/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha }}|${{ github.event.pull_request.head.sha }}>"
# },
# {
# "type": "mrkdwn",
# "text": "*Deployed to:*\n<https://${{ env.PR_NUMBER }}.flowfuse.dev|https://${{ env.PR_NUMBER }}.flowfuse.dev>"
# },
# {
# "type": "mrkdwn",
# "text": "*Logs:*\n<https://${{ env.GRAFANA_URL }}/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-${{ env.PR_NUMBER }}%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|View>"
# }
# ]
# }
# ]
# }
# env:
# SLACK_BOT_TOKEN: ${{ secrets.SLACK_GHBOT_TOKEN }}
# GRAFANA_URL: ${{ secrets.STAGING_GRAFANA_URL }}
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 }}
steps:
- name: Configure AWS credentials for EKS interaction
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_ACCESS_KEY_SECRET }}
aws-region: eu-west-1
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/K8sAdmin
role-duration-seconds: 1200
- 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