Skip to content

Commit 833ef61

Browse files
authored
feat: support descriptor-only message contracts (#790)
* feat: support descriptor-only message contracts * fix: avoid message contract range copy
1 parent 8f3489e commit 833ef61

30 files changed

Lines changed: 1292 additions & 78 deletions

.github/workflows/create-release.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
runs-on: ubuntu-latest
2828
outputs:
2929
tag: ${{ steps.next_version.outputs.tag }}
30+
release_tag: ${{ steps.next_version.outputs.tag }}
3031

3132
steps:
3233
- name: Check out code
@@ -81,12 +82,24 @@ jobs:
8182
git tag "${{ steps.next_version.outputs.tag }}"
8283
git push origin "${{ steps.next_version.outputs.tag }}"
8384
85+
- name: Write release metadata
86+
run: |
87+
mkdir -p release-metadata
88+
printf '%s\n' "${{ steps.next_version.outputs.tag }}" > release-metadata/release-tag.txt
89+
90+
- name: Upload release metadata
91+
uses: actions/upload-artifact@v4
92+
with:
93+
name: release-metadata
94+
path: release-metadata/release-tag.txt
95+
if-no-files-found: error
96+
8497
release:
8598
name: Build and publish release
8699
needs: tag
87100
uses: ./.github/workflows/release.yml
88101
with:
89-
tag_name: ${{ needs.tag.outputs.tag }}
102+
tag_name: ${{ needs.tag.outputs.release_tag }}
90103
secrets:
91104
repo_dispatch_token: ${{ secrets.repo_dispatch_token }}
92105
permissions:

cmd/wfctl/audit.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,11 +284,12 @@ func renderPluginAuditReport(out io.Writer, report pluginAuditReport) {
284284
}
285285

286286
func pluginContractCoverageSummary(coverage pluginContractCoverage) string {
287-
return fmt.Sprintf("module %d/%d strict, step %d/%d strict, trigger %d/%d strict, service method %d/%d strict",
287+
return fmt.Sprintf("module %d/%d strict, step %d/%d strict, trigger %d/%d strict, service method %d/%d strict, message %d/%d strict",
288288
coverage.Modules.Strict, coverage.Modules.Total,
289289
coverage.Steps.Strict, coverage.Steps.Total,
290290
coverage.Triggers.Strict, coverage.Triggers.Total,
291-
coverage.ServiceMethods.Strict, coverage.ServiceMethods.Total)
291+
coverage.ServiceMethods.Strict, coverage.ServiceMethods.Total,
292+
coverage.Messages.Strict, coverage.Messages.Total)
292293
}
293294

294295
func pluginFindingCodes(findings []planFinding) []string {

cmd/wfctl/editor_bundle.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@ func editorBundleContractIDFromPluginDescriptor(descriptor *pluginContractDescri
292292
return id, nil
293293
}
294294
return "", fmt.Errorf("malformed service_method contract descriptor: serviceName and method are required when moduleType or serviceName is set")
295+
case "message":
296+
if typ := descriptor.contractType(kind); typ != "" {
297+
return "message:" + typ, nil
298+
}
295299
}
296300
return "", nil
297301
}
@@ -349,6 +353,14 @@ func contractDescriptorFromPluginDescriptor(descriptor *pluginContractDescriptor
349353
if _, ok := editorBundleServiceContractID(contract.ModuleType, contract.ServiceName, contract.Method); !ok {
350354
return nil, fmt.Errorf("malformed service_method contract descriptor: serviceName and method are required when moduleType or serviceName is set")
351355
}
356+
case "message":
357+
contract.Kind = pb.ContractKind_CONTRACT_KIND_MESSAGE
358+
contract.ContractType = descriptor.ContractType
359+
contract.ProtoPackage = descriptor.ProtoPackage
360+
contract.MessageNames = append([]string(nil), descriptor.MessageNames...)
361+
contract.GoImportPath = descriptor.GoImportPath
362+
contract.SchemaDigest = descriptor.SchemaDigest
363+
contract.ProtocolVersion = descriptor.ProtocolVersion
352364
default:
353365
return nil, nil
354366
}

cmd/wfctl/editor_bundle_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,44 @@ func TestRunEditorBundleLoadsPluginContractDescriptorSetReference(t *testing.T)
111111
}
112112
}
113113

