grafana/k6 · error

both K6_CLOUD_METRICS_PUSH_URL and K6_CLOUD_TEST_RUN_TOKEN m

Error message

both K6_CLOUD_METRICS_PUSH_URL and K6_CLOUD_TEST_RUN_TOKEN must be set together

What it means

Thrown by applyExternalProvisioningCreds when exactly one of K6_CLOUD_METRICS_PUSH_URL and K6_CLOUD_TEST_RUN_TOKEN is set (the `(pushURL == "") != (token == "")` XOR check). An externally-provisioned cloud run needs both the metrics endpoint and the scoped token that authenticates to it.

Source

Thrown at internal/cmd/outputs_cloud.go:225

// values the self-provision flow obtains from the provisioning response.
// It requires both-or-neither and, when both are present, sets them on conf
// (so downstream consumers such as the log pusher see them) and bakes them
// into the serialized cloud config that the Output reads. It also reads the
// K6_CLOUD_LOGS_* log-push config, which the external flow supplies the same
// way.
func applyExternalProvisioningCreds(
	gs *state.GlobalState, test *loadedAndConfiguredTest, conf *cloudapi.Config,
) error {
	// The log-push config is env-supplied for an external run and likewise not
	// env-bound on the Config, so read it explicitly here (before the checks
	// below, which require the token when a logs URL is set).
	if err := applyExternalLogsConfig(gs, conf); err != nil {
		return err
	}
	pushURL := gs.Env["K6_CLOUD_METRICS_PUSH_URL"]
	token := gs.Env["K6_CLOUD_TEST_RUN_TOKEN"]
	if (pushURL == "") != (token == "") {
		return errors.New("both K6_CLOUD_METRICS_PUSH_URL and " +
			"K6_CLOUD_TEST_RUN_TOKEN must be set together")
	}
	// A logs push URL is authenticated with the same scoped token, so reject a
	// logs URL supplied without it rather than silently streaming nothing.
	if token == "" && conf.LogsPushURL.Valid && conf.LogsPushURL.String != "" {
		return errors.New("K6_CLOUD_LOGS_PUSH_URL requires K6_CLOUD_TEST_RUN_TOKEN")
	}
	if pushURL == "" {
		return nil
	}

	conf.MetricsPushURL = null.StringFrom(pushURL)
	conf.TestRunToken = null.StringFrom(token)
	raw, err := cloudConfToRawMessage(*conf)
	if err != nil {
		return fmt.Errorf("could not serialize cloud configuration: %w", err)
	}
	if test.derivedConfig.Collectors == nil {

View on GitHub (pinned to 93accf6570)

Solutions

  1. Export both variables together, exactly as emitted by the cloud UI/CLI (they are generated as a pair)
  2. If an external run was not intended, unset both and authenticate with K6_CLOUD_TOKEN instead
  3. Verify the CI secret manager did not redact, truncate, or rename K6_CLOUD_TEST_RUN_TOKEN

Example fix

# before
export K6_CLOUD_METRICS_PUSH_URL=https://.../api/v1/write
k6 run --out cloud script.js

# after
export K6_CLOUD_METRICS_PUSH_URL=https://.../api/v1/write
export K6_CLOUD_TEST_RUN_TOKEN=eyJ...
k6 run --out cloud script.js
Defensive patterns

Strategy: validation

Validate before calling

# Both or neither
if [ -n "$K6_CLOUD_METRICS_PUSH_URL" ] && [ -z "$K6_CLOUD_TEST_RUN_TOKEN" ] || [ -z "$K6_CLOUD_METRICS_PUSH_URL" ] && [ -n "$K6_CLOUD_TEST_RUN_TOKEN" ]; then
  echo "K6_CLOUD_METRICS_PUSH_URL and K6_CLOUD_TEST_RUN_TOKEN must be set together" >&2; exit 1;
fi

Prevention

When it happens

Trigger: Launching k6 with a PushRefID-based external run where K6_CLOUD_METRICS_PUSH_URL is exported but K6_CLOUD_TEST_RUN_TOKEN is missing (or the reverse).

Common situations: CI secret masking/redaction stripping the token env var; copying only half of a Grafana Cloud 'external run' command; push URL set globally in CI while the token is passed per-job (or vice versa).

Related errors


AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15). Data as JSON: /api/errors/ad409226c652d004. Report an issue: GitHub.