From 4fedcbdd86a4c2fb11dc36428dc36b1820114c0a Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 6 Sep 2024 15:09:31 -0400 Subject: [PATCH 1/6] Participle should unquote strings. --- pkg/buildscript/buildscript.go | 2 +- pkg/buildscript/marshal.go | 6 +- pkg/buildscript/marshal_buildexpression.go | 14 ++--- pkg/buildscript/mutations.go | 16 +++--- pkg/buildscript/queries.go | 12 ++-- pkg/buildscript/raw.go | 17 +----- pkg/buildscript/raw_test.go | 60 ++++++++++---------- pkg/buildscript/unmarshal.go | 6 +- pkg/buildscript/unmarshal_buildexpression.go | 11 ++-- 9 files changed, 65 insertions(+), 79 deletions(-) diff --git a/pkg/buildscript/buildscript.go b/pkg/buildscript/buildscript.go index fcaf7981c7..865d182cee 100644 --- a/pkg/buildscript/buildscript.go +++ b/pkg/buildscript/buildscript.go @@ -162,7 +162,7 @@ func exportValue(v *value) any { } return result case v.Str != nil: - return strValue(v) + return *v.Str case v.Number != nil: return *v.Number case v.Null != nil: diff --git a/pkg/buildscript/marshal.go b/pkg/buildscript/marshal.go index c647616f47..74a11c9a0f 100644 --- a/pkg/buildscript/marshal.go +++ b/pkg/buildscript/marshal.go @@ -8,6 +8,8 @@ import ( "github.com/go-openapi/strfmt" "github.com/thoas/go-funk" + + "github.com/ActiveState/cli/internal/rtutils/ptr" ) const ( @@ -30,7 +32,7 @@ func (b *BuildScript) Marshal() ([]byte, error) { if b.raw.AtTime != nil { buf.WriteString(assignmentString( - &assignment{atTimeKey, newString(b.raw.AtTime.Format(strfmt.RFC3339Millis))})) + &assignment{atTimeKey, &value{Str: ptr.To(b.raw.AtTime.Format(strfmt.RFC3339Millis))}})) buf.WriteString("\n") } @@ -77,7 +79,7 @@ func valueString(v *value) string { return buf.String() case v.Str != nil: - return *v.Str // keep quoted + return strconv.Quote(*v.Str) case v.Number != nil: return strconv.FormatFloat(*v.Number, 'G', -1, 64) // 64-bit float with minimum digits on display diff --git a/pkg/buildscript/marshal_buildexpression.go b/pkg/buildscript/marshal_buildexpression.go index cb8a152803..79a371fc7f 100644 --- a/pkg/buildscript/marshal_buildexpression.go +++ b/pkg/buildscript/marshal_buildexpression.go @@ -37,9 +37,9 @@ func (b *BuildScript) MarshalBuildExpression() ([]byte, error) { if value.Str == nil { return nil, errs.New("String timestamp expected for '%s'", key) } - atTime, err := strfmt.ParseDateTime(strValue(value)) + atTime, err := strfmt.ParseDateTime(*value.Str) if err != nil { - return nil, errs.Wrap(err, "Invalid timestamp: %s", strValue(value)) + return nil, errs.Wrap(err, "Invalid timestamp: %s", *value.Str) } b.raw.AtTime = ptr.To(time.Time(atTime)) continue // do not include this custom assignment in the let block @@ -68,7 +68,7 @@ func (v *value) MarshalJSON() ([]byte, error) { case v.List != nil: return json.Marshal(v.List) case v.Str != nil: - return json.Marshal(strValue(v)) + return json.Marshal(*v.Str) case v.Number != nil: return json.Marshal(*v.Number) case v.Null != nil: @@ -125,12 +125,12 @@ func marshalReq(fn *funcCall) ([]byte, error) { switch { // Marshal the name argument (e.g. name = "") into {"name": ""} case assignment.Key == requirementNameKey && assignment.Value.Str != nil: - requirement[requirementNameKey] = strValue(assignment.Value) + requirement[requirementNameKey] = *assignment.Value.Str // Marshal the namespace argument (e.g. namespace = "") into // {"namespace": ""} case assignment.Key == requirementNamespaceKey && assignment.Value.Str != nil: - requirement[requirementNamespaceKey] = strValue(assignment.Value) + requirement[requirementNamespaceKey] = *assignment.Value.Str // Marshal the version argument (e.g. version = (value = "")) into // {"version_requirements": [{"comparator": "", "version": ""}]} @@ -143,10 +143,10 @@ func marshalReq(fn *funcCall) ([]byte, error) { req := make(map[string]string) req[requirementComparatorKey] = strings.ToLower(name) if len(funcCall.Arguments) == 0 || funcCall.Arguments[0].Assignment == nil || - funcCall.Arguments[0].Assignment.Value.Str == nil || strValue(funcCall.Arguments[0].Assignment.Value) == "value" { + funcCall.Arguments[0].Assignment.Value.Str == nil || *funcCall.Arguments[0].Assignment.Value.Str == "value" { return errs.New(`Illegal argument for version comparator '%s': 'value = ""' expected`, name) } - req[requirementVersionKey] = strValue(funcCall.Arguments[0].Assignment.Value) + req[requirementVersionKey] = *funcCall.Arguments[0].Assignment.Value.Str requirements = append(requirements, req) case andFuncName: if len(funcCall.Arguments) != 2 { diff --git a/pkg/buildscript/mutations.go b/pkg/buildscript/mutations.go index 5114f1afb0..c974641474 100644 --- a/pkg/buildscript/mutations.go +++ b/pkg/buildscript/mutations.go @@ -42,8 +42,8 @@ func (b *BuildScript) AddRequirement(requirement types.Requirement) error { // Use object form for now, and then transform it into function form later. obj := []*assignment{ - {requirementNameKey, newString(requirement.Name)}, - {requirementNamespaceKey, newString(requirement.Namespace)}, + {requirementNameKey, &value{Str: &requirement.Name}}, + {requirementNamespaceKey, &value{Str: &requirement.Namespace}}, } if requirement.Revision != nil { @@ -54,8 +54,8 @@ func (b *BuildScript) AddRequirement(requirement types.Requirement) error { values := []*value{} for _, req := range requirement.VersionRequirement { values = append(values, &value{Object: &[]*assignment{ - {requirementComparatorKey, newString(req[requirementComparatorKey])}, - {requirementVersionKey, newString(req[requirementVersionKey])}, + {requirementComparatorKey, &value{Str: ptr.To(req[requirementComparatorKey])}}, + {requirementVersionKey, &value{Str: ptr.To(req[requirementVersionKey])}}, }}) } obj = append(obj, &assignment{requirementVersionRequirementsKey, &value{List: &values}}) @@ -94,13 +94,13 @@ func (b *BuildScript) RemoveRequirement(requirement types.Requirement) error { for _, arg := range req.FuncCall.Arguments { if arg.Assignment.Key == requirementNameKey { - match = strValue(arg.Assignment.Value) == requirement.Name + match = *arg.Assignment.Value.Str == requirement.Name if !match || requirement.Namespace == "" { break } } if requirement.Namespace != "" && arg.Assignment.Key == requirementNamespaceKey { - match = strValue(arg.Assignment.Value) == requirement.Namespace + match = *arg.Assignment.Value.Str == requirement.Namespace if !match { break } @@ -132,7 +132,7 @@ func (b *BuildScript) AddPlatform(platformID strfmt.UUID) error { } list := *platformsNode.List - list = append(list, newString(platformID.String())) + list = append(list, &value{Str: ptr.To(platformID.String())}) platformsNode.List = &list return nil @@ -151,7 +151,7 @@ func (b *BuildScript) RemovePlatform(platformID strfmt.UUID) error { var found bool for i, value := range *platformsNode.List { - if value.Str != nil && strValue(value) == platformID.String() { + if value.Str != nil && *value.Str == platformID.String() { list := *platformsNode.List list = append(list[:i], list[i+1:]...) platformsNode.List = &list diff --git a/pkg/buildscript/queries.go b/pkg/buildscript/queries.go index 172f74e0cf..eec73943d0 100644 --- a/pkg/buildscript/queries.go +++ b/pkg/buildscript/queries.go @@ -97,9 +97,9 @@ func parseRequirement(req *value) Requirement { for _, arg := range req.FuncCall.Arguments { switch arg.Assignment.Key { case requirementNameKey: - r.Name = strValue(arg.Assignment.Value) + r.Name = *arg.Assignment.Value.Str case requirementNamespaceKey: - r.Namespace = strValue(arg.Assignment.Value) + r.Namespace = *arg.Assignment.Value.Str case requirementVersionKey: r.VersionRequirement = getVersionRequirements(arg.Assignment.Value) } @@ -110,9 +110,9 @@ func parseRequirement(req *value) Requirement { for _, arg := range req.FuncCall.Arguments { switch arg.Assignment.Key { case requirementNameKey: - r.Name = strValue(arg.Assignment.Value) + r.Name = *arg.Assignment.Value.Str case requirementRevisionIDKey: - r.RevisionID = strfmt.UUID(strValue(arg.Assignment.Value)) + r.RevisionID = strfmt.UUID(*arg.Assignment.Value.Str) } } return r @@ -161,7 +161,7 @@ func getVersionRequirements(v *value) []types.VersionRequirement { case eqFuncName, neFuncName, gtFuncName, gteFuncName, ltFuncName, lteFuncName: reqs = append(reqs, types.VersionRequirement{ requirementComparatorKey: strings.ToLower(v.FuncCall.Name), - requirementVersionKey: strValue(v.FuncCall.Arguments[0].Assignment.Value), + requirementVersionKey: *v.FuncCall.Arguments[0].Assignment.Value.Str, }) // e.g. And(left = Gte(value = "1.0"), right = Lt(value = "2.0")) @@ -305,7 +305,7 @@ func (b *BuildScript) Platforms(targets ...string) ([]strfmt.UUID, error) { list := []strfmt.UUID{} for _, value := range *node.List { - list = append(list, strfmt.UUID(strValue(value))) + list = append(list, strfmt.UUID(*value.Str)) } return list, nil } diff --git a/pkg/buildscript/raw.go b/pkg/buildscript/raw.go index 8a2dbc3149..76fc2916dc 100644 --- a/pkg/buildscript/raw.go +++ b/pkg/buildscript/raw.go @@ -1,11 +1,8 @@ package buildscript import ( - "strconv" - "strings" "time" - "github.com/ActiveState/cli/internal/rtutils/ptr" "github.com/brunoga/deep" ) @@ -62,7 +59,7 @@ type assignment struct { type value struct { FuncCall *funcCall `parser:"@@"` List *[]*value `parser:"| '[' (@@ (',' @@)* ','?)? ']'"` - Str *string `parser:"| @String"` // note: this value is ALWAYS quoted + Str *string `parser:"| @String"` Number *float64 `parser:"| (@Float | @Int)"` Null *null `parser:"| @@"` @@ -79,15 +76,3 @@ type funcCall struct { Name string `parser:"@Ident"` Arguments []*value `parser:"'(' @@ (',' @@)* ','? ')'"` } - -// newString is a convenience function for constructing a string value from an unquoted string. -// Use this instead of &value{Str: ptr.To(strconv.Quote(s))} -func newString(s string) *value { - return &value{Str: ptr.To(strconv.Quote(s))} -} - -// strValue is a convenience function for retrieving an unquoted string from value. -// Use this instead of strings.Trim(*v.Str, `"`) -func strValue(v *value) string { - return strings.Trim(*v.Str, `"`) -} diff --git a/pkg/buildscript/raw_test.go b/pkg/buildscript/raw_test.go index fb56c6f46c..a3ddc8d503 100644 --- a/pkg/buildscript/raw_test.go +++ b/pkg/buildscript/raw_test.go @@ -32,14 +32,14 @@ main = runtime atTime := time.Time(atTimeStrfmt) assert.Equal(t, &rawBuildScript{ - []*assignment{ + Assignments: []*assignment{ {"runtime", &value{ FuncCall: &funcCall{"solve", []*value{ {Assignment: &assignment{"at_time", &value{Ident: ptr.To(`at_time`)}}}, {Assignment: &assignment{ "platforms", &value{List: &[]*value{ - {Str: ptr.To(`"linux"`)}, - {Str: ptr.To(`"windows"`)}, + {Str: ptr.To(`linux`)}, + {Str: ptr.To(`windows`)}, }}, }}, {Assignment: &assignment{ @@ -47,19 +47,19 @@ main = runtime {FuncCall: &funcCall{ Name: "Req", Arguments: []*value{ - {Assignment: &assignment{"name", newString("python")}}, - {Assignment: &assignment{"namespace", newString("language")}}, + {Assignment: &assignment{"name", &value{Str: ptr.To("python")}}}, + {Assignment: &assignment{"namespace", &value{Str: ptr.To("language")}}}, }}}, {FuncCall: &funcCall{ Name: "Req", Arguments: []*value{ - {Assignment: &assignment{"name", newString("requests")}}, - {Assignment: &assignment{"namespace", newString("language/python")}}, + {Assignment: &assignment{"name", &value{Str: ptr.To("requests")}}}, + {Assignment: &assignment{"namespace", &value{Str: ptr.To("language/python")}}}, {Assignment: &assignment{ "version", &value{FuncCall: &funcCall{ Name: "Eq", Arguments: []*value{ - {Assignment: &assignment{"value", newString("3.10.10")}}, + {Assignment: &assignment{"value", &value{Str: ptr.To("3.10.10")}}}, }, }}, }}, @@ -72,7 +72,7 @@ main = runtime }}, {"main", &value{Ident: ptr.To("runtime")}}, }, - &atTime, + AtTime: &atTime, }, script.raw) } @@ -107,7 +107,7 @@ main = merge( atTime := time.Time(atTimeStrfmt) assert.Equal(t, &rawBuildScript{ - []*assignment{ + Assignments: []*assignment{ {"linux_runtime", &value{ FuncCall: &funcCall{"solve", []*value{ {Assignment: &assignment{"at_time", &value{Ident: ptr.To(`at_time`)}}}, @@ -116,14 +116,14 @@ main = merge( {FuncCall: &funcCall{ Name: "Req", Arguments: []*value{ - {Assignment: &assignment{"name", newString("python")}}, - {Assignment: &assignment{"namespace", newString("language")}}, + {Assignment: &assignment{"name", &value{Str: ptr.To("python")}}}, + {Assignment: &assignment{"namespace", &value{Str: ptr.To("language")}}}, }, }}, }}, }}, {Assignment: &assignment{ - "platforms", &value{List: &[]*value{{Str: ptr.To(`"67890"`)}}}, + "platforms", &value{List: &[]*value{{Str: ptr.To(`67890`)}}}, }}, }}, }}, @@ -135,14 +135,14 @@ main = merge( {FuncCall: &funcCall{ Name: "Req", Arguments: []*value{ - {Assignment: &assignment{"name", newString("perl")}}, - {Assignment: &assignment{"namespace", newString("language")}}, + {Assignment: &assignment{"name", &value{Str: ptr.To("perl")}}}, + {Assignment: &assignment{"namespace", &value{Str: ptr.To("language")}}}, }, }}, }}, }}, {Assignment: &assignment{ - "platforms", &value{List: &[]*value{{Str: ptr.To(`"12345"`)}}}, + "platforms", &value{List: &[]*value{{Str: ptr.To(`12345`)}}}, }}, }}, }}, @@ -152,7 +152,7 @@ main = merge( {FuncCall: &funcCall{"tar_installer", []*value{{Ident: ptr.To("linux_runtime")}}}}, }}}}, }, - &atTime, + AtTime: &atTime, }, script.raw) } @@ -179,14 +179,14 @@ func TestComplexVersions(t *testing.T) { atTime := time.Time(atTimeStrfmt) assert.Equal(t, &rawBuildScript{ - []*assignment{ + Assignments: []*assignment{ {"runtime", &value{ FuncCall: &funcCall{"solve", []*value{ {Assignment: &assignment{"at_time", &value{Ident: ptr.To(`at_time`)}}}, {Assignment: &assignment{ "platforms", &value{List: &[]*value{ - {Str: ptr.To(`"96b7e6f2-bebf-564c-bc1c-f04482398f38"`)}, - {Str: ptr.To(`"96b7e6f2-bebf-564c-bc1c-f04482398f38"`)}, + {Str: ptr.To(`96b7e6f2-bebf-564c-bc1c-f04482398f38`)}, + {Str: ptr.To(`96b7e6f2-bebf-564c-bc1c-f04482398f38`)}, }}, }}, {Assignment: &assignment{ @@ -194,20 +194,20 @@ func TestComplexVersions(t *testing.T) { {FuncCall: &funcCall{ Name: "Req", Arguments: []*value{ - {Assignment: &assignment{"name", newString("python")}}, - {Assignment: &assignment{"namespace", newString("language")}}, + {Assignment: &assignment{"name", &value{Str: ptr.To("python")}}}, + {Assignment: &assignment{"namespace", &value{Str: ptr.To("language")}}}, }, }}, {FuncCall: &funcCall{ Name: "Req", Arguments: []*value{ - {Assignment: &assignment{"name", newString("requests")}}, - {Assignment: &assignment{"namespace", newString("language/python")}}, + {Assignment: &assignment{"name", &value{Str: ptr.To("requests")}}}, + {Assignment: &assignment{"namespace", &value{Str: ptr.To("language/python")}}}, {Assignment: &assignment{ "version", &value{FuncCall: &funcCall{ Name: "Eq", Arguments: []*value{ - {Assignment: &assignment{Key: "value", Value: newString("3.10.10")}}, + {Assignment: &assignment{Key: "value", Value: &value{Str: ptr.To("3.10.10")}}}, }, }}, }}, @@ -216,8 +216,8 @@ func TestComplexVersions(t *testing.T) { {FuncCall: &funcCall{ Name: "Req", Arguments: []*value{ - {Assignment: &assignment{"name", newString("argparse")}}, - {Assignment: &assignment{"namespace", newString("language/python")}}, + {Assignment: &assignment{"name", &value{Str: ptr.To("argparse")}}}, + {Assignment: &assignment{"namespace", &value{Str: ptr.To("language/python")}}}, {Assignment: &assignment{ "version", &value{FuncCall: &funcCall{ Name: "And", @@ -225,13 +225,13 @@ func TestComplexVersions(t *testing.T) { {Assignment: &assignment{Key: "left", Value: &value{FuncCall: &funcCall{ Name: "Gt", Arguments: []*value{ - {Assignment: &assignment{Key: "value", Value: newString("1.0")}}, + {Assignment: &assignment{Key: "value", Value: &value{Str: ptr.To("1.0")}}}, }, }}}}, {Assignment: &assignment{Key: "right", Value: &value{FuncCall: &funcCall{ Name: "Lt", Arguments: []*value{ - {Assignment: &assignment{Key: "value", Value: newString("2.0")}}, + {Assignment: &assignment{Key: "value", Value: &value{Str: ptr.To("2.0")}}}, }, }}}}, }, @@ -246,6 +246,6 @@ func TestComplexVersions(t *testing.T) { }}, {"main", &value{Ident: ptr.To("runtime")}}, }, - &atTime, + AtTime: &atTime, }, script.raw) } diff --git a/pkg/buildscript/unmarshal.go b/pkg/buildscript/unmarshal.go index ffbab27f9e..3001ad04b4 100644 --- a/pkg/buildscript/unmarshal.go +++ b/pkg/buildscript/unmarshal.go @@ -17,7 +17,7 @@ const atTimeKey = "at_time" // Unmarshal returns a structured form of the given AScript (on-disk format). func Unmarshal(data []byte) (*BuildScript, error) { - parser, err := participle.Build[rawBuildScript]() + parser, err := participle.Build[rawBuildScript](participle.Unquote()) if err != nil { return nil, errs.Wrap(err, "Could not create parser for build script") } @@ -42,9 +42,9 @@ func Unmarshal(data []byte) (*BuildScript, error) { if value.Str == nil { break } - atTime, err := strfmt.ParseDateTime(strValue(value)) + atTime, err := strfmt.ParseDateTime(*value.Str) if err != nil { - return nil, errs.Wrap(err, "Invalid timestamp: %s", strValue(value)) + return nil, errs.Wrap(err, "Invalid timestamp: %s", *value.Str) } raw.AtTime = ptr.To(time.Time(atTime)) break diff --git a/pkg/buildscript/unmarshal_buildexpression.go b/pkg/buildscript/unmarshal_buildexpression.go index 77af730583..5efd0229c0 100644 --- a/pkg/buildscript/unmarshal_buildexpression.go +++ b/pkg/buildscript/unmarshal_buildexpression.go @@ -3,7 +3,6 @@ package buildscript import ( "encoding/json" "sort" - "strconv" "strings" "time" @@ -69,10 +68,10 @@ func (b *BuildScript) UnmarshalBuildExpression(data []byte) error { // Extract the 'at_time' from the solve node, if it exists, and change its value to be a // reference to "$at_time", which is how we want to show it in AScript format. - if atTimeNode, err := b.getSolveAtTimeValue(); err == nil && atTimeNode.Str != nil && !strings.HasPrefix(strValue(atTimeNode), `$`) { - atTime, err := strfmt.ParseDateTime(strValue(atTimeNode)) + if atTimeNode, err := b.getSolveAtTimeValue(); err == nil && atTimeNode.Str != nil && !strings.HasPrefix(*atTimeNode.Str, `$`) { + atTime, err := strfmt.ParseDateTime(*atTimeNode.Str) if err != nil { - return errs.Wrap(err, "Invalid timestamp: %s", strValue(atTimeNode)) + return errs.Wrap(err, "Invalid timestamp: %s", *atTimeNode.Str) } atTimeNode.Str = nil atTimeNode.Ident = ptr.To("at_time") @@ -171,7 +170,7 @@ func unmarshalValue(path []string, valueInterface interface{}) (*value, error) { if sliceutils.Contains(path, ctxIn) || strings.HasPrefix(v, "$") { result.Ident = ptr.To(strings.TrimPrefix(v, "$")) } else { - result.Str = ptr.To(strconv.Quote(v)) // quoting is mandatory + result.Str = ptr.To(v) } case float64: @@ -349,7 +348,7 @@ func transformVersion(requirements *assignment) *funcCall { {Assignment: &assignment{"value", o.Value}}, } case requirementComparatorKey: - f.Name = cases.Title(language.English).String(strValue(o.Value)) + f.Name = cases.Title(language.English).String(*o.Value.Str) } } funcs = append(funcs, f) From 2d3411535825b42ff2c6531cc344ce79ab085c21 Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 6 Sep 2024 18:23:09 -0400 Subject: [PATCH 2/6] Initial support for commit info in buildscript. Of the form: ``` Project: URL Time: time ``` --- internal/migrator/migrator.go | 4 +- .../runbits/buildscript/buildscript_test.go | 29 ++++++-- internal/runbits/buildscript/file.go | 70 ++++++++++++++----- .../buildscript/testdata/buildscript1.as | 8 ++- .../buildscript/testdata/buildscript2.as | 8 ++- internal/runbits/checkout/checkout.go | 7 +- internal/runbits/commits_runbit/time.go | 2 +- internal/runners/initialize/init.go | 2 +- internal/runners/reset/reset.go | 2 +- pkg/buildscript/buildscript.go | 36 ++++++++-- pkg/buildscript/buildscript_test.go | 21 ++++-- pkg/buildscript/marshal.go | 11 ++- pkg/buildscript/marshal_buildexpression.go | 13 ---- pkg/buildscript/merge.go | 2 +- pkg/buildscript/merge_test.go | 31 ++++---- pkg/buildscript/raw.go | 5 +- pkg/buildscript/raw_test.go | 46 +++++++++--- pkg/buildscript/unmarshal.go | 51 +++++++++----- pkg/buildscript/unmarshal_buildexpression.go | 2 +- scripts/to-buildscript/main.go | 2 + 20 files changed, 239 insertions(+), 113 deletions(-) diff --git a/internal/migrator/migrator.go b/internal/migrator/migrator.go index 23c7b7fa1f..cfb69eec18 100644 --- a/internal/migrator/migrator.go +++ b/internal/migrator/migrator.go @@ -1,8 +1,6 @@ package migrator import ( - "path/filepath" - "github.com/ActiveState/cli/internal/config" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" @@ -29,7 +27,7 @@ func NewMigrator(auth *authentication.Auth, cfg *config.Instance, svcm *model.Sv case 0: if cfg.GetBool(constants.OptinBuildscriptsConfig) { logging.Debug("Creating buildscript") - if err := buildscript_runbit.Initialize(filepath.Dir(project.Path()), auth, svcm); err != nil { + if err := buildscript_runbit.Initialize(project, auth, svcm); err != nil { return v, errs.Wrap(err, "Failed to initialize buildscript") } } diff --git a/internal/runbits/buildscript/buildscript_test.go b/internal/runbits/buildscript/buildscript_test.go index 5ef9836dde..eadad09d57 100644 --- a/internal/runbits/buildscript/buildscript_test.go +++ b/internal/runbits/buildscript/buildscript_test.go @@ -11,9 +11,19 @@ import ( "github.com/stretchr/testify/require" ) +const testProject = "https://platform.activestate.com/org/project?branch=main&commitID=00000000-0000-0000-0000-000000000000" +const testTime = "2000-01-01T00:00:00.000Z" + +func checkoutInfo(project, time string) string { + return "```\n" + + "Project: " + project + "\n" + + "Time: " + time + "\n" + + "```\n" +} + func TestDiff(t *testing.T) { script, err := buildscript.Unmarshal([]byte( - `at_time = "2000-01-01T00:00:00.000Z" + checkoutInfo(testProject, testTime) + ` runtime = solve( at_time = at_time, platforms = [ @@ -38,7 +48,7 @@ main = runtime`)) // Generate the difference between the modified script and the original expression. result, err := generateDiff(modifiedScript, script) require.NoError(t, err) - assert.Equal(t, `at_time = "2000-01-01T00:00:00.000Z" + assert.Equal(t, checkoutInfo(testProject, testTime)+` runtime = solve( at_time = at_time, platforms = [ @@ -71,11 +81,16 @@ func TestRealWorld(t *testing.T) { require.NoError(t, err) result, err := generateDiff(script1, script2) require.NoError(t, err) - assert.Equal(t, `<<<<<<< local -at_time = "2023-10-16T22:20:29.000Z" -======= -at_time = "2023-08-01T16:20:11.985Z" ->>>>>>> remote + assert.Equal(t, + "```\n"+ + "<<<<<<< local\n"+ + "Project: https://platform.activestate.com/ActiveState-CLI/Merge?branch=main&commitID=d908a758-6a81-40d4-b0eb-87069cd7f07d\n"+ + "Time: 2024-05-10T00:00:13.138Z\n"+ + "=======\n"+ + "Project: https://platform.activestate.com/ActiveState-CLI/Merge?branch=main&commitID=f3263ee4-ac4c-41ee-b778-2585333f49f7\n"+ + "Time: 2023-08-01T16:20:11.985Z\n"+ + ">>>>>>> remote\n"+ + "```\n"+` runtime = state_tool_artifacts_v1( build_flags = [ ], diff --git a/internal/runbits/buildscript/file.go b/internal/runbits/buildscript/file.go index 72e66d5ff6..90576a5094 100644 --- a/internal/runbits/buildscript/file.go +++ b/internal/runbits/buildscript/file.go @@ -2,6 +2,8 @@ package buildscript_runbit import ( "errors" + "fmt" + "net/url" "os" "path/filepath" @@ -19,15 +21,16 @@ import ( // projecter is a union between project.Project and setup.Targeter type projecter interface { - ProjectDir() string + Dir() string Owner() string Name() string + BranchName() string } var ErrBuildscriptNotExist = errors.New("Build script does not exist") func ScriptFromProject(proj projecter) (*buildscript.BuildScript, error) { - path := filepath.Join(proj.ProjectDir(), constants.BuildScriptFileName) + path := filepath.Join(proj.Dir(), constants.BuildScriptFileName) return ScriptFromFile(path) } @@ -47,33 +50,34 @@ type primeable interface { primer.SvcModeler } -func Initialize(path string, auth *authentication.Auth, svcm *model.SvcModel) error { - scriptPath := filepath.Join(path, constants.BuildScriptFileName) - script, err := ScriptFromFile(scriptPath) - if err == nil { - return nil // nothing to do, buildscript already exists - } - if !errors.Is(err, os.ErrNotExist) { - return errs.Wrap(err, "Could not read build script from file") - } - - logging.Debug("Build script does not exist. Creating one.") - commitId, err := localcommit.Get(path) +// Initialize creates a new build script for the local project. It will overwrite an existing one so +// commands like `state reset` will work. +func Initialize(proj projecter, auth *authentication.Auth, svcm *model.SvcModel) error { + logging.Debug("Initializing build script") + commitId, err := localcommit.Get(proj.Dir()) if err != nil { return errs.Wrap(err, "Unable to get the local commit ID") } buildplanner := buildplanner.NewBuildPlannerModel(auth, svcm) - script, err = buildplanner.GetBuildScript(commitId.String()) + script, err := buildplanner.GetBuildScript(commitId.String()) if err != nil { return errs.Wrap(err, "Unable to get the remote build expression and time") } + if url, err := projectURL(proj, commitId.String()); err == nil { + script.SetProject(url) + } else { + return errs.Wrap(err, "Unable to set project") + } + // Note: script.SetAtTime() was done in GetBuildScript(). + scriptBytes, err := script.Marshal() if err != nil { return errs.Wrap(err, "Unable to marshal build script") } + scriptPath := filepath.Join(proj.Dir(), constants.BuildScriptFileName) logging.Debug("Initializing build script at %s", scriptPath) err = fileutils.WriteFile(scriptPath, scriptBytes) if err != nil { @@ -83,6 +87,23 @@ func Initialize(path string, auth *authentication.Auth, svcm *model.SvcModel) er return nil } +func projectURL(proj projecter, commitID string) (string, error) { + // Note: cannot use api.GetPlatformURL() due to import cycle. + host := constants.DefaultAPIHost + if hostOverride := os.Getenv(constants.APIHostEnvVarName); hostOverride != "" { + host = hostOverride + } + u, err := url.Parse(fmt.Sprintf("https://%s/%s/%s", host, proj.Owner(), proj.Name())) + if err != nil { + return "", errs.Wrap(err, "Unable to parse URL") + } + q := u.Query() + q.Set("branch", proj.BranchName()) + q.Set("commitID", commitID) + u.RawQuery = q.Encode() + return u.String(), nil +} + func Update(proj projecter, newScript *buildscript.BuildScript) error { script, err := ScriptFromProject(proj) if err != nil { @@ -97,13 +118,28 @@ func Update(proj projecter, newScript *buildscript.BuildScript) error { return nil // no changes to write } - sb, err := newScript.Marshal() + // Update the new script's project field to match the current one, except for a new commit ID. + commitID, err := localcommit.Get(proj.Dir()) + if err != nil { + return errs.Wrap(err, "Unable to get the local commit ID") + } + url, err := projectURL(proj, commitID.String()) + if err != nil { + return errs.Wrap(err, "Could not construct project URL") + } + newScript2, err := newScript.Clone() + if err != nil { + return errs.Wrap(err, "Could not clone buildscript") + } + newScript2.SetProject(url) + + sb, err := newScript2.Marshal() if err != nil { return errs.Wrap(err, "Could not marshal build script") } logging.Debug("Writing build script") - if err := fileutils.WriteFile(filepath.Join(proj.ProjectDir(), constants.BuildScriptFileName), sb); err != nil { + if err := fileutils.WriteFile(filepath.Join(proj.Dir(), constants.BuildScriptFileName), sb); err != nil { return errs.Wrap(err, "Could not write build script to file") } return nil diff --git a/internal/runbits/buildscript/testdata/buildscript1.as b/internal/runbits/buildscript/testdata/buildscript1.as index 1bbc21f6b3..3477380cdb 100644 --- a/internal/runbits/buildscript/testdata/buildscript1.as +++ b/internal/runbits/buildscript/testdata/buildscript1.as @@ -1,4 +1,8 @@ -at_time = "2023-10-16T22:20:29.000000Z" +``` +Project: https://platform.activestate.com/ActiveState-CLI/Merge?branch=main&commitID=d908a758-6a81-40d4-b0eb-87069cd7f07d +Time: 2024-05-10T00:00:13.138Z +``` + runtime = state_tool_artifacts_v1( build_flags = [ ], @@ -20,4 +24,4 @@ sources = solve( solver_version = null ) -main = runtime \ No newline at end of file +main = runtime diff --git a/internal/runbits/buildscript/testdata/buildscript2.as b/internal/runbits/buildscript/testdata/buildscript2.as index 335b721546..c5f4580161 100644 --- a/internal/runbits/buildscript/testdata/buildscript2.as +++ b/internal/runbits/buildscript/testdata/buildscript2.as @@ -1,4 +1,8 @@ -at_time = "2023-08-01T16:20:11.985000Z" +``` +Project: https://platform.activestate.com/ActiveState-CLI/Merge?branch=main&commitID=f3263ee4-ac4c-41ee-b778-2585333f49f7 +Time: 2023-08-01T16:20:11.985000Z +``` + runtime = state_tool_artifacts_v1( build_flags = [ ], @@ -20,4 +24,4 @@ sources = solve( solver_version = null ) -main = runtime \ No newline at end of file +main = runtime diff --git a/internal/runbits/checkout/checkout.go b/internal/runbits/checkout/checkout.go index 5c505f4cc4..74efe258b8 100644 --- a/internal/runbits/checkout/checkout.go +++ b/internal/runbits/checkout/checkout.go @@ -99,7 +99,12 @@ func (r *Checkout) Run(ns *project.Namespaced, branchName, cachePath, targetPath } if r.prime.Config().GetBool(constants.OptinBuildscriptsConfig) { - if err := buildscript_runbit.Initialize(path, r.prime.Auth(), r.prime.SvcModel()); err != nil { + pjf, err := projectfile.FromPath(path) + if err != nil { + return "", errs.Wrap(err, "Unable to load project file") + } + + if err := buildscript_runbit.Initialize(pjf, r.prime.Auth(), r.prime.SvcModel()); err != nil { return "", errs.Wrap(err, "Unable to initialize buildscript") } } diff --git a/internal/runbits/commits_runbit/time.go b/internal/runbits/commits_runbit/time.go index af7f2e33e9..2ee606bd19 100644 --- a/internal/runbits/commits_runbit/time.go +++ b/internal/runbits/commits_runbit/time.go @@ -74,7 +74,7 @@ func ExpandTimeForBuildScript(ts *captain.TimeValue, auth *authentication.Auth, } atTime := script.AtTime() - if atTime.After(timestamp) { + if atTime != nil && atTime.After(timestamp) { return *atTime, nil } diff --git a/internal/runners/initialize/init.go b/internal/runners/initialize/init.go index 85ecd17d1f..607809fd38 100644 --- a/internal/runners/initialize/init.go +++ b/internal/runners/initialize/init.go @@ -281,7 +281,7 @@ func (r *Initialize) Run(params *RunParams) (rerr error) { } if r.config.GetBool(constants.OptinBuildscriptsConfig) { - if err := buildscript_runbit.Initialize(proj.Dir(), r.auth, r.svcModel); err != nil { + if err := buildscript_runbit.Initialize(proj, r.auth, r.svcModel); err != nil { return errs.Wrap(err, "Unable to initialize buildscript") } } diff --git a/internal/runners/reset/reset.go b/internal/runners/reset/reset.go index 0e2b5c89fb..c046635e63 100644 --- a/internal/runners/reset/reset.go +++ b/internal/runners/reset/reset.go @@ -134,7 +134,7 @@ func (r *Reset) Run(params *Params) error { // Ensure the buildscript exists. Normally we should never do this, but reset is used for resetting from a corrupted // state, so it is appropriate. if r.cfg.GetBool(constants.OptinBuildscriptsConfig) { - if err := buildscript_runbit.Initialize(r.project.Dir(), r.auth, r.svcModel); err != nil { + if err := buildscript_runbit.Initialize(r.project, r.auth, r.svcModel); err != nil { return errs.Wrap(err, "Unable to initialize buildscript") } } diff --git a/pkg/buildscript/buildscript.go b/pkg/buildscript/buildscript.go index 865d182cee..739a097c75 100644 --- a/pkg/buildscript/buildscript.go +++ b/pkg/buildscript/buildscript.go @@ -16,6 +16,9 @@ import ( // methods that are easy to understand and work with. type BuildScript struct { raw *rawBuildScript + + project string + atTime *time.Time } func init() { @@ -41,22 +44,43 @@ func New() *BuildScript { return bs } +func (b *BuildScript) Project() string { + return b.project +} + +func (b *BuildScript) SetProject(url string) { + b.project = url +} + func (b *BuildScript) AtTime() *time.Time { - return b.raw.AtTime + return b.atTime } func (b *BuildScript) SetAtTime(t time.Time) { - b.raw.AtTime = &t + b.atTime = &t } func (b *BuildScript) Equals(other *BuildScript) (bool, error) { - myBytes, err := b.Marshal() + b2, err := b.Clone() + if err != nil { + return false, errs.Wrap(err, "Unable to clone buildscript") + } + other2, err := other.Clone() + if err != nil { + return false, errs.Wrap(err, "Unable to clone other buildscript") + } + + // Do not compare project URLs. + b2.SetProject("") + other2.SetProject("") + + myBytes, err := b2.Marshal() if err != nil { - return false, errs.New("Unable to marshal this buildscript: %s", errs.JoinMessage(err)) + return false, errs.Wrap(err, "Unable to marshal this buildscript") } - otherBytes, err := other.Marshal() + otherBytes, err := other2.Marshal() if err != nil { - return false, errs.New("Unable to marshal other buildscript: %s", errs.JoinMessage(err)) + return false, errs.Wrap(err, "Unable to marshal other buildscript") } return string(myBytes) == string(otherBytes), nil } diff --git a/pkg/buildscript/buildscript_test.go b/pkg/buildscript/buildscript_test.go index d4cff2a3b4..42504f1824 100644 --- a/pkg/buildscript/buildscript_test.go +++ b/pkg/buildscript/buildscript_test.go @@ -1,7 +1,6 @@ package buildscript import ( - "fmt" "testing" "time" @@ -10,10 +9,8 @@ import ( "github.com/stretchr/testify/require" ) -var atTime = "2000-01-01T00:00:00.000Z" - -var basicBuildScript = []byte(fmt.Sprintf( - `at_time = "%s" +var basicBuildScript = []byte( + checkoutInfoString(testProject, testTime) + ` runtime = state_tool_artifacts( src = sources ) @@ -29,7 +26,7 @@ sources = solve( solver_version = null ) -main = runtime`, atTime)) +main = runtime`) var basicBuildExpression = []byte(`{ "let": { @@ -100,10 +97,11 @@ func TestRoundTripFromBuildExpression(t *testing.T) { // TestExpressionToScript tests that creating a build script from a given Platform build expression // and at time produces the expected result. func TestExpressionToScript(t *testing.T) { - ts, err := time.Parse(strfmt.RFC3339Millis, atTime) + ts, err := time.Parse(strfmt.RFC3339Millis, testTime) require.NoError(t, err) script := New() + script.SetProject(testProject) script.SetAtTime(ts) require.NoError(t, script.UnmarshalBuildExpression(basicBuildExpression)) @@ -124,3 +122,12 @@ func TestScriptToExpression(t *testing.T) { require.Equal(t, string(basicBuildExpression), string(data)) } + +func TestOutdatedScript(t *testing.T) { + _, err := Unmarshal([]byte( + `at_time = "2000-01-01T00:00:00.000Z" + main = runtime + `)) + assert.Error(t, err) + assert.ErrorIs(t, err, ErrOutdatedAtTime) +} diff --git a/pkg/buildscript/marshal.go b/pkg/buildscript/marshal.go index 74a11c9a0f..6eefd259f8 100644 --- a/pkg/buildscript/marshal.go +++ b/pkg/buildscript/marshal.go @@ -8,8 +8,6 @@ import ( "github.com/go-openapi/strfmt" "github.com/thoas/go-funk" - - "github.com/ActiveState/cli/internal/rtutils/ptr" ) const ( @@ -30,11 +28,12 @@ const ( func (b *BuildScript) Marshal() ([]byte, error) { buf := strings.Builder{} - if b.raw.AtTime != nil { - buf.WriteString(assignmentString( - &assignment{atTimeKey, &value{Str: ptr.To(b.raw.AtTime.Format(strfmt.RFC3339Millis))}})) - buf.WriteString("\n") + buf.WriteString("```\n") + buf.WriteString("Project: " + b.project + "\n") + if b.atTime != nil { + buf.WriteString("Time: " + b.atTime.Format(strfmt.RFC3339Millis) + "\n") } + buf.WriteString("```\n\n") var main *assignment for _, assignment := range b.raw.Assignments { diff --git a/pkg/buildscript/marshal_buildexpression.go b/pkg/buildscript/marshal_buildexpression.go index 79a371fc7f..e3a9a13620 100644 --- a/pkg/buildscript/marshal_buildexpression.go +++ b/pkg/buildscript/marshal_buildexpression.go @@ -3,12 +3,9 @@ package buildscript import ( "encoding/json" "strings" - "time" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/logging" - "github.com/ActiveState/cli/internal/rtutils/ptr" - "github.com/go-openapi/strfmt" ) const ( @@ -33,16 +30,6 @@ func (b *BuildScript) MarshalBuildExpression() ([]byte, error) { key := assignment.Key value := assignment.Value switch key { - case atTimeKey: - if value.Str == nil { - return nil, errs.New("String timestamp expected for '%s'", key) - } - atTime, err := strfmt.ParseDateTime(*value.Str) - if err != nil { - return nil, errs.Wrap(err, "Invalid timestamp: %s", *value.Str) - } - b.raw.AtTime = ptr.To(time.Time(atTime)) - continue // do not include this custom assignment in the let block case mainKey: key = inKey // rename } diff --git a/pkg/buildscript/merge.go b/pkg/buildscript/merge.go index 370cc5d8a6..b81e153596 100644 --- a/pkg/buildscript/merge.go +++ b/pkg/buildscript/merge.go @@ -57,7 +57,7 @@ func (b *BuildScript) Merge(other *BuildScript, strategies *mono_models.MergeStr // When merging build scripts we want to use the most recent timestamp atTime := other.AtTime() - if atTime != nil && atTime.After(*b.AtTime()) { + if atTime != nil && b.AtTime() != nil && atTime.After(*b.AtTime()) { b.SetAtTime(*atTime) } diff --git a/pkg/buildscript/merge_test.go b/pkg/buildscript/merge_test.go index 78e058165f..8a11d3a579 100644 --- a/pkg/buildscript/merge_test.go +++ b/pkg/buildscript/merge_test.go @@ -8,9 +8,12 @@ import ( "github.com/stretchr/testify/require" ) +const mergeATime = "2000-01-01T00:00:00.000Z" +const mergeBTime = "2000-01-02T00:00:00.000Z" + func TestMergeAdd(t *testing.T) { - scriptA, err := Unmarshal([]byte(` -at_time = "2000-01-01T00:00:00.000Z" + scriptA, err := Unmarshal([]byte( + checkoutInfoString(testProject, mergeATime) + ` runtime = solve( at_time = at_time, platforms = [ @@ -27,8 +30,8 @@ main = runtime `)) require.NoError(t, err) - scriptB, err := Unmarshal([]byte(` -at_time = "2000-01-02T00:00:00.000Z" + scriptB, err := Unmarshal([]byte( + checkoutInfoString(testProject, mergeBTime) + ` runtime = solve( at_time = at_time, platforms = [ @@ -60,7 +63,7 @@ main = runtime require.NoError(t, err) assert.Equal(t, - `at_time = "2000-01-02T00:00:00.000Z" + checkoutInfoString(testProject, mergeBTime)+` runtime = solve( at_time = at_time, platforms = [ @@ -78,8 +81,8 @@ main = runtime`, string(v)) } func TestMergeRemove(t *testing.T) { - scriptA, err := Unmarshal([]byte(` -at_time = "2000-01-02T00:00:00.000Z" + scriptA, err := Unmarshal([]byte( + checkoutInfoString(testProject, mergeBTime) + ` runtime = solve( at_time = at_time, platforms = [ @@ -97,8 +100,8 @@ main = runtime `)) require.NoError(t, err) - scriptB, err := Unmarshal([]byte(` -at_time = "2000-01-01T00:00:00.000Z" + scriptB, err := Unmarshal([]byte( + checkoutInfoString(testProject, mergeATime) + ` runtime = solve( at_time = at_time, platforms = [ @@ -129,7 +132,7 @@ main = runtime require.NoError(t, err) assert.Equal(t, - `at_time = "2000-01-02T00:00:00.000Z" + checkoutInfoString(testProject, mergeBTime)+` runtime = solve( at_time = at_time, platforms = [ @@ -146,8 +149,8 @@ main = runtime`, string(v)) } func TestMergeConflict(t *testing.T) { - scriptA, err := Unmarshal([]byte(` -at_time = "2000-01-01T00:00:00.000Z" + scriptA, err := Unmarshal([]byte( + checkoutInfoString(testProject, mergeATime) + ` runtime = solve( at_time = at_time, platforms = [ @@ -163,8 +166,8 @@ main = runtime `)) require.NoError(t, err) - scriptB, err := Unmarshal([]byte(` -at_time = "2000-01-01T00:00:00.000Z" + scriptB, err := Unmarshal([]byte( + checkoutInfoString(testProject, mergeATime) + ` runtime = solve( at_time = at_time, platforms = [ diff --git a/pkg/buildscript/raw.go b/pkg/buildscript/raw.go index 76fc2916dc..f60df48453 100644 --- a/pkg/buildscript/raw.go +++ b/pkg/buildscript/raw.go @@ -1,16 +1,13 @@ package buildscript import ( - "time" - "github.com/brunoga/deep" ) // Tagged fields will be filled in by Participle. type rawBuildScript struct { + Info *string `parser:"(RawString @RawString RawString)?"` Assignments []*assignment `parser:"@@+"` - - AtTime *time.Time // set after initial read } // clone is meant to facilitate making modifications to functions at marshal time. The idea is that these modifications diff --git a/pkg/buildscript/raw_test.go b/pkg/buildscript/raw_test.go index a3ddc8d503..d82ae3c112 100644 --- a/pkg/buildscript/raw_test.go +++ b/pkg/buildscript/raw_test.go @@ -10,9 +10,25 @@ import ( "github.com/stretchr/testify/require" ) +const testProject = "https://platform.activestate.com/org/project?branch=main&commitID=00000000-0000-0000-0000-000000000000" +const testTime = "2000-01-01T00:00:00.000Z" + +func checkoutInfoString(project, time string) string { + return "```\n" + + "Project: " + project + "\n" + + "Time: " + time + "\n" + + "```\n" +} + +var testCheckoutInfo string + +func init() { + testCheckoutInfo = checkoutInfoString(testProject, testTime) +} + func TestRawRepresentation(t *testing.T) { script, err := Unmarshal([]byte( - `at_time = "2000-01-01T00:00:00.000Z" + testCheckoutInfo + ` runtime = solve( at_time = at_time, platforms = ["linux", "windows"], @@ -32,6 +48,7 @@ main = runtime atTime := time.Time(atTimeStrfmt) assert.Equal(t, &rawBuildScript{ + Info: ptr.To(testCheckoutInfo[2 : len(testCheckoutInfo)-3]), Assignments: []*assignment{ {"runtime", &value{ FuncCall: &funcCall{"solve", []*value{ @@ -72,13 +89,15 @@ main = runtime }}, {"main", &value{Ident: ptr.To("runtime")}}, }, - AtTime: &atTime, }, script.raw) + + assert.Equal(t, testProject, script.Project()) + assert.Equal(t, &atTime, script.AtTime()) } func TestComplex(t *testing.T) { script, err := Unmarshal([]byte( - `at_time = "2000-01-01T00:00:00.000Z" + testCheckoutInfo + ` linux_runtime = solve( at_time = at_time, requirements=[ @@ -107,6 +126,7 @@ main = merge( atTime := time.Time(atTimeStrfmt) assert.Equal(t, &rawBuildScript{ + Info: ptr.To(testCheckoutInfo[2 : len(testCheckoutInfo)-3]), Assignments: []*assignment{ {"linux_runtime", &value{ FuncCall: &funcCall{"solve", []*value{ @@ -152,11 +172,16 @@ main = merge( {FuncCall: &funcCall{"tar_installer", []*value{{Ident: ptr.To("linux_runtime")}}}}, }}}}, }, - AtTime: &atTime, }, script.raw) + + assert.Equal(t, testProject, script.Project()) + assert.Equal(t, &atTime, script.AtTime()) } -const buildscriptWithComplexVersions = `at_time = "2023-04-27T17:30:05.999Z" +func TestComplexVersions(t *testing.T) { + checkoutInfo := checkoutInfoString(testProject, "2023-04-27T17:30:05.999Z") + script, err := Unmarshal([]byte( + checkoutInfo + ` runtime = solve( at_time = at_time, platforms = ["96b7e6f2-bebf-564c-bc1c-f04482398f38", "96b7e6f2-bebf-564c-bc1c-f04482398f38"], @@ -168,10 +193,8 @@ runtime = solve( solver_version = 0 ) -main = runtime` - -func TestComplexVersions(t *testing.T) { - script, err := Unmarshal([]byte(buildscriptWithComplexVersions)) +main = runtime +`)) require.NoError(t, err) atTimeStrfmt, err := strfmt.ParseDateTime("2023-04-27T17:30:05.999Z") @@ -179,6 +202,7 @@ func TestComplexVersions(t *testing.T) { atTime := time.Time(atTimeStrfmt) assert.Equal(t, &rawBuildScript{ + Info: ptr.To(checkoutInfo[2 : len(checkoutInfo)-3]), Assignments: []*assignment{ {"runtime", &value{ FuncCall: &funcCall{"solve", []*value{ @@ -246,6 +270,8 @@ func TestComplexVersions(t *testing.T) { }}, {"main", &value{Ident: ptr.To("runtime")}}, }, - AtTime: &atTime, }, script.raw) + + assert.Equal(t, testProject, script.Project()) + assert.Equal(t, &atTime, script.AtTime()) } diff --git a/pkg/buildscript/unmarshal.go b/pkg/buildscript/unmarshal.go index 3001ad04b4..9602aef76c 100644 --- a/pkg/buildscript/unmarshal.go +++ b/pkg/buildscript/unmarshal.go @@ -2,10 +2,12 @@ package buildscript import ( "errors" + "strings" "time" "github.com/alecthomas/participle/v2" "github.com/go-openapi/strfmt" + "gopkg.in/yaml.v2" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" @@ -15,6 +17,13 @@ import ( const atTimeKey = "at_time" +var ErrOutdatedAtTime = errs.New("outdated at_time on top") + +type checkoutInfo struct { + Project string `yaml:"Project"` + Time string `yaml:"Time"` +} + // Unmarshal returns a structured form of the given AScript (on-disk format). func Unmarshal(data []byte) (*BuildScript, error) { parser, err := participle.Build[rawBuildScript](participle.Unquote()) @@ -31,23 +40,12 @@ func Unmarshal(data []byte) (*BuildScript, error) { return nil, locale.WrapError(err, "err_parse_buildscript_bytes", "Could not parse build script: {{.V0}}", err.Error()) } - // Extract 'at_time' value from the list of assignments, if it exists. - for i, assignment := range raw.Assignments { - key := assignment.Key - value := assignment.Value - if key != atTimeKey { + // If 'at_time' is among the list of assignments, this is an outdated build script, so error out. + for _, assignment := range raw.Assignments { + if assignment.Key != atTimeKey { continue } - raw.Assignments = append(raw.Assignments[:i], raw.Assignments[i+1:]...) - if value.Str == nil { - break - } - atTime, err := strfmt.ParseDateTime(*value.Str) - if err != nil { - return nil, errs.Wrap(err, "Invalid timestamp: %s", *value.Str) - } - raw.AtTime = ptr.To(time.Time(atTime)) - break + return nil, ErrOutdatedAtTime } // Verify there are no duplicate key assignments. @@ -60,5 +58,26 @@ func Unmarshal(data []byte) (*BuildScript, error) { seen[assignment.Key] = true } - return &BuildScript{raw}, nil + var project string + var atTime *time.Time + if raw.Info != nil { + info := checkoutInfo{} + + err := yaml.Unmarshal([]byte(strings.Trim(*raw.Info, "`\n")), &info) + if err != nil { + return nil, locale.NewInputError( + "err_buildscript_checkoutinfo", + "Could not parse checkout information in the buildscript. The parser produced the following error: {{.V0}}", err.Error()) + } + + project = info.Project + + atTimeStr, err := strfmt.ParseDateTime(info.Time) + if err != nil { + return nil, errs.Wrap(err, "Invalid timestamp: %s", info.Time) + } + atTime = ptr.To(time.Time(atTimeStr)) + } + + return &BuildScript{raw, project, atTime}, nil } diff --git a/pkg/buildscript/unmarshal_buildexpression.go b/pkg/buildscript/unmarshal_buildexpression.go index 5efd0229c0..117db2231d 100644 --- a/pkg/buildscript/unmarshal_buildexpression.go +++ b/pkg/buildscript/unmarshal_buildexpression.go @@ -75,7 +75,7 @@ func (b *BuildScript) UnmarshalBuildExpression(data []byte) error { } atTimeNode.Str = nil atTimeNode.Ident = ptr.To("at_time") - b.raw.AtTime = ptr.To(time.Time(atTime)) + b.atTime = ptr.To(time.Time(atTime)) } else if err != nil { return errs.Wrap(err, "Could not get at_time node") } diff --git a/scripts/to-buildscript/main.go b/scripts/to-buildscript/main.go index 9817cf8d1a..60c7cde520 100644 --- a/scripts/to-buildscript/main.go +++ b/scripts/to-buildscript/main.go @@ -42,6 +42,7 @@ func main() { os.Exit(1) } + project := "https://platform.activestate.com/org/project?branch=main&commitID=00000000-0000-0000-0000-000000000000" var atTime *time.Time if len(os.Args) == (2 + argOffset) { t, err := time.Parse(strfmt.RFC3339Millis, os.Args[1+argOffset]) @@ -52,6 +53,7 @@ func main() { } bs := buildscript.New() + bs.SetProject(project) if atTime != nil { bs.SetAtTime(*atTime) } From ba3aec7f2dfa96d52bcb1d079631920110d92e2d Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 9 Sep 2024 16:58:41 -0400 Subject: [PATCH 3/6] Add warning about using an outdated buildscript. --- internal/captain/rationalize.go | 7 +++++++ internal/locale/locales/en-us.yaml | 2 ++ 2 files changed, 9 insertions(+) diff --git a/internal/captain/rationalize.go b/internal/captain/rationalize.go index 4abbc6fd94..c44d7dbfdf 100644 --- a/internal/captain/rationalize.go +++ b/internal/captain/rationalize.go @@ -6,6 +6,7 @@ import ( "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/runbits/rationalize" + "github.com/ActiveState/cli/pkg/buildscript" "github.com/ActiveState/cli/pkg/localcommit" ) @@ -31,5 +32,11 @@ func rationalizeError(err *error) { *err = errs.WrapUserFacing(*err, locale.Tr("err_commit_id_invalid", errInvalidCommitID.CommitID), errs.SetInput()) + + // Outdated build script. + case errors.Is(*err, buildscript.ErrOutdatedAtTime): + *err = errs.WrapUserFacing(*err, + locale.T("err_outdated_buildscript"), + errs.SetInput()) } } diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index bf6e64f9db..6eb637bf9f 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -1371,6 +1371,8 @@ err_commit_id_invalid_given: other: "Invalid commit ID: '{{.V0}}'." err_commit_id_not_in_history: other: "The project '[ACTIONABLE]{{.V0}}[/RESET]' does not contain the provided commit: '[ACTIONABLE]{{.V1}}[/RESET]'." +err_outdated_buildscript: + other: "[WARNING]Warning:[/RESET] You are using an outdated version of the buildscript. Please run '[ACTIONABLE]state reset LOCAL[/RESET]' in order to reinitialize your buildscript file." err_shortcutdir_writable: other: Could not continue as we don't have permission to create [ACTIONABLE]{{.V0}}[/RESET]. move_prompt: From 1acb72d92bd8c61855ae48ecd4c00967e6095982 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 30 Sep 2024 13:31:23 -0400 Subject: [PATCH 4/6] Use TIME instead of at_time in build scripts. --- .../runbits/buildscript/buildscript_test.go | 6 +++--- .../buildscript/testdata/buildscript1.as | 2 +- .../buildscript/testdata/buildscript2.as | 2 +- pkg/buildscript/buildscript_test.go | 2 +- pkg/buildscript/marshal_buildexpression.go | 7 ++++++- pkg/buildscript/merge.go | 3 +-- pkg/buildscript/merge_test.go | 16 +++++++-------- pkg/buildscript/raw_test.go | 16 +++++++-------- pkg/buildscript/unmarshal_buildexpression.go | 20 +++++++++++-------- 9 files changed, 41 insertions(+), 33 deletions(-) diff --git a/internal/runbits/buildscript/buildscript_test.go b/internal/runbits/buildscript/buildscript_test.go index eadad09d57..24e47de8ef 100644 --- a/internal/runbits/buildscript/buildscript_test.go +++ b/internal/runbits/buildscript/buildscript_test.go @@ -25,7 +25,7 @@ func TestDiff(t *testing.T) { script, err := buildscript.Unmarshal([]byte( checkoutInfo(testProject, testTime) + ` runtime = solve( - at_time = at_time, + at_time = TIME, platforms = [ "12345", "67890" @@ -50,7 +50,7 @@ main = runtime`)) require.NoError(t, err) assert.Equal(t, checkoutInfo(testProject, testTime)+` runtime = solve( - at_time = at_time, + at_time = TIME, platforms = [ <<<<<<< local "77777", @@ -99,7 +99,7 @@ runtime = state_tool_artifacts_v1( src = sources ) sources = solve( - at_time = at_time, + at_time = TIME, platforms = [ "78977bc8-0f32-519d-80f3-9043f059398c", "7c998ec2-7491-4e75-be4d-8885800ef5f2", diff --git a/internal/runbits/buildscript/testdata/buildscript1.as b/internal/runbits/buildscript/testdata/buildscript1.as index 3477380cdb..1d33f4566e 100644 --- a/internal/runbits/buildscript/testdata/buildscript1.as +++ b/internal/runbits/buildscript/testdata/buildscript1.as @@ -11,7 +11,7 @@ runtime = state_tool_artifacts_v1( src = sources ) sources = solve( - at_time = at_time, + at_time = TIME, platforms = [ "78977bc8-0f32-519d-80f3-9043f059398c", "7c998ec2-7491-4e75-be4d-8885800ef5f2", diff --git a/internal/runbits/buildscript/testdata/buildscript2.as b/internal/runbits/buildscript/testdata/buildscript2.as index c5f4580161..2fe6441d1d 100644 --- a/internal/runbits/buildscript/testdata/buildscript2.as +++ b/internal/runbits/buildscript/testdata/buildscript2.as @@ -11,7 +11,7 @@ runtime = state_tool_artifacts_v1( src = sources ) sources = solve( - at_time = at_time, + at_time = TIME, platforms = [ "78977bc8-0f32-519d-80f3-9043f059398c", "7c998ec2-7491-4e75-be4d-8885800ef5f2", diff --git a/pkg/buildscript/buildscript_test.go b/pkg/buildscript/buildscript_test.go index 42504f1824..4a96fb9766 100644 --- a/pkg/buildscript/buildscript_test.go +++ b/pkg/buildscript/buildscript_test.go @@ -15,7 +15,7 @@ runtime = state_tool_artifacts( src = sources ) sources = solve( - at_time = at_time, + at_time = TIME, platforms = [ "12345", "67890" diff --git a/pkg/buildscript/marshal_buildexpression.go b/pkg/buildscript/marshal_buildexpression.go index e3a9a13620..c161cfad24 100644 --- a/pkg/buildscript/marshal_buildexpression.go +++ b/pkg/buildscript/marshal_buildexpression.go @@ -69,7 +69,12 @@ func (v *value) MarshalJSON() ([]byte, error) { } return json.Marshal(m) case v.Ident != nil: - return json.Marshal("$" + *v.Ident) + name := *v.Ident + switch name { + case "TIME": + name = "at_time" // build expression uses this variable name + } + return json.Marshal("$" + name) } return json.Marshal([]*value{}) // participle does not create v.List if it's empty } diff --git a/pkg/buildscript/merge.go b/pkg/buildscript/merge.go index b81e153596..889cafffb1 100644 --- a/pkg/buildscript/merge.go +++ b/pkg/buildscript/merge.go @@ -85,7 +85,7 @@ func isAutoMergePossible(scriptA *BuildScript, scriptB *BuildScript) bool { } // getComparableJson returns a comparable JSON map[string]interface{} structure for the given build -// script. The map will not have a "requirements" field, nor will it have an "at_time" field. +// script. The map will not have a "requirements" field. func getComparableJson(script *BuildScript) (map[string]interface{}, error) { data, err := script.MarshalBuildExpression() if err != nil { @@ -103,7 +103,6 @@ func getComparableJson(script *BuildScript) (map[string]interface{}, error) { return nil, errs.New("'let' key is not a JSON object") } deleteKey(&letMap, "requirements") - deleteKey(&letMap, "at_time") return m, nil } diff --git a/pkg/buildscript/merge_test.go b/pkg/buildscript/merge_test.go index 8a11d3a579..260bb50867 100644 --- a/pkg/buildscript/merge_test.go +++ b/pkg/buildscript/merge_test.go @@ -15,7 +15,7 @@ func TestMergeAdd(t *testing.T) { scriptA, err := Unmarshal([]byte( checkoutInfoString(testProject, mergeATime) + ` runtime = solve( - at_time = at_time, + at_time = TIME, platforms = [ "12345", "67890" @@ -33,7 +33,7 @@ main = runtime scriptB, err := Unmarshal([]byte( checkoutInfoString(testProject, mergeBTime) + ` runtime = solve( - at_time = at_time, + at_time = TIME, platforms = [ "12345", "67890" @@ -65,7 +65,7 @@ main = runtime assert.Equal(t, checkoutInfoString(testProject, mergeBTime)+` runtime = solve( - at_time = at_time, + at_time = TIME, platforms = [ "12345", "67890" @@ -84,7 +84,7 @@ func TestMergeRemove(t *testing.T) { scriptA, err := Unmarshal([]byte( checkoutInfoString(testProject, mergeBTime) + ` runtime = solve( - at_time = at_time, + at_time = TIME, platforms = [ "12345", "67890" @@ -103,7 +103,7 @@ main = runtime scriptB, err := Unmarshal([]byte( checkoutInfoString(testProject, mergeATime) + ` runtime = solve( - at_time = at_time, + at_time = TIME, platforms = [ "12345", "67890" @@ -134,7 +134,7 @@ main = runtime assert.Equal(t, checkoutInfoString(testProject, mergeBTime)+` runtime = solve( - at_time = at_time, + at_time = TIME, platforms = [ "12345", "67890" @@ -152,7 +152,7 @@ func TestMergeConflict(t *testing.T) { scriptA, err := Unmarshal([]byte( checkoutInfoString(testProject, mergeATime) + ` runtime = solve( - at_time = at_time, + at_time = TIME, platforms = [ "12345", "67890" @@ -169,7 +169,7 @@ main = runtime scriptB, err := Unmarshal([]byte( checkoutInfoString(testProject, mergeATime) + ` runtime = solve( - at_time = at_time, + at_time = TIME, platforms = [ "12345" ], diff --git a/pkg/buildscript/raw_test.go b/pkg/buildscript/raw_test.go index d82ae3c112..306a71dbbe 100644 --- a/pkg/buildscript/raw_test.go +++ b/pkg/buildscript/raw_test.go @@ -30,7 +30,7 @@ func TestRawRepresentation(t *testing.T) { script, err := Unmarshal([]byte( testCheckoutInfo + ` runtime = solve( - at_time = at_time, + at_time = TIME, platforms = ["linux", "windows"], requirements = [ Req(name = "python", namespace = "language"), @@ -52,7 +52,7 @@ main = runtime Assignments: []*assignment{ {"runtime", &value{ FuncCall: &funcCall{"solve", []*value{ - {Assignment: &assignment{"at_time", &value{Ident: ptr.To(`at_time`)}}}, + {Assignment: &assignment{"at_time", &value{Ident: ptr.To(`TIME`)}}}, {Assignment: &assignment{ "platforms", &value{List: &[]*value{ {Str: ptr.To(`linux`)}, @@ -99,7 +99,7 @@ func TestComplex(t *testing.T) { script, err := Unmarshal([]byte( testCheckoutInfo + ` linux_runtime = solve( - at_time = at_time, + at_time = TIME, requirements=[ Req(name = "python", namespace = "language") ], @@ -107,7 +107,7 @@ linux_runtime = solve( ) win_runtime = solve( - at_time = at_time, + at_time = TIME, requirements=[ Req(name = "perl", namespace = "language") ], @@ -130,7 +130,7 @@ main = merge( Assignments: []*assignment{ {"linux_runtime", &value{ FuncCall: &funcCall{"solve", []*value{ - {Assignment: &assignment{"at_time", &value{Ident: ptr.To(`at_time`)}}}, + {Assignment: &assignment{"at_time", &value{Ident: ptr.To(`TIME`)}}}, {Assignment: &assignment{ "requirements", &value{List: &[]*value{ {FuncCall: &funcCall{ @@ -149,7 +149,7 @@ main = merge( }}, {"win_runtime", &value{ FuncCall: &funcCall{"solve", []*value{ - {Assignment: &assignment{"at_time", &value{Ident: ptr.To(`at_time`)}}}, + {Assignment: &assignment{"at_time", &value{Ident: ptr.To(`TIME`)}}}, {Assignment: &assignment{ "requirements", &value{List: &[]*value{ {FuncCall: &funcCall{ @@ -183,7 +183,7 @@ func TestComplexVersions(t *testing.T) { script, err := Unmarshal([]byte( checkoutInfo + ` runtime = solve( - at_time = at_time, + at_time = TIME, platforms = ["96b7e6f2-bebf-564c-bc1c-f04482398f38", "96b7e6f2-bebf-564c-bc1c-f04482398f38"], requirements = [ Req(name = "python", namespace = "language"), @@ -206,7 +206,7 @@ main = runtime Assignments: []*assignment{ {"runtime", &value{ FuncCall: &funcCall{"solve", []*value{ - {Assignment: &assignment{"at_time", &value{Ident: ptr.To(`at_time`)}}}, + {Assignment: &assignment{"at_time", &value{Ident: ptr.To(`TIME`)}}}, {Assignment: &assignment{ "platforms", &value{List: &[]*value{ {Str: ptr.To(`96b7e6f2-bebf-564c-bc1c-f04482398f38`)}, diff --git a/pkg/buildscript/unmarshal_buildexpression.go b/pkg/buildscript/unmarshal_buildexpression.go index 117db2231d..f3459240f6 100644 --- a/pkg/buildscript/unmarshal_buildexpression.go +++ b/pkg/buildscript/unmarshal_buildexpression.go @@ -67,15 +67,19 @@ func (b *BuildScript) UnmarshalBuildExpression(data []byte) error { b.raw.Assignments = assignments // Extract the 'at_time' from the solve node, if it exists, and change its value to be a - // reference to "$at_time", which is how we want to show it in AScript format. - if atTimeNode, err := b.getSolveAtTimeValue(); err == nil && atTimeNode.Str != nil && !strings.HasPrefix(*atTimeNode.Str, `$`) { - atTime, err := strfmt.ParseDateTime(*atTimeNode.Str) - if err != nil { - return errs.Wrap(err, "Invalid timestamp: %s", *atTimeNode.Str) + // reference to "TIME", which is how we want to show it in AScript format. + if atTimeNode, err := b.getSolveAtTimeValue(); err == nil { + if atTimeNode.Str != nil && !strings.HasPrefix(*atTimeNode.Str, `$`) { + atTime, err := strfmt.ParseDateTime(*atTimeNode.Str) + if err != nil { + return errs.Wrap(err, "Invalid timestamp: %s", *atTimeNode.Str) + } + atTimeNode.Str = nil + atTimeNode.Ident = ptr.To("TIME") + b.atTime = ptr.To(time.Time(atTime)) + } else if atTimeNode.Ident != nil && *atTimeNode.Ident == "at_time" { + atTimeNode.Ident = ptr.To("TIME") } - atTimeNode.Str = nil - atTimeNode.Ident = ptr.To("at_time") - b.atTime = ptr.To(time.Time(atTime)) } else if err != nil { return errs.Wrap(err, "Could not get at_time node") } From 6430b8187b58735c3a0767f0601db797f053cc80 Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 4 Oct 2024 11:22:36 -0400 Subject: [PATCH 5/6] Attempt to fix failing test. --- test/integration/manifest_int_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/integration/manifest_int_test.go b/test/integration/manifest_int_test.go index 8a8e780100..1928c6986c 100644 --- a/test/integration/manifest_int_test.go +++ b/test/integration/manifest_int_test.go @@ -70,7 +70,7 @@ func (suite *ManifestIntegrationTestSuite) TestManifest_JSON() { } func (suite *ManifestIntegrationTestSuite) TestManifest_Advanced_Reqs() { - suite.OnlyRunForTags(tagsuite.Manifest) + suite.OnlyRunForTags(tagsuite.Manifest, tagsuite.BuildScripts) ts := e2e.New(suite.T(), false) defer ts.Close() @@ -81,8 +81,11 @@ func (suite *ManifestIntegrationTestSuite) TestManifest_Advanced_Reqs() { ts.PrepareProject("ActiveState-CLI-Testing/Python-With-Custom-Reqs", "92ac7df2-0b0c-42f5-9b25-75b0cb4063f7") bsf := filepath.Join(ts.Dirs.Work, constants.BuildScriptFileName) - fileutils.WriteFile(bsf, []byte(fmt.Sprintf(` -at_time = "2022-07-07T19:51:01.140Z" + fileutils.WriteFile(bsf, []byte(fmt.Sprintf( + "```\n"+ + "Project: ActiveState-CLI-Testing/Python-With-Custom-Reqs\n"+ + "Time: 2022-07-07T19:51:01.140Z\n"+ + "```\n"+` runtime = state_tool_artifacts_v1(src = sources) sources = solve( at_time = at_time, From 34781f05abd49449489e2c20aba3b3b0e159c3cb Mon Sep 17 00:00:00 2001 From: mitchell Date: Fri, 18 Oct 2024 16:37:47 -0400 Subject: [PATCH 6/6] Leverage project's URL instead of constructing a full one. Also, ensure an updated build script with commit ID is always written. --- internal/runbits/buildscript/file.go | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/internal/runbits/buildscript/file.go b/internal/runbits/buildscript/file.go index 90576a5094..e1b30fcc79 100644 --- a/internal/runbits/buildscript/file.go +++ b/internal/runbits/buildscript/file.go @@ -2,7 +2,6 @@ package buildscript_runbit import ( "errors" - "fmt" "net/url" "os" "path/filepath" @@ -22,9 +21,7 @@ import ( // projecter is a union between project.Project and setup.Targeter type projecter interface { Dir() string - Owner() string - Name() string - BranchName() string + URL() string } var ErrBuildscriptNotExist = errors.New("Build script does not exist") @@ -87,37 +84,19 @@ func Initialize(proj projecter, auth *authentication.Auth, svcm *model.SvcModel) return nil } +// projectURL returns proj.URL(), but with the given, updated commitID. func projectURL(proj projecter, commitID string) (string, error) { - // Note: cannot use api.GetPlatformURL() due to import cycle. - host := constants.DefaultAPIHost - if hostOverride := os.Getenv(constants.APIHostEnvVarName); hostOverride != "" { - host = hostOverride - } - u, err := url.Parse(fmt.Sprintf("https://%s/%s/%s", host, proj.Owner(), proj.Name())) + u, err := url.Parse(proj.URL()) if err != nil { return "", errs.Wrap(err, "Unable to parse URL") } q := u.Query() - q.Set("branch", proj.BranchName()) q.Set("commitID", commitID) u.RawQuery = q.Encode() return u.String(), nil } func Update(proj projecter, newScript *buildscript.BuildScript) error { - script, err := ScriptFromProject(proj) - if err != nil { - return errs.Wrap(err, "Could not read build script") - } - - equals, err := script.Equals(newScript) - if err != nil { - return errs.Wrap(err, "Could not compare build script") - } - if script != nil && equals { - return nil // no changes to write - } - // Update the new script's project field to match the current one, except for a new commit ID. commitID, err := localcommit.Get(proj.Dir()) if err != nil {