|
| 1 | +on: |
| 2 | + workflow_call: |
| 3 | + inputs: |
| 4 | + artifact-prefix: |
| 5 | + description: | |
| 6 | + Prefix for charm package GitHub artifact(s) |
| 7 | + |
| 8 | + Use canonical/data-platform-workflows build_charm.yaml to build the charm(s) |
| 9 | + required: true |
| 10 | + type: string |
| 11 | + |
| 12 | +jobs: |
| 13 | + collect-integration-tests: |
| 14 | + name: Collect integration test spread jobs |
| 15 | + runs-on: ubuntu-latest |
| 16 | + timeout-minutes: 5 |
| 17 | + steps: |
| 18 | + - name: Checkout |
| 19 | + uses: actions/checkout@v4 |
| 20 | + - name: Set up environment |
| 21 | + run: | |
| 22 | + sudo snap install charmcraft --classic |
| 23 | + pipx install tox poetry |
| 24 | + - name: Collect spread jobs |
| 25 | + id: collect-jobs |
| 26 | + shell: python |
| 27 | + run: | |
| 28 | + import json |
| 29 | + import os |
| 30 | + import subprocess |
| 31 | +
|
| 32 | + spread_jobs = ( |
| 33 | + subprocess.run( |
| 34 | + ["charmcraft", "test", "--list", "github-ci"], capture_output=True, check=True, text=True |
| 35 | + ) |
| 36 | + .stdout.strip() |
| 37 | + .split("\n") |
| 38 | + ) |
| 39 | + jobs = [] |
| 40 | + for job in spread_jobs: |
| 41 | + # Example `job`: "github-ci:ubuntu-24.04:tests/spread/test_charm.py:juju36" |
| 42 | + _, runner, task, variant = job.split(":") |
| 43 | + # Example: "test_charm.py" |
| 44 | + task = task.removeprefix("tests/spread/") |
| 45 | + if runner.endswith("-arm"): |
| 46 | + architecture = "arm64" |
| 47 | + else: |
| 48 | + architecture = "amd64" |
| 49 | + # Example: "test_charm.py:juju36 | amd64" |
| 50 | + name = f"{task}:{variant} | {architecture}" |
| 51 | + # ":" character not valid in GitHub Actions artifact |
| 52 | + name_in_artifact = f"{task}-{variant}-{architecture}" |
| 53 | + jobs.append({ |
| 54 | + "spread_job": job, |
| 55 | + "name": name, |
| 56 | + "name_in_artifact": name_in_artifact, |
| 57 | + "runner": runner, |
| 58 | + }) |
| 59 | + output = f"jobs={json.dumps(jobs)}" |
| 60 | + print(output) |
| 61 | + with open(os.environ["GITHUB_OUTPUT"], "a") as file: |
| 62 | + file.write(output) |
| 63 | + - name: Generate Allure default test results |
| 64 | + if: ${{ github.event_name == 'schedule' && github.run_attempt == '1' }} |
| 65 | + run: tox run -e integration -- tests/integration --allure-default-dir=allure-default-results |
| 66 | + - name: Upload Allure default results |
| 67 | + # Default test results in case the integration tests time out or runner set up fails |
| 68 | + # (So that Allure report will show "unknown"/"failed" test result, instead of omitting the test) |
| 69 | + if: ${{ github.event_name == 'schedule' && github.run_attempt == '1' }} |
| 70 | + uses: actions/upload-artifact@v4 |
| 71 | + with: |
| 72 | + name: allure-default-results-integration-test |
| 73 | + path: allure-default-results/ |
| 74 | + if-no-files-found: error |
| 75 | + outputs: |
| 76 | + jobs: ${{ steps.collect-jobs.outputs.jobs }} |
| 77 | + |
| 78 | + integration-test: |
| 79 | + strategy: |
| 80 | + fail-fast: false |
| 81 | + matrix: |
| 82 | + job: ${{ fromJSON(needs.collect-integration-tests.outputs.jobs) }} |
| 83 | + name: ${{ matrix.job.name }} |
| 84 | + needs: |
| 85 | + - collect-integration-tests |
| 86 | + runs-on: ${{ matrix.job.runner }} |
| 87 | + timeout-minutes: 217 # Sum of steps `timeout-minutes` + 5 |
| 88 | + steps: |
| 89 | + - name: Free up disk space |
| 90 | + timeout-minutes: 1 |
| 91 | + run: | |
| 92 | + printf '\nDisk usage before cleanup\n' |
| 93 | + df --human-readable |
| 94 | + # Based on https://github.com/actions/runner-images/issues/2840#issuecomment-790492173 |
| 95 | + rm -r /opt/hostedtoolcache/ |
| 96 | + printf '\nDisk usage after cleanup\n' |
| 97 | + df --human-readable |
| 98 | + - name: Checkout |
| 99 | + timeout-minutes: 3 |
| 100 | + uses: actions/checkout@v4 |
| 101 | + - name: Set up environment |
| 102 | + timeout-minutes: 5 |
| 103 | + run: sudo snap install charmcraft --classic |
| 104 | + # TODO: remove when https://github.com/canonical/charmcraft/issues/2105 and |
| 105 | + # https://github.com/canonical/charmcraft/issues/2130 fixed |
| 106 | + - run: | |
| 107 | + sudo snap install go --classic |
| 108 | + go install github.com/snapcore/spread/cmd/spread@latest |
| 109 | + - name: Download packed charm(s) |
| 110 | + timeout-minutes: 5 |
| 111 | + uses: actions/download-artifact@v4 |
| 112 | + with: |
| 113 | + pattern: ${{ inputs.artifact-prefix }}-* |
| 114 | + merge-multiple: true |
| 115 | + - name: Run spread job |
| 116 | + timeout-minutes: 180 |
| 117 | + id: spread |
| 118 | + # TODO: replace with `charmcraft test` when |
| 119 | + # https://github.com/canonical/charmcraft/issues/2105 and |
| 120 | + # https://github.com/canonical/charmcraft/issues/2130 fixed |
| 121 | + run: ~/go/bin/spread -vv -artifacts=artifacts '${{ matrix.job.spread_job }}' |
| 122 | + env: |
| 123 | + AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }} |
| 124 | + AWS_SECRET_KEY: ${{ secrets.AWS_SECRET_KEY }} |
| 125 | + GCP_ACCESS_KEY: ${{ secrets.GCP_ACCESS_KEY }} |
| 126 | + GCP_SECRET_KEY: ${{ secrets.GCP_SECRET_KEY }} |
| 127 | + UBUNTU_PRO_TOKEN: ${{ secrets.UBUNTU_PRO_TOKEN }} |
| 128 | + LANDSCAPE_ACCOUNT_NAME: ${{ secrets.LANDSCAPE_ACCOUNT_NAME }} |
| 129 | + LANDSCAPE_REGISTRATION_KEY: ${{ secrets.LANDSCAPE_REGISTRATION_KEY }} |
| 130 | + - name: Upload Allure results |
| 131 | + timeout-minutes: 3 |
| 132 | + # Only upload results from one spread system & one spread variant |
| 133 | + # Allure can only process one result per pytest test ID. If parameterization is done via |
| 134 | + # spread instead of pytest, there will be overlapping pytest test IDs. |
| 135 | + if: ${{ (success() || (failure() && steps.spread.outcome == 'failure')) && startsWith(matrix.job.spread_job, 'github-ci:ubuntu-24.04:') && endsWith(matrix.job.spread_job, ':juju36') && github.event_name == 'schedule' && github.run_attempt == '1' }} |
| 136 | + uses: actions/upload-artifact@v4 |
| 137 | + with: |
| 138 | + name: allure-results-integration-test-${{ matrix.job.name_in_artifact }} |
| 139 | + path: artifacts/${{ matrix.job.spread_job }}/allure-results/ |
| 140 | + if-no-files-found: error |
| 141 | + - timeout-minutes: 1 |
| 142 | + if: ${{ success() || (failure() && steps.spread.outcome == 'failure') }} |
| 143 | + run: snap list |
| 144 | + - name: Select model |
| 145 | + timeout-minutes: 1 |
| 146 | + # `!contains(matrix.job.spread_job, 'juju29')` workaround for juju 2 error: |
| 147 | + # "ERROR cannot acquire lock file to read controller concierge-microk8s: unable to open |
| 148 | + # /tmp/juju-store-lock-3635383939333230: permission denied" |
| 149 | + # Unable to workaround error with `sudo rm /tmp/juju-*` |
| 150 | + if: ${{ !contains(matrix.job.spread_job, 'juju29') && (success() || (failure() && steps.spread.outcome == 'failure')) }} |
| 151 | + id: juju-switch |
| 152 | + run: | |
| 153 | + # sudo needed since spread runs scripts as root |
| 154 | + # "testing" is default model created by concierge |
| 155 | + sudo juju switch testing |
| 156 | + mkdir ~/logs/ |
| 157 | + - name: juju status |
| 158 | + timeout-minutes: 1 |
| 159 | + if: ${{ !contains(matrix.job.spread_job, 'juju29') && (success() || (failure() && steps.spread.outcome == 'failure')) }} |
| 160 | + run: sudo juju status --color --relations | tee ~/logs/juju-status.txt |
| 161 | + - name: juju debug-log |
| 162 | + timeout-minutes: 3 |
| 163 | + if: ${{ !contains(matrix.job.spread_job, 'juju29') && (success() || (failure() && steps.spread.outcome == 'failure')) }} |
| 164 | + run: sudo juju debug-log --color --replay --no-tail | tee ~/logs/juju-debug-log.txt |
| 165 | + - name: jhack tail |
| 166 | + timeout-minutes: 3 |
| 167 | + if: ${{ !contains(matrix.job.spread_job, 'juju29') && (success() || (failure() && steps.spread.outcome == 'failure')) }} |
| 168 | + run: sudo jhack tail --printer raw --replay --no-watch | tee ~/logs/jhack-tail.txt |
| 169 | + - name: Upload logs |
| 170 | + timeout-minutes: 5 |
| 171 | + if: ${{ !contains(matrix.job.spread_job, 'juju29') && (success() || (failure() && steps.spread.outcome == 'failure')) }} |
| 172 | + uses: actions/upload-artifact@v4 |
| 173 | + with: |
| 174 | + name: logs-integration-test-${{ matrix.job.name_in_artifact }} |
| 175 | + path: ~/logs/ |
| 176 | + if-no-files-found: error |
| 177 | + - name: Disk usage |
| 178 | + timeout-minutes: 1 |
| 179 | + if: ${{ success() || (failure() && steps.spread.outcome == 'failure') }} |
| 180 | + run: df --human-readable |
| 181 | + |
| 182 | + allure-report: |
| 183 | + # TODO future improvement: use concurrency group for job |
| 184 | + name: Publish Allure report |
| 185 | + if: ${{ !cancelled() && github.event_name == 'schedule' && github.run_attempt == '1' }} |
| 186 | + needs: |
| 187 | + - integration-test |
| 188 | + runs-on: ubuntu-latest |
| 189 | + timeout-minutes: 5 |
| 190 | + steps: |
| 191 | + - name: Download Allure |
| 192 | + # Following instructions from https://allurereport.org/docs/install-for-linux/#install-from-a-deb-package |
| 193 | + run: gh release download --repo allure-framework/allure2 --pattern 'allure_*.deb' |
| 194 | + env: |
| 195 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 196 | + - name: Install Allure |
| 197 | + run: | |
| 198 | + sudo apt-get update |
| 199 | + sudo apt-get install ./allure_*.deb -y |
| 200 | + # For first run, manually create branch with no history |
| 201 | + # (e.g. |
| 202 | + # git checkout --orphan gh-pages-beta |
| 203 | + # git rm -rf . |
| 204 | + # touch .nojekyll |
| 205 | + # git add .nojekyll |
| 206 | + # git commit -m "Initial commit" |
| 207 | + # git push origin gh-pages-beta |
| 208 | + # ) |
| 209 | + - name: Checkout GitHub pages branch |
| 210 | + uses: actions/checkout@v4 |
| 211 | + with: |
| 212 | + ref: gh-pages-beta |
| 213 | + path: repo/ |
| 214 | + - name: Download default test results |
| 215 | + # Default test results in case the integration tests time out or runner set up fails |
| 216 | + # (So that Allure report will show "unknown"/"failed" test result, instead of omitting the test) |
| 217 | + uses: actions/download-artifact@v4 |
| 218 | + with: |
| 219 | + path: allure-default-results/ |
| 220 | + name: allure-default-results-integration-test |
| 221 | + - name: Download test results |
| 222 | + uses: actions/download-artifact@v4 |
| 223 | + with: |
| 224 | + path: allure-results/ |
| 225 | + pattern: allure-results-integration-test-* |
| 226 | + merge-multiple: true |
| 227 | + - name: Combine Allure default results & actual results |
| 228 | + # For every test: if actual result available, use that. Otherwise, use default result |
| 229 | + # So that, if actual result not available, Allure report will show "unknown"/"failed" test result |
| 230 | + # instead of omitting the test |
| 231 | + shell: python |
| 232 | + run: | |
| 233 | + import dataclasses |
| 234 | + import json |
| 235 | + import pathlib |
| 236 | +
|
| 237 | +
|
| 238 | + @dataclasses.dataclass(frozen=True) |
| 239 | + class Result: |
| 240 | + test_case_id: str |
| 241 | + path: pathlib.Path |
| 242 | +
|
| 243 | + def __eq__(self, other): |
| 244 | + if not isinstance(other, type(self)): |
| 245 | + return False |
| 246 | + return self.test_case_id == other.test_case_id |
| 247 | +
|
| 248 | +
|
| 249 | + actual_results = pathlib.Path("allure-results") |
| 250 | + default_results = pathlib.Path("allure-default-results") |
| 251 | +
|
| 252 | + results: dict[pathlib.Path, set[Result]] = { |
| 253 | + actual_results: set(), |
| 254 | + default_results: set(), |
| 255 | + } |
| 256 | + for directory, results_ in results.items(): |
| 257 | + for path in directory.glob("*-result.json"): |
| 258 | + with path.open("r") as file: |
| 259 | + id_ = json.load(file)["testCaseId"] |
| 260 | + results_.add(Result(id_, path)) |
| 261 | +
|
| 262 | + actual_results.mkdir(exist_ok=True) |
| 263 | +
|
| 264 | + missing_results = results[default_results] - results[actual_results] |
| 265 | + for default_result in missing_results: |
| 266 | + # Move to `actual_results` directory |
| 267 | + default_result.path.rename(actual_results / default_result.path.name) |
| 268 | + - name: Load test report history |
| 269 | + run: | |
| 270 | + if [[ -d repo/_latest/history/ ]] |
| 271 | + then |
| 272 | + echo 'Loading history' |
| 273 | + cp -r repo/_latest/history/ allure-results/ |
| 274 | + fi |
| 275 | + - name: Create executor.json |
| 276 | + shell: python |
| 277 | + run: | |
| 278 | + # Reverse engineered from https://github.com/simple-elf/allure-report-action/blob/eca283b643d577c69b8e4f048dd6cd8eb8457cfd/entrypoint.sh |
| 279 | + import json |
| 280 | +
|
| 281 | + DATA = { |
| 282 | + "name": "GitHub Actions", |
| 283 | + "type": "github", |
| 284 | + "buildOrder": ${{ github.run_number }}, # TODO future improvement: use run ID |
| 285 | + "buildName": "Run ${{ github.run_id }}", |
| 286 | + "buildUrl": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", |
| 287 | + "reportUrl": "../${{ github.run_number }}/", |
| 288 | + } |
| 289 | + with open("allure-results/executor.json", "w") as file: |
| 290 | + json.dump(DATA, file) |
| 291 | + - name: Generate Allure report |
| 292 | + run: allure generate |
| 293 | + - name: Create index.html |
| 294 | + shell: python |
| 295 | + run: | |
| 296 | + DATA = f"""<!DOCTYPE html> |
| 297 | + <meta charset="utf-8"> |
| 298 | + <meta http-equiv="cache-control" content="no-cache"> |
| 299 | + <meta http-equiv="refresh" content="0; url=${{ github.run_number }}"> |
| 300 | + """ |
| 301 | + with open("repo/index.html", "w") as file: |
| 302 | + file.write(DATA) |
| 303 | + - name: Update GitHub pages branch |
| 304 | + working-directory: repo/ |
| 305 | + # TODO future improvement: commit message |
| 306 | + run: | |
| 307 | + mkdir '${{ github.run_number }}' |
| 308 | + rm -f _latest |
| 309 | + ln -s '${{ github.run_number }}' _latest |
| 310 | + cp -r ../allure-report/. _latest/ |
| 311 | + git add . |
| 312 | + git config user.name "GitHub Actions" |
| 313 | + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" |
| 314 | + git commit -m "Allure report ${{ github.run_number }}" |
| 315 | + # Uses token set in checkout step |
| 316 | + git push origin gh-pages-beta |
0 commit comments