diff --git a/.github/workflows/pr-e2e-tests.yml b/.github/workflows/pr-e2e-tests.yml index c01507f29230a..7042aa751abe9 100644 --- a/.github/workflows/pr-e2e-tests.yml +++ b/.github/workflows/pr-e2e-tests.yml @@ -19,101 +19,85 @@ env: RUNNER_DEBUG: 1 jobs: - build-grafana: - name: Build & Package Grafana - runs-on: blacksmith-8vcpu-ubuntu-2404 + run-playwright-tests: + name: Playwright E2E tests (${{ matrix.shard }}/${{ matrix.shardTotal }}) + runs-on: blacksmith-2vcpu-ubuntu-2404 permissions: contents: read + + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4, 5, 6, 7, 8] + shardTotal: [8] + steps: - uses: actions/checkout@v5 with: persist-credentials: false - # TODO: add a cleanup workflow to remove the cache when the PR is closed - # https://github.com/actions/cache/blob/main/tips-and-workarounds.md#force-deletion-of-caches-overriding-default-cache-eviction-policy - # TODO: maybe we could just use the cache to store the build, instead of uploading as an artifact? - - uses: actions/cache@v4 - id: cache + - name: Setup Go + uses: actions/setup-go@v5 with: - key: "build-grafana-${{ runner.os }}-${{ hashFiles('yarn.lock', 'public/*', 'packages/*', 'conf/*', 'pkg/**/*.go', '**/go.mod', '**/go.sum', '!**_test.go', '!**.test.ts', '!**.test.tsx', 'Dockerfile') }}" - path: | - build-dir + go-version: "1.25.3" + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "24.11.0" - # If no cache hit, build Grafana - - name: Build Grafana - if: steps.cache.outputs.cache-hit != 'true' - uses: dagger/dagger-for-github@d913e70051faf3b907d4dd96ef1161083c88c644 + - name: Cache Node modules + uses: actions/cache@v4 with: - version: 0.18.8 - verb: run - 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 artifacts - if: steps.cache.outputs.cache-hit != 'true' + path: | + ~/.npm + .yarn/cache + key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies run: | - mkdir -p build-dir - 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 + yarn install --frozen-lockfile - # If cache hit, validate the artifact is present - - name: Validate artifact - if: steps.cache.outputs.cache-hit == 'true' + - name: Build app run: | - if [ ! -f build-dir/grafana.tar.gz ]; then - echo "Error: build-dir/grafana.tar.gz not found in cache" - exit 1 - fi + yarn build - - name: Set artifact name - run: echo "artifact=grafana-server-${{github.run_number}}" >> "$GITHUB_OUTPUT" - id: artifact + - name: Build test plugins + run: | + yarn e2e:plugin:build - - name: Upload grafana.tar.gz - uses: actions/upload-artifact@v5 - with: - retention-days: 1 - name: grafana-tar-gz - path: build-dir/grafana.tar.gz + - name: Start Grafana server + run: | + make run > grafana-server.log 2>&1 & + GRAFANA_PID=$! + echo "GRAFANA_PID=$GRAFANA_PID" >> $GITHUB_ENV - - name: Upload grafana docker tarball - uses: actions/upload-artifact@v5 - with: - retention-days: 1 - name: grafana-docker-tar-gz - path: build-dir/grafana.docker.tar.gz + # Wait for Grafana to be ready + timeout 240 bash -c 'until curl -f http://localhost:3000/api/health; do sleep 2; done' - run-playwright-tests: - needs: - - build-grafana - name: Playwright E2E tests (${{ matrix.shard }}/${{ matrix.shardTotal }}) - runs-on: blacksmith-8vcpu-ubuntu-2404 - permissions: - contents: read + - name: Run E2E tests + run: | + export CI=true + export PLAYWRIGHT_HTML_OPEN=never + export PLAYWRIGHT_BLOB_OUTPUT_DIR=./blob-report + export ENDFORM_API_KEY=${{ secrets.ENDFORM_API_KEY }} - strategy: - fail-fast: false - matrix: - shard: [1] - shardTotal: [8] + yarn e2e:playwright --shard=${{ matrix.shard }}/${{ matrix.shardTotal }} - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - - uses: actions/download-artifact@v6 - with: - name: grafana-tar-gz - - name: Run E2E tests - uses: dagger/dagger-for-github@d913e70051faf3b907d4dd96ef1161083c88c644 - with: - version: 0.18.8 - verb: run - args: go run ./pkg/build/e2e-playwright --package=grafana.tar.gz --shard=${{ matrix.shard }}/${{ matrix.shardTotal }} --blob-dir=./blob-report - - uses: actions/upload-artifact@v5 - if: success() || failure() + - name: Stop Grafana server + if: always() + run: | + if [ -n "$GRAFANA_PID" ]; then + kill $GRAFANA_PID || true + fi + + - name: Upload server logs + if: always() + uses: actions/upload-artifact@v4 with: - name: playwright-blob-${{ github.run_number }}-${{ matrix.shard }} - path: ./blob-report - retention-days: 1 + name: server-logs-${{ matrix.shard }} + path: | + grafana-server.log + playwright-webserver.log diff --git a/e2e/old-arch/utils/index.ts b/e2e/old-arch/utils/index.ts index 011250f7b8a35..d4d28a62cdfc0 100644 --- a/e2e/old-arch/utils/index.ts +++ b/e2e/old-arch/utils/index.ts @@ -1,4 +1,4 @@ -import { E2ESelectors, Selectors, selectors } from '@grafana/e2e-selectors'; +import { type E2ESelectors, type Selectors, selectors } from '@grafana/e2e-selectors'; import * as flows from './flows'; import { e2eFactory } from './support'; diff --git a/e2e/utils/index.ts b/e2e/utils/index.ts index 011250f7b8a35..d4d28a62cdfc0 100644 --- a/e2e/utils/index.ts +++ b/e2e/utils/index.ts @@ -1,4 +1,4 @@ -import { E2ESelectors, Selectors, selectors } from '@grafana/e2e-selectors'; +import { type E2ESelectors, type Selectors, selectors } from '@grafana/e2e-selectors'; import * as flows from './flows'; import { e2eFactory } from './support'; diff --git a/endform.jsonc b/endform.jsonc new file mode 100644 index 0000000000000..6a3afb78aec05 --- /dev/null +++ b/endform.jsonc @@ -0,0 +1,3 @@ +{ + "proxyNetworkHosts": ["", "*.dagger.local", "host.docker.internal", "*:3001", "10.87.0.25:3001", "3ugvf97q49rve", "3ugvf97q49rve:3001"] +} diff --git a/package.json b/package.json index 5d7dd234e25f1..440a09a466bde 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "e2e:enterprise": "./e2e/start-and-run-suite enterprise", "e2e:enterprise:dev": "./e2e/start-and-run-suite enterprise dev", "e2e:enterprise:debug": "./e2e/start-and-run-suite enterprise debug", - "e2e:playwright": "yarn playwright test --grep-invert @cloud-plugins", + "e2e:playwright": "yarn endform test --grep-invert @cloud-plugins", "e2e:playwright:cloud-plugins": "yarn playwright test --grep @cloud-plugins", "e2e:playwright:storybook": "yarn playwright test -c playwright.storybook.config.ts", "e2e:acceptance": "yarn playwright test --grep @acceptance", @@ -182,6 +182,7 @@ "cypress": "14.3.2", "cypress-file-upload": "5.0.8", "cypress-recurse": "^1.35.3", + "endform": "^0.45.12", "enquirer": "^2.4.1", "esbuild": "0.25.8", "esbuild-loader": "4.3.0", diff --git a/packages/grafana-e2e-selectors/src/selectors/index.ts b/packages/grafana-e2e-selectors/src/selectors/index.ts index bfe90848026b8..4121569bc1a6b 100644 --- a/packages/grafana-e2e-selectors/src/selectors/index.ts +++ b/packages/grafana-e2e-selectors/src/selectors/index.ts @@ -1,8 +1,8 @@ import { resolveSelectors } from '../resolver'; import { E2ESelectors } from '../types'; -import { versionedComponents, VersionedComponents } from './components'; -import { versionedPages, VersionedPages } from './pages'; +import { versionedComponents, type VersionedComponents } from './components'; +import { versionedPages, type VersionedPages } from './pages'; const Pages = resolveSelectors(versionedPages); const Components = resolveSelectors(versionedComponents); diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index d8b252ba40694..967cc34808790 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -780,19 +780,16 @@ func (hs *HTTPServer) mapStatic(m *web.Mux, rootDir string, dir string, prefix s if prefix == "public/build" { headers = func(c *web.Context) { - c.Resp.Header().Set("Cache-Control", "public, max-age=31536000") + c.Resp.Header().Set("Cache-Control", "public, max-age=31536000, immutable") } - } - - if hs.Cfg.Env == setting.Dev { + } else if prefix == "mockServiceWorker.js" { headers = func(c *web.Context) { c.Resp.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache") + c.Resp.Header().Set("Content-Type", "application/javascript") } - } - - if prefix == "mockServiceWorker.js" { + } else if hs.Cfg.Env == setting.Dev { headers = func(c *web.Context) { - c.Resp.Header().Set("Content-Type", "application/javascript") + c.Resp.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache") } } diff --git a/pkg/build/e2e-playwright/e2e.go b/pkg/build/e2e-playwright/e2e.go index f23cbdeafe819..584966e4a3481 100644 --- a/pkg/build/e2e-playwright/e2e.go +++ b/pkg/build/e2e-playwright/e2e.go @@ -25,6 +25,7 @@ type RunTestOpts struct { TestResultsExportDir string PlaywrightCommand string CloudPluginCreds *dagger.File + EndformAPIKey string } func RunTest( @@ -56,6 +57,10 @@ func RunTest( e2eContainer = e2eContainer.WithMountedFile("/tmp/outputs.json", opts.CloudPluginCreds) } + if opts.EndformAPIKey != "" { + e2eContainer = e2eContainer.WithEnvVariable("ENDFORM_API_KEY", opts.EndformAPIKey) + } + e2eContainer = e2eContainer.WithExec(playwrightCommand, dagger.ContainerWithExecOpts{ Expect: dagger.ReturnTypeAny, }) @@ -99,12 +104,7 @@ func buildPlaywrightCommand(opts RunTestOpts) []string { playwrightExec := strings.Split(opts.PlaywrightCommand, " ") - playwrightCommand := append(playwrightExec, - "--reporter", - strings.Join(playwrightReporters, ","), - "--output", - testResultsDir, - ) + playwrightCommand := playwrightExec if opts.Shard != "" { playwrightCommand = append(playwrightCommand, "--shard", opts.Shard) diff --git a/pkg/build/e2e-playwright/main.go b/pkg/build/e2e-playwright/main.go index 97f4caf2e0efd..6a2699e06f5c6 100644 --- a/pkg/build/e2e-playwright/main.go +++ b/pkg/build/e2e-playwright/main.go @@ -97,6 +97,7 @@ func run(ctx context.Context, cmd *cli.Command) error { htmlDir := cmd.String("html-dir") blobDir := cmd.String("blob-dir") playwrightCommand := cmd.String("playwright-command") + endformAPIKey := os.Getenv("ENDFORM_API_KEY") // pa11yConfigPath := cmd.String("config") // pa11yResultsPath := cmd.String("results") // noThresholdFail := cmd.Bool("no-threshold-fail") @@ -194,6 +195,7 @@ func run(ctx context.Context, cmd *cli.Command) error { BlobReportExportDir: blobDir, PlaywrightCommand: playwrightCommand, CloudPluginCreds: cloudPluginCreds, + EndformAPIKey: endformAPIKey, } c, runErr := RunTest(ctx, d, runOpts) diff --git a/pkg/build/e2eutil/frontend_container.go b/pkg/build/e2eutil/frontend_container.go index e9de7e6c975cd..522240dae08f9 100644 --- a/pkg/build/e2eutil/frontend_container.go +++ b/pkg/build/e2eutil/frontend_container.go @@ -25,9 +25,8 @@ func WithFrontendContainer(ctx context.Context, d *dagger.Client, yarnHostSrc *d } nodeBase := WithNode(d, deps.NodeVersion) - playwrightBase := WithPlaywright(d, nodeBase, deps.PlaywrightVersion) - return WithYarnInstall(d, playwrightBase, yarnHostSrc), nil + return WithYarnInstall(d, nodeBase, yarnHostSrc), nil } func GetVersions(ctx context.Context, src *dagger.Directory) (Deps, error) { @@ -56,15 +55,10 @@ func GetVersions(ctx context.Context, src *dagger.Directory) (Deps, error) { func WithNode(d *dagger.Client, version string) *dagger.Container { nodeImage := fmt.Sprintf("node:%s-slim", strings.TrimPrefix(version, "v")) - return d.Container().From(nodeImage) -} - -func WithPlaywright(d *dagger.Client, base *dagger.Container, version string) *dagger.Container { - brCache := d.CacheVolume("playwright-browsers") - return base. - WithEnvVariable("PLAYWRIGHT_BROWSERS_PATH", "/playwright-cache"). - WithMountedCache("/playwright-cache", brCache). - WithExec([]string{"npx", "-y", "playwright@" + version, "install", "--with-deps"}) + return d.Container().From(nodeImage). + WithExec([]string{"apt-get", "update"}). + WithExec([]string{"apt-get", "install", "-y", "ca-certificates"}). + WithExec([]string{"rm", "-rf", "/var/lib/apt/lists/*"}) } func WithYarnInstall(d *dagger.Client, base *dagger.Container, yarnHostSrc *dagger.Directory) *dagger.Container { diff --git a/playwright.config.ts b/playwright.config.ts index decf031fbc1d4..acca37b42f654 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -7,6 +7,8 @@ export const testDirRoot = 'e2e-playwright'; const pluginDirRoot = path.join(testDirRoot, 'plugin-e2e'); export const DEFAULT_URL = 'http://localhost:3001'; +process.env.GRAFANA_VERSION = '12.2.1'; + export function withAuth(project: Project): Project { project.dependencies ??= []; project.use ??= {}; @@ -26,16 +28,15 @@ export const baseConfig: PlaywrightTestConfig = { retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, - reporter: [ - ['html'], // pretty - ], + reporter: [['list']], + timeout: 120_000, expect: { - timeout: 10_000, + timeout: 20_000, }, use: { ...devices['Desktop Chrome'], baseURL: process.env.GRAFANA_URL ?? DEFAULT_URL, - trace: 'retain-on-failure', + trace: 'on', httpCredentials: { username: 'admin', password: 'admin', @@ -50,10 +51,11 @@ export default defineConfig({ ...baseConfig, ...(!process.env.GRAFANA_URL && { webServer: { - command: 'yarn e2e:plugin:build && ./e2e-playwright/start-server', - url: DEFAULT_URL, + command: 'yarn e2e:plugin:build && ./e2e-playwright/start-server > playwright-webserver.log 2>&1', + port: 3001, stdout: 'pipe', stderr: 'pipe', + reuseExistingServer: true, }, }), projects: [ diff --git a/public/app/plugins/datasource/azuremonitor/e2e/selectors.ts b/public/app/plugins/datasource/azuremonitor/e2e/selectors.ts index fade27c12bbf6..ddfc761e28cb1 100644 --- a/public/app/plugins/datasource/azuremonitor/e2e/selectors.ts +++ b/public/app/plugins/datasource/azuremonitor/e2e/selectors.ts @@ -1,4 +1,4 @@ -import { E2ESelectors } from '@grafana/e2e-selectors'; +import type { E2ESelectors } from '@grafana/e2e-selectors'; export const components = { configEditor: { diff --git a/public/app/plugins/datasource/cloud-monitoring/e2e/selectors.ts b/public/app/plugins/datasource/cloud-monitoring/e2e/selectors.ts index fd88d490408a9..8f979bf6f3437 100644 --- a/public/app/plugins/datasource/cloud-monitoring/e2e/selectors.ts +++ b/public/app/plugins/datasource/cloud-monitoring/e2e/selectors.ts @@ -1,4 +1,4 @@ -import { E2ESelectors } from '@grafana/e2e-selectors'; +import type { E2ESelectors } from '@grafana/e2e-selectors'; export const components = { queryEditor: { diff --git a/yarn.lock b/yarn.lock index 9a61d066022c6..49225b795674c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16118,6 +16118,57 @@ __metadata: languageName: node linkType: hard +"endform-darwin-arm64@npm:0.45.12": + version: 0.45.12 + resolution: "endform-darwin-arm64@npm:0.45.12" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"endform-darwin-x86@npm:0.45.12": + version: 0.45.12 + resolution: "endform-darwin-x86@npm:0.45.12" + conditions: os=darwin & cpu=x86 + languageName: node + linkType: hard + +"endform-linux-arm64@npm:0.45.12": + version: 0.45.12 + resolution: "endform-linux-arm64@npm:0.45.12" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"endform-linux-x86@npm:0.45.12": + version: 0.45.12 + resolution: "endform-linux-x86@npm:0.45.12" + conditions: os=linux & cpu=x86 + languageName: node + linkType: hard + +"endform@npm:^0.45.12": + version: 0.45.12 + resolution: "endform@npm:0.45.12" + dependencies: + endform-darwin-arm64: "npm:0.45.12" + endform-darwin-x86: "npm:0.45.12" + endform-linux-arm64: "npm:0.45.12" + endform-linux-x86: "npm:0.45.12" + dependenciesMeta: + endform-darwin-arm64: + optional: true + endform-darwin-x86: + optional: true + endform-linux-arm64: + optional: true + endform-linux-x86: + optional: true + bin: + endform: bin/endform + checksum: 10/beba5282d625f1c20a3f1462a7d7ac731734dd9e5502da718b0b58886c669d91562280795a782c748b9dcfdcf6502459fab90ad5d6910dfb359b84e4085bafcc + languageName: node + linkType: hard + "engine.io-parser@npm:~5.2.1": version: 5.2.3 resolution: "engine.io-parser@npm:5.2.3" @@ -19004,6 +19055,7 @@ __metadata: date-fns: "npm:4.1.0" debounce-promise: "npm:3.1.2" diff: "npm:^8.0.0" + endform: "npm:^0.45.12" enquirer: "npm:^2.4.1" esbuild: "npm:0.25.8" esbuild-loader: "npm:4.3.0"