airbnb/caravel

View on GitHub
.github/workflows/ephemeral-env.yml

Summary

Maintainability
Test Coverage
name: Ephemeral env workflow

on:
  issue_comment:
    types: [created]

jobs:
  config:
    runs-on: "ubuntu-latest"
    if: github.event.issue.pull_request
    outputs:
      has-secrets: ${{ steps.check.outputs.has-secrets }}
    steps:
      - name: "Check for secrets"
        id: check
        shell: bash
        run: |
          if [ -n "${{ (secrets.AWS_ACCESS_KEY_ID != '' && secrets.AWS_SECRET_ACCESS_KEY != '') || '' }}" ]; then
            echo "has-secrets=1" >> "$GITHUB_OUTPUT"
          fi

  ephemeral-env-comment:
    concurrency:
      group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}-comment
      cancel-in-progress: true
    needs: config
    if: needs.config.outputs.has-secrets
    name: Evaluate ephemeral env comment trigger (/testenv)
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    outputs:
      slash-command: ${{ steps.eval-body.outputs.result }}
      feature-flags: ${{ steps.eval-feature-flags.outputs.result }}

    steps:
    - name: Debug
      run: |
        echo "Comment on PR #${{ github.event.issue.number }} by ${{ github.event.issue.user.login }}, ${{ github.event.comment.author_association }}"

    - name: Eval comment body for /testenv slash command
      uses: actions/github-script@v7
      id: eval-body
      with:
        result-encoding: string
        script: |
          const pattern = /^\/testenv (up|down)/
          const result = pattern.exec(context.payload.comment.body)
          return result === null ? 'noop' : result[1]

    - name: Eval comment body for feature flags
      uses: actions/github-script@v7
      id: eval-feature-flags
      with:
        script: |
          const pattern = /FEATURE_(\w+)=(\w+)/g;
          let results = [];
          [...context.payload.comment.body.matchAll(pattern)].forEach(match => {
            const config = {
              name: `SUPERSET_FEATURE_${match[1]}`,
              value: match[2],
            };
            results.push(config);
          });
          return results;

    - name: Limit to committers
      if: >
        steps.eval-body.outputs.result != 'noop' &&
        github.event.comment.author_association != 'MEMBER' &&
        github.event.comment.author_association != 'OWNER'
      uses: actions/github-script@v7
      with:
        github-token: ${{github.token}}
        script: |
          const errMsg = '@${{ github.event.comment.user.login }} Ephemeral environment creation is currently limited to committers.'
          github.rest.issues.createComment({
            issue_number: ${{ github.event.issue.number }},
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: errMsg
          })
          core.setFailed(errMsg)

  ephemeral-docker-build:
    concurrency:
      group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}-build
      cancel-in-progress: true
    needs: ephemeral-env-comment
    name: ephemeral-docker-build
    runs-on: ubuntu-latest
    steps:
      - name: Get Info from comment
        uses: actions/github-script@v7
        id: get-pr-info
        with:
          script: |
            const request = {
                owner: context.repo.owner,
                repo: context.repo.repo,
                pull_number: ${{ github.event.issue.number }},
            }
            core.info(`Getting PR #${request.pull_number} from ${request.owner}/${request.repo}`)
            const pr = await github.rest.pulls.get(request);
            return pr.data;

      - name: Debug
        id: get-sha
        run: |
          echo "sha=${{ fromJSON(steps.get-pr-info.outputs.result).head.sha }}" >> $GITHUB_OUTPUT

      - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} : ${{steps.get-sha.outputs.sha}} )"
        uses: actions/checkout@v4
        with:
          ref: ${{ steps.get-sha.outputs.sha }}
          persist-credentials: false

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build ephemeral env image
        run: |
          ./scripts/build_docker.py \
            "ci" \
            "pull_request" \
            --build_context_ref ${{ github.event.issue.number }}

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-west-2

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Load, tag and push image to ECR
        id: push-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: superset-ci
          IMAGE_TAG: apache/superset:${{ steps.get-sha.outputs.sha }}-ci
        run: |
          docker tag $IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:pr-${{ github.event.issue.number }}-ci
          docker push -a $ECR_REGISTRY/$ECR_REPOSITORY

  ephemeral-env-up:
    needs: [ephemeral-env-comment, ephemeral-docker-build]
    if: needs.ephemeral-env-comment.outputs.slash-command == 'up'
    name: Spin up an ephemeral environment
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write

    steps:
    - uses: actions/checkout@v4
      with:
        persist-credentials: false

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: us-west-2

    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v2

    - name: Check target image exists in ECR
      id: check-image
      continue-on-error: true
      run: |
        aws ecr describe-images \
        --registry-id $(echo "${{ steps.login-ecr.outputs.registry }}" | grep -Eo "^[0-9]+") \
        --repository-name superset-ci \
        --image-ids imageTag=pr-${{ github.event.issue.number }}-ci

    - name: Fail on missing container image
      if: steps.check-image.outcome == 'failure'
      uses: actions/github-script@v7
      with:
        github-token: ${{github.token}}
        script: |
          const errMsg = '@${{ github.event.comment.user.login }} Container image not yet published for this PR. Please try again when build is complete.'
          github.rest.issues.createComment({
            issue_number: ${{ github.event.issue.number }},
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: errMsg
          })
          core.setFailed(errMsg)

    - name: Fill in the new image ID in the Amazon ECS task definition
      id: task-def
      uses: aws-actions/amazon-ecs-render-task-definition@v1
      with:
        task-definition: .github/workflows/ecs-task-definition.json
        container-name: superset-ci
        image: ${{ steps.login-ecr.outputs.registry }}/superset-ci:pr-${{ github.event.issue.number }}-ci

    - name: Update env vars in the Amazon ECS task definition
      run: |
        cat <<< "$(jq '.containerDefinitions[0].environment += ${{ needs.ephemeral-env-comment.outputs.feature-flags }}' < ${{ steps.task-def.outputs.task-definition }})" > ${{ steps.task-def.outputs.task-definition }}

    - name: Describe ECS service
      id: describe-services
      run: |
        echo "active=$(aws ecs describe-services --cluster superset-ci --services pr-${{ github.event.issue.number }}-service | jq '.services[] | select(.status == "ACTIVE") | any')" >> $GITHUB_OUTPUT
    - name: Create ECS service
      if: steps.describe-services.outputs.active != 'true'
      id: create-service
      env:
        ECR_SUBNETS: subnet-0e15a5034b4121710,subnet-0e8efef4a72224974
        ECR_SECURITY_GROUP: sg-092ff3a6ae0574d91
      run: |
        aws ecs create-service \
        --cluster superset-ci \
        --service-name pr-${{ github.event.issue.number }}-service \
        --task-definition superset-ci \
        --launch-type FARGATE \
        --desired-count 1 \
        --platform-version LATEST \
        --network-configuration "awsvpcConfiguration={subnets=[$ECR_SUBNETS],securityGroups=[$ECR_SECURITY_GROUP],assignPublicIp=ENABLED}" \
        --tags key=pr,value=${{ github.event.issue.number }} key=github_user,value=${{ github.actor }}

    - name: Deploy Amazon ECS task definition
      id: deploy-task
      uses: aws-actions/amazon-ecs-deploy-task-definition@v1
      with:
        task-definition: ${{ steps.task-def.outputs.task-definition }}
        service: pr-${{ github.event.issue.number }}-service
        cluster: superset-ci
        wait-for-service-stability: true
        wait-for-minutes: 10

    - name: List tasks
      id: list-tasks
      run: |
        echo "task=$(aws ecs list-tasks --cluster superset-ci --service-name pr-${{ github.event.issue.number }}-service | jq '.taskArns | first')" >> $GITHUB_OUTPUT

    - name: Get network interface
      id: get-eni
      run: |
        echo "eni=$(aws ecs describe-tasks --cluster superset-ci --tasks ${{ steps.list-tasks.outputs.task }} | jq '.tasks | .[0] | .attachments | .[0] | .details | map(select(.name=="networkInterfaceId")) | .[0] | .value')" >> $GITHUB_OUTPUT

    - name: Get public IP
      id: get-ip
      run: |
        echo "ip=$(aws ec2 describe-network-interfaces --network-interface-ids ${{ steps.get-eni.outputs.eni }} | jq -r '.NetworkInterfaces | first | .Association.PublicIp')" >> $GITHUB_OUTPUT

    - name: Comment (success)
      if: ${{ success() }}
      uses: actions/github-script@v7
      with:
        github-token: ${{github.token}}
        script: |
          github.rest.issues.createComment({
            issue_number: ${{ github.event.issue.number }},
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: '@${{ github.event.comment.user.login }} Ephemeral environment spinning up at http://${{ steps.get-ip.outputs.ip }}:8080. Credentials are `admin`/`admin`. Please allow several minutes for bootstrapping and startup.'
          })

    - name: Comment (failure)
      if: ${{ failure() }}
      uses: actions/github-script@v7
      with:
        github-token: ${{github.token}}
        script: |
          github.rest.issues.createComment({
            issue_number: ${{ github.event.issue.number }},
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: '@${{ github.event.comment.user.login }} Ephemeral environment creation failed. Please check the Actions logs for details.'
          })