114+
func TestRunEditorBundleLoadsMessageContractDescriptor(t *testing.T) {
115+
dir := t.TempDir()
116+
outPath := filepath.Join(dir, "editor-bundle.json")
117+
118+
if err := runEditorBundle([]string{"--registry=false", "--plugin-dir", "testdata/plugins/message-contract", "--output", outPath}); err != nil {
119+
t.Fatalf("editor-bundle failed: %v", err)
120+
}
121+
122+
data, err := os.ReadFile(outPath)
123+
if err != nil {
124+
t.Fatalf("read output: %v", err)
125+
}
126+
var bundle struct {
127+
Contracts map[string]struct {
128+
DescriptorSetRef string `json:"descriptorSetRef"`
129+
ProtoPackage string `json:"protoPackage"`
130+
MessageNames []string `json:"messageNames"`
131+
ProtocolVersion string `json:"protocolVersion"`
132+
} `json:"contracts"`
133+
}
134+
if err := json.Unmarshal(data, &bundle); err != nil {
135+
t.Fatalf("bundle is not valid JSON: %v", err)
136+
}
137+
contract := bundle.Contracts["message:compute.network_audit_evidence.v1"]
138+
if contract.DescriptorSetRef != "descriptors/message.pb" {
139+
t.Fatalf("descriptorSetRef = %q", contract.DescriptorSetRef)
140+
}
141+
if contract.ProtoPackage != "workflow_plugin_compute_core.protocol.v1" {
142+
t.Fatalf("protoPackage = %q", contract.ProtoPackage)
143+
}
144+
if len(contract.MessageNames) != 2 || contract.MessageNames[0] != "NetworkAuditRecord" {
145+
t.Fatalf("messageNames = %v", contract.MessageNames)
146+
}
147+
if contract.ProtocolVersion != "compute.v1alpha1" {
148+
t.Fatalf("protocolVersion = %q", contract.ProtocolVersion)
149+
}
150+
}
151+
114152
func TestRunEditorBundleRejectsMalformedPluginContractDescriptors(t *testing.T) {
115153
dir := t.TempDir()
116154
pluginDir := filepath.Join(dir, "workflow-plugin-bad-contracts")
@@ -298,6 +336,26 @@ func TestRunEditorBundlePreservesPerContractDescriptorSetReferences(t *testing.T
298336
"input": "workflow.two.Input",
299337
"output": "workflow.two.Output",
300338
"descriptorSetRef": "proto/two.pb"
339+
},
340+
{
341+
"kind": "message",
342+
"contractType": "message.one",
343+
"mode": "strict",
344+
"protoPackage": "workflow.one",
345+
"messageNames": ["Event"],
346+
"schemaDigest": "sha256:one",
347+
"protocolVersion": "v1",
348+
"descriptorSetRef": "proto/message-one.pb"
349+
},
350+
{
351+
"kind": "message",
352+
"contractType": "message.two",
353+
"mode": "strict",
354+
"protoPackage": "workflow.two",
355+
"messageNames": ["Event"],
356+
"schemaDigest": "sha256:two",
357+
"protocolVersion": "v1",
358+
"descriptorSetRef": "proto/message-two.pb"
301359
}
302360
]
303361
}`), 0644); err != nil {
@@ -339,6 +397,18 @@ func TestRunEditorBundlePreservesPerContractDescriptorSetReferences(t *testing.T
339397
if got := bundle.Messages["workflow.two.Input"].DescriptorSetRef; got != "proto/two.pb" {
340398
t.Fatalf("workflow.two.Input descriptorSetRef = %q", got)
341399
}
400+
if got := bundle.Contracts["message:message.one"].DescriptorSetRef; got != "proto/message-one.pb" {
401+
t.Fatalf("message.one descriptorSetRef = %q", got)
402+
}
403+
if got := bundle.Contracts["message:message.two"].DescriptorSetRef; got != "proto/message-two.pb" {
404+
t.Fatalf("message.two descriptorSetRef = %q", got)
405+
}
406+
if got := bundle.Messages["workflow.one.Event"].DescriptorSetRef; got != "proto/message-one.pb" {
407+
t.Fatalf("workflow.one.Event descriptorSetRef = %q", got)
408+
}
409+
if got := bundle.Messages["workflow.two.Event"].DescriptorSetRef; got != "proto/message-two.pb" {
410+
t.Fatalf("workflow.two.Event descriptorSetRef = %q", got)
411+
}
342412
if bundle.DescriptorSets["proto/one.pb"].ExternalRef != "proto/one.pb" {
343413
t.Fatalf("descriptor set one reference missing: %+v", bundle.DescriptorSets)
344414
}

cmd/wfctl/plugin.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ func runPlugin(args []string) error {
3535
return runPluginRemove(args[1:])
3636
case "validate":
3737
return runPluginValidate(args[1:])
38+
case "audit":
39+
return runPluginAudit(args[1:])
3840
case "validate-contract":
3941
return runPluginValidateContract(args[1:])
4042
case "verify-capabilities":
@@ -69,6 +71,7 @@ Subcommands:
6971
update Update an installed plugin to its latest version
7072
remove Uninstall a plugin (also removes from manifest + lockfile)
7173
validate Validate a plugin manifest from the registry or a local file
74+
audit Audit a single plugin source directory
7275
validate-contract Validate a plugin source directory against the release contract (workflow#758)
7376
verify-capabilities Spawn plugin binary, verify runtime GetManifest matches plugin.json
7477
registry-sync Sync registry manifest versions/capabilities from upstream release tags; subcommands: core, readme (workflow#762)

0 commit comments

Comments
 (0)