Skip to content

Commit e344ab5

Browse files
authored
feat: provenance tracking (#1488)
1 parent c50c293 commit e344ab5

File tree

14 files changed

+398
-73
lines changed

14 files changed

+398
-73
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ require (
2121
github.com/getkin/kin-openapi v0.128.0
2222
github.com/go-git/go-git/v5 v5.12.0
2323
github.com/gofrs/flock v0.12.1
24+
github.com/google/go-cmp v0.6.0
2425
github.com/google/go-github/v58 v58.0.0
2526
github.com/hashicorp/go-multierror v1.1.1
2627
github.com/hashicorp/go-version v1.7.0
@@ -38,7 +39,7 @@ require (
3839
github.com/speakeasy-api/openapi-overlay v0.10.1
3940
github.com/speakeasy-api/sdk-gen-config v1.31.1
4041
github.com/speakeasy-api/speakeasy-client-sdk-go/v3 v3.26.1
41-
github.com/speakeasy-api/speakeasy-core v0.19.8
42+
github.com/speakeasy-api/speakeasy-core v0.20.0
4243
github.com/speakeasy-api/speakeasy-proxy v0.0.2
4344
github.com/speakeasy-api/versioning-reports v0.6.0
4445
github.com/spf13/cobra v1.9.1
@@ -133,7 +134,6 @@ require (
133134
github.com/gogo/protobuf v1.3.2 // indirect
134135
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
135136
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 // indirect
136-
github.com/google/go-cmp v0.6.0 // indirect
137137
github.com/google/go-querystring v1.1.0 // indirect
138138
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
139139
github.com/google/uuid v1.6.0 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -589,8 +589,8 @@ github.com/speakeasy-api/sdk-gen-config v1.31.1 h1:peGHmojOH4OzCpKHoaQ++a7J6hLy6
589589
github.com/speakeasy-api/sdk-gen-config v1.31.1/go.mod h1:e9PjnCRHGa4K4EFKVU+kKmihOZjJ2V4utcU+274+bnQ=
590590
github.com/speakeasy-api/speakeasy-client-sdk-go/v3 v3.26.1 h1:cX+ip9YHkunvIHBALjDDIyEvo8rCLUCfIo7ldzcIeU4=
591591
github.com/speakeasy-api/speakeasy-client-sdk-go/v3 v3.26.1/go.mod h1:k9JD6Rj0+Iizc5COoLZHyRIOGGITpKZ2qBuFFO8SqNI=
592-
github.com/speakeasy-api/speakeasy-core v0.19.8 h1:JuHPTV9JRzdC2z0bRRjn9sf3f5K4twxSYBrxtSmGJiQ=
593-
github.com/speakeasy-api/speakeasy-core v0.19.8/go.mod h1:KqEmQy04aCZJuUed8mt6sywpLcqbeQfRJk9UDWD0cCk=
592+
github.com/speakeasy-api/speakeasy-core v0.20.0 h1:htH4AsRUIx1dciukesF2ICI81KkmLXg4NHYQ8m66woA=
593+
github.com/speakeasy-api/speakeasy-core v0.20.0/go.mod h1:KqEmQy04aCZJuUed8mt6sywpLcqbeQfRJk9UDWD0cCk=
594594
github.com/speakeasy-api/speakeasy-go-sdk v1.8.1 h1:atzohw12oQ5ipaLb1q7ntTu4vvAgKDJsrvaUoOu6sw0=
595595
github.com/speakeasy-api/speakeasy-go-sdk v1.8.1/go.mod h1:XbzaM0sMjj8bGooz/uEtNkOh1FQiJK7RFuNG3LPBSAU=
596596
github.com/speakeasy-api/speakeasy-proxy v0.0.2 h1:u4rQ8lXvuYRCSxiLQGb5JxkZRwNIDlyh+pMFYD6OGjA=

integration/multi_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package integration_tests
2+
3+
import (
4+
"github.com/google/go-cmp/cmp"
5+
"os"
6+
"path/filepath"
7+
"runtime"
8+
"testing"
9+
10+
"github.com/speakeasy-api/sdk-gen-config/workflow"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestMultiFileStability(t *testing.T) {
15+
if runtime.GOOS == "windows" {
16+
t.Skip("Skipping test on Windows")
17+
}
18+
// If windows, skip
19+
temp := setupTestDir(t)
20+
21+
// Copy the multi-file OpenAPI spec files
22+
err := copyFile("resources/multi_root.yaml", filepath.Join(temp, "multi_root.yaml"))
23+
require.NoError(t, err)
24+
err = copyFile("resources/multi_components.yaml", filepath.Join(temp, "multi_components.yaml"))
25+
require.NoError(t, err)
26+
27+
// Create a workflow file with multi-file input
28+
workflowFile := &workflow.Workflow{
29+
Version: workflow.WorkflowVersion,
30+
Sources: map[string]workflow.Source{
31+
"multi-file-source": {
32+
Inputs: []workflow.Document{
33+
{Location: workflow.LocationString("multi_root.yaml")},
34+
},
35+
},
36+
},
37+
Targets: map[string]workflow.Target{
38+
"multi-file-target": {
39+
Target: "typescript",
40+
Source: "multi-file-source",
41+
},
42+
},
43+
}
44+
45+
err = os.MkdirAll(filepath.Join(temp, ".speakeasy"), 0o755)
46+
require.NoError(t, err)
47+
err = workflow.Save(temp, workflowFile)
48+
require.NoError(t, err)
49+
50+
// Run the initial generation
51+
var initialChecksums map[string]string
52+
initialArgs := []string{"run", "-t", "all", "--force", "--pinned", "--skip-versioning", "--skip-compile"}
53+
cmdErr := execute(t, temp, initialArgs...).Run()
54+
require.NoError(t, cmdErr)
55+
56+
// Calculate checksums of generated files
57+
initialChecksums, err = filesToString(temp)
58+
require.NoError(t, err)
59+
60+
// Re-run the generation. We should have stable digests.
61+
cmdErr = execute(t, temp, initialArgs...).Run()
62+
require.NoError(t, cmdErr)
63+
rerunChecksums, err := filesToString(temp)
64+
require.NoError(t, err)
65+
66+
// Compare checksums to ensure stability
67+
require.Equal(t, initialChecksums, rerunChecksums, "Generated files should be identical for multi-file OpenAPI specs")
68+
69+
// Test frozen workflow lock behavior
70+
frozenArgs := []string{"run", "-t", "all", "--pinned", "--frozen-workflow-lockfile", "--skip-compile"}
71+
cmdErr = execute(t, temp, frozenArgs...).Run()
72+
require.NoError(t, cmdErr)
73+
74+
// Calculate checksums after frozen run
75+
frozenChecksums, err := filesToString(temp)
76+
require.NoError(t, err)
77+
78+
// exclude gen.lock -- we could (we do) reformat the document inside the frozen one
79+
delete(frozenChecksums, ".speakeasy/gen.lock")
80+
delete(initialChecksums, ".speakeasy/gen.lock")
81+
82+
// Compare checksums
83+
if diff := cmp.Diff(initialChecksums, frozenChecksums); diff != "" {
84+
t.Fatalf("Generated files should be identical when using --frozen-workflow-lock with multi-file specs. Mismatch (-want +got):\n%s", diff)
85+
}
86+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
components:
2+
schemas:
3+
TestResponse:
4+
type: object
5+
properties:
6+
status:
7+
type: string
8+
enum: ["success", "error"]
9+
data:
10+
type: array
11+
items:
12+
type: object
13+
properties:
14+
value:
15+
type: string
16+
timestamp:
17+
type: string
18+
format: date-time

integration/resources/multi_root.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
openapi: 3.1.0
2+
info:
3+
title: Multi-File Test API
4+
version: 1.0.0
5+
paths:
6+
/test:
7+
get:
8+
operationId: getTest
9+
summary: Test endpoint
10+
responses:
11+
"200":
12+
description: Successful response
13+
content:
14+
application/json:
15+
schema:
16+
$ref: "multi_components.yaml#/components/schemas/TestResponse"
17+
components:
18+
schemas:
19+
LocalSchema:
20+
type: object
21+
properties:
22+
id:
23+
type: string
24+
name:
25+
type: string

integration/workflow_registry_test.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package integration_tests
22

33
import (
4-
"crypto/md5"
5-
"fmt"
64
"gopkg.in/yaml.v3"
75
"os"
86
"path/filepath"
@@ -13,8 +11,6 @@ import (
1311
)
1412

1513
func TestStability(t *testing.T) {
16-
t.Skip("Skipping stability test until we can figure out how to make it work on CI")
17-
t.Parallel()
1814
temp := setupTestDir(t)
1915

2016
// Create a basic workflow file
@@ -47,15 +43,15 @@ func TestStability(t *testing.T) {
4743
require.NoError(t, cmdErr)
4844

4945
// Calculate checksums of generated files
50-
initialChecksums, err = calculateChecksums(temp)
46+
initialChecksums, err = filesToString(temp)
5147
require.NoError(t, err)
5248

5349
// Re-run the generation. We should have stable digests.
5450
cmdErr = execute(t, temp, initialArgs...).Run()
5551
require.NoError(t, cmdErr)
56-
rerunChecksums, err := calculateChecksums(temp)
52+
rerunChecksums, err := filesToString(temp)
5753
require.NoError(t, err)
58-
require.Equal(t, initialChecksums, rerunChecksums, "Generated files should be identical when using --frozen-workflow-lock")
54+
require.Equal(t, initialChecksums, rerunChecksums, "Generated files should be identical")
5955
// Modify the workflow file to simulate a change
6056
// Shouldn't do anything; we'll validate that later.
6157
workflowFile.Sources["test-source"].Inputs[0].Location = "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.1/petstore.yaml"
@@ -67,9 +63,14 @@ func TestStability(t *testing.T) {
6763
require.NoError(t, cmdErr)
6864

6965
// Calculate checksums after frozen run
70-
frozenChecksums, err := calculateChecksums(temp)
66+
frozenChecksums, err := filesToString(temp)
7167
require.NoError(t, err)
7268

69+
// exclude gen.lock -- we could (we do) reformat the document inside the frozen one
70+
delete(frozenChecksums, ".speakeasy/gen.lock")
71+
delete(frozenChecksums, ".speakeasy\\gen.lock") // windows
72+
delete(initialChecksums, ".speakeasy/gen.lock")
73+
delete(initialChecksums, ".speakeasy\\gen.lock") // windows
7374
// Compare checksums
7475
require.Equal(t, initialChecksums, frozenChecksums, "Generated files should be identical when using --frozen-workflow-lock")
7576
}
@@ -168,7 +169,7 @@ func TestRegistryFlow_JSON(t *testing.T) {
168169
require.NoError(t, cmdErr)
169170
}
170171

171-
func calculateChecksums(dir string) (map[string]string, error) {
172+
func filesToString(dir string) (map[string]string, error) {
172173
checksums := make(map[string]string)
173174
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
174175
if err != nil {
@@ -180,7 +181,7 @@ func calculateChecksums(dir string) (map[string]string, error) {
180181
return err
181182
}
182183
relPath, _ := filepath.Rel(dir, path)
183-
checksums[relPath] = fmt.Sprintf("%x", md5.Sum(data))
184+
checksums[relPath] = string(data)
184185
}
185186
return nil
186187
})

integration/workflow_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ func execute(t *testing.T, wd string, args ...string) Runnable {
248248
}
249249

250250
// executeI is a helper function to execute the main.go file inline. It can help when debugging integration tests
251+
// We should not use it on multiple tests at once as they will share memory: this can create issues.
252+
// so we leave it around as a little helper method: swap out execute for executeI and debug breakpoints work
251253
var mutex sync.Mutex
252254
var rootCmd = cmd.CmdForTest(version, artifactArch)
253255

internal/run/merge.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ type Merge struct {
2020
ruleset string
2121
}
2222

23-
var _ SourceStep = Merge{}
23+
type MergeResult struct {
24+
Location string
25+
InputSchemaLocation []string
26+
}
2427

2528
func NewMerge(w *Workflow, parentStep *workflowTracking.WorkflowStep, source workflow.Source, ruleset string) Merge {
2629
return Merge{
@@ -31,29 +34,29 @@ func NewMerge(w *Workflow, parentStep *workflowTracking.WorkflowStep, source wor
3134
}
3235
}
3336

34-
func (m Merge) Do(ctx context.Context, _ string) (string, error) {
37+
func (m Merge) Do(ctx context.Context, _ string) (result MergeResult, err error) {
3538
mergeStep := m.parentStep.NewSubstep("Merge Documents")
3639

37-
mergeLocation := m.source.GetTempMergeLocation()
40+
result.Location = m.source.GetTempMergeLocation()
3841

39-
log.From(ctx).Infof("Merging %d schemas into %s...", len(m.source.Inputs), mergeLocation)
42+
log.From(ctx).Infof("Merging %d schemas into %s...", len(m.source.Inputs), result.Location)
4043

41-
inSchemas := []string{}
4244
for _, input := range m.source.Inputs {
43-
resolvedPath, err := schemas.ResolveDocument(ctx, input, nil, mergeStep)
45+
var resolvedPath string
46+
resolvedPath, err = schemas.ResolveDocument(ctx, input, nil, mergeStep)
4447
if err != nil {
45-
return "", err
48+
return
4649
}
47-
inSchemas = append(inSchemas, resolvedPath)
50+
result.InputSchemaLocation = append(result.InputSchemaLocation, resolvedPath)
4851
}
4952

5053
mergeStep.NewSubstep(fmt.Sprintf("Merge %d documents", len(m.source.Inputs)))
5154

52-
if err := mergeDocuments(ctx, inSchemas, mergeLocation, m.ruleset, m.workflow.ProjectDir, m.workflow.SkipGenerateLintReport); err != nil {
53-
return "", err
55+
if err = mergeDocuments(ctx, result.InputSchemaLocation, result.Location, m.ruleset, m.workflow.ProjectDir, m.workflow.SkipGenerateLintReport); err != nil {
56+
return
5457
}
5558

56-
return mergeLocation, nil
59+
return result, nil
5760
}
5861

5962
func mergeDocuments(ctx context.Context, inSchemas []string, outFile, defaultRuleset, workingDir string, skipGenerateLintReport bool) error {

internal/run/overlay.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,36 +21,39 @@ type Overlay struct {
2121
source workflow.Source
2222
}
2323

24-
var _ SourceStep = Overlay{}
25-
2624
func NewOverlay(parentStep *workflowTracking.WorkflowStep, source workflow.Source) Overlay {
2725
return Overlay{
2826
parentStep: parentStep,
2927
source: source,
3028
}
3129
}
3230

33-
func (o Overlay) Do(ctx context.Context, inputPath string) (string, error) {
31+
type OverlayResult struct {
32+
Location string
33+
InputSchemaLocation []string
34+
}
35+
36+
func (o Overlay) Do(ctx context.Context, inputPath string) (result OverlayResult, err error) {
3437
overlayStep := o.parentStep.NewSubstep("Applying Overlays")
3538

3639
overlayLocation := o.source.GetTempOverlayLocation()
40+
result.Location = overlayLocation
3741

3842
log.From(ctx).Infof("Applying %d overlays into %s...", len(o.source.Overlays), overlayLocation)
3943

40-
var err error
4144
var overlaySchemas []string
4245
for _, overlay := range o.source.Overlays {
4346
overlayFilePath := ""
4447
if overlay.Document != nil {
4548
overlayFilePath, err = schemas.ResolveDocument(ctx, *overlay.Document, nil, overlayStep)
4649
if err != nil {
47-
return "", err
50+
return
4851
}
4952
} else if overlay.FallbackCodeSamples != nil {
5053
// Make temp file for the overlay output
5154
overlayFilePath = filepath.Join(workflow.GetTempDir(), fmt.Sprintf("fallback_code_samples_overlay_%s.yaml", randStringBytes(10)))
52-
if err := os.MkdirAll(filepath.Dir(overlayFilePath), 0o755); err != nil {
53-
return "", err
55+
if err = os.MkdirAll(filepath.Dir(overlayFilePath), 0o755); err != nil {
56+
return
5457
}
5558

5659
err = defaultcodesamples.DefaultCodeSamples(ctx, defaultcodesamples.DefaultCodeSamplesFlags{
@@ -60,21 +63,22 @@ func (o Overlay) Do(ctx context.Context, inputPath string) (string, error) {
6063
})
6164
if err != nil {
6265
log.From(ctx).Errorf("failed to generate default code samples: %s", err.Error())
63-
return "", err
66+
return
6467
}
6568
}
6669

6770
overlaySchemas = append(overlaySchemas, overlayFilePath)
71+
result.InputSchemaLocation = append(result.InputSchemaLocation, overlayFilePath)
6872
}
6973

7074
overlayStep.NewSubstep(fmt.Sprintf("Apply %d overlay(s)", len(o.source.Overlays)))
7175

72-
if err := overlayDocument(ctx, inputPath, overlaySchemas, overlayLocation); err != nil {
73-
return "", err
76+
if err = overlayDocument(ctx, inputPath, overlaySchemas, overlayLocation); err != nil {
77+
return
7478
}
7579

7680
overlayStep.Succeed()
77-
return overlayLocation, nil
81+
return
7882
}
7983

8084
func overlayDocument(ctx context.Context, schema string, overlayFiles []string, outFile string) error {

internal/run/run.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ import (
2525
"github.com/speakeasy-api/speakeasy/internal/workflowTracking"
2626
)
2727

28+
type SourceStep interface {
29+
Do(ctx context.Context, inputPath string) (string, error)
30+
}
31+
2832
const speakeasySelf = "speakeasy-self"
2933

3034
func ParseSourcesAndTargets() ([]string, []string, error) {

0 commit comments

Comments
 (0)