|
| 1 | +# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch |
| 2 | +name: e2e |
| 3 | + |
| 4 | +on: |
| 5 | + workflow_dispatch: |
| 6 | + inputs: |
| 7 | + repositories: |
| 8 | + description: Comma-separated repositories to patch |
| 9 | + required: false |
| 10 | + type: string |
| 11 | + default: | |
| 12 | + balena-io-modules/open-balena-base, |
| 13 | + balena-io/balena-api, |
| 14 | + balena-io/docs, |
| 15 | + balena-io/environment-staging, |
| 16 | + balena-os/balena-engine, |
| 17 | + balena-os/fatrw, |
| 18 | + product-os/environment-staging |
| 19 | + flowzone_ref: |
| 20 | + description: Flowzone branch, or tag, or commit SHA |
| 21 | + required: false |
| 22 | + type: string |
| 23 | + default: master |
| 24 | + required_checks: |
| 25 | + description: Comma-separated list of required successful checks |
| 26 | + required: false |
| 27 | + type: string |
| 28 | + default: | |
| 29 | + Flowzone / All tests, |
| 30 | + Flowzone / All jobs |
| 31 | + dry_run: |
| 32 | + description: Patch files but do not push changes |
| 33 | + required: false |
| 34 | + type: boolean |
| 35 | + default: false |
| 36 | + auto_close: |
| 37 | + description: Close the pull requests at the end of the run |
| 38 | + required: false |
| 39 | + type: boolean |
| 40 | + default: true |
| 41 | + token_app_id: |
| 42 | + description: GitHub App id to request a temporary token |
| 43 | + type: string |
| 44 | + required: false |
| 45 | + # https://github.com/organizations/product-os/settings/apps/flowzone-app |
| 46 | + default: "291899" |
| 47 | + token_installation_id: |
| 48 | + description: GitHub App installation id to request a temporary token |
| 49 | + type: string |
| 50 | + required: false |
| 51 | + default: "" |
| 52 | + |
| 53 | +# https://docs.github.com/en/actions/using-jobs/using-concurrency |
| 54 | +concurrency: |
| 55 | + group: ${{ github.workflow }}-${{ inputs.flowzone_ref }} |
| 56 | + cancel-in-progress: false |
| 57 | + |
| 58 | +jobs: |
| 59 | + process_inputs: |
| 60 | + name: Process Inputs |
| 61 | + runs-on: ubuntu-latest |
| 62 | + timeout-minutes: 20 |
| 63 | + |
| 64 | + outputs: |
| 65 | + matrix: ${{ steps.matrix.outputs.build }} |
| 66 | + |
| 67 | + env: |
| 68 | + # https://github.com/organizations/product-os/settings/installations |
| 69 | + # https://github.com/organizations/balena-os/settings/installations |
| 70 | + # https://github.com/organizations/balena-io/settings/installations |
| 71 | + # https://github.com/organizations/balena-io-modules/settings/installations |
| 72 | + KNOWN_INSTALLATION_IDS: > |
| 73 | + { |
| 74 | + "balena-io-modules": 34046903, |
| 75 | + "balena-io": 34046749, |
| 76 | + "balena-os": 34046907, |
| 77 | + "product-os": 34040165 |
| 78 | + } |
| 79 | +
|
| 80 | + steps: |
| 81 | + - name: Log GitHub context |
| 82 | + env: |
| 83 | + GITHUB_CONTEXT: ${{ toJSON(github) }} |
| 84 | + run: echo "${GITHUB_CONTEXT}" || true |
| 85 | + |
| 86 | + - name: Process repositories list |
| 87 | + id: repositories_csv |
| 88 | + env: |
| 89 | + INPUT: ${{ inputs.repositories }} |
| 90 | + run: | |
| 91 | + while IFS=',' read -r item |
| 92 | + do |
| 93 | + item="$(echo "${item}" | awk '{$1=$1};NF')" |
| 94 | + if [ -n "${item}" ] |
| 95 | + then |
| 96 | + out="${out},${item}" |
| 97 | + fi |
| 98 | + done <<< "${INPUT}" |
| 99 | + echo "build=${out:1}" >> $GITHUB_OUTPUT |
| 100 | +
|
| 101 | + # https://github.com/kanga333/json-array-builder |
| 102 | + - name: Build JSON array |
| 103 | + id: repositories_json |
| 104 | + uses: kanga333/json-array-builder@c7cd9d3a8b17cd368e9c2210bc3c16b0e2714ce5 # v0.2.1 |
| 105 | + with: |
| 106 | + str: ${{ steps.repositories_csv.outputs.build }} |
| 107 | + separator: "," |
| 108 | + |
| 109 | + - name: Create JSON matrix |
| 110 | + id: matrix |
| 111 | + env: |
| 112 | + REPOS: ${{ steps.repositories_json.outputs.build }} |
| 113 | + INSTALLATION_IDS: ${{ env.KNOWN_INSTALLATION_IDS }} |
| 114 | + run: | |
| 115 | + echo "build=$(jq -cr --argjson installation_ids "${INSTALLATION_IDS}" '{ |
| 116 | + include: map({ |
| 117 | + repository: ., |
| 118 | + installation_id: $installation_ids[split("/")[0]] | tostring |
| 119 | + }) |
| 120 | + }' <<< "${REPOS}")" >> $GITHUB_OUTPUT |
| 121 | +
|
| 122 | + pull_request: |
| 123 | + name: Pull Request |
| 124 | + runs-on: ubuntu-22.04 |
| 125 | + timeout-minutes: 90 |
| 126 | + needs: process_inputs |
| 127 | + |
| 128 | + strategy: |
| 129 | + fail-fast: false |
| 130 | + matrix: ${{ fromJSON(needs.process_inputs.outputs.matrix) }} |
| 131 | + |
| 132 | + env: |
| 133 | + # https://cli.github.com/manual/gh_help_environment |
| 134 | + GH_REPO: ${{ matrix.repository }} |
| 135 | + GH_PROMPT_DISABLED: "true" |
| 136 | + GH_DEBUG: "true" |
| 137 | + GH_PAGER: "cat" |
| 138 | + |
| 139 | + PR_BRANCH: dispatch/flowzone-${{ inputs.flowzone_ref }} |
| 140 | + PR_TITLE: Test Flowzone @ ${{ inputs.flowzone_ref }} |
| 141 | + PR_BODY: | |
| 142 | + Auto-generated by https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} |
| 143 | + PR_LABELS: flowzone,e2e,do-not-merge,dispatch |
| 144 | + |
| 145 | + steps: |
| 146 | + # https://github.com/tibdex/github-app-token |
| 147 | + - name: Generate GitHub App token |
| 148 | + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0 |
| 149 | + id: gh_token |
| 150 | + with: |
| 151 | + app_id: ${{ inputs.token_app_id }} |
| 152 | + installation_id: ${{ inputs.token_installation_id || matrix.installation_id }} |
| 153 | + private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} |
| 154 | + repository: ${{ matrix.repository }} |
| 155 | + permissions: >- |
| 156 | + { |
| 157 | + "actions": "read", |
| 158 | + "administration": "write", |
| 159 | + "checks": "read", |
| 160 | + "contents": "write", |
| 161 | + "members": "read", |
| 162 | + "metadata": "read", |
| 163 | + "pull_requests": "write", |
| 164 | + "statuses": "read", |
| 165 | + "workflows": "write" |
| 166 | + } |
| 167 | +
|
| 168 | + # https://cli.github.com/manual/gh_api |
| 169 | + - name: Get repository settings |
| 170 | + id: repo |
| 171 | + env: |
| 172 | + GH_TOKEN: ${{ steps.gh_token.outputs.token }} |
| 173 | + run: | |
| 174 | + echo "default_branch=$(gh api repos/{owner}/{repo} --jq '.default_branch')" >> $GITHUB_OUTPUT |
| 175 | +
|
| 176 | + # https://github.com/actions/checkout |
| 177 | + - name: Checkout base branch |
| 178 | + uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3 |
| 179 | + with: |
| 180 | + repository: ${{ matrix.repository }} |
| 181 | + token: ${{ steps.gh_token.outputs.token }} |
| 182 | + ref: ${{ steps.repo.outputs.default_branch }} |
| 183 | + |
| 184 | + # https://github.com/crazy-max/ghaction-import-gpg |
| 185 | + - name: Import GPG key |
| 186 | + id: import-gpg |
| 187 | + uses: crazy-max/ghaction-import-gpg@111c56156bcc6918c056dbef52164cfa583dc549 # v5.2.0 |
| 188 | + with: |
| 189 | + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} |
| 190 | + passphrase: ${{ secrets.GPG_PASSPHRASE }} |
| 191 | + git_config_global: true |
| 192 | + git_user_signingkey: true |
| 193 | + git_commit_gpgsign: true |
| 194 | + |
| 195 | + # update flowzone workflow to point to the provided git ref |
| 196 | + - name: Update workflow |
| 197 | + env: |
| 198 | + GIT_AUTHOR_NAME: ${{ steps.import-gpg.outputs.name }} |
| 199 | + GIT_AUTHOR_EMAIL: ${{ steps.import-gpg.outputs.email }} |
| 200 | + GIT_COMMITTER_NAME: ${{ steps.import-gpg.outputs.name }} |
| 201 | + GIT_COMMITTER_EMAIL: ${{ steps.import-gpg.outputs.email }} |
| 202 | + WORKFLOW_FILE: .github/workflows/flowzone.yml |
| 203 | + run: | |
| 204 | + yq '.jobs.flowzone.uses |= sub("(?P<uses>.+)@.+"; "$1@${{ inputs.flowzone_ref }}")' -i "${WORKFLOW_FILE}" |
| 205 | + yq '.jobs.flowzone.uses |= . line_comment="${{ github.run_id }}-${{ github.run_attempt }}"' -i "${WORKFLOW_FILE}" |
| 206 | +
|
| 207 | + git add "${WORKFLOW_FILE}" |
| 208 | + git commit -m "patch: ${PR_TITLE}" |
| 209 | +
|
| 210 | + # https://github.com/peter-evans/create-pull-request |
| 211 | + - name: Create pull request |
| 212 | + uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54 # v4.2.4 |
| 213 | + if: inputs.dry_run != true |
| 214 | + id: cpr |
| 215 | + with: |
| 216 | + token: ${{ steps.gh_token.outputs.token }} |
| 217 | + branch: ${{ env.PR_BRANCH }} |
| 218 | + title: ${{ env.PR_TITLE }} |
| 219 | + body: | |
| 220 | + ${{ env.PR_BODY }} |
| 221 | + labels: | |
| 222 | + ${{ env.PR_LABELS }} |
| 223 | + draft: true |
| 224 | + delete-branch: true |
| 225 | + |
| 226 | + - name: Update summary |
| 227 | + if: steps.cpr.outputs.pull-request-number != '' |
| 228 | + run: | |
| 229 | + echo "Pull Request Number: ${{ steps.cpr.outputs.pull-request-number }}" | tee -a $GITHUB_STEP_SUMMARY |
| 230 | + echo "Pull Request URL: ${{ steps.cpr.outputs.pull-request-url }}" | tee -a $GITHUB_STEP_SUMMARY |
| 231 | + echo "Pull Request Operation: ${{ steps.cpr.outputs.pull-request-operation }}" | tee -a $GITHUB_STEP_SUMMARY |
| 232 | + echo "Pull Request Head SHA: ${{ steps.cpr.outputs.pull-request-head-sha }}" | tee -a $GITHUB_STEP_SUMMARY |
| 233 | +
|
| 234 | + - name: Wait for required checks |
| 235 | + if: steps.cpr.outputs.pull-request-number != '' |
| 236 | + env: |
| 237 | + GH_TOKEN: ${{ steps.gh_token.outputs.token }} |
| 238 | + REQUIRED_CHECKS: ${{ inputs.required_checks }} |
| 239 | + run: | |
| 240 | + while true |
| 241 | + do |
| 242 | + sleep $(((RANDOM % 15) + 5)) |
| 243 | +
|
| 244 | + all_checks="$(gh pr checks ${{ steps.cpr.outputs.pull-request-number }} | cat)" |
| 245 | +
|
| 246 | + while IFS='\n' read -r check |
| 247 | + do |
| 248 | + test -n "${check}" || continue 1 |
| 249 | + status="$(echo "${check}" | awk -F'\t' '{print $2}')" |
| 250 | +
|
| 251 | + case ${status} in |
| 252 | + pass|skipping|queued|pending) |
| 253 | + continue 1 |
| 254 | + ;; |
| 255 | + *) |
| 256 | + echo "::error::One or more jobs finished with status ${status}" |
| 257 | + echo "${all_checks}" | awk -vSTATUS="${status}" -F'\t' '$2 == STATUS' |
| 258 | + exit 1 |
| 259 | + ;; |
| 260 | + esac |
| 261 | +
|
| 262 | + done <<< "${all_checks}" |
| 263 | +
|
| 264 | + while IFS=',' read -r required |
| 265 | + do |
| 266 | + required="$(echo "${required}" | awk '{$1=$1};NF')" |
| 267 | + test -n "${required}" || continue 1 |
| 268 | +
|
| 269 | + status="$(echo "${all_checks}" | awk -vCHECK="${required}" -F'\t' '$1 == CHECK {print $2}')" |
| 270 | +
|
| 271 | + case ${status} in |
| 272 | + pass) |
| 273 | + continue 1 |
| 274 | + ;; |
| 275 | + queued|pending|"") |
| 276 | + echo "Waiting for ${required}..." |
| 277 | + echo "${all_checks}" | awk -vSTATUS="pending" -F'\t' '$2 == STATUS' |
| 278 | + continue 2 |
| 279 | + ;; |
| 280 | + *) |
| 281 | + echo "::error::A required job finished with status ${status}" |
| 282 | + echo "${all_checks}" | awk -vSTATUS="${status}" -F'\t' '$2 == STATUS' |
| 283 | + exit 1 |
| 284 | + ;; |
| 285 | + esac |
| 286 | + done <<< "${REQUIRED_CHECKS}" |
| 287 | +
|
| 288 | + break |
| 289 | + done |
| 290 | +
|
| 291 | + # always close the PR and delete the branch |
| 292 | + - name: Close pull request |
| 293 | + if: | |
| 294 | + always() && inputs.auto_close == true && steps.cpr.outputs.pull-request-number != '' |
| 295 | + uses: peter-evans/close-pull@f47e95b46e45ebf8a3a792e3a60e831ec2563f81 # v2.0.1 |
| 296 | + with: |
| 297 | + token: ${{ steps.gh_token.outputs.token }} |
| 298 | + repository: ${{ matrix.repository }} |
| 299 | + pull-request-number: ${{ steps.cpr.outputs.pull-request-number }} |
| 300 | + comment: Auto-closing pull request |
| 301 | + delete-branch: true |
0 commit comments