mirror of https://github.com/grafana/grafana
parent
9b8924fe74
commit
cea1f84437
@ -0,0 +1,146 @@ |
|||||||
|
name: Playwright E2E Tests |
||||||
|
|
||||||
|
on: |
||||||
|
pull_request: |
||||||
|
push: |
||||||
|
branches: |
||||||
|
- main |
||||||
|
- release-*.*.* |
||||||
|
|
||||||
|
concurrency: |
||||||
|
group: ${{ github.workflow }}-${{ github.ref }} |
||||||
|
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} |
||||||
|
|
||||||
|
permissions: {} |
||||||
|
|
||||||
|
jobs: |
||||||
|
build-grafana: |
||||||
|
name: Build & Package Grafana |
||||||
|
runs-on: ubuntu-latest-16-cores |
||||||
|
permissions: |
||||||
|
contents: read |
||||||
|
outputs: |
||||||
|
artifact: ${{ steps.artifact.outputs.artifact }} |
||||||
|
steps: |
||||||
|
- uses: actions/checkout@v4 |
||||||
|
with: |
||||||
|
path: ./grafana |
||||||
|
persist-credentials: false |
||||||
|
- uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e |
||||||
|
with: |
||||||
|
verb: run |
||||||
|
args: go -C grafana run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="${PWD}/grafana" > out.txt |
||||||
|
- run: mv "$(cat out.txt)" grafana.tar.gz |
||||||
|
- run: echo "artifact=grafana-playwright-${{github.run_number}}" >> "$GITHUB_OUTPUT" |
||||||
|
id: artifact |
||||||
|
- uses: actions/upload-artifact@v4 |
||||||
|
id: upload |
||||||
|
with: |
||||||
|
retention-days: 1 |
||||||
|
name: ${{ steps.artifact.outputs.artifact }} |
||||||
|
path: grafana.tar.gz |
||||||
|
|
||||||
|
run-playwright-tests: |
||||||
|
needs: |
||||||
|
- build-grafana |
||||||
|
strategy: |
||||||
|
fail-fast: false |
||||||
|
matrix: |
||||||
|
include: |
||||||
|
- suite: plugin-e2e-api-tests |
||||||
|
project: admin |
||||||
|
- suite: plugin-e2e-api-tests |
||||||
|
project: viewer |
||||||
|
- suite: plugin-e2e |
||||||
|
project: elasticsearch |
||||||
|
- suite: plugin-e2e |
||||||
|
project: mysql |
||||||
|
- suite: plugin-e2e |
||||||
|
project: mssql |
||||||
|
- suite: plugin-e2e |
||||||
|
project: cloudwatch |
||||||
|
- suite: plugin-e2e |
||||||
|
project: azuremonitor |
||||||
|
- suite: plugin-e2e |
||||||
|
project: cloudmonitoring |
||||||
|
- suite: plugin-e2e |
||||||
|
project: graphite |
||||||
|
- suite: plugin-e2e |
||||||
|
project: influxdb |
||||||
|
- suite: plugin-e2e |
||||||
|
project: jaeger |
||||||
|
- suite: plugin-e2e |
||||||
|
project: zipkin |
||||||
|
- suite: core-tests |
||||||
|
project: panels |
||||||
|
- suite: core-tests |
||||||
|
project: smoke |
||||||
|
- suite: core-tests |
||||||
|
project: dashboards |
||||||
|
- suite: core-tests |
||||||
|
project: loki |
||||||
|
- suite: test-plugins |
||||||
|
project: extensions-test-app |
||||||
|
- suite: test-plugins |
||||||
|
project: grafana-e2etest-datasource |
||||||
|
name: ${{ matrix.suite }}-${{ matrix.project }} |
||||||
|
runs-on: ubuntu-latest-8-cores |
||||||
|
permissions: |
||||||
|
contents: read |
||||||
|
|
||||||
|
steps: |
||||||
|
- uses: actions/checkout@v4 |
||||||
|
with: |
||||||
|
persist-credentials: false |
||||||
|
- uses: actions/download-artifact@v4 |
||||||
|
with: |
||||||
|
name: ${{ needs.build-grafana.outputs.artifact }} |
||||||
|
- name: Run Playwright E2E tests |
||||||
|
uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e |
||||||
|
with: |
||||||
|
verb: run |
||||||
|
args: go run ./pkg/build/daggerbuild/playwright run --package=grafana.tar.gz --project=${{ matrix.project }} |
||||||
|
- name: Set suite name |
||||||
|
id: set-suite-name |
||||||
|
if: success() || failure() |
||||||
|
env: |
||||||
|
SUITE: ${{ matrix.suite }}-${{ matrix.project }} |
||||||
|
run: | |
||||||
|
set -euo pipefail |
||||||
|
echo "suite=$(echo "$SUITE" | sed 's/\//-/g')" >> "$GITHUB_OUTPUT" |
||||||
|
- uses: actions/upload-artifact@v4 |
||||||
|
if: success() || failure() |
||||||
|
with: |
||||||
|
name: playwright-report-${{ steps.set-suite-name.outputs.suite }}-${{ github.run_number }} |
||||||
|
path: playwright-report |
||||||
|
retention-days: 3 |
||||||
|
- uses: actions/upload-artifact@v4 |
||||||
|
if: success() || failure() |
||||||
|
with: |
||||||
|
name: test-results-${{ steps.set-suite-name.outputs.suite }}-${{ github.run_number }} |
||||||
|
path: test-results |
||||||
|
retention-days: 3 |
||||||
|
|
||||||
|
# This is the job that is actually required by rulesets. |
||||||
|
# We want to only require one job instead of all the individual tests. |
||||||
|
required-playwright-tests: |
||||||
|
needs: |
||||||
|
- run-playwright-tests |
||||||
|
# always() is the best function here. |
||||||
|
# success() || failure() will skip this function if any need is also skipped. |
||||||
|
# That means conditional test suites will fail the entire requirement check. |
||||||
|
if: always() |
||||||
|
|
||||||
|
name: All Playwright E2E tests complete |
||||||
|
runs-on: ubuntu-latest |
||||||
|
steps: |
||||||
|
- name: Check test suites |
||||||
|
env: |
||||||
|
NEEDS: ${{ toJson(needs) }} |
||||||
|
run: | |
||||||
|
FAILURES="$(echo "$NEEDS" | jq 'with_entries(select(.value.result == "failure")) | map_values(.result)')" |
||||||
|
echo "$FAILURES" |
||||||
|
if [ "$(echo "$FAILURES" | jq '. | length')" != "0" ]; then |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
echo "All OK!" |
@ -0,0 +1,22 @@ |
|||||||
|
# Pa11y accessability tests |
||||||
|
|
||||||
|
We use pa11y to run some automated simple accessability tests. They're ran with dagger to help orchestrate starting server + tests in a reproducable manner. |
||||||
|
|
||||||
|
To run the tests locally: |
||||||
|
|
||||||
|
1. Install dagger locally https://docs.dagger.io/install/ |
||||||
|
2. Grab the grafana.tar.gz artifact by either |
||||||
|
1. Downloading it from the Github Action artifact from your PR |
||||||
|
1. Build it locally with: |
||||||
|
```sh |
||||||
|
dagger run go run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="$PWD" > dist/files.txt |
||||||
|
cat dist/files.txt # Will output the path to the grafana.tar.gz |
||||||
|
``` |
||||||
|
3. Run the dagger pipeline with: |
||||||
|
```sh |
||||||
|
dagger -v run go run ./pkg/build/a11y --package=(full path to .tar.gz) --results=./pa11y-ci-results.json |
||||||
|
``` |
||||||
|
The JSON results file will be saved to the file from the `--results` arg |
||||||
|
4. If they fail and you want to see the full output |
||||||
|
1. Run the dagger command with `dagger -vE [...]` |
||||||
|
2. At the end, arrow up to the exec pa11y-ci segment and hit Enter |
@ -0,0 +1,177 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"os" |
||||||
|
"os/signal" |
||||||
|
|
||||||
|
"dagger.io/dagger" |
||||||
|
"github.com/urfave/cli/v3" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
grafanaHost = "grafana" |
||||||
|
grafanaPort = 3001 |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) |
||||||
|
defer cancel() |
||||||
|
|
||||||
|
if err := NewApp().Run(ctx, os.Args); err != nil { |
||||||
|
cancel() |
||||||
|
fmt.Println(err) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func NewApp() *cli.Command { |
||||||
|
return &cli.Command{ |
||||||
|
Name: "a11y", |
||||||
|
Usage: "Run Grafana playwright e2e tests", |
||||||
|
Flags: []cli.Flag{ |
||||||
|
&cli.StringFlag{ |
||||||
|
Name: "grafana-dir", |
||||||
|
Usage: "Path to the grafana/grafana clone directory", |
||||||
|
Value: ".", |
||||||
|
Validator: mustBeDir("grafana-dir"), |
||||||
|
TakesFile: true, |
||||||
|
}, |
||||||
|
&cli.StringFlag{ |
||||||
|
Name: "package", |
||||||
|
Usage: "Path to the grafana tar.gz package", |
||||||
|
Value: "grafana.tar.gz", |
||||||
|
Validator: mustBeFile("package", false), |
||||||
|
TakesFile: true, |
||||||
|
}, |
||||||
|
&cli.StringFlag{ |
||||||
|
Name: "license", |
||||||
|
Usage: "Path to the Grafana Enterprise license file (optional)", |
||||||
|
Validator: mustBeFile("license", true), |
||||||
|
TakesFile: true, |
||||||
|
}, |
||||||
|
// &cli.StringFlag{
|
||||||
|
// Name: "config",
|
||||||
|
// Usage: "Path to the pa11y config file to use",
|
||||||
|
// Value: "e2e/pa11yci.conf.js",
|
||||||
|
// Validator: mustBeFile("config", true),
|
||||||
|
// TakesFile: true,
|
||||||
|
// },
|
||||||
|
// &cli.StringFlag{
|
||||||
|
// Name: "results",
|
||||||
|
// Usage: "Path to the pa11y results file to export",
|
||||||
|
// TakesFile: true,
|
||||||
|
// },
|
||||||
|
// &cli.BoolFlag{
|
||||||
|
// Name: "no-threshold-fail",
|
||||||
|
// Usage: "Don't fail the task if any of the tests fail. Use this in combination with --results to list all violations even if they're within thresholds",
|
||||||
|
// Value: false,
|
||||||
|
// },
|
||||||
|
}, |
||||||
|
Action: run, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func run(ctx context.Context, cmd *cli.Command) error { |
||||||
|
grafanaDir := cmd.String("grafana-dir") |
||||||
|
targzPath := cmd.String("package") |
||||||
|
licensePath := cmd.String("license") |
||||||
|
// pa11yConfigPath := cmd.String("config")
|
||||||
|
// pa11yResultsPath := cmd.String("results")
|
||||||
|
// noThresholdFail := cmd.Bool("no-threshold-fail")
|
||||||
|
|
||||||
|
d, err := dagger.Connect(ctx) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("failed to connect to Dagger: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Explicitly only the files used by the grafana-server service
|
||||||
|
hostSrc := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{ |
||||||
|
Include: []string{ |
||||||
|
"./devenv", |
||||||
|
"./e2e/test-plugins", // Directory is included so provisioning works, but they're not actually build
|
||||||
|
"./scripts/grafana-server/custom.ini", |
||||||
|
"./scripts/grafana-server/start-server", |
||||||
|
"./scripts/grafana-server/kill-server", |
||||||
|
"./scripts/grafana-server/variables", |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
targz := d.Host().File(targzPath) |
||||||
|
// pa11yConfig := d.Host().File(pa11yConfigPath)
|
||||||
|
|
||||||
|
var license *dagger.File |
||||||
|
if licensePath != "" { |
||||||
|
license = d.Host().File(licensePath) |
||||||
|
} |
||||||
|
|
||||||
|
svc, err := GrafanaService(ctx, d, GrafanaServiceOpts{ |
||||||
|
HostSrc: hostSrc, |
||||||
|
GrafanaTarGz: targz, |
||||||
|
License: license, |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("failed to create Grafana service: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
c, runErr := RunTest(ctx, d, svc) |
||||||
|
if runErr != nil { |
||||||
|
return fmt.Errorf("failed to run a11y test suite: %w", runErr) |
||||||
|
} |
||||||
|
|
||||||
|
c, syncErr := c.Sync(ctx) |
||||||
|
if syncErr != nil { |
||||||
|
return fmt.Errorf("failed to sync a11y test suite: %w", syncErr) |
||||||
|
} |
||||||
|
|
||||||
|
code, codeErr := c.ExitCode(ctx) |
||||||
|
if codeErr != nil { |
||||||
|
return fmt.Errorf("failed to get exit code of a11y test suite: %w", codeErr) |
||||||
|
} |
||||||
|
|
||||||
|
if code == 0 { |
||||||
|
log.Printf("a11y tests passed with exit code %d", code) |
||||||
|
} else { |
||||||
|
return fmt.Errorf("a11y tests failed with exit code %d", code) |
||||||
|
} |
||||||
|
|
||||||
|
log.Println("a11y tests completed successfully") |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func mustBeFile(arg string, emptyOk bool) func(string) error { |
||||||
|
return func(s string) error { |
||||||
|
if s == "" { |
||||||
|
if emptyOk { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return cli.Exit(arg+" cannot be empty", 1) |
||||||
|
} |
||||||
|
stat, err := os.Stat(s) |
||||||
|
if err != nil { |
||||||
|
return cli.Exit(arg+" does not exist or cannot be read: "+s, 1) |
||||||
|
} |
||||||
|
if stat.IsDir() { |
||||||
|
return cli.Exit(arg+" must be a file, not a directory: "+s, 1) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func mustBeDir(arg string) func(string) error { |
||||||
|
return func(s string) error { |
||||||
|
if s == "" { |
||||||
|
return cli.Exit(arg+" cannot be empty", 1) |
||||||
|
} |
||||||
|
stat, err := os.Stat(s) |
||||||
|
if err != nil { |
||||||
|
return cli.Exit(arg+" does not exist or cannot be read: "+s, 1) |
||||||
|
} |
||||||
|
if !stat.IsDir() { |
||||||
|
return cli.Exit(arg+" must be a directory: "+s, 1) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,79 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"dagger.io/dagger" |
||||||
|
) |
||||||
|
|
||||||
|
// NodeVersionContainer returns a container whose `stdout` will return the node version from the '.nvmrc' file in the directory 'src'.
|
||||||
|
func NodeVersion(d *dagger.Client, src *dagger.File) *dagger.Container { |
||||||
|
return d.Container().From("alpine:3"). |
||||||
|
WithMountedFile("/src/.nvmrc", src). |
||||||
|
WithWorkdir("/src"). |
||||||
|
WithExec([]string{"cat", ".nvmrc"}) |
||||||
|
} |
||||||
|
|
||||||
|
func NodeImage(version string) string { |
||||||
|
return fmt.Sprintf("node:%s-slim", strings.TrimPrefix(strings.TrimSpace(version), "v")) |
||||||
|
} |
||||||
|
|
||||||
|
func RunTest( |
||||||
|
ctx context.Context, |
||||||
|
d *dagger.Client, |
||||||
|
grafanaService *dagger.Service, |
||||||
|
) (*dagger.Container, error) { |
||||||
|
|
||||||
|
// Explicitly only the files u'sed by e2e tests
|
||||||
|
hostSrc := d.Host().Directory(".", dagger.HostDirectoryOpts{ |
||||||
|
Include: []string{ |
||||||
|
// Include all files for a valid yarn workspace install
|
||||||
|
"package.json", |
||||||
|
"yarn.lock", |
||||||
|
".yarnrc.yml", |
||||||
|
".yarn", |
||||||
|
"packages/*/package.json", |
||||||
|
"public/app/plugins/*/*/package.json", |
||||||
|
"e2e/test-plugins/*/package.json", |
||||||
|
".nvmrc", |
||||||
|
"public/app/types/*.d.ts", |
||||||
|
|
||||||
|
// packages we use in playwright tests
|
||||||
|
"packages", |
||||||
|
|
||||||
|
// e2e files
|
||||||
|
"e2e-playwright", |
||||||
|
}, |
||||||
|
Exclude: []string{ |
||||||
|
"packages/*/dist", |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
nodeVersion, err := NodeVersion(d, hostSrc.File(".nvmrc")).Stdout(ctx) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
yarnCache := d.CacheVolume("yarn-cache") |
||||||
|
yarnCacheDir := "/yarn-cache" |
||||||
|
|
||||||
|
pa11yContainer := d.Container().From(NodeImage(nodeVersion)). |
||||||
|
WithExec([]string{"npx", "-y", "playwright@1.52.0", "install", "--with-deps"}). // TODO: sync version from package.json
|
||||||
|
WithWorkdir("/src"). |
||||||
|
WithDirectory("/src", hostSrc). |
||||||
|
WithMountedCache(yarnCacheDir, yarnCache). |
||||||
|
WithExec([]string{"corepack", "enable"}). |
||||||
|
WithExec([]string{"corepack", "install"}). |
||||||
|
WithEnvVariable("YARN_CACHE_FOLDER", yarnCacheDir). |
||||||
|
WithExec([]string{"yarn", "config", "get", "cacheFolder"}). |
||||||
|
WithExec([]string{"yarn", "install", "--immutable"}). |
||||||
|
// WithExec([]string{"yarn", "e2e:plugin:build"}).
|
||||||
|
WithEnvVariable("HOST", grafanaHost). |
||||||
|
WithEnvVariable("PORT", fmt.Sprint(grafanaPort)). |
||||||
|
WithExec([]string{"yarn", "packages:build"}). |
||||||
|
WithExec([]string{"yarn", "e2e:playwright"}) |
||||||
|
|
||||||
|
return pa11yContainer, nil |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"dagger.io/dagger" |
||||||
|
) |
||||||
|
|
||||||
|
type GrafanaServiceOpts struct { |
||||||
|
HostSrc *dagger.Directory |
||||||
|
GrafanaTarGz *dagger.File |
||||||
|
License *dagger.File |
||||||
|
} |
||||||
|
|
||||||
|
func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOpts) (*dagger.Service, error) { |
||||||
|
container := d.Container().From("alpine:3"). |
||||||
|
WithExec([]string{"apk", "add", "--no-cache", "bash", "tar", "netcat-openbsd"}). |
||||||
|
WithMountedFile("/src/grafana.tar.gz", opts.GrafanaTarGz). |
||||||
|
WithExec([]string{"mkdir", "-p", "/src/grafana"}). |
||||||
|
WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/grafana.tar.gz", "-C", "/src/grafana"}). |
||||||
|
WithDirectory("/src/grafana/devenv", opts.HostSrc.Directory("./devenv")). |
||||||
|
WithDirectory("/src/grafana/e2e/test-plugins", opts.HostSrc.Directory("./e2e/test-plugins")). |
||||||
|
WithDirectory("/src/grafana/scripts", opts.HostSrc.Directory("./scripts")). |
||||||
|
WithWorkdir("/src/grafana"). |
||||||
|
WithEnvVariable("GF_APP_MODE", "development"). |
||||||
|
WithEnvVariable("GF_SERVER_HTTP_PORT", fmt.Sprint(grafanaPort)). |
||||||
|
WithEnvVariable("GF_SERVER_ROUTER_LOGGING", "1"). |
||||||
|
WithExposedPort(grafanaPort) |
||||||
|
|
||||||
|
var licenseArg string |
||||||
|
if opts.License != nil { |
||||||
|
licenseArg = "/src/license.jwt" |
||||||
|
container = container.WithMountedFile(licenseArg, opts.License) |
||||||
|
} |
||||||
|
|
||||||
|
// We add all GF_ environment variables to allow for overriding Grafana configuration.
|
||||||
|
// It is unlikely the runner has any such otherwise.
|
||||||
|
for _, env := range os.Environ() { |
||||||
|
if strings.HasPrefix(env, "GF_") { |
||||||
|
parts := strings.SplitN(env, "=", 2) |
||||||
|
container = container.WithEnvVariable(parts[0], parts[1]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
svc := container.AsService(dagger.ContainerAsServiceOpts{Args: []string{"bash", "-x", "scripts/grafana-server/start-server", licenseArg}}) |
||||||
|
|
||||||
|
return svc, nil |
||||||
|
} |
Loading…
Reference in new issue