From 598cc585bdb4dbb928199a89449094c2006421ce Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:25:59 -0500 Subject: [PATCH] CI: Push dev images from PRs and remove Drone from PRs (#109004) * Push docker images in PRs and remove Drone * fix actionlint errors * grep for docker doesn't need another grep --- .drone.star | 2 - .drone.yml | 497 ----------------------------- .github/workflows/pr-e2e-tests.yml | 89 +++++- 3 files changed, 77 insertions(+), 511 deletions(-) diff --git a/.drone.star b/.drone.star index a430359e27d..a563ee0682a 100644 --- a/.drone.star +++ b/.drone.star @@ -8,7 +8,6 @@ This module returns a Drone configuration including pipelines and secrets. """ load("scripts/drone/events/main.star", "main_pipelines") -load("scripts/drone/events/pr.star", "pr_pipelines") load( "scripts/drone/events/release.star", "publish_artifacts_pipelines", @@ -28,7 +27,6 @@ load("scripts/drone/vault.star", "secrets") def main(_ctx): return ( - pr_pipelines() + main_pipelines() + rrc_patch_pipelines() + publish_image_pipelines_public() + diff --git a/.drone.yml b/.drone.yml index 2ca377f28a9..52bdd9dc125 100644 --- a/.drone.yml +++ b/.drone.yml @@ -8,498 +8,6 @@ image_pull_secrets: - gcr - gar kind: pipeline -name: pr-verify-drone -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: [] -steps: -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.21.3 - name: identify-runner -- commands: - - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd - depends_on: [] - environment: - CGO_ENABLED: 0 - image: golang:1.24.5-alpine - name: compile-build-cmd -- commands: - - ./bin/build verify-drone - depends_on: - - compile-build-cmd - image: byrnedo/alpine-curl:0.1.8 - name: lint-drone -trigger: - event: - - pull_request - paths: - exclude: - - docs/** - - '*.md' - include: - - scripts/drone/** - - .drone.yml - - .drone.star -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - retries: 3 -depends_on: [] -environment: - EDITION: oss -image_pull_secrets: -- gcr -- gar -kind: pipeline -name: pr-verify-starlark -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: [] -steps: -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.21.3 - name: identify-runner -- commands: - - go install github.com/bazelbuild/buildtools/buildifier@latest - - buildifier --lint=warn -mode=check -r . - depends_on: [] - image: golang:1.24.5-alpine - name: lint-starlark -trigger: - event: - - pull_request - paths: - exclude: - - docs/** - - '*.md' - include: - - scripts/drone/** - - .drone.star -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - retries: 3 -depends_on: [] -environment: - EDITION: oss -image_pull_secrets: -- gcr -- gar -kind: pipeline -name: pr-build-e2e -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: [] -steps: -- commands: - - echo $(/usr/bin/github-app-external-token) > /github-app/token - environment: - GITHUB_APP_ID: - from_secret: github-app-app-id - GITHUB_APP_INSTALLATION_ID: - from_secret: github-app-installation-id - GITHUB_APP_PRIVATE_KEY: - from_secret: github-app-private-key - failure: ignore - image: us-docker.pkg.dev/grafanalabs-global/docker-deployment-tools-prod/github-app-secret-writer:2024-11-05-v11688112090.1-83920c59 - name: github-app-generate-token - volumes: - - name: github-app - path: /github-app -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.21.3 - name: identify-runner -- commands: - - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.1.2/grabpl - - chmod +x bin/grabpl - image: byrnedo/alpine-curl:0.1.8 - name: grabpl -- commands: - - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd - depends_on: [] - environment: - CGO_ENABLED: 0 - image: golang:1.24.5-alpine - name: compile-build-cmd -- commands: - - '# It is required that code generated from Thema/CUE be committed and in sync - with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - apk add --update make - - CODEGEN_VERIFY=1 make gen-cue - depends_on: [] - image: golang:1.24.5-alpine - name: verify-gen-cue -- commands: - - '# It is required that generated jsonnet is committed and in sync with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - apk add --update make - - CODEGEN_VERIFY=1 make gen-jsonnet - depends_on: [] - image: golang:1.24.5-alpine - name: verify-gen-jsonnet -- commands: - - yarn install --immutable || yarn install --immutable - depends_on: [] - image: node:22.16.0-alpine - name: yarn-install -- commands: - - apk add --update jq bash - - yarn packages:build - - yarn packages:pack - - ./scripts/validate-npm-packages.sh - depends_on: - - yarn-install - environment: - NODE_OPTIONS: --max_old_space_size=8192 - image: node:22.16.0-alpine - name: build-frontend-packages -- failure: ignore - image: grafana/drone-downstream - name: trigger-enterprise-downstream - settings: - params: - - SOURCE_BUILD_NUMBER=${DRONE_COMMIT} - - SOURCE_COMMIT=${DRONE_COMMIT} - - OSS_PULL_REQUEST=${DRONE_PULL_REQUEST} - repositories: - - grafana/grafana-enterprise@${DRONE_SOURCE_BRANCH} - server: https://drone.grafana.net - token: - from_secret: drone_token -- commands: - - wget -qO- https://github.com/dagger/dagger/releases/download/v0.18.8/dagger_v0.18.8_linux_amd64.tar.gz - | tar zx -C /bin - - apk add docker - - docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --version - - docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --uninstall 'qemu-*' - - docker run --privileged --rm tonistiigi/binfmt:qemu-v7.0.0-28 --install all - - go run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 -a targz:grafana:linux/arm64 - -a targz:grafana:linux/arm/v7 -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu - -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 - -a docker:grafana:linux/arm/v7:ubuntu --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER - --ubuntu-base=ubuntu-base --alpine-base=alpine-base --tag-format='{{ .version_base - }}-{{ .buildID }}-{{ .arch }}' --ubuntu-tag-format='{{ .version_base }}-{{ .buildID - }}-ubuntu-{{ .arch }}' --verify='false' --grafana-dir=$$PWD > packages.txt - - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i - depends_on: - - yarn-install - environment: - _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: - from_secret: dagger_token - image: golang:1.24.5-alpine - name: rgm-package - pull: always - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - ./bin/grabpl artifacts docker publish --dockerhub-repo grafana/grafana - depends_on: - - rgm-package - environment: - DOCKER_PASSWORD: - from_secret: docker_password - DOCKER_USER: - from_secret: docker_username - GITHUB_APP_ID: "329617" - GITHUB_APP_INSTALLATION_ID: "37346161" - GITHUB_APP_PRIVATE_KEY: - from_secret: delivery-bot-app-private-key - failure: ignore - image: google/cloud-sdk:431.0.0 - name: publish-images-grafana - volumes: - - name: docker - path: /var/run/docker.sock -- commands: - - yarn e2e:plugin:build - depends_on: - - yarn-install - environment: - NODE_OPTIONS: --max_old_space_size=8192 - image: node:22.16.0-alpine - name: build-test-plugins -- commands: - - apk add --update tar bash - - mkdir grafana - - tar --strip-components=1 -xvf ./dist/*amd64.tar.gz -C grafana - - cp -r devenv scripts tools grafana && cd grafana && ./scripts/grafana-server/start-server - depends_on: - - rgm-package - detach: true - environment: - GF_APP_MODE: development - GF_SERVER_HTTP_PORT: "3001" - GF_SERVER_ROUTER_LOGGING: "1" - image: alpine:3.21.3 - name: grafana-server -- commands: - - GITHUB_TOKEN=$(cat /github-app/token) - - cd / - - ./cpp-e2e/scripts/ci-run.sh azure ${DRONE_SOURCE_BRANCH} - depends_on: - - grafana-server - - github-app-generate-token - environment: - AZURE_SP_APP_ID: - from_secret: azure_sp_app_id - AZURE_SP_PASSWORD: - from_secret: azure_sp_app_pw - AZURE_TENANT: - from_secret: azure_tenant - CYPRESS_CI: "true" - HOST: grafana-server - image: us-docker.pkg.dev/grafanalabs-dev/docker-oss-plugin-partnerships-dev/e2e-14.3.2:1.0.0 - name: end-to-end-tests-cloud-plugins-suite-azure - volumes: - - name: github-app - path: /github-app - when: - paths: - include: - - pkg/tsdb/azuremonitor/** - - public/app/plugins/datasource/azuremonitor/** - - e2e/cloud-plugins-suite/azure-monitor.spec.ts - repo: - - grafana/grafana -- commands: - - npx wait-on@7.0.1 http://$HOST:$PORT - - yarn playwright install --with-deps chromium - - GRAFANA_URL=http://$HOST:$PORT yarn e2e:playwright --grep @plugins - depends_on: - - grafana-server - - build-test-plugins - environment: - HOST: grafana-server - PORT: "3001" - PROV_DIR: /grafana/scripts/grafana-server/tmp/conf/provisioning - image: node:22-bookworm - name: playwright-plugin-e2e -- commands: - - apt-get update - - apt-get install -yq zip - - printenv GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY > /tmp/gcpkey_upload_artifacts.json - - gcloud auth activate-service-account --key-file=/tmp/gcpkey_upload_artifacts.json - - gsutil cp -r ./playwright-report/. gs://releng-pipeline-artifacts-dev/${DRONE_BUILD_NUMBER}/playwright-report - - export E2E_PLAYWRIGHT_REPORT_URL=https://storage.googleapis.com/releng-pipeline-artifacts-dev/${DRONE_BUILD_NUMBER}/playwright-report/index.html - - "echo \"E2E Playwright report uploaded to: \n $${E2E_PLAYWRIGHT_REPORT_URL}\"" - depends_on: - - playwright-plugin-e2e - environment: - GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY: - from_secret: gcp_upload_artifacts_key - failure: ignore - image: google/cloud-sdk:431.0.0 - name: playwright-e2e-report-upload - when: - status: - - success - - failure -- commands: - - GITHUB_TOKEN=$(cat /github-app/token) - - if [ ! -d ./playwright-report/trace ]; then echo 'all tests passed'; exit 0; fi - - export E2E_PLAYWRIGHT_REPORT_URL=https://storage.googleapis.com/releng-pipeline-artifacts-dev/${DRONE_BUILD_NUMBER}/playwright-report/index.html - - 'curl -L -X POST https://api.github.com/repos/grafana/grafana/issues/${DRONE_PULL_REQUEST}/comments - -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $${GITHUB_TOKEN}" - -H "X-GitHub-Api-Version: 2022-11-28" -d "{\"body\":\"❌ Failed to run Playwright - plugin e2e tests.

Click [here]($${E2E_PLAYWRIGHT_REPORT_URL}) to - browse the Playwright report and trace viewer.
For information on how to - run Playwright tests locally, refer to the [Developer guide](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#to-run-the-playwright-tests). - \"}"' - depends_on: - - playwright-e2e-report-upload - - github-app-generate-token - failure: ignore - image: byrnedo/alpine-curl:0.1.8 - name: playwright-e2e-report-post-link - volumes: - - name: github-app - path: /github-app - when: - status: - - success - - failure -- commands: - - export GITHUB_TOKEN=$(cat /github-app/token) - - if [ -z `find ./e2e -type f -name *spec.ts.mp4` ]; then echo 'no e2e videos found - from remaining tests'; exit 0; fi - - apt-get update - - apt-get install -yq zip - - printenv GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY > /tmp/gcpkey_upload_artifacts.json - - gcloud auth activate-service-account --key-file=/tmp/gcpkey_upload_artifacts.json - - find ./e2e -type f -name "*spec.ts.mp4" | zip e2e/videos.zip -@ - - gsutil cp e2e/videos.zip gs://$${E2E_TEST_ARTIFACTS_BUCKET}/${DRONE_BUILD_NUMBER}/artifacts/videos/videos.zip - - export E2E_ARTIFACTS_VIDEO_ZIP=https://storage.googleapis.com/$${E2E_TEST_ARTIFACTS_BUCKET}/${DRONE_BUILD_NUMBER}/artifacts/videos/videos.zip - - 'echo "E2E Test artifacts uploaded to: $${E2E_ARTIFACTS_VIDEO_ZIP}"' - - 'curl -X POST https://api.github.com/repos/${DRONE_REPO}/statuses/${DRONE_COMMIT_SHA} - -H "Authorization: token $${GITHUB_TOKEN}" -d "{\"state\":\"success\",\"target_url\":\"$${E2E_ARTIFACTS_VIDEO_ZIP}\", - \"description\": \"Click on the details to download e2e recording videos\", \"context\": - \"e2e_artifacts\"}"' - depends_on: - - end-to-end-tests-cloud-plugins-suite-azure - - playwright-plugin-e2e - - github-app-generate-token - environment: - E2E_TEST_ARTIFACTS_BUCKET: releng-pipeline-artifacts-dev - GCP_GRAFANA_UPLOAD_ARTIFACTS_KEY: - from_secret: gcp_upload_artifacts_key - failure: ignore - image: google/cloud-sdk:431.0.0 - name: e2e-tests-artifacts-upload - volumes: - - name: github-app - path: /github-app - when: - status: - - success - - failure -- commands: - - yarn storybook:build - - ./bin/build verify-storybook - depends_on: - - rgm-package - - build-frontend-packages - environment: - NODE_OPTIONS: --max_old_space_size=4096 - image: node:22.16.0-alpine - name: build-storybook - when: - paths: - include: - - packages/grafana-ui/** -- commands: - - npx wait-on@7.0.1 http://$HOST:$PORT - - pa11y-ci --config e2e/pa11yci.conf.js - depends_on: - - grafana-server - environment: - GRAFANA_MISC_STATS_API_KEY: - from_secret: grafana_misc_stats_api_key - HOST: grafana-server - NO_THRESHOLDS: "false" - PORT: 3001 - failure: always - image: grafana/docker-puppeteer:1.1.0 - name: test-a11y-frontend -trigger: - event: - - pull_request - paths: - exclude: - - '*.md' - - docs/** - - latest.json -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker -- name: github-app - temp: {} ---- -clone: - retries: 3 -depends_on: [] -environment: - EDITION: oss -image_pull_secrets: -- gcr -- gar -kind: pipeline -name: pr-docs -node: - type: no-parallel -platform: - arch: amd64 - os: linux -services: [] -steps: -- commands: - - echo $DRONE_RUNNER_NAME - image: alpine:3.21.3 - name: identify-runner -- commands: - - yarn install --immutable || yarn install --immutable - depends_on: [] - image: node:22.16.0-alpine - name: yarn-install -- commands: - - yarn run prettier:checkDocs - depends_on: - - yarn-install - environment: - NODE_OPTIONS: --max_old_space_size=8192 - image: node:22.16.0-alpine - name: lint-docs -- commands: - - mkdir -p /hugo/content/docs/grafana/latest - - 'echo -e ''---\nredirectURL: /docs/grafana/latest/\ntype: redirect\nversioned: - true\n---\n'' > /hugo/content/docs/grafana/_index.md' - - cp -r docs/sources/* /hugo/content/docs/grafana/latest/ - - cd /hugo && make prod - image: grafana/docs-base:latest - name: build-docs-website - pull: always -- commands: - - '# It is required that code generated from Thema/CUE be committed and in sync - with its inputs.' - - '# The following command will fail if running code generators produces any diff - in output.' - - apk add --update make - - CODEGEN_VERIFY=1 make gen-cue - depends_on: [] - image: golang:1.24.5-alpine - name: verify-gen-cue -trigger: - event: - - pull_request - paths: - include: - - '*.md' - - docs/** - - packages/**/*.md - - latest.json - repo: - - grafana/grafana -type: docker -volumes: -- host: - path: /var/run/docker.sock - name: docker ---- -clone: - retries: 3 -depends_on: [] -environment: - EDITION: oss -image_pull_secrets: -- gcr -- gar -kind: pipeline name: main-docs node: type: no-parallel @@ -2365,8 +1873,3 @@ get: path: secret/data/common/gcr kind: secret name: gcr_credentials ---- -kind: signature -hmac: bfbdbe4aeedbcdc8778dd67abdfe94a75a20bdb314d1c53418cf2c2ea7a9f0c9 - -... diff --git a/.github/workflows/pr-e2e-tests.yml b/.github/workflows/pr-e2e-tests.yml index 6ceff2b9d45..5c9ad0e71d4 100644 --- a/.github/workflows/pr-e2e-tests.yml +++ b/.github/workflows/pr-e2e-tests.yml @@ -44,8 +44,6 @@ jobs: runs-on: ubuntu-latest-16-cores permissions: contents: read - outputs: - artifact: ${{ steps.artifact.outputs.artifact }} steps: - uses: actions/checkout@v4 with: @@ -57,7 +55,7 @@ jobs: - uses: actions/cache@v4 id: cache with: - key: "build-grafana-${{ runner.os }}-${{ hashFiles('yarn.lock', 'public/*', 'packages/*', 'pkg/**/*.go', '**/go.mod', '**/go.sum', '!**_test.go', '!**.test.ts', '!**.test.tsx') }}" + key: "build-grafana-${{ runner.os }}-${{ hashFiles('yarn.lock', 'public/*', 'packages/*', 'pkg/**/*.go', '**/go.mod', '**/go.sum', '!**_test.go', '!**.test.ts', '!**.test.tsx', 'Dockerfile') }}" path: | build-dir @@ -67,15 +65,16 @@ jobs: uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e with: verb: run - args: go run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="${PWD}" > out.txt + args: go run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 -a docker:grafana:linux/amd64 --grafana-dir="${PWD}" > out.txt - name: Cat built artifact if: steps.cache.outputs.cache-hit != 'true' run: cat out.txt - - name: Move built artifact + - name: Move built artifacts if: steps.cache.outputs.cache-hit != 'true' run: | mkdir -p build-dir - mv "$(cat out.txt)" build-dir/grafana.tar.gz + mv "$(grep 'grafana_.*tar.gz$' out.txt | grep -Fv -m1 'docker')" build-dir/grafana.tar.gz + mv "$(grep 'grafana_.*docker.tar.gz$' out.txt)" build-dir/grafana.docker.tar.gz # If cache hit, validate the artifact is present - name: Validate artifact @@ -90,14 +89,20 @@ jobs: run: echo "artifact=grafana-server-${{github.run_number}}" >> "$GITHUB_OUTPUT" id: artifact - - name: Upload artifact + - name: Upload grafana.tar.gz uses: actions/upload-artifact@v4 - id: upload with: retention-days: 1 - name: ${{ steps.artifact.outputs.artifact }} + name: grafana-tar-gz path: build-dir/grafana.tar.gz + - name: Upload grafana docker tarball + uses: actions/upload-artifact@v4 + with: + retention-days: 1 + name: grafana-docker-tar-gz + path: build-dir/grafana.docker.tar.gz + # TODO: we won't need this when we only have playwright build-e2e-runner: needs: detect-changes @@ -131,6 +136,66 @@ jobs: name: ${{ steps.artifact.outputs.artifact }} path: e2e-runner + push-docker-image: + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + needs: + - build-grafana + steps: + - id: vault-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@main + with: + repo_secrets: | + GRAFANA_DELIVERY_BOT_APP_PEM=delivery-bot-app:PRIVATE_KEY + - name: Generate token + id: generate_token + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a + with: + app_id: ${{ vars.DELIVERY_BOT_APP_ID }} + private_key: ${{ env.GRAFANA_DELIVERY_BOT_APP_PEM }} + repositories: '["grafana"]' + permissions: '{"checks": "write"}' + - uses: grafana/shared-workflows/actions/login-to-gar@main + id: login-to-gar + with: + registry: 'us-docker.pkg.dev' + environment: 'dev' + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: grafana-docker-tar-gz + path: . + - name: Load & Push Docker image + env: + BUILD_ID: ${{ github.run_id }} + run: | + set -euo pipefail + LOADED_IMAGE_NAME=$(docker load -i grafana.docker.tar.gz | sed 's/Loaded image: //g') + VERSION=$(echo "${LOADED_IMAGE_NAME}" | cut -d ':' -f 2 | cut -d '-' -f 1) + DOCKER_IMAGE="us-docker.pkg.dev/grafanalabs-dev/docker-grafana-dev/grafana:${VERSION}-${BUILD_ID}" + docker tag "${LOADED_IMAGE_NAME}" "${DOCKER_IMAGE}" + docker push "${DOCKER_IMAGE}" + echo "IMAGE=${DOCKER_IMAGE}" >> "$GITHUB_ENV" + - name: Add PR status check + env: + GH_TOKEN: ${{ steps.generate_token.outputs.token }} + SHA: ${{ github.event.pull_request.head.sha }} + run: | + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/grafana/grafana/check-runs \ + -f "name=${IMAGE}" \ + -f "head_sha=${SHA}" \ + -f 'status=completed' \ + -f 'conclusion=neutral' \ + -f 'output[title]=Docker image' \ + -f "output[summary]=${IMAGE}" \ + -f "output[text]=${IMAGE}" + run-e2e-tests: needs: - build-grafana @@ -166,7 +231,7 @@ jobs: persist-credentials: false - uses: actions/download-artifact@v4 with: - name: ${{ needs.build-grafana.outputs.artifact }} + name: grafana-tar-gz - uses: actions/download-artifact@v4 with: name: ${{ needs.build-e2e-runner.outputs.artifact }} @@ -242,7 +307,7 @@ jobs: persist-credentials: false - uses: actions/download-artifact@v4 with: - name: ${{ needs.build-grafana.outputs.artifact }} + name: grafana-tar-gz - name: Run E2E tests uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e with: @@ -354,7 +419,7 @@ jobs: persist-credentials: false - uses: actions/download-artifact@v4 with: - name: ${{ needs.build-grafana.outputs.artifact }} + name: grafana-tar-gz - name: Run PR a11y test if: github.event_name == 'pull_request' uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e