From af85dab70b835b76bde8df3b76f7b2167d348e07 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Thu, 2 Jul 2026 17:57:18 -0400 Subject: [PATCH 1/3] docs: lock upstream release monitor scope --- .../0001-upstream-release-monitor-boundary.md | 20 ++++++++ ...6-07-02-upstream-release-monitor-design.md | 46 ++++++++++++++++++ .../2026-07-02-upstream-release-monitor.md | 48 +++++++++++++++++++ ...-02-upstream-release-monitor.md.scope-lock | 1 + 4 files changed, 115 insertions(+) create mode 100644 decisions/0001-upstream-release-monitor-boundary.md create mode 100644 docs/plans/2026-07-02-upstream-release-monitor-design.md create mode 100644 docs/plans/2026-07-02-upstream-release-monitor.md create mode 100644 docs/plans/2026-07-02-upstream-release-monitor.md.scope-lock diff --git a/decisions/0001-upstream-release-monitor-boundary.md b/decisions/0001-upstream-release-monitor-boundary.md new file mode 100644 index 0000000..c70e939 --- /dev/null +++ b/decisions/0001-upstream-release-monitor-boundary.md @@ -0,0 +1,20 @@ +# 0001. Keep Release Drift Read-Only + +**Status:** Accepted +**Date:** 2026-07-02 +**Decision-makers:** Workflow maintainers +**Related:** docs/plans/2026-07-02-upstream-release-monitor-design.md + +## Context + +Signal-related repos now repeat the same upstream-release drift pattern, but each repo has different update scripts, compatibility tests, branch rules, and release gates. A Signal-specific updater would duplicate GitHub plumbing, while a fully mutating generic updater would need to execute repo-specific code and own automerge policy. + +## Decision + +Add a read-only GitHub release monitor step. It reports whether an upstream latest release differs from a pinned tag. Repository mutation remains composed from existing Workflow primitives and repo-owned scripts. + +Rejected: a Signal-only monitor because the pattern applies beyond Signal. Rejected: a one-step automerge updater because it would hide repo-specific update and safety policy inside a generic plugin. + +## Consequences + +Workflow apps can build reusable upstream drift pipelines without hard-coding Signal semantics. Repos still own their update commands, tests, and automerge criteria. A later non-GitHub provider can add a sibling primitive without changing this boundary. diff --git a/docs/plans/2026-07-02-upstream-release-monitor-design.md b/docs/plans/2026-07-02-upstream-release-monitor-design.md new file mode 100644 index 0000000..ec16fe2 --- /dev/null +++ b/docs/plans/2026-07-02-upstream-release-monitor-design.md @@ -0,0 +1,46 @@ +# GitHub Upstream Release Monitor Design + +**Status:** Approved +**Date:** 2026-07-02 +**Related:** decisions/0001-upstream-release-monitor-boundary.md + +## Context + +The Signal work now has repeated repo-owned drift workflows: + +- `libsignal-go` monitors `signalapp/libsignal` releases and runs its own pin update. +- `libsignal-service-go` monitors Signal service upstream material and runs its own fetch/update scripts. +- `encrypted-spaces-go` monitors upstream Encrypted Spaces changes. + +That repeated shape is no longer Signal-specific. Workflow should expose the reusable GitHub primitive that lets applications detect upstream release drift, then compose existing PR/action/merge steps with target-repo update scripts. + +## Decision + +Add a first-class `step.gh_upstream_release_monitor` to `workflow-plugin-github`. + +The step is read-only. It fetches the latest release for a configured upstream GitHub repository, compares that tag to a configured pinned tag, and emits structured drift metadata. It does not clone repositories, run update commands, create branches, create PRs, enable automerge, or know any Signal-specific rules. + +## Requirements + +- `R1`: Expose a Workflow step named `step.gh_upstream_release_monitor`. +- `R2`: Require `upstream_owner`, `upstream_repo`, and `pinned_tag`. +- `R3`: Allow an optional `token` for authenticated GitHub API reads, while supporting public unauthenticated reads. +- `R4`: Resolve template expressions for the owner, repo, and pinned tag at runtime. +- `R5`: Emit `upstream_owner`, `upstream_repo`, `pinned_tag`, `latest_tag`, `update_available`, `release_id`, `release_url`, and `published_at`. +- `R6`: Return a pipeline-stopping error result when GitHub lookup fails. +- `R7`: Register strict proto config/input/output contracts and keep `plugin.json`, `plugin.contracts.json`, and runtime step types in sync. +- `R8`: Document composition with existing `step.gh_pr_create`, `step.gh_action_trigger`, `step.gh_action_status`, and `step.gh_pr_merge` for repo-specific re-pin automation. + +## Non-Goals + +- No official Signal account registration, live send/receive, linked-device automation, or Signal service interaction. +- No automatic branch mutation, PR creation, or automerge inside this new step. +- No generic shell execution primitive. +- No non-GitHub release source support in this PR. + +## Acceptance + +- Unit tests cover update/no-update/error behavior and missing required config. +- Contract tests prove the new step has strict proto descriptors. +- Schema tests prove the manifest advertises the step and its fields. +- `go test ./...` passes with `GOWORK=off GOPRIVATE=github.com/GoCodeAlone/*`. diff --git a/docs/plans/2026-07-02-upstream-release-monitor.md b/docs/plans/2026-07-02-upstream-release-monitor.md new file mode 100644 index 0000000..0ba2d22 --- /dev/null +++ b/docs/plans/2026-07-02-upstream-release-monitor.md @@ -0,0 +1,48 @@ +# GitHub Upstream Release Monitor Plan + +**Status:** Locked 2026-07-02T21:56:54Z +**Date:** 2026-07-02 +**Design:** docs/plans/2026-07-02-upstream-release-monitor-design.md + +## Scope Manifest + +**PR Count:** 1 +**Tasks:** 4 +**Estimated Lines of Change:** ~550 + +**Out of scope:** +- Signal service live traffic, account registration, linked devices, or official app automation. +- A mutating one-step updater that runs repo scripts, opens PRs, or enables automerge. +- Non-GitHub release sources. + +**PR Grouping:** + +| PR # | Title | Tasks | Branch | +|------|-------|-------|--------| +| 1 | Add upstream release monitor step | Task 1, Task 2, Task 3, Task 4 | feat/upstream-release-monitor | + +**Status:** Draft + +### Task 1: Record Boundary + +Add design and ADR documentation that captures why this belongs in `workflow-plugin-github` as a read-only primitive instead of another Signal-specific package. + +Verification: design, plan, and ADR are present and self-consistent. + +### Task 2: Add Failing Runtime Tests + +Add tests for `step.gh_upstream_release_monitor` covering drift detected, no drift, API failure, and required config validation. + +Verification: focused new tests fail before implementation because the step constructor/runtime does not exist. + +### Task 3: Implement Step And Contracts + +Implement the step, register it in runtime step creation, add proto config/input/output messages, regenerate Go bindings, and update strict contract registry. + +Verification: focused runtime and contract tests pass. + +### Task 4: Update Manifest, Docs, And Full Verification + +Update `plugin.json`, `plugin.contracts.json`, README, and an example Workflow config showing composition with existing GitHub steps. Run full plugin verification. + +Verification: `GOWORK=off GOPRIVATE=github.com/GoCodeAlone/* go test ./...` passes. diff --git a/docs/plans/2026-07-02-upstream-release-monitor.md.scope-lock b/docs/plans/2026-07-02-upstream-release-monitor.md.scope-lock new file mode 100644 index 0000000..416dbb6 --- /dev/null +++ b/docs/plans/2026-07-02-upstream-release-monitor.md.scope-lock @@ -0,0 +1 @@ +de155f0cc3a5e9387586eb759425075e12bb50bf06c18614ee5d5aa7b05e8f67 From e6dd23a89e4403f9a4c34060810d89eb7b7bdfbd Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Thu, 2 Jul 2026 18:05:15 -0400 Subject: [PATCH 2/3] feat: add upstream release monitor step --- README.md | 22 + examples/upstream-release-monitor/config.yaml | 54 ++ gen/github.pb.go | 464 +++++++++++++----- internal/contracts.go | 8 + internal/contracts_test.go | 7 +- internal/plugin.go | 3 + internal/schemas_test.go | 2 +- internal/step_upstream_release_monitor.go | 130 +++++ .../step_upstream_release_monitor_test.go | 195 ++++++++ plugin.contracts.json | 8 + plugin.json | 31 ++ proto/github/v1/github.proto | 25 + 12 files changed, 833 insertions(+), 116 deletions(-) create mode 100644 examples/upstream-release-monitor/config.yaml create mode 100644 internal/step_upstream_release_monitor.go create mode 100644 internal/step_upstream_release_monitor_test.go diff --git a/README.md b/README.md index 04c2aee..80fe1c3 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,28 @@ Checks or polls the status of a GitHub Actions workflow run. timeout: "30m" ``` +### Step: `step.gh_upstream_release_monitor` + +Checks the latest release tag for an upstream GitHub repository and reports +whether it differs from the tag your application has pinned. Public repositories +can be checked without a token; private repositories or higher-rate checks can +provide `token`. + +```yaml +- name: check_upstream + type: step.gh_upstream_release_monitor + config: + upstream_owner: "{{ .upstream_owner }}" + upstream_repo: "{{ .upstream_repo }}" + pinned_tag: "{{ .pinned_tag }}" + token: "${GITHUB_TOKEN}" +``` + +The step is intentionally read-only. Compose `update_available` with +`step.conditional`, repo-owned update workflows, `step.gh_action_trigger`, +`step.gh_action_status`, `step.gh_pr_create`, and `step.gh_pr_merge` when an +application should re-pin an upstream dependency. + Workflow-compute workloads should be routed through a workflow-compute provider or through GitHub's normal self-hosted runner/webhook surfaces. This plugin does not expose workflow-compute gateway client steps or generic check-run creation diff --git a/examples/upstream-release-monitor/config.yaml b/examples/upstream-release-monitor/config.yaml new file mode 100644 index 0000000..8f6acfa --- /dev/null +++ b/examples/upstream-release-monitor/config.yaml @@ -0,0 +1,54 @@ +# Example Workflow app that checks an upstream GitHub release and dispatches a +# repo-owned update workflow only when the pinned tag is stale. + +version: 1 + +requires: + plugins: + - name: pipeline-steps + - name: workflow-plugin-github + +workflows: + upstream-release-monitor: + trigger: + type: http + config: + path: /upstream-release-monitor + method: POST + steps: + - name: check_upstream + type: step.gh_upstream_release_monitor + config: + upstream_owner: "{{ .body.upstream_owner }}" + upstream_repo: "{{ .body.upstream_repo }}" + pinned_tag: "{{ .body.pinned_tag }}" + token: "${GITHUB_TOKEN}" + + - name: route_update + type: step.conditional + config: + field: steps.check_upstream.update_available + routes: + "false": no_update + default: dispatch_update + + - name: no_update + type: step.set + config: + values: + status: current + latest_tag: "{{ .steps.check_upstream.latest_tag }}" + + - name: dispatch_update + type: step.gh_action_trigger + config: + owner: "{{ .body.target_owner }}" + repo: "{{ .body.target_repo }}" + workflow: "{{ .body.update_workflow }}" + ref: "{{ .body.target_ref }}" + inputs: + upstream_owner: "{{ .body.upstream_owner }}" + upstream_repo: "{{ .body.upstream_repo }}" + pinned_tag: "{{ .body.pinned_tag }}" + latest_tag: "{{ .steps.check_upstream.latest_tag }}" + token: "${GITHUB_TOKEN}" diff --git a/gen/github.pb.go b/gen/github.pb.go index aa4d592..204265e 100644 --- a/gen/github.pb.go +++ b/gen/github.pb.go @@ -2444,6 +2444,221 @@ func (x *ReleaseUploadOutput) GetSize() int64 { return 0 } +// UpstreamReleaseMonitorConfig is the typed config for step.gh_upstream_release_monitor. +type UpstreamReleaseMonitorConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + UpstreamOwner string `protobuf:"bytes,1,opt,name=upstream_owner,json=upstreamOwner,proto3" json:"upstream_owner,omitempty"` + UpstreamRepo string `protobuf:"bytes,2,opt,name=upstream_repo,json=upstreamRepo,proto3" json:"upstream_repo,omitempty"` + PinnedTag string `protobuf:"bytes,3,opt,name=pinned_tag,json=pinnedTag,proto3" json:"pinned_tag,omitempty"` + Token string `protobuf:"bytes,4,opt,name=token,proto3" json:"token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpstreamReleaseMonitorConfig) Reset() { + *x = UpstreamReleaseMonitorConfig{} + mi := &file_github_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpstreamReleaseMonitorConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpstreamReleaseMonitorConfig) ProtoMessage() {} + +func (x *UpstreamReleaseMonitorConfig) ProtoReflect() protoreflect.Message { + mi := &file_github_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpstreamReleaseMonitorConfig.ProtoReflect.Descriptor instead. +func (*UpstreamReleaseMonitorConfig) Descriptor() ([]byte, []int) { + return file_github_proto_rawDescGZIP(), []int{36} +} + +func (x *UpstreamReleaseMonitorConfig) GetUpstreamOwner() string { + if x != nil { + return x.UpstreamOwner + } + return "" +} + +func (x *UpstreamReleaseMonitorConfig) GetUpstreamRepo() string { + if x != nil { + return x.UpstreamRepo + } + return "" +} + +func (x *UpstreamReleaseMonitorConfig) GetPinnedTag() string { + if x != nil { + return x.PinnedTag + } + return "" +} + +func (x *UpstreamReleaseMonitorConfig) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +// UpstreamReleaseMonitorInput carries runtime inputs for step.gh_upstream_release_monitor. +type UpstreamReleaseMonitorInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *structpb.Struct `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpstreamReleaseMonitorInput) Reset() { + *x = UpstreamReleaseMonitorInput{} + mi := &file_github_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpstreamReleaseMonitorInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpstreamReleaseMonitorInput) ProtoMessage() {} + +func (x *UpstreamReleaseMonitorInput) ProtoReflect() protoreflect.Message { + mi := &file_github_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpstreamReleaseMonitorInput.ProtoReflect.Descriptor instead. +func (*UpstreamReleaseMonitorInput) Descriptor() ([]byte, []int) { + return file_github_proto_rawDescGZIP(), []int{37} +} + +func (x *UpstreamReleaseMonitorInput) GetData() *structpb.Struct { + if x != nil { + return x.Data + } + return nil +} + +// UpstreamReleaseMonitorOutput holds the result of step.gh_upstream_release_monitor. +type UpstreamReleaseMonitorOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + UpstreamOwner string `protobuf:"bytes,1,opt,name=upstream_owner,json=upstreamOwner,proto3" json:"upstream_owner,omitempty"` + UpstreamRepo string `protobuf:"bytes,2,opt,name=upstream_repo,json=upstreamRepo,proto3" json:"upstream_repo,omitempty"` + PinnedTag string `protobuf:"bytes,3,opt,name=pinned_tag,json=pinnedTag,proto3" json:"pinned_tag,omitempty"` + LatestTag string `protobuf:"bytes,4,opt,name=latest_tag,json=latestTag,proto3" json:"latest_tag,omitempty"` + UpdateAvailable bool `protobuf:"varint,5,opt,name=update_available,json=updateAvailable,proto3" json:"update_available,omitempty"` + ReleaseId int64 `protobuf:"varint,6,opt,name=release_id,json=releaseId,proto3" json:"release_id,omitempty"` + ReleaseUrl string `protobuf:"bytes,7,opt,name=release_url,json=releaseUrl,proto3" json:"release_url,omitempty"` + PublishedAt string `protobuf:"bytes,8,opt,name=published_at,json=publishedAt,proto3" json:"published_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpstreamReleaseMonitorOutput) Reset() { + *x = UpstreamReleaseMonitorOutput{} + mi := &file_github_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpstreamReleaseMonitorOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpstreamReleaseMonitorOutput) ProtoMessage() {} + +func (x *UpstreamReleaseMonitorOutput) ProtoReflect() protoreflect.Message { + mi := &file_github_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpstreamReleaseMonitorOutput.ProtoReflect.Descriptor instead. +func (*UpstreamReleaseMonitorOutput) Descriptor() ([]byte, []int) { + return file_github_proto_rawDescGZIP(), []int{38} +} + +func (x *UpstreamReleaseMonitorOutput) GetUpstreamOwner() string { + if x != nil { + return x.UpstreamOwner + } + return "" +} + +func (x *UpstreamReleaseMonitorOutput) GetUpstreamRepo() string { + if x != nil { + return x.UpstreamRepo + } + return "" +} + +func (x *UpstreamReleaseMonitorOutput) GetPinnedTag() string { + if x != nil { + return x.PinnedTag + } + return "" +} + +func (x *UpstreamReleaseMonitorOutput) GetLatestTag() string { + if x != nil { + return x.LatestTag + } + return "" +} + +func (x *UpstreamReleaseMonitorOutput) GetUpdateAvailable() bool { + if x != nil { + return x.UpdateAvailable + } + return false +} + +func (x *UpstreamReleaseMonitorOutput) GetReleaseId() int64 { + if x != nil { + return x.ReleaseId + } + return 0 +} + +func (x *UpstreamReleaseMonitorOutput) GetReleaseUrl() string { + if x != nil { + return x.ReleaseUrl + } + return "" +} + +func (x *UpstreamReleaseMonitorOutput) GetPublishedAt() string { + if x != nil { + return x.PublishedAt + } + return "" +} + // RepoDispatchConfig is the typed config for step.gh_repo_dispatch. type RepoDispatchConfig struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2458,7 +2673,7 @@ type RepoDispatchConfig struct { func (x *RepoDispatchConfig) Reset() { *x = RepoDispatchConfig{} - mi := &file_github_proto_msgTypes[36] + mi := &file_github_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2470,7 +2685,7 @@ func (x *RepoDispatchConfig) String() string { func (*RepoDispatchConfig) ProtoMessage() {} func (x *RepoDispatchConfig) ProtoReflect() protoreflect.Message { - mi := &file_github_proto_msgTypes[36] + mi := &file_github_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2483,7 +2698,7 @@ func (x *RepoDispatchConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use RepoDispatchConfig.ProtoReflect.Descriptor instead. func (*RepoDispatchConfig) Descriptor() ([]byte, []int) { - return file_github_proto_rawDescGZIP(), []int{36} + return file_github_proto_rawDescGZIP(), []int{39} } func (x *RepoDispatchConfig) GetOwner() string { @@ -2531,7 +2746,7 @@ type RepoDispatchInput struct { func (x *RepoDispatchInput) Reset() { *x = RepoDispatchInput{} - mi := &file_github_proto_msgTypes[37] + mi := &file_github_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2543,7 +2758,7 @@ func (x *RepoDispatchInput) String() string { func (*RepoDispatchInput) ProtoMessage() {} func (x *RepoDispatchInput) ProtoReflect() protoreflect.Message { - mi := &file_github_proto_msgTypes[37] + mi := &file_github_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2556,7 +2771,7 @@ func (x *RepoDispatchInput) ProtoReflect() protoreflect.Message { // Deprecated: Use RepoDispatchInput.ProtoReflect.Descriptor instead. func (*RepoDispatchInput) Descriptor() ([]byte, []int) { - return file_github_proto_rawDescGZIP(), []int{37} + return file_github_proto_rawDescGZIP(), []int{40} } func (x *RepoDispatchInput) GetData() *structpb.Struct { @@ -2579,7 +2794,7 @@ type RepoDispatchOutput struct { func (x *RepoDispatchOutput) Reset() { *x = RepoDispatchOutput{} - mi := &file_github_proto_msgTypes[38] + mi := &file_github_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2591,7 +2806,7 @@ func (x *RepoDispatchOutput) String() string { func (*RepoDispatchOutput) ProtoMessage() {} func (x *RepoDispatchOutput) ProtoReflect() protoreflect.Message { - mi := &file_github_proto_msgTypes[38] + mi := &file_github_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2604,7 +2819,7 @@ func (x *RepoDispatchOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use RepoDispatchOutput.ProtoReflect.Descriptor instead. func (*RepoDispatchOutput) Descriptor() ([]byte, []int) { - return file_github_proto_rawDescGZIP(), []int{38} + return file_github_proto_rawDescGZIP(), []int{41} } func (x *RepoDispatchOutput) GetDispatched() bool { @@ -2651,7 +2866,7 @@ type DeploymentCreateConfig struct { func (x *DeploymentCreateConfig) Reset() { *x = DeploymentCreateConfig{} - mi := &file_github_proto_msgTypes[39] + mi := &file_github_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2663,7 +2878,7 @@ func (x *DeploymentCreateConfig) String() string { func (*DeploymentCreateConfig) ProtoMessage() {} func (x *DeploymentCreateConfig) ProtoReflect() protoreflect.Message { - mi := &file_github_proto_msgTypes[39] + mi := &file_github_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2676,7 +2891,7 @@ func (x *DeploymentCreateConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use DeploymentCreateConfig.ProtoReflect.Descriptor instead. func (*DeploymentCreateConfig) Descriptor() ([]byte, []int) { - return file_github_proto_rawDescGZIP(), []int{39} + return file_github_proto_rawDescGZIP(), []int{42} } func (x *DeploymentCreateConfig) GetOwner() string { @@ -2738,7 +2953,7 @@ type DeploymentCreateInput struct { func (x *DeploymentCreateInput) Reset() { *x = DeploymentCreateInput{} - mi := &file_github_proto_msgTypes[40] + mi := &file_github_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2750,7 +2965,7 @@ func (x *DeploymentCreateInput) String() string { func (*DeploymentCreateInput) ProtoMessage() {} func (x *DeploymentCreateInput) ProtoReflect() protoreflect.Message { - mi := &file_github_proto_msgTypes[40] + mi := &file_github_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2763,7 +2978,7 @@ func (x *DeploymentCreateInput) ProtoReflect() protoreflect.Message { // Deprecated: Use DeploymentCreateInput.ProtoReflect.Descriptor instead. func (*DeploymentCreateInput) Descriptor() ([]byte, []int) { - return file_github_proto_rawDescGZIP(), []int{40} + return file_github_proto_rawDescGZIP(), []int{43} } func (x *DeploymentCreateInput) GetData() *structpb.Struct { @@ -2787,7 +3002,7 @@ type DeploymentCreateOutput struct { func (x *DeploymentCreateOutput) Reset() { *x = DeploymentCreateOutput{} - mi := &file_github_proto_msgTypes[41] + mi := &file_github_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2799,7 +3014,7 @@ func (x *DeploymentCreateOutput) String() string { func (*DeploymentCreateOutput) ProtoMessage() {} func (x *DeploymentCreateOutput) ProtoReflect() protoreflect.Message { - mi := &file_github_proto_msgTypes[41] + mi := &file_github_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2812,7 +3027,7 @@ func (x *DeploymentCreateOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use DeploymentCreateOutput.ProtoReflect.Descriptor instead. func (*DeploymentCreateOutput) Descriptor() ([]byte, []int) { - return file_github_proto_rawDescGZIP(), []int{41} + return file_github_proto_rawDescGZIP(), []int{44} } func (x *DeploymentCreateOutput) GetDeploymentId() int64 { @@ -2864,7 +3079,7 @@ type SecretSetConfig struct { func (x *SecretSetConfig) Reset() { *x = SecretSetConfig{} - mi := &file_github_proto_msgTypes[42] + mi := &file_github_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2876,7 +3091,7 @@ func (x *SecretSetConfig) String() string { func (*SecretSetConfig) ProtoMessage() {} func (x *SecretSetConfig) ProtoReflect() protoreflect.Message { - mi := &file_github_proto_msgTypes[42] + mi := &file_github_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2889,7 +3104,7 @@ func (x *SecretSetConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use SecretSetConfig.ProtoReflect.Descriptor instead. func (*SecretSetConfig) Descriptor() ([]byte, []int) { - return file_github_proto_rawDescGZIP(), []int{42} + return file_github_proto_rawDescGZIP(), []int{45} } func (x *SecretSetConfig) GetOwner() string { @@ -2937,7 +3152,7 @@ type SecretSetInput struct { func (x *SecretSetInput) Reset() { *x = SecretSetInput{} - mi := &file_github_proto_msgTypes[43] + mi := &file_github_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2949,7 +3164,7 @@ func (x *SecretSetInput) String() string { func (*SecretSetInput) ProtoMessage() {} func (x *SecretSetInput) ProtoReflect() protoreflect.Message { - mi := &file_github_proto_msgTypes[43] + mi := &file_github_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2962,7 +3177,7 @@ func (x *SecretSetInput) ProtoReflect() protoreflect.Message { // Deprecated: Use SecretSetInput.ProtoReflect.Descriptor instead. func (*SecretSetInput) Descriptor() ([]byte, []int) { - return file_github_proto_rawDescGZIP(), []int{43} + return file_github_proto_rawDescGZIP(), []int{46} } func (x *SecretSetInput) GetData() *structpb.Struct { @@ -2985,7 +3200,7 @@ type SecretSetOutput struct { func (x *SecretSetOutput) Reset() { *x = SecretSetOutput{} - mi := &file_github_proto_msgTypes[44] + mi := &file_github_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2997,7 +3212,7 @@ func (x *SecretSetOutput) String() string { func (*SecretSetOutput) ProtoMessage() {} func (x *SecretSetOutput) ProtoReflect() protoreflect.Message { - mi := &file_github_proto_msgTypes[44] + mi := &file_github_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3010,7 +3225,7 @@ func (x *SecretSetOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use SecretSetOutput.ProtoReflect.Descriptor instead. func (*SecretSetOutput) Descriptor() ([]byte, []int) { - return file_github_proto_rawDescGZIP(), []int{44} + return file_github_proto_rawDescGZIP(), []int{47} } func (x *SecretSetOutput) GetName() string { @@ -3053,7 +3268,7 @@ type GraphQLConfig struct { func (x *GraphQLConfig) Reset() { *x = GraphQLConfig{} - mi := &file_github_proto_msgTypes[45] + mi := &file_github_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3065,7 +3280,7 @@ func (x *GraphQLConfig) String() string { func (*GraphQLConfig) ProtoMessage() {} func (x *GraphQLConfig) ProtoReflect() protoreflect.Message { - mi := &file_github_proto_msgTypes[45] + mi := &file_github_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3078,7 +3293,7 @@ func (x *GraphQLConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use GraphQLConfig.ProtoReflect.Descriptor instead. func (*GraphQLConfig) Descriptor() ([]byte, []int) { - return file_github_proto_rawDescGZIP(), []int{45} + return file_github_proto_rawDescGZIP(), []int{48} } func (x *GraphQLConfig) GetQuery() string { @@ -3112,7 +3327,7 @@ type GraphQLInput struct { func (x *GraphQLInput) Reset() { *x = GraphQLInput{} - mi := &file_github_proto_msgTypes[46] + mi := &file_github_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3124,7 +3339,7 @@ func (x *GraphQLInput) String() string { func (*GraphQLInput) ProtoMessage() {} func (x *GraphQLInput) ProtoReflect() protoreflect.Message { - mi := &file_github_proto_msgTypes[46] + mi := &file_github_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3137,7 +3352,7 @@ func (x *GraphQLInput) ProtoReflect() protoreflect.Message { // Deprecated: Use GraphQLInput.ProtoReflect.Descriptor instead. func (*GraphQLInput) Descriptor() ([]byte, []int) { - return file_github_proto_rawDescGZIP(), []int{46} + return file_github_proto_rawDescGZIP(), []int{49} } func (x *GraphQLInput) GetData() *structpb.Struct { @@ -3158,7 +3373,7 @@ type GraphQLOutput struct { func (x *GraphQLOutput) Reset() { *x = GraphQLOutput{} - mi := &file_github_proto_msgTypes[47] + mi := &file_github_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3170,7 +3385,7 @@ func (x *GraphQLOutput) String() string { func (*GraphQLOutput) ProtoMessage() {} func (x *GraphQLOutput) ProtoReflect() protoreflect.Message { - mi := &file_github_proto_msgTypes[47] + mi := &file_github_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3183,7 +3398,7 @@ func (x *GraphQLOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use GraphQLOutput.ProtoReflect.Descriptor instead. func (*GraphQLOutput) Descriptor() ([]byte, []int) { - return file_github_proto_rawDescGZIP(), []int{47} + return file_github_proto_rawDescGZIP(), []int{50} } func (x *GraphQLOutput) GetData() *structpb.Struct { @@ -3386,7 +3601,28 @@ const file_github_proto_rawDesc = "" + "\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x10\n" + "\x03url\x18\x02 \x01(\tR\x03url\x12\x12\n" + "\x04name\x18\x03 \x01(\tR\x04name\x12\x12\n" + - "\x04size\x18\x04 \x01(\x03R\x04size\"\xa6\x01\n" + + "\x04size\x18\x04 \x01(\x03R\x04size\"\x9f\x01\n" + + "\x1cUpstreamReleaseMonitorConfig\x12%\n" + + "\x0eupstream_owner\x18\x01 \x01(\tR\rupstreamOwner\x12#\n" + + "\rupstream_repo\x18\x02 \x01(\tR\fupstreamRepo\x12\x1d\n" + + "\n" + + "pinned_tag\x18\x03 \x01(\tR\tpinnedTag\x12\x14\n" + + "\x05token\x18\x04 \x01(\tR\x05token\"J\n" + + "\x1bUpstreamReleaseMonitorInput\x12+\n" + + "\x04data\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x04data\"\xb6\x02\n" + + "\x1cUpstreamReleaseMonitorOutput\x12%\n" + + "\x0eupstream_owner\x18\x01 \x01(\tR\rupstreamOwner\x12#\n" + + "\rupstream_repo\x18\x02 \x01(\tR\fupstreamRepo\x12\x1d\n" + + "\n" + + "pinned_tag\x18\x03 \x01(\tR\tpinnedTag\x12\x1d\n" + + "\n" + + "latest_tag\x18\x04 \x01(\tR\tlatestTag\x12)\n" + + "\x10update_available\x18\x05 \x01(\bR\x0fupdateAvailable\x12\x1d\n" + + "\n" + + "release_id\x18\x06 \x01(\x03R\treleaseId\x12\x1f\n" + + "\vrelease_url\x18\a \x01(\tR\n" + + "releaseUrl\x12!\n" + + "\fpublished_at\x18\b \x01(\tR\vpublishedAt\"\xa6\x01\n" + "\x12RepoDispatchConfig\x12\x14\n" + "\x05owner\x18\x01 \x01(\tR\x05owner\x12\x12\n" + "\x04repo\x18\x02 \x01(\tR\x04repo\x12\x1d\n" + @@ -3456,83 +3692,87 @@ func file_github_proto_rawDescGZIP() []byte { return file_github_proto_rawDescData } -var file_github_proto_msgTypes = make([]protoimpl.MessageInfo, 48) +var file_github_proto_msgTypes = make([]protoimpl.MessageInfo, 51) var file_github_proto_goTypes = []any{ - (*WebhookModuleConfig)(nil), // 0: workflow.plugin.github.v1.WebhookModuleConfig - (*GitHubAppModuleConfig)(nil), // 1: workflow.plugin.github.v1.GitHubAppModuleConfig - (*RunnerProviderModuleConfig)(nil), // 2: workflow.plugin.github.v1.RunnerProviderModuleConfig - (*ActionTriggerConfig)(nil), // 3: workflow.plugin.github.v1.ActionTriggerConfig - (*ActionTriggerInput)(nil), // 4: workflow.plugin.github.v1.ActionTriggerInput - (*ActionTriggerOutput)(nil), // 5: workflow.plugin.github.v1.ActionTriggerOutput - (*ActionStatusConfig)(nil), // 6: workflow.plugin.github.v1.ActionStatusConfig - (*ActionStatusInput)(nil), // 7: workflow.plugin.github.v1.ActionStatusInput - (*ActionStatusOutput)(nil), // 8: workflow.plugin.github.v1.ActionStatusOutput - (*PRCreateConfig)(nil), // 9: workflow.plugin.github.v1.PRCreateConfig - (*PRCreateInput)(nil), // 10: workflow.plugin.github.v1.PRCreateInput - (*PRCreateOutput)(nil), // 11: workflow.plugin.github.v1.PRCreateOutput - (*PRMergeConfig)(nil), // 12: workflow.plugin.github.v1.PRMergeConfig - (*PRMergeInput)(nil), // 13: workflow.plugin.github.v1.PRMergeInput - (*PRMergeOutput)(nil), // 14: workflow.plugin.github.v1.PRMergeOutput - (*PRCommentConfig)(nil), // 15: workflow.plugin.github.v1.PRCommentConfig - (*PRCommentInput)(nil), // 16: workflow.plugin.github.v1.PRCommentInput - (*PRCommentOutput)(nil), // 17: workflow.plugin.github.v1.PRCommentOutput - (*PRReviewConfig)(nil), // 18: workflow.plugin.github.v1.PRReviewConfig - (*PRReviewInput)(nil), // 19: workflow.plugin.github.v1.PRReviewInput - (*PRReviewOutput)(nil), // 20: workflow.plugin.github.v1.PRReviewOutput - (*IssueCreateConfig)(nil), // 21: workflow.plugin.github.v1.IssueCreateConfig - (*IssueCreateInput)(nil), // 22: workflow.plugin.github.v1.IssueCreateInput - (*IssueCreateOutput)(nil), // 23: workflow.plugin.github.v1.IssueCreateOutput - (*IssueCloseConfig)(nil), // 24: workflow.plugin.github.v1.IssueCloseConfig - (*IssueCloseInput)(nil), // 25: workflow.plugin.github.v1.IssueCloseInput - (*IssueCloseOutput)(nil), // 26: workflow.plugin.github.v1.IssueCloseOutput - (*IssueLabelConfig)(nil), // 27: workflow.plugin.github.v1.IssueLabelConfig - (*IssueLabelInput)(nil), // 28: workflow.plugin.github.v1.IssueLabelInput - (*IssueLabelOutput)(nil), // 29: workflow.plugin.github.v1.IssueLabelOutput - (*ReleaseCreateConfig)(nil), // 30: workflow.plugin.github.v1.ReleaseCreateConfig - (*ReleaseCreateInput)(nil), // 31: workflow.plugin.github.v1.ReleaseCreateInput - (*ReleaseCreateOutput)(nil), // 32: workflow.plugin.github.v1.ReleaseCreateOutput - (*ReleaseUploadConfig)(nil), // 33: workflow.plugin.github.v1.ReleaseUploadConfig - (*ReleaseUploadInput)(nil), // 34: workflow.plugin.github.v1.ReleaseUploadInput - (*ReleaseUploadOutput)(nil), // 35: workflow.plugin.github.v1.ReleaseUploadOutput - (*RepoDispatchConfig)(nil), // 36: workflow.plugin.github.v1.RepoDispatchConfig - (*RepoDispatchInput)(nil), // 37: workflow.plugin.github.v1.RepoDispatchInput - (*RepoDispatchOutput)(nil), // 38: workflow.plugin.github.v1.RepoDispatchOutput - (*DeploymentCreateConfig)(nil), // 39: workflow.plugin.github.v1.DeploymentCreateConfig - (*DeploymentCreateInput)(nil), // 40: workflow.plugin.github.v1.DeploymentCreateInput - (*DeploymentCreateOutput)(nil), // 41: workflow.plugin.github.v1.DeploymentCreateOutput - (*SecretSetConfig)(nil), // 42: workflow.plugin.github.v1.SecretSetConfig - (*SecretSetInput)(nil), // 43: workflow.plugin.github.v1.SecretSetInput - (*SecretSetOutput)(nil), // 44: workflow.plugin.github.v1.SecretSetOutput - (*GraphQLConfig)(nil), // 45: workflow.plugin.github.v1.GraphQLConfig - (*GraphQLInput)(nil), // 46: workflow.plugin.github.v1.GraphQLInput - (*GraphQLOutput)(nil), // 47: workflow.plugin.github.v1.GraphQLOutput - (*structpb.Struct)(nil), // 48: google.protobuf.Struct + (*WebhookModuleConfig)(nil), // 0: workflow.plugin.github.v1.WebhookModuleConfig + (*GitHubAppModuleConfig)(nil), // 1: workflow.plugin.github.v1.GitHubAppModuleConfig + (*RunnerProviderModuleConfig)(nil), // 2: workflow.plugin.github.v1.RunnerProviderModuleConfig + (*ActionTriggerConfig)(nil), // 3: workflow.plugin.github.v1.ActionTriggerConfig + (*ActionTriggerInput)(nil), // 4: workflow.plugin.github.v1.ActionTriggerInput + (*ActionTriggerOutput)(nil), // 5: workflow.plugin.github.v1.ActionTriggerOutput + (*ActionStatusConfig)(nil), // 6: workflow.plugin.github.v1.ActionStatusConfig + (*ActionStatusInput)(nil), // 7: workflow.plugin.github.v1.ActionStatusInput + (*ActionStatusOutput)(nil), // 8: workflow.plugin.github.v1.ActionStatusOutput + (*PRCreateConfig)(nil), // 9: workflow.plugin.github.v1.PRCreateConfig + (*PRCreateInput)(nil), // 10: workflow.plugin.github.v1.PRCreateInput + (*PRCreateOutput)(nil), // 11: workflow.plugin.github.v1.PRCreateOutput + (*PRMergeConfig)(nil), // 12: workflow.plugin.github.v1.PRMergeConfig + (*PRMergeInput)(nil), // 13: workflow.plugin.github.v1.PRMergeInput + (*PRMergeOutput)(nil), // 14: workflow.plugin.github.v1.PRMergeOutput + (*PRCommentConfig)(nil), // 15: workflow.plugin.github.v1.PRCommentConfig + (*PRCommentInput)(nil), // 16: workflow.plugin.github.v1.PRCommentInput + (*PRCommentOutput)(nil), // 17: workflow.plugin.github.v1.PRCommentOutput + (*PRReviewConfig)(nil), // 18: workflow.plugin.github.v1.PRReviewConfig + (*PRReviewInput)(nil), // 19: workflow.plugin.github.v1.PRReviewInput + (*PRReviewOutput)(nil), // 20: workflow.plugin.github.v1.PRReviewOutput + (*IssueCreateConfig)(nil), // 21: workflow.plugin.github.v1.IssueCreateConfig + (*IssueCreateInput)(nil), // 22: workflow.plugin.github.v1.IssueCreateInput + (*IssueCreateOutput)(nil), // 23: workflow.plugin.github.v1.IssueCreateOutput + (*IssueCloseConfig)(nil), // 24: workflow.plugin.github.v1.IssueCloseConfig + (*IssueCloseInput)(nil), // 25: workflow.plugin.github.v1.IssueCloseInput + (*IssueCloseOutput)(nil), // 26: workflow.plugin.github.v1.IssueCloseOutput + (*IssueLabelConfig)(nil), // 27: workflow.plugin.github.v1.IssueLabelConfig + (*IssueLabelInput)(nil), // 28: workflow.plugin.github.v1.IssueLabelInput + (*IssueLabelOutput)(nil), // 29: workflow.plugin.github.v1.IssueLabelOutput + (*ReleaseCreateConfig)(nil), // 30: workflow.plugin.github.v1.ReleaseCreateConfig + (*ReleaseCreateInput)(nil), // 31: workflow.plugin.github.v1.ReleaseCreateInput + (*ReleaseCreateOutput)(nil), // 32: workflow.plugin.github.v1.ReleaseCreateOutput + (*ReleaseUploadConfig)(nil), // 33: workflow.plugin.github.v1.ReleaseUploadConfig + (*ReleaseUploadInput)(nil), // 34: workflow.plugin.github.v1.ReleaseUploadInput + (*ReleaseUploadOutput)(nil), // 35: workflow.plugin.github.v1.ReleaseUploadOutput + (*UpstreamReleaseMonitorConfig)(nil), // 36: workflow.plugin.github.v1.UpstreamReleaseMonitorConfig + (*UpstreamReleaseMonitorInput)(nil), // 37: workflow.plugin.github.v1.UpstreamReleaseMonitorInput + (*UpstreamReleaseMonitorOutput)(nil), // 38: workflow.plugin.github.v1.UpstreamReleaseMonitorOutput + (*RepoDispatchConfig)(nil), // 39: workflow.plugin.github.v1.RepoDispatchConfig + (*RepoDispatchInput)(nil), // 40: workflow.plugin.github.v1.RepoDispatchInput + (*RepoDispatchOutput)(nil), // 41: workflow.plugin.github.v1.RepoDispatchOutput + (*DeploymentCreateConfig)(nil), // 42: workflow.plugin.github.v1.DeploymentCreateConfig + (*DeploymentCreateInput)(nil), // 43: workflow.plugin.github.v1.DeploymentCreateInput + (*DeploymentCreateOutput)(nil), // 44: workflow.plugin.github.v1.DeploymentCreateOutput + (*SecretSetConfig)(nil), // 45: workflow.plugin.github.v1.SecretSetConfig + (*SecretSetInput)(nil), // 46: workflow.plugin.github.v1.SecretSetInput + (*SecretSetOutput)(nil), // 47: workflow.plugin.github.v1.SecretSetOutput + (*GraphQLConfig)(nil), // 48: workflow.plugin.github.v1.GraphQLConfig + (*GraphQLInput)(nil), // 49: workflow.plugin.github.v1.GraphQLInput + (*GraphQLOutput)(nil), // 50: workflow.plugin.github.v1.GraphQLOutput + (*structpb.Struct)(nil), // 51: google.protobuf.Struct } var file_github_proto_depIdxs = []int32{ - 48, // 0: workflow.plugin.github.v1.ActionTriggerConfig.inputs:type_name -> google.protobuf.Struct - 48, // 1: workflow.plugin.github.v1.ActionTriggerInput.data:type_name -> google.protobuf.Struct - 48, // 2: workflow.plugin.github.v1.ActionStatusInput.data:type_name -> google.protobuf.Struct - 48, // 3: workflow.plugin.github.v1.PRCreateInput.data:type_name -> google.protobuf.Struct - 48, // 4: workflow.plugin.github.v1.PRMergeInput.data:type_name -> google.protobuf.Struct - 48, // 5: workflow.plugin.github.v1.PRCommentInput.data:type_name -> google.protobuf.Struct - 48, // 6: workflow.plugin.github.v1.PRReviewInput.data:type_name -> google.protobuf.Struct - 48, // 7: workflow.plugin.github.v1.IssueCreateInput.data:type_name -> google.protobuf.Struct - 48, // 8: workflow.plugin.github.v1.IssueCloseInput.data:type_name -> google.protobuf.Struct - 48, // 9: workflow.plugin.github.v1.IssueLabelInput.data:type_name -> google.protobuf.Struct - 48, // 10: workflow.plugin.github.v1.ReleaseCreateInput.data:type_name -> google.protobuf.Struct - 48, // 11: workflow.plugin.github.v1.ReleaseUploadInput.data:type_name -> google.protobuf.Struct - 48, // 12: workflow.plugin.github.v1.RepoDispatchConfig.payload:type_name -> google.protobuf.Struct - 48, // 13: workflow.plugin.github.v1.RepoDispatchInput.data:type_name -> google.protobuf.Struct - 48, // 14: workflow.plugin.github.v1.DeploymentCreateInput.data:type_name -> google.protobuf.Struct - 48, // 15: workflow.plugin.github.v1.SecretSetInput.data:type_name -> google.protobuf.Struct - 48, // 16: workflow.plugin.github.v1.GraphQLConfig.variables:type_name -> google.protobuf.Struct - 48, // 17: workflow.plugin.github.v1.GraphQLInput.data:type_name -> google.protobuf.Struct - 48, // 18: workflow.plugin.github.v1.GraphQLOutput.data:type_name -> google.protobuf.Struct - 19, // [19:19] is the sub-list for method output_type - 19, // [19:19] is the sub-list for method input_type - 19, // [19:19] is the sub-list for extension type_name - 19, // [19:19] is the sub-list for extension extendee - 0, // [0:19] is the sub-list for field type_name + 51, // 0: workflow.plugin.github.v1.ActionTriggerConfig.inputs:type_name -> google.protobuf.Struct + 51, // 1: workflow.plugin.github.v1.ActionTriggerInput.data:type_name -> google.protobuf.Struct + 51, // 2: workflow.plugin.github.v1.ActionStatusInput.data:type_name -> google.protobuf.Struct + 51, // 3: workflow.plugin.github.v1.PRCreateInput.data:type_name -> google.protobuf.Struct + 51, // 4: workflow.plugin.github.v1.PRMergeInput.data:type_name -> google.protobuf.Struct + 51, // 5: workflow.plugin.github.v1.PRCommentInput.data:type_name -> google.protobuf.Struct + 51, // 6: workflow.plugin.github.v1.PRReviewInput.data:type_name -> google.protobuf.Struct + 51, // 7: workflow.plugin.github.v1.IssueCreateInput.data:type_name -> google.protobuf.Struct + 51, // 8: workflow.plugin.github.v1.IssueCloseInput.data:type_name -> google.protobuf.Struct + 51, // 9: workflow.plugin.github.v1.IssueLabelInput.data:type_name -> google.protobuf.Struct + 51, // 10: workflow.plugin.github.v1.ReleaseCreateInput.data:type_name -> google.protobuf.Struct + 51, // 11: workflow.plugin.github.v1.ReleaseUploadInput.data:type_name -> google.protobuf.Struct + 51, // 12: workflow.plugin.github.v1.UpstreamReleaseMonitorInput.data:type_name -> google.protobuf.Struct + 51, // 13: workflow.plugin.github.v1.RepoDispatchConfig.payload:type_name -> google.protobuf.Struct + 51, // 14: workflow.plugin.github.v1.RepoDispatchInput.data:type_name -> google.protobuf.Struct + 51, // 15: workflow.plugin.github.v1.DeploymentCreateInput.data:type_name -> google.protobuf.Struct + 51, // 16: workflow.plugin.github.v1.SecretSetInput.data:type_name -> google.protobuf.Struct + 51, // 17: workflow.plugin.github.v1.GraphQLConfig.variables:type_name -> google.protobuf.Struct + 51, // 18: workflow.plugin.github.v1.GraphQLInput.data:type_name -> google.protobuf.Struct + 51, // 19: workflow.plugin.github.v1.GraphQLOutput.data:type_name -> google.protobuf.Struct + 20, // [20:20] is the sub-list for method output_type + 20, // [20:20] is the sub-list for method input_type + 20, // [20:20] is the sub-list for extension type_name + 20, // [20:20] is the sub-list for extension extendee + 0, // [0:20] is the sub-list for field type_name } func init() { file_github_proto_init() } @@ -3546,7 +3786,7 @@ func file_github_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_github_proto_rawDesc), len(file_github_proto_rawDesc)), NumEnums: 0, - NumMessages: 48, + NumMessages: 51, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/contracts.go b/internal/contracts.go index fa91cc9..131d242 100644 --- a/internal/contracts.go +++ b/internal/contracts.go @@ -136,6 +136,14 @@ var githubContractRegistry = &pb.ContractRegistry{ OutputMessage: githubProtoPkg + "ReleaseUploadOutput", Mode: pb.ContractMode_CONTRACT_MODE_STRICT_PROTO, }, + { + Kind: pb.ContractKind_CONTRACT_KIND_STEP, + StepType: "step.gh_upstream_release_monitor", + ConfigMessage: githubProtoPkg + "UpstreamReleaseMonitorConfig", + InputMessage: githubProtoPkg + "UpstreamReleaseMonitorInput", + OutputMessage: githubProtoPkg + "UpstreamReleaseMonitorOutput", + Mode: pb.ContractMode_CONTRACT_MODE_STRICT_PROTO, + }, { Kind: pb.ContractKind_CONTRACT_KIND_STEP, StepType: "step.gh_repo_dispatch", diff --git a/internal/contracts_test.go b/internal/contracts_test.go index c4497cc..b726149 100644 --- a/internal/contracts_test.go +++ b/internal/contracts_test.go @@ -63,6 +63,7 @@ func TestContractRegistry_CoversAllStepTypes(t *testing.T) { "step.gh_issue_label", "step.gh_release_create", "step.gh_release_upload", + "step.gh_upstream_release_monitor", "step.gh_repo_dispatch", "step.gh_deployment_create", "step.gh_secret_set", @@ -115,8 +116,8 @@ func TestT916_GitHubPluginDoesNotExposeComputeGatewayOrSyntheticCheckSteps(t *te func TestContractRegistry_ContractCount(t *testing.T) { p := &githubPlugin{} reg := p.ContractRegistry() - // 3 modules + 15 steps = 18 total - if len(reg.Contracts) != 18 { - t.Errorf("expected 18 contracts (3 modules + 15 steps), got %d", len(reg.Contracts)) + // 3 modules + 16 steps = 19 total + if len(reg.Contracts) != 19 { + t.Errorf("expected 19 contracts (3 modules + 16 steps), got %d", len(reg.Contracts)) } } diff --git a/internal/plugin.go b/internal/plugin.go index e3cee4c..ebe22b9 100644 --- a/internal/plugin.go +++ b/internal/plugin.go @@ -83,6 +83,7 @@ func (p *githubPlugin) StepTypes() []string { // Release steps "step.gh_release_create", "step.gh_release_upload", + "step.gh_upstream_release_monitor", // Repository steps "step.gh_repo_dispatch", "step.gh_deployment_create", @@ -117,6 +118,8 @@ func (p *githubPlugin) CreateStep(typeName, name string, config map[string]any) return newReleaseCreateStep(name, config) case "step.gh_release_upload": return newReleaseUploadStep(name, config) + case "step.gh_upstream_release_monitor": + return newUpstreamReleaseMonitorStep(name, config, nil) case "step.gh_repo_dispatch": return newRepoDispatchStep(name, config) case "step.gh_deployment_create": diff --git a/internal/schemas_test.go b/internal/schemas_test.go index 8dbdf06..16a3e9e 100644 --- a/internal/schemas_test.go +++ b/internal/schemas_test.go @@ -250,7 +250,7 @@ func TestStepSchemaFieldContracts(t *testing.T) { for _, s := range manifest.StepSchemas { fields := fieldsByKey(s) if tok, ok := fields["token"]; ok { - if !tok.Required { + if !tok.Required && s.Type != "step.gh_upstream_release_monitor" { t.Errorf("%s: token field must be required=true (step fails at runtime without it)", s.Type) } if !tok.Sensitive { diff --git a/internal/step_upstream_release_monitor.go b/internal/step_upstream_release_monitor.go new file mode 100644 index 0000000..7daffc0 --- /dev/null +++ b/internal/step_upstream_release_monitor.go @@ -0,0 +1,130 @@ +package internal + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/google/go-github/v69/github" + + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +type upstreamReleaseMonitorStep struct { + name string + config upstreamReleaseMonitorConfig + ghClient upstreamReleaseClient +} + +type upstreamReleaseMonitorConfig struct { + UpstreamOwner string `yaml:"upstream_owner"` + UpstreamRepo string `yaml:"upstream_repo"` + PinnedTag string `yaml:"pinned_tag"` + Token string `yaml:"token"` +} + +type upstreamReleaseInfo struct { + ID int64 + TagName string + HTMLURL string + PublishedAt time.Time +} + +type upstreamReleaseClient interface { + LatestRelease(ctx context.Context, owner, repo, token string) (upstreamReleaseInfo, error) +} + +type githubUpstreamReleaseClient struct{} + +func newUpstreamReleaseMonitorStep(name string, raw map[string]any, client upstreamReleaseClient) (*upstreamReleaseMonitorStep, error) { + cfg, err := parseUpstreamReleaseMonitorConfig(raw) + if err != nil { + return nil, fmt.Errorf("step.gh_upstream_release_monitor %q: %w", name, err) + } + if client == nil { + client = githubUpstreamReleaseClient{} + } + return &upstreamReleaseMonitorStep{name: name, config: cfg, ghClient: client}, nil +} + +func parseUpstreamReleaseMonitorConfig(raw map[string]any) (upstreamReleaseMonitorConfig, error) { + var cfg upstreamReleaseMonitorConfig + cfg.UpstreamOwner, _ = raw["upstream_owner"].(string) + if cfg.UpstreamOwner == "" { + return cfg, fmt.Errorf("config.upstream_owner is required") + } + cfg.UpstreamRepo, _ = raw["upstream_repo"].(string) + if cfg.UpstreamRepo == "" { + return cfg, fmt.Errorf("config.upstream_repo is required") + } + cfg.PinnedTag, _ = raw["pinned_tag"].(string) + if cfg.PinnedTag == "" { + return cfg, fmt.Errorf("config.pinned_tag is required") + } + cfg.Token, _ = raw["token"].(string) + cfg.Token = os.ExpandEnv(cfg.Token) + return cfg, nil +} + +func (s *upstreamReleaseMonitorStep) Execute( + ctx context.Context, + triggerData map[string]any, + stepOutputs map[string]map[string]any, + current map[string]any, + _ map[string]any, + _ map[string]any, +) (*sdk.StepResult, error) { + owner := resolveField(s.config.UpstreamOwner, triggerData, stepOutputs, current) + repo := resolveField(s.config.UpstreamRepo, triggerData, stepOutputs, current) + pinnedTag := resolveField(s.config.PinnedTag, triggerData, stepOutputs, current) + + release, err := s.ghClient.LatestRelease(ctx, owner, repo, s.config.Token) + if err != nil { + return errorResult(fmt.Sprintf("get latest upstream release: %v", err)), nil + } + + publishedAt := "" + if !release.PublishedAt.IsZero() { + publishedAt = release.PublishedAt.UTC().Format(time.RFC3339) + } + + return &sdk.StepResult{ + Output: map[string]any{ + "upstream_owner": owner, + "upstream_repo": repo, + "pinned_tag": pinnedTag, + "latest_tag": release.TagName, + "update_available": release.TagName != pinnedTag, + "release_id": release.ID, + "release_url": release.HTMLURL, + "published_at": publishedAt, + }, + }, nil +} + +func (githubUpstreamReleaseClient) LatestRelease(ctx context.Context, owner, repo, token string) (upstreamReleaseInfo, error) { + client := github.NewClient(nil) + if token != "" { + client = client.WithAuthToken(token) + } + + release, _, err := client.Repositories.GetLatestRelease(ctx, owner, repo) + if err != nil { + return upstreamReleaseInfo{}, err + } + if release == nil { + return upstreamReleaseInfo{}, fmt.Errorf("empty release response") + } + + var publishedAt time.Time + if release.PublishedAt != nil { + publishedAt = release.PublishedAt.Time + } + return upstreamReleaseInfo{ + ID: release.GetID(), + TagName: release.GetTagName(), + HTMLURL: release.GetHTMLURL(), + PublishedAt: publishedAt, + }, nil +} diff --git a/internal/step_upstream_release_monitor_test.go b/internal/step_upstream_release_monitor_test.go new file mode 100644 index 0000000..306df3b --- /dev/null +++ b/internal/step_upstream_release_monitor_test.go @@ -0,0 +1,195 @@ +package internal + +import ( + "context" + "errors" + "testing" + "time" +) + +type mockUpstreamReleaseClient struct { + latestReleaseFunc func(ctx context.Context, owner, repo, token string) (upstreamReleaseInfo, error) +} + +func (m mockUpstreamReleaseClient) LatestRelease(ctx context.Context, owner, repo, token string) (upstreamReleaseInfo, error) { + if m.latestReleaseFunc != nil { + return m.latestReleaseFunc(ctx, owner, repo, token) + } + return upstreamReleaseInfo{}, nil +} + +func TestUpstreamReleaseMonitorStep_UpdateAvailable(t *testing.T) { + publishedAt := time.Date(2026, 6, 25, 12, 30, 0, 0, time.UTC) + var capturedOwner, capturedRepo, capturedToken string + client := mockUpstreamReleaseClient{ + latestReleaseFunc: func(_ context.Context, owner, repo, token string) (upstreamReleaseInfo, error) { + capturedOwner = owner + capturedRepo = repo + capturedToken = token + return upstreamReleaseInfo{ + ID: 96, + TagName: "v0.96.4", + HTMLURL: "https://github.com/signalapp/libsignal/releases/tag/v0.96.4", + PublishedAt: publishedAt, + }, nil + }, + } + + step, err := newUpstreamReleaseMonitorStep("check", map[string]any{ + "upstream_owner": "signalapp", + "upstream_repo": "libsignal", + "pinned_tag": "v0.96.3", + "token": "gh-token", + }, client) + if err != nil { + t.Fatalf("newUpstreamReleaseMonitorStep: %v", err) + } + + result, err := step.Execute(context.Background(), nil, nil, nil, nil, nil) + if err != nil { + t.Fatalf("Execute: %v", err) + } + if result.StopPipeline { + t.Fatalf("expected StopPipeline=false, got true with output %#v", result.Output) + } + if capturedOwner != "signalapp" || capturedRepo != "libsignal" || capturedToken != "gh-token" { + t.Fatalf("captured request = %s/%s token %q", capturedOwner, capturedRepo, capturedToken) + } + if got := result.Output["update_available"]; got != true { + t.Fatalf("update_available = %v, want true", got) + } + if got := result.Output["latest_tag"]; got != "v0.96.4" { + t.Fatalf("latest_tag = %v, want v0.96.4", got) + } + if got := result.Output["pinned_tag"]; got != "v0.96.3" { + t.Fatalf("pinned_tag = %v, want v0.96.3", got) + } + if got := result.Output["release_id"]; got != int64(96) { + t.Fatalf("release_id = %v, want 96", got) + } + if got := result.Output["published_at"]; got != publishedAt.Format(time.RFC3339) { + t.Fatalf("published_at = %v, want %s", got, publishedAt.Format(time.RFC3339)) + } +} + +func TestUpstreamReleaseMonitorStep_NoUpdate(t *testing.T) { + step, err := newUpstreamReleaseMonitorStep("check", map[string]any{ + "upstream_owner": "signalapp", + "upstream_repo": "libsignal", + "pinned_tag": "v0.96.4", + }, mockUpstreamReleaseClient{ + latestReleaseFunc: func(context.Context, string, string, string) (upstreamReleaseInfo, error) { + return upstreamReleaseInfo{TagName: "v0.96.4"}, nil + }, + }) + if err != nil { + t.Fatalf("newUpstreamReleaseMonitorStep: %v", err) + } + + result, err := step.Execute(context.Background(), nil, nil, nil, nil, nil) + if err != nil { + t.Fatalf("Execute: %v", err) + } + if result.StopPipeline { + t.Fatalf("expected StopPipeline=false, got true") + } + if got := result.Output["update_available"]; got != false { + t.Fatalf("update_available = %v, want false", got) + } +} + +func TestUpstreamReleaseMonitorStep_ResolvesRuntimeFields(t *testing.T) { + var capturedOwner, capturedRepo string + step, err := newUpstreamReleaseMonitorStep("check", map[string]any{ + "upstream_owner": "{{.owner}}", + "upstream_repo": "{{.repo}}", + "pinned_tag": "{{.current.current_pin}}", + }, mockUpstreamReleaseClient{ + latestReleaseFunc: func(_ context.Context, owner, repo, _ string) (upstreamReleaseInfo, error) { + capturedOwner = owner + capturedRepo = repo + return upstreamReleaseInfo{TagName: "v2"}, nil + }, + }) + if err != nil { + t.Fatalf("newUpstreamReleaseMonitorStep: %v", err) + } + + result, err := step.Execute( + context.Background(), + map[string]any{"owner": "upstream", "repo": "project"}, + nil, + map[string]any{"current_pin": "v1"}, + nil, + nil, + ) + if err != nil { + t.Fatalf("Execute: %v", err) + } + if capturedOwner != "upstream" || capturedRepo != "project" { + t.Fatalf("captured request = %s/%s, want upstream/project", capturedOwner, capturedRepo) + } + if got := result.Output["pinned_tag"]; got != "v1" { + t.Fatalf("pinned_tag = %v, want v1", got) + } + if got := result.Output["update_available"]; got != true { + t.Fatalf("update_available = %v, want true", got) + } +} + +func TestUpstreamReleaseMonitorStep_APIErrorStopsPipeline(t *testing.T) { + step, err := newUpstreamReleaseMonitorStep("check", map[string]any{ + "upstream_owner": "signalapp", + "upstream_repo": "libsignal", + "pinned_tag": "v0.96.3", + }, mockUpstreamReleaseClient{ + latestReleaseFunc: func(context.Context, string, string, string) (upstreamReleaseInfo, error) { + return upstreamReleaseInfo{}, errors.New("rate limited") + }, + }) + if err != nil { + t.Fatalf("newUpstreamReleaseMonitorStep: %v", err) + } + + result, err := step.Execute(context.Background(), nil, nil, nil, nil, nil) + if err != nil { + t.Fatalf("Execute: %v", err) + } + if !result.StopPipeline { + t.Fatalf("expected StopPipeline=true on API error") + } +} + +func TestParseUpstreamReleaseMonitorConfig_RequiredFields(t *testing.T) { + tests := []struct { + name string + raw map[string]any + }{ + {name: "missing owner", raw: map[string]any{"upstream_repo": "libsignal", "pinned_tag": "v1"}}, + {name: "missing repo", raw: map[string]any{"upstream_owner": "signalapp", "pinned_tag": "v1"}}, + {name: "missing pinned tag", raw: map[string]any{"upstream_owner": "signalapp", "upstream_repo": "libsignal"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := parseUpstreamReleaseMonitorConfig(tt.raw); err == nil { + t.Fatal("expected error") + } + }) + } +} + +func TestGitHubPluginCreateStep_UpstreamReleaseMonitor(t *testing.T) { + p := &githubPlugin{} + step, err := p.CreateStep("step.gh_upstream_release_monitor", "check", map[string]any{ + "upstream_owner": "signalapp", + "upstream_repo": "libsignal", + "pinned_tag": "v0.96.4", + }) + if err != nil { + t.Fatalf("CreateStep: %v", err) + } + if _, ok := step.(*upstreamReleaseMonitorStep); !ok { + t.Fatalf("CreateStep returned %T, want *upstreamReleaseMonitorStep", step) + } +} diff --git a/plugin.contracts.json b/plugin.contracts.json index 849da23..b956f66 100644 --- a/plugin.contracts.json +++ b/plugin.contracts.json @@ -107,6 +107,14 @@ "input": "workflow.plugin.github.v1.ReleaseUploadInput", "output": "workflow.plugin.github.v1.ReleaseUploadOutput" }, + { + "kind": "step", + "type": "step.gh_upstream_release_monitor", + "mode": "strict_proto", + "config": "workflow.plugin.github.v1.UpstreamReleaseMonitorConfig", + "input": "workflow.plugin.github.v1.UpstreamReleaseMonitorInput", + "output": "workflow.plugin.github.v1.UpstreamReleaseMonitorOutput" + }, { "kind": "step", "type": "step.gh_repo_dispatch", diff --git a/plugin.json b/plugin.json index d9ca3f4..4b181d1 100644 --- a/plugin.json +++ b/plugin.json @@ -58,6 +58,7 @@ "step.gh_issue_label", "step.gh_release_create", "step.gh_release_upload", + "step.gh_upstream_release_monitor", "step.gh_repo_dispatch", "step.gh_deployment_create", "step.gh_secret_set", @@ -79,6 +80,7 @@ "step.gh_issue_label", "step.gh_release_create", "step.gh_release_upload", + "step.gh_upstream_release_monitor", "step.gh_repo_dispatch", "step.gh_deployment_create", "step.gh_secret_set", @@ -193,6 +195,14 @@ "input": "workflow.plugin.github.v1.ReleaseUploadInput", "output": "workflow.plugin.github.v1.ReleaseUploadOutput" }, + { + "kind": "step", + "type": "step.gh_upstream_release_monitor", + "mode": "strict_proto", + "config": "workflow.plugin.github.v1.UpstreamReleaseMonitorConfig", + "input": "workflow.plugin.github.v1.UpstreamReleaseMonitorInput", + "output": "workflow.plugin.github.v1.UpstreamReleaseMonitorOutput" + }, { "kind": "step", "type": "step.gh_repo_dispatch", @@ -436,6 +446,27 @@ {"key": "size", "type": "number", "description": "Asset file size in bytes"} ] }, + { + "type": "step.gh_upstream_release_monitor", + "plugin": "workflow-plugin-github", + "description": "Checks the latest release tag for an upstream GitHub repository and reports whether it differs from a pinned tag.", + "configFields": [ + {"key": "upstream_owner", "type": "string", "description": "Upstream GitHub repository owner", "required": true}, + {"key": "upstream_repo", "type": "string", "description": "Upstream GitHub repository name", "required": true}, + {"key": "pinned_tag", "type": "string", "description": "Currently pinned upstream release tag", "required": true}, + {"key": "token", "type": "string", "description": "Optional GitHub token for private repositories or higher rate limits", "sensitive": true} + ], + "outputs": [ + {"key": "upstream_owner", "type": "string", "description": "Upstream repository owner"}, + {"key": "upstream_repo", "type": "string", "description": "Upstream repository name"}, + {"key": "pinned_tag", "type": "string", "description": "Configured pinned tag"}, + {"key": "latest_tag", "type": "string", "description": "Latest release tag returned by GitHub"}, + {"key": "update_available", "type": "boolean", "description": "Whether latest_tag differs from pinned_tag"}, + {"key": "release_id", "type": "number", "description": "Latest release ID"}, + {"key": "release_url", "type": "string", "description": "Latest release HTML URL"}, + {"key": "published_at", "type": "string", "description": "Latest release publish timestamp in RFC3339 format"} + ] + }, { "type": "step.gh_repo_dispatch", "plugin": "workflow-plugin-github", diff --git a/proto/github/v1/github.proto b/proto/github/v1/github.proto index ab5059e..2920a87 100644 --- a/proto/github/v1/github.proto +++ b/proto/github/v1/github.proto @@ -301,6 +301,31 @@ message ReleaseUploadOutput { int64 size = 4; } +// UpstreamReleaseMonitorConfig is the typed config for step.gh_upstream_release_monitor. +message UpstreamReleaseMonitorConfig { + string upstream_owner = 1; + string upstream_repo = 2; + string pinned_tag = 3; + string token = 4; +} + +// UpstreamReleaseMonitorInput carries runtime inputs for step.gh_upstream_release_monitor. +message UpstreamReleaseMonitorInput { + google.protobuf.Struct data = 1; +} + +// UpstreamReleaseMonitorOutput holds the result of step.gh_upstream_release_monitor. +message UpstreamReleaseMonitorOutput { + string upstream_owner = 1; + string upstream_repo = 2; + string pinned_tag = 3; + string latest_tag = 4; + bool update_available = 5; + int64 release_id = 6; + string release_url = 7; + string published_at = 8; +} + // RepoDispatchConfig is the typed config for step.gh_repo_dispatch. message RepoDispatchConfig { string owner = 1; From 58f44dc1c49266e627c52ae3c50565e448bc28b5 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Thu, 2 Jul 2026 18:15:00 -0400 Subject: [PATCH 3/3] fix: address upstream monitor review --- .../2026-07-02-upstream-release-monitor.md | 3 +- ...-02-upstream-release-monitor.md.scope-lock | 2 +- internal/step_upstream_release_monitor.go | 12 ++++++- .../step_upstream_release_monitor_test.go | 31 +++++++++++++++++++ 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/docs/plans/2026-07-02-upstream-release-monitor.md b/docs/plans/2026-07-02-upstream-release-monitor.md index 0ba2d22..e119df3 100644 --- a/docs/plans/2026-07-02-upstream-release-monitor.md +++ b/docs/plans/2026-07-02-upstream-release-monitor.md @@ -1,6 +1,5 @@ # GitHub Upstream Release Monitor Plan -**Status:** Locked 2026-07-02T21:56:54Z **Date:** 2026-07-02 **Design:** docs/plans/2026-07-02-upstream-release-monitor-design.md @@ -21,7 +20,7 @@ |------|-------|-------|--------| | 1 | Add upstream release monitor step | Task 1, Task 2, Task 3, Task 4 | feat/upstream-release-monitor | -**Status:** Draft +**Status:** Locked 2026-07-02T21:56:54Z ### Task 1: Record Boundary diff --git a/docs/plans/2026-07-02-upstream-release-monitor.md.scope-lock b/docs/plans/2026-07-02-upstream-release-monitor.md.scope-lock index 416dbb6..f38e585 100644 --- a/docs/plans/2026-07-02-upstream-release-monitor.md.scope-lock +++ b/docs/plans/2026-07-02-upstream-release-monitor.md.scope-lock @@ -1 +1 @@ -de155f0cc3a5e9387586eb759425075e12bb50bf06c18614ee5d5aa7b05e8f67 +e1ce0cd7375029d9cd689119fbce94210e632829de2c024a1876b19012a2875f diff --git a/internal/step_upstream_release_monitor.go b/internal/step_upstream_release_monitor.go index 7daffc0..7dff2ea 100644 --- a/internal/step_upstream_release_monitor.go +++ b/internal/step_upstream_release_monitor.go @@ -109,7 +109,10 @@ func (githubUpstreamReleaseClient) LatestRelease(ctx context.Context, owner, rep client = client.WithAuthToken(token) } - release, _, err := client.Repositories.GetLatestRelease(ctx, owner, repo) + requestCtx, cancel := githubReleaseLookupContext(ctx) + defer cancel() + + release, _, err := client.Repositories.GetLatestRelease(requestCtx, owner, repo) if err != nil { return upstreamReleaseInfo{}, err } @@ -128,3 +131,10 @@ func (githubUpstreamReleaseClient) LatestRelease(ctx context.Context, owner, rep PublishedAt: publishedAt, }, nil } + +func githubReleaseLookupContext(ctx context.Context) (context.Context, context.CancelFunc) { + if _, ok := ctx.Deadline(); ok { + return ctx, func() {} + } + return context.WithTimeout(ctx, 30*time.Second) +} diff --git a/internal/step_upstream_release_monitor_test.go b/internal/step_upstream_release_monitor_test.go index 306df3b..8fa4511 100644 --- a/internal/step_upstream_release_monitor_test.go +++ b/internal/step_upstream_release_monitor_test.go @@ -193,3 +193,34 @@ func TestGitHubPluginCreateStep_UpstreamReleaseMonitor(t *testing.T) { t.Fatalf("CreateStep returned %T, want *upstreamReleaseMonitorStep", step) } } + +func TestGitHubReleaseLookupContextAddsDefaultTimeout(t *testing.T) { + ctx, cancel := githubReleaseLookupContext(context.Background()) + defer cancel() + + deadline, ok := ctx.Deadline() + if !ok { + t.Fatal("expected deadline") + } + remaining := time.Until(deadline) + if remaining <= 0 || remaining > 30*time.Second { + t.Fatalf("deadline remaining = %s, want within 30s", remaining) + } +} + +func TestGitHubReleaseLookupContextPreservesCallerDeadline(t *testing.T) { + callerDeadline := time.Now().Add(5 * time.Second) + parent, parentCancel := context.WithDeadline(context.Background(), callerDeadline) + defer parentCancel() + + ctx, cancel := githubReleaseLookupContext(parent) + defer cancel() + + deadline, ok := ctx.Deadline() + if !ok { + t.Fatal("expected deadline") + } + if !deadline.Equal(callerDeadline) { + t.Fatalf("deadline = %s, want caller deadline %s", deadline, callerDeadline) + } +}