|
| 1 | +name: Cypress staging run (reusable) |
| 2 | + |
| 3 | +on: |
| 4 | + workflow_call: |
| 5 | + inputs: |
| 6 | + spec: |
| 7 | + description: Cypress spec file(s) to run (newline-separated) |
| 8 | + required: true |
| 9 | + type: string |
| 10 | + slack_failure_message: |
| 11 | + description: Parent Slack message text posted on failure |
| 12 | + required: false |
| 13 | + type: string |
| 14 | + report_to_slack: |
| 15 | + description: Whether to post failure notifications to Slack |
| 16 | + required: false |
| 17 | + type: boolean |
| 18 | + default: true |
| 19 | + secrets: |
| 20 | + CYPRESS_ENV_JSON: |
| 21 | + required: true |
| 22 | + SLACK_BOT_TOKEN: |
| 23 | + required: false |
| 24 | + SLACK_CHANNEL: |
| 25 | + required: false |
| 26 | + |
| 27 | +defaults: |
| 28 | + run: |
| 29 | + shell: bash |
| 30 | + |
| 31 | +jobs: |
| 32 | + cypress-tests: |
| 33 | + runs-on: ubuntu-latest |
| 34 | + continue-on-error: true |
| 35 | + steps: |
| 36 | + - name: checkout |
| 37 | + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 |
| 38 | + - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 |
| 39 | + with: |
| 40 | + node-version: 22.22.3 |
| 41 | + |
| 42 | + - name: Write the cypress.env.json file |
| 43 | + run: | |
| 44 | + echo '${{ secrets.CYPRESS_ENV_JSON }}' > tests_cypress/cypress.env.json |
| 45 | +
|
| 46 | + - name: Run the cypress tests |
| 47 | + uses: cypress-io/github-action@248bde77443c376edc45906ede03a1aba9da0462 # v5.8.4 |
| 48 | + with: |
| 49 | + record: false |
| 50 | + config: "video=true,screenshotOnRunFailure=true" |
| 51 | + build: npx cypress info |
| 52 | + working-directory: tests_cypress |
| 53 | + spec: ${{ inputs.spec }} |
| 54 | + |
| 55 | + - name: Upload test artifacts |
| 56 | + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 |
| 57 | + if: always() |
| 58 | + with: |
| 59 | + name: cypress-artifacts |
| 60 | + path: | |
| 61 | + tests_cypress/cypress/videos |
| 62 | + tests_cypress/cypress/screenshots |
| 63 | + tests_cypress/cypress/results |
| 64 | + retention-days: 30 |
| 65 | + |
| 66 | + - name: Install jq for Slack notifications |
| 67 | + if: failure() && inputs.report_to_slack |
| 68 | + run: sudo apt-get update && sudo apt-get install -y jq |
| 69 | + |
| 70 | + - name: Build failure previews from mochawesome |
| 71 | + if: failure() && inputs.report_to_slack |
| 72 | + run: | |
| 73 | + set -euo pipefail |
| 74 | + RESULTS_DIR=tests_cypress/cypress/results |
| 75 | + TMP=/tmp/failures.json |
| 76 | +
|
| 77 | + mapfile -t files < <(find "$RESULTS_DIR" -maxdepth 1 -type f -name 'mochawesome*.json' -print 2>/dev/null || true) |
| 78 | +
|
| 79 | + if [ "${#files[@]}" -eq 0 ]; then |
| 80 | + echo "No mochawesome files found in $RESULTS_DIR" >&2 |
| 81 | + echo "failures=[]" > "$TMP" |
| 82 | + exit 0 |
| 83 | + fi |
| 84 | +
|
| 85 | + valid_files=() |
| 86 | + for f in "${files[@]}"; do |
| 87 | + if jq -e 'has("results")' "$f" >/dev/null 2>&1; then |
| 88 | + valid_files+=("$f") |
| 89 | + else |
| 90 | + echo "Skipping non-mochawesome JSON: $f" >&2 |
| 91 | + fi |
| 92 | + done |
| 93 | +
|
| 94 | + if [ "${#valid_files[@]}" -eq 0 ]; then |
| 95 | + echo "No valid mochawesome JSONs found" >&2 |
| 96 | + echo "failures=[]" > "$TMP" |
| 97 | + exit 0 |
| 98 | + fi |
| 99 | +
|
| 100 | + jq -s -c '[ .[] | .results[]? | .. | objects |
| 101 | + | select(.state=="failed") |
| 102 | + | { fullTitle: (.fullTitle // .title), message: (.err.message // .err.estack // "No error") } |
| 103 | + ] |
| 104 | + | unique_by(.fullTitle) |
| 105 | + | map({ title: .fullTitle, preview: ( .message |
| 106 | + | split("\n") |
| 107 | + | map(gsub("^\\s+|\\s+$"; "")) |
| 108 | + | map(select(length>0)) |
| 109 | + | .[0:5] |
| 110 | + | map("> " + .) |
| 111 | + | join("\n") ) }) |
| 112 | + | .[0:5]' "${valid_files[@]}" > "$TMP" || echo "failures=[]" > "$TMP" |
| 113 | +
|
| 114 | + jq . "$TMP" || true |
| 115 | +
|
| 116 | + - name: Fetch artifact download URL |
| 117 | + if: failure() && inputs.report_to_slack |
| 118 | + id: artifact_link |
| 119 | + env: |
| 120 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 121 | + run: | |
| 122 | + set -euo pipefail |
| 123 | + resp=$(curl -s -H "Authorization: Bearer ${GITHUB_TOKEN}" "https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts") |
| 124 | + artifact_id=$(echo "$resp" | jq -r '.artifacts[0].id // empty') |
| 125 | + if [ -n "$artifact_id" ]; then |
| 126 | + url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$artifact_id" |
| 127 | + else |
| 128 | + url='' |
| 129 | + fi |
| 130 | + echo "artifact_url=$url" >> $GITHUB_OUTPUT |
| 131 | +
|
| 132 | + - name: Post parent message (chat.postMessage) |
| 133 | + if: failure() && inputs.report_to_slack |
| 134 | + id: post_parent |
| 135 | + env: |
| 136 | + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} |
| 137 | + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} |
| 138 | + run: | |
| 139 | + set -euo pipefail |
| 140 | + if [ -z "${SLACK_BOT_TOKEN:-}" ] || [ -z "${SLACK_CHANNEL:-}" ]; then |
| 141 | + echo "SLACK_BOT_TOKEN or SLACK_CHANNEL secret missing" >&2 |
| 142 | + exit 1 |
| 143 | + fi |
| 144 | +
|
| 145 | + PARENT_TEXT='*:rotating_light: ${{ inputs.slack_failure_message }} :rotating_light:*' |
| 146 | + RUN_LINK='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' |
| 147 | + LINK_TEXT=':github: Workflow run' |
| 148 | + ARTIFACTS_LINK='${{ steps.artifact_link.outputs.artifact_url }}' |
| 149 | + ARTIFACTS_TEXT=':file_cabinet: Artifacts download' |
| 150 | + if [ -z "$ARTIFACTS_LINK" ]; then |
| 151 | + ARTIFACTS_LINK='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts' |
| 152 | + ARTIFACTS_TEXT=':file_cabinet: Artifacts' |
| 153 | + fi |
| 154 | + printf -v FULL_TEXT '%s\n<%s|%s>\n<%s|%s>' "$PARENT_TEXT" "$RUN_LINK" "$LINK_TEXT" "$ARTIFACTS_LINK" "$ARTIFACTS_TEXT" |
| 155 | +
|
| 156 | + resp=$(curl -s -X POST \ |
| 157 | + -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ |
| 158 | + -H "Content-type: application/json" \ |
| 159 | + --data "$(jq -nc --arg ch "$SLACK_CHANNEL" --arg txt "$FULL_TEXT" '{channel:$ch, text:$txt, mrkdwn:true}')" \ |
| 160 | + https://slack.com/api/chat.postMessage) |
| 161 | +
|
| 162 | + echo "Parent response: $resp" |
| 163 | + ok=$(echo "$resp" | jq -r '.ok') |
| 164 | + if [ "$ok" != "true" ]; then |
| 165 | + echo "Failed to post parent message: $(echo "$resp" | jq -r '.error')" >&2 |
| 166 | + exit 1 |
| 167 | + fi |
| 168 | +
|
| 169 | + ts=$(echo "$resp" | jq -r '.ts') |
| 170 | + echo "ts=$ts" >> $GITHUB_OUTPUT |
| 171 | +
|
| 172 | + - name: Post threaded replies (max 5) |
| 173 | + if: failure() && inputs.report_to_slack |
| 174 | + env: |
| 175 | + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} |
| 176 | + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} |
| 177 | + run: | |
| 178 | + set -euo pipefail |
| 179 | + TS="${{ steps.post_parent.outputs.ts }}" |
| 180 | + if [ -z "$TS" ]; then |
| 181 | + echo "No parent ts, skipping replies" |
| 182 | + exit 0 |
| 183 | + fi |
| 184 | +
|
| 185 | + mapfile -t items < <(jq -c '.[]' /tmp/failures.json) |
| 186 | + emojis=(":one:" ":two:" ":three:" ":four:" ":five:") |
| 187 | + for idx in "${!items[@]}"; do |
| 188 | + item=${items[$idx]} |
| 189 | + title=$(echo "$item" | jq -r '.title') |
| 190 | + preview=$(echo "$item" | jq -r '.preview') |
| 191 | + prefix=${emojis[$idx]:-":hash:"} |
| 192 | + printf -v prefixed_title '%s Test *`%s`* failed' "$prefix" "$title" |
| 193 | + body=$(jq -nc --arg ch "$SLACK_CHANNEL" --arg thread "$TS" --arg text "$prefixed_title" --arg preview "$preview" '{channel:$ch, text: ($text + "\n\n" + $preview + "\n\n:wavy_dash:\n"), thread_ts:$thread, mrkdwn:true}') |
| 194 | +
|
| 195 | + echo "Posting reply for $title" |
| 196 | + curl -s -X POST -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" -H "Content-type: application/json" --data "$body" https://slack.com/api/chat.postMessage | jq . || true |
| 197 | + sleep 0.5 |
| 198 | + done |
0 commit comments