diff --git a/docs/cli.md b/docs/cli.md index 9aa4c114..d64eea02 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -368,6 +368,9 @@ The selected snapshot establishes one event for applicability, compilation, group conditions, and provider-check names. An explicit event is never replaced with live Buildkite fields. +Buildkite's coalesced first-party pull request push maps to the `pull_request` +synchronization described in [Names and triggers](compatibility.md#names-and-triggers). + Linked webhook data can provide `merge_group` and `release`. Those events need matching Buildkite refs, commits, and activity. Release also needs a valid payload and a tag matching `BUILDKITE_TAG` and `BUILDKITE_BRANCH`. The GitHub diff --git a/docs/compatibility.md b/docs/compatibility.md index 5a1c01c8..9cf05d2a 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -165,7 +165,16 @@ Upload selects one effective event, in this order: 1. The GitHub event name accompanying Buildkite's reserved linked-webhook metadata. 1. A Buildkite environment fallback. -The fallback preserves `push`, `pull_request`, `workflow_dispatch`, and +Buildkite's GitHub integration coalesces a first-party pull request +synchronization into the branch's linked push build. When a GitHub `push` build +has a positive Buildkite pull request number, upload selects one `pull_request` +event with `synchronize` activity, the pull request base branch, and +`refs/pull//head`. It does not also select `push`. A push without pull +request metadata remains a `push`, so push filters retain their normal +semantics. + +Except for that coalesced mapping, the fallback preserves `push`, +`pull_request`, `workflow_dispatch`, and `schedule` from `BUILDKITE_GITHUB_EVENT` across rebuilds. Otherwise: | Buildkite source | Effective event | @@ -252,6 +261,10 @@ commits, synthetic merge, base branch, and workflow file must agree. The check uses the checkout's existing Git access for public, private, and fork pull requests. It does not call GitHub or use Buildkite `if_changed`. +A coalesced first-party synchronization has a linked push payload, not a pull +request payload. The push payload cannot admit pull request path filters because +it covers only that push, not the complete pull request comparison. + | Admitted | Rejected | | --- | --- | | A matching added, modified, deleted, or type-changed path | No local match | diff --git a/docs/security.md b/docs/security.md index 7dece282..8ffbc6d2 100644 --- a/docs/security.md +++ b/docs/security.md @@ -54,6 +54,9 @@ trusted or grant credentials. it to the Buildkite repository, commit, workflow, and bounded local Git history. Missing, shallow, ambiguous, or mismatched evidence blocks admission. +- A coalesced first-party pull request synchronization can be linked to a push + payload. Buildkite pull request metadata models the trigger; the push payload + cannot grant pull request path-filter admission. - Release ingestion matches the webhook activity and tag to Buildkite's event, branch, and tag. The GitHub Code Access App supplies server-resolved commit provenance. A local `HEAD` fallback preserves compatibility but cannot grant diff --git a/internal/buildkite/triggers.go b/internal/buildkite/triggers.go index 67334d57..fb6f5550 100644 --- a/internal/buildkite/triggers.go +++ b/internal/buildkite/triggers.go @@ -346,7 +346,7 @@ func LiveEventPredicate(event string) string { fallbackEvent += " || (" + unsupportedEvent + "))" switch event { case "push": - return "(" + predicate + " || (" + fallbackEvent + ` && build.pull_request.id == null && build.source != "ui" && build.source != "api" && build.source != "schedule"))` + return "((" + predicate + ` && build.pull_request.id == null) || (` + fallbackEvent + ` && build.pull_request.id == null && build.source != "ui" && build.source != "api" && build.source != "schedule"))` case "pull_request": return "(" + predicate + " || (" + fallbackEvent + " && build.pull_request.id != null))" case "workflow_dispatch": @@ -360,6 +360,13 @@ func LiveEventPredicate(event string) string { } } +// LivePullRequestPushPredicate matches the push delivery that Buildkite's +// GitHub integration uses for a first-party pull request synchronization. +func LivePullRequestPushPredicate() string { + githubEvent := "build.env(" + yamlScalar("BUILDKITE_GITHUB_EVENT") + ")" + return "(" + githubEvent + ` == "push" && build.pull_request.id != null)` +} + func translateTrigger(t workflow.Trigger, expressions TriggerConditionExpressions, snapshot TriggerEventSnapshot, selected bool) (string, bool, error) { if !SupportedTriggerEvent(t.Event) { return "", false, &UnsupportedTriggerEventError{Event: t.Event} diff --git a/internal/buildkite/triggers_test.go b/internal/buildkite/triggers_test.go index 0402b33f..50d7eaf0 100644 --- a/internal/buildkite/triggers_test.go +++ b/internal/buildkite/triggers_test.go @@ -63,6 +63,23 @@ func TestLiveEventPredicatePreservesNonWebhookMappings(t *testing.T) { } } +func TestLivePushPredicatesPartitionPullRequestBuilds(t *testing.T) { + pullRequestPush := LivePullRequestPushPredicate() + for _, want := range []string{ + `build.env("BUILDKITE_GITHUB_EVENT") == "push"`, + `build.pull_request.id != null`, + } { + if !strings.Contains(pullRequestPush, want) { + t.Errorf("pull request push predicate missing %q: %s", want, pullRequestPush) + } + } + push := LiveEventPredicate("push") + want := `build.env("BUILDKITE_GITHUB_EVENT") == "push" && build.pull_request.id == null` + if !strings.Contains(push, want) { + t.Fatalf("push predicate does not exclude pull request builds: %s", push) + } +} + func TestTranslateTriggerConditionRejectsUnsafeTriggers(t *testing.T) { tests := []struct { name string diff --git a/internal/cli/buildkite_event.go b/internal/cli/buildkite_event.go index 602c31f2..5685dc76 100644 --- a/internal/cli/buildkite_event.go +++ b/internal/cli/buildkite_event.go @@ -103,17 +103,21 @@ func buildkiteEventSource(getenv func(string) string) ([]byte, error) { if githubEvent := strings.TrimSpace(getenv("BUILDKITE_GITHUB_EVENT")); githubEventNamePattern.MatchString(githubEvent) { switch githubEvent { case "push", "pull_request", "workflow_dispatch", "schedule": - event = githubEvent - // Rebuilds retain the original GitHub event even though Buildkite reports - // their source as UI. A push may also be associated with an open pull - // request, so restore its authoritative branch or tag ref. - if event == "push" { - if strings.TrimSpace(tag) != "" { - ref = "refs/tags/" + tag - } else if strings.TrimSpace(branch) != "" { - ref = "refs/heads/" + branch + // Buildkite's GitHub integration coalesces a first-party pull request + // synchronization into its linked push build. Preserve the pull request + // compatibility snapshot when the build carries that PR identity. + if githubEvent != "push" || !buildkitePushRepresentsPullRequest(getenv) { + event = githubEvent + // Rebuilds retain the original GitHub event even though Buildkite + // reports their source as UI. Restore a push's branch or tag ref. + if event == "push" { + if strings.TrimSpace(tag) != "" { + ref = "refs/tags/" + tag + } else if strings.TrimSpace(branch) != "" { + ref = "refs/heads/" + branch + } + payload = map[string]any{"ref": ref} } - payload = map[string]any{"ref": ref} } } } @@ -160,17 +164,23 @@ func buildkiteWebhookEventSource(getenv func(string) string, webhook []byte) ([] if err := decoder.Decode(&snapshot); err != nil { return nil, fmt.Errorf("decode Buildkite compatibility snapshot: %w", err) } + compatibilityPayload := snapshot["payload"] snapshot["payload"] = payload if event := strings.TrimSpace(getenv("BUILDKITE_GITHUB_EVENT")); githubEventNamePattern.MatchString(event) { - snapshot["event"] = event - // Buildkite can associate a push-created build with an open pull - // request. Keep the authoritative execution ref consistent with the - // linked webhook event rather than retaining refs/pull//head. - if event == "push" { - if tag := strings.TrimSpace(getenv("BUILDKITE_TAG")); tag != "" { - snapshot["ref"] = "refs/tags/" + tag - } else if branch := strings.TrimSpace(getenv("BUILDKITE_BRANCH")); branch != "" { - snapshot["ref"] = "refs/heads/" + branch + if event == "push" && buildkitePushRepresentsPullRequest(getenv) { + // A push payload describes only the latest branch update, not the + // complete pull request. Retain the trusted compatibility payload and + // do not promote the push webhook to pull request admission evidence. + snapshot["event"] = "pull_request" + snapshot["payload"] = compatibilityPayload + } else { + snapshot["event"] = event + if event == "push" { + if tag := strings.TrimSpace(getenv("BUILDKITE_TAG")); tag != "" { + snapshot["ref"] = "refs/tags/" + tag + } else if branch := strings.TrimSpace(getenv("BUILDKITE_BRANCH")); branch != "" { + snapshot["ref"] = "refs/heads/" + branch + } } } } @@ -199,6 +209,15 @@ func buildkiteWebhookEventSource(getenv func(string) string, webhook []byte) ([] return result, nil } +func buildkitePushRepresentsPullRequest(getenv func(string) string) bool { + provider, _, _, _, err := parseBuildkiteRepository(getenv("BUILDKITE_REPO")) + if err != nil || provider != "github" || strings.TrimSpace(getenv("BUILDKITE_GITHUB_EVENT")) != "push" { + return false + } + number, err := strconv.Atoi(getenv("BUILDKITE_PULL_REQUEST")) + return err == nil && number > 0 +} + func validateBuildkiteMergeGroup(snapshot map[string]any, getenv func(string) string) error { payload := snapshot["payload"].(map[string]any) mergeGroup, ok := payload["merge_group"].(map[string]any) diff --git a/internal/cli/buildkite_event_test.go b/internal/cli/buildkite_event_test.go index b4e15611..5ca14ffa 100644 --- a/internal/cli/buildkite_event_test.go +++ b/internal/cli/buildkite_event_test.go @@ -394,16 +394,17 @@ func TestBuildkiteEventSourceDoesNotInventReleaseFromEnvironment(t *testing.T) { } } -func TestBuildkiteWebhookPushUsesBranchRefForPullRequestAssociatedBuild(t *testing.T) { +func TestBuildkiteWebhookPushUsesPullRequestCompatibilitySnapshot(t *testing.T) { env := map[string]string{ "BUILDKITE": "true", "BUILDKITE_STEP_KEY": "step", - "BUILDKITE_REPO": "https://github.com/buildkite/buildkite-gha", - "BUILDKITE_COMMIT": strings.Repeat("a", 40), - "BUILDKITE_BRANCH": "feature", - "BUILDKITE_PULL_REQUEST": "42", - "BUILDKITE_GITHUB_EVENT": "push", - } - source, err := buildkiteWebhookEventSource(func(key string) string { return env[key] }, []byte(`{"ref":"refs/heads/feature"}`)) + "BUILDKITE_REPO": "https://github.com/buildkite/buildkite-gha", + "BUILDKITE_COMMIT": strings.Repeat("a", 40), + "BUILDKITE_BRANCH": "feature", + "BUILDKITE_PULL_REQUEST": "42", + "BUILDKITE_PULL_REQUEST_BASE_BRANCH": "main", + "BUILDKITE_GITHUB_EVENT": "push", + } + source, err := buildkiteWebhookEventSource(func(key string) string { return env[key] }, []byte(`{"ref":"refs/heads/feature","push_marker":"discarded"}`)) if err != nil { t.Fatal(err) } @@ -411,20 +412,25 @@ func TestBuildkiteWebhookPushUsesBranchRefForPullRequestAssociatedBuild(t *testi if err := json.Unmarshal(source, &snapshot); err != nil { t.Fatal(err) } - if snapshot["event"] != "push" || snapshot["ref"] != "refs/heads/feature" { - t.Fatalf("snapshot event/ref = %q / %q", snapshot["event"], snapshot["ref"]) + payload := snapshot["payload"].(map[string]any) + pullRequest := payload["pull_request"].(map[string]any) + if snapshot["event"] != "pull_request" || snapshot["ref"] != "refs/pull/42/head" || + payload["action"] != "synchronize" || payload["number"] != float64(42) || payload["push_marker"] != nil || + pullRequest["head"].(map[string]any)["ref"] != "feature" || pullRequest["base"].(map[string]any)["ref"] != "main" { + t.Fatalf("pull request synchronization snapshot = %#v", snapshot) } } -func TestBuildkiteEventSourceRebuiltPushResetsPullRequestPayload(t *testing.T) { +func TestBuildkiteEventSourceRebuiltPushPreservesPullRequestCompatibilitySnapshot(t *testing.T) { env := map[string]string{ "BUILDKITE": "true", "BUILDKITE_STEP_KEY": "step", - "BUILDKITE_REPO": "https://github.com/buildkite/buildkite-gha", - "BUILDKITE_COMMIT": strings.Repeat("a", 40), - "BUILDKITE_BRANCH": "feature", - "BUILDKITE_PULL_REQUEST": "42", - "BUILDKITE_GITHUB_EVENT": "push", - "BUILDKITE_SOURCE": "ui", + "BUILDKITE_REPO": "https://github.com/buildkite/buildkite-gha", + "BUILDKITE_COMMIT": strings.Repeat("a", 40), + "BUILDKITE_BRANCH": "feature", + "BUILDKITE_PULL_REQUEST": "42", + "BUILDKITE_PULL_REQUEST_BASE_BRANCH": "main", + "BUILDKITE_GITHUB_EVENT": "push", + "BUILDKITE_SOURCE": "ui", } source, err := buildkiteEventSource(func(key string) string { return env[key] }) if err != nil { @@ -435,7 +441,9 @@ func TestBuildkiteEventSourceRebuiltPushResetsPullRequestPayload(t *testing.T) { t.Fatal(err) } payload := snapshot["payload"].(map[string]any) - if snapshot["event"] != "push" || snapshot["ref"] != "refs/heads/feature" || payload["ref"] != "refs/heads/feature" || len(payload) != 1 { + pullRequest := payload["pull_request"].(map[string]any) + if snapshot["event"] != "pull_request" || snapshot["ref"] != "refs/pull/42/head" || + payload["action"] != "synchronize" || pullRequest["base"].(map[string]any)["ref"] != "main" { t.Fatalf("rebuilt push snapshot = %#v", snapshot) } } diff --git a/internal/cli/effective_event.go b/internal/cli/effective_event.go index 37fc3461..fa8ed598 100644 --- a/internal/cli/effective_event.go +++ b/internal/cli/effective_event.go @@ -21,9 +21,10 @@ const maxWebhookMetadataBytes = 25 << 20 type effectiveEventOrigin string const ( - effectiveEventFromPath effectiveEventOrigin = "event-path" - effectiveEventFromWebhook effectiveEventOrigin = "buildkite-webhook" - effectiveEventFromBuild effectiveEventOrigin = "buildkite-environment" + effectiveEventFromPath effectiveEventOrigin = "event-path" + effectiveEventFromWebhook effectiveEventOrigin = "buildkite-webhook" + effectiveEventFromBuild effectiveEventOrigin = "buildkite-environment" + effectiveEventFromPullRequestPush effectiveEventOrigin = "buildkite-pull-request-push" ) type effectiveEventSelection struct { @@ -47,10 +48,18 @@ func loadEffectiveEventSource(ctx context.Context, eventPath string, agent trans return nil, "", fmt.Errorf("buildkite:webhook exceeds %d bytes", maxWebhookMetadataBytes) } source, err := buildkiteWebhookEventSource(os.Getenv, webhook) - return source, effectiveEventFromWebhook, err + origin := effectiveEventFromWebhook + if buildkitePushRepresentsPullRequest(os.Getenv) { + origin = effectiveEventFromPullRequestPush + } + return source, origin, err case errors.Is(metadataErr, transport.ErrMetadataUnavailable): source, err := buildkiteEventSource(os.Getenv) - return source, effectiveEventFromBuild, err + origin := effectiveEventFromBuild + if buildkitePushRepresentsPullRequest(os.Getenv) { + origin = effectiveEventFromPullRequestPush + } + return source, origin, err default: return nil, "", metadataErr } @@ -70,7 +79,11 @@ func newEffectiveEvent(source []byte, origin effectiveEventOrigin) (effectiveEve if origin == effectiveEventFromPath { return effective, nil } - effective.TriggerExpressions.EventPredicate = buildkitepipeline.LiveEventPredicate(event.Event) + if origin == effectiveEventFromPullRequestPush { + effective.TriggerExpressions.EventPredicate = buildkitepipeline.LivePullRequestPushPredicate() + } else { + effective.TriggerExpressions.EventPredicate = buildkitepipeline.LiveEventPredicate(event.Event) + } return effective, nil } diff --git a/internal/cli/effective_event_test.go b/internal/cli/effective_event_test.go index 8eeff5bc..7832be3f 100644 --- a/internal/cli/effective_event_test.go +++ b/internal/cli/effective_event_test.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" "reflect" + "strings" "testing" buildkitepipeline "github.com/buildkite/buildkite-gha/internal/buildkite" @@ -43,3 +44,29 @@ func TestNewEffectiveEventSeparatesExpressionsAndSnapshot(t *testing.T) { t.Fatalf("webhook effective event = expressions %#v, snapshot %#v", webhook.TriggerExpressions, webhook.TriggerSnapshot) } } + +func TestNewEffectiveEventUsesPullRequestPushPredicate(t *testing.T) { + env := map[string]string{ + "BUILDKITE": "true", "BUILDKITE_STEP_KEY": "importer", + "BUILDKITE_REPO": "https://github.com/acme/widgets", + "BUILDKITE_COMMIT": strings.Repeat("a", 40), + "BUILDKITE_BRANCH": "feature", + "BUILDKITE_PULL_REQUEST": "42", + "BUILDKITE_PULL_REQUEST_BASE_BRANCH": "main", + "BUILDKITE_GITHUB_EVENT": "push", + } + source, err := buildkiteEventSource(func(key string) string { return env[key] }) + if err != nil { + t.Fatal(err) + } + effective, err := newEffectiveEvent(source, effectiveEventFromPullRequestPush) + if err != nil { + t.Fatal(err) + } + if effective.Event.Event != "pull_request" || effective.Event.Ref != "refs/pull/42/head" || + effective.TriggerExpressions.EventPredicate != buildkitepipeline.LivePullRequestPushPredicate() || + effective.TriggerSnapshot.PullRequestBaseBranch == nil || *effective.TriggerSnapshot.PullRequestBaseBranch != "main" || + effective.TriggerSnapshot.PullRequestAction == nil || *effective.TriggerSnapshot.PullRequestAction != "synchronize" { + t.Fatalf("pull request push effective event = %#v", effective) + } +} diff --git a/internal/cli/path_filters_test.go b/internal/cli/path_filters_test.go index dcefe2a4..58dff313 100644 --- a/internal/cli/path_filters_test.go +++ b/internal/cli/path_filters_test.go @@ -440,13 +440,21 @@ func TestBoundedCommandOutput(t *testing.T) { } func TestPopulateChangedPathsRequiresLinkedWebhook(t *testing.T) { - for _, event := range []string{"push", "pull_request"} { - t.Run(event, func(t *testing.T) { + for _, test := range []struct { + name string + event string + origin effectiveEventOrigin + }{ + {name: "explicit push", event: "push", origin: effectiveEventFromPath}, + {name: "explicit pull request", event: "pull_request", origin: effectiveEventFromPath}, + {name: "pull request push", event: "pull_request", origin: effectiveEventFromPullRequestPush}, + } { + t.Run(test.name, func(t *testing.T) { snapshot := buildkitepipeline.TriggerEventSnapshot{} - populateChangedPaths(&snapshot, compiler.Event{Event: event}, effectiveEventFromPath, []workflowInput{{ - Triggers: []workflow.Trigger{{Event: event, Paths: []string{"src/**"}}}, + populateChangedPaths(&snapshot, compiler.Event{Event: test.event}, test.origin, []workflowInput{{ + Triggers: []workflow.Trigger{{Event: test.event, Paths: []string{"src/**"}}}, }}) - if snapshot.ChangedPaths.Paths != nil || !strings.Contains(snapshot.ChangedPaths.UnavailableReason, event+" path filters require linked Buildkite webhook") { + if snapshot.ChangedPaths.Paths != nil || !strings.Contains(snapshot.ChangedPaths.UnavailableReason, test.event+" path filters require linked Buildkite webhook") { t.Fatalf("changed-path snapshot = %#v", snapshot) } }) diff --git a/internal/cli/plugin_test.go b/internal/cli/plugin_test.go index 17f2b1fc..f9e5fdfa 100644 --- a/internal/cli/plugin_test.go +++ b/internal/cli/plugin_test.go @@ -317,6 +317,94 @@ func TestPluginUsesJSONConfigurationAndOnlyRequiredRuntime(t *testing.T) { } } +func TestPluginModelsFirstPartyPullRequestSynchronizationFromPush(t *testing.T) { + requireImporterHost(t) + repository := writeUploadWorkflowRepository(t, map[string]string{ + "ci.yml": "name: CI\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\njobs:\n test:\n runs-on: ubuntu-latest\n steps: [{run: true}]\n", + }) + t.Chdir(repository) + configuration, err := json.Marshal(map[string]any{"workflow": ".github/workflows/ci.yml"}) + if err != nil { + t.Fatal(err) + } + t.Setenv(pluginConfigurationEnvironment, string(configuration)) + setCLIPluginBuildkiteEnvironment(t, "pull-request-push-importer") + sha := strings.Repeat("a", 40) + t.Setenv("BUILDKITE_COMMIT", sha) + t.Setenv("BUILDKITE_BRANCH", "amp/buildkite-gha") + t.Setenv("BUILDKITE_PULL_REQUEST", "583") + t.Setenv("BUILDKITE_PULL_REQUEST_BASE_BRANCH", "main") + t.Setenv("BUILDKITE_GITHUB_EVENT", "push") + runner := &cliCaptureRunner{webhook: []byte(`{ + "ref":"refs/heads/amp/buildkite-gha", + "before":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "after":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "repository":{"full_name":"buildkite/buildkite-gha"}, + "sender":{"login":"octocat"} +}`)} + var stdout, stderr bytes.Buffer + if code := run([]string{"plugin"}, &stdout, &stderr, "dev", runner); code != 0 { + t.Fatalf("run() code = %d, stderr = %q", code, stderr.String()) + } + var pipeline struct { + Steps []struct { + Group string `yaml:"group"` + Condition string `yaml:"if"` + Skip string `yaml:"skip"` + Steps []struct { + Notify []struct { + GitHubCheck struct { + Name string `yaml:"name"` + } `yaml:"github_check"` + } `yaml:"notify"` + } `yaml:"steps"` + } `yaml:"steps"` + } + if err := yaml.Unmarshal(runner.commands[len(runner.commands)-1].stdin, &pipeline); err != nil { + t.Fatal(err) + } + if len(pipeline.Steps) != 1 || pipeline.Steps[0].Group != ":github: workflow ยท CI" || pipeline.Steps[0].Skip != "" || len(pipeline.Steps[0].Steps) != 1 { + t.Fatalf("pull request synchronization pipeline = %#v", pipeline.Steps) + } + condition := pipeline.Steps[0].Condition + for _, want := range []string{ + `build.env("BUILDKITE_GITHUB_EVENT") == "push"`, + `build.pull_request.id != null`, + `"main" =~ /^main$/`, + `"synchronize" == "synchronize"`, + } { + if !strings.Contains(condition, want) { + t.Errorf("pull request synchronization condition missing %q: %s", want, condition) + } + } + if strings.Contains(condition, "amp/buildkite-gha") { + t.Errorf("pull request condition retained the false push branch: %s", condition) + } + check := pipeline.Steps[0].Steps[0].Notify + if len(check) != 1 || check[0].GitHubCheck.Name != "CI / test (pull_request)" { + t.Fatalf("pull request synchronization check = %#v", check) + } + planCount := 0 + for path, contents := range runner.uploaded { + if !strings.HasSuffix(path, ".json") { + continue + } + job, err := plan.Decode(contents) + if err != nil { + t.Fatal(err) + } + planCount++ + if job.Event.Name != "pull_request" || job.Event.Ref != "refs/pull/583/head" || + job.Event.HeadRef != "amp/buildkite-gha" || job.Event.BaseRef != "main" || + job.Event.SHA != sha || job.Event.Actor != "octocat" { + t.Fatalf("pull request synchronization plan event = %#v", job.Event) + } + } + if planCount != 1 { + t.Fatalf("plan count = %d, want 1", planCount) + } +} + func TestPluginIgnoresJobPermissionsForHostedGitHubToken(t *testing.T) { requireImporterHost(t) repository := writeUploadWorkflowRepository(t, map[string]string{