diff --git a/internal/runbits/runtime/requirements/requirements.go b/internal/runbits/runtime/requirements/requirements.go index f7625b5e1d..4678e6f406 100644 --- a/internal/runbits/runtime/requirements/requirements.go +++ b/internal/runbits/runtime/requirements/requirements.go @@ -296,8 +296,7 @@ func (r *RequirementOperation) prepareBuildScript(bp *bpModel.BuildPlanner, pare if err != nil { return nil, errs.Wrap(err, "Unable to fetch latest Platform timestamp") } - atTime := script.AtTime() - if atTime == nil || latest.After(*atTime) { + if latest.After(script.AtTime()) { script.SetAtTime(latest) } } diff --git a/pkg/buildscript/buildscript.go b/pkg/buildscript/buildscript.go index 159c15b1ff..38f427a202 100644 --- a/pkg/buildscript/buildscript.go +++ b/pkg/buildscript/buildscript.go @@ -18,12 +18,12 @@ func New() (*BuildScript, error) { return UnmarshalBuildExpression([]byte(emptyBuildExpression), nil) } -func (b *BuildScript) AtTime() *time.Time { - return b.raw.AtTime +func (b *BuildScript) AtTime() time.Time { + return b.raw.CommitInfo.AtTime } func (b *BuildScript) SetAtTime(t time.Time) { - b.raw.AtTime = &t + b.raw.CommitInfo.AtTime = t } func (b *BuildScript) Equals(other *BuildScript) (bool, error) { diff --git a/pkg/buildscript/buildscript_test.go b/pkg/buildscript/buildscript_test.go index d327bcdddc..641f1ca7bd 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( + commitInfo(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": { @@ -99,10 +96,10 @@ 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, err := UnmarshalBuildExpression(basicBuildExpression, &ts) + script, err := UnmarshalBuildExpression(basicBuildExpression, &CommitInfo{testProject, ts}) require.NoError(t, err) data, err := script.Marshal() @@ -122,3 +119,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 8481d61287..40390fb3b5 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,10 @@ 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.raw.CommitInfo.Project + "\n") + buf.WriteString("Time: " + b.raw.CommitInfo.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 5d9ab642f8..352524e76c 100644 --- a/pkg/buildscript/marshal_buildexpression.go +++ b/pkg/buildscript/marshal_buildexpression.go @@ -3,13 +3,9 @@ package buildscript import ( "encoding/json" "strings" - "time" - - "github.com/go-openapi/strfmt" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/logging" - "github.com/ActiveState/cli/internal/rtutils/ptr" ) const ( @@ -39,16 +35,6 @@ func (b *BuildScript) MarshalJSON() ([]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..57e1a28c91 100644 --- a/pkg/buildscript/merge.go +++ b/pkg/buildscript/merge.go @@ -57,8 +57,8 @@ 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()) { - b.SetAtTime(*atTime) + if atTime.After(b.AtTime()) { + b.SetAtTime(atTime) } return nil diff --git a/pkg/buildscript/merge_test.go b/pkg/buildscript/merge_test.go index 78e058165f..8982aacaa8 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( + commitInfo(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( + commitInfo(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" + commitInfo(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( + commitInfo(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( + commitInfo(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" + commitInfo(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( + commitInfo(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( + commitInfo(testProject, mergeATime) + ` runtime = solve( at_time = at_time, platforms = [ diff --git a/pkg/buildscript/raw.go b/pkg/buildscript/raw.go index c9ffaf2c0b..9f1030e3e4 100644 --- a/pkg/buildscript/raw.go +++ b/pkg/buildscript/raw.go @@ -6,9 +6,10 @@ import ( // 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 + CommitInfo CommitInfo // set after initial read } type Assignment struct { @@ -36,3 +37,8 @@ type FuncCall struct { Name string `parser:"@Ident"` Arguments []*Value `parser:"'(' @@ (',' @@)* ','? ')'"` } + +type CommitInfo struct { + Project string + AtTime time.Time +} diff --git a/pkg/buildscript/raw_test.go b/pkg/buildscript/raw_test.go index 2234fd673f..6fdb5216ca 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 commitInfo(project, time string) string { + return "```\n" + + "Project: " + project + "\n" + + "Time: " + time + "\n" + + "```\n" +} + +var testCommitInfo string + +func init() { + testCommitInfo = commitInfo(testProject, testTime) +} + func TestRawRepresentation(t *testing.T) { script, err := Unmarshal([]byte( - `at_time = "2000-01-01T00:00:00.000Z" + testCommitInfo + ` 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(testCommitInfo[2 : len(testCommitInfo)-3]), Assignments: []*Assignment{ {"runtime", &Value{ FuncCall: &FuncCall{"solve", []*Value{ @@ -72,13 +89,13 @@ main = runtime }}, {"main", &Value{Ident: ptr.To("runtime")}}, }, - AtTime: &atTime, + CommitInfo: CommitInfo{testProject, atTime}, }, script.raw) } func TestComplex(t *testing.T) { script, err := Unmarshal([]byte( - `at_time = "2000-01-01T00:00:00.000Z" + testCommitInfo + ` linux_runtime = solve( at_time = at_time, requirements=[ @@ -107,6 +124,7 @@ main = merge( atTime := time.Time(atTimeStrfmt) assert.Equal(t, &rawBuildScript{ + Info: ptr.To(testCommitInfo[2 : len(testCommitInfo)-3]), Assignments: []*Assignment{ {"linux_runtime", &Value{ FuncCall: &FuncCall{"solve", []*Value{ @@ -152,11 +170,14 @@ main = merge( {FuncCall: &FuncCall{"tar_installer", []*Value{{Ident: ptr.To("linux_runtime")}}}}, }}}}, }, - AtTime: &atTime, + CommitInfo: CommitInfo{testProject, atTime}, }, script.raw) } -const buildscriptWithComplexVersions = `at_time = "2023-04-27T17:30:05.999Z" +func TestComplexVersions(t *testing.T) { + commitInfo := commitInfo(testProject, "2023-04-27T17:30:05.999Z") + script, err := Unmarshal([]byte( + commitInfo + ` runtime = solve( at_time = at_time, platforms = ["96b7e6f2-bebf-564c-bc1c-f04482398f38", "96b7e6f2-bebf-564c-bc1c-f04482398f38"], @@ -168,10 +189,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 +198,7 @@ func TestComplexVersions(t *testing.T) { atTime := time.Time(atTimeStrfmt) assert.Equal(t, &rawBuildScript{ + Info: ptr.To(commitInfo[2 : len(commitInfo)-3]), Assignments: []*Assignment{ {"runtime", &Value{ FuncCall: &FuncCall{"solve", []*Value{ @@ -246,6 +266,6 @@ func TestComplexVersions(t *testing.T) { }}, {"main", &Value{Ident: ptr.To("runtime")}}, }, - AtTime: &atTime, + CommitInfo: CommitInfo{testProject, atTime}, }, script.raw) } diff --git a/pkg/buildscript/unmarshal.go b/pkg/buildscript/unmarshal.go index 97070561a9..a4049490b5 100644 --- a/pkg/buildscript/unmarshal.go +++ b/pkg/buildscript/unmarshal.go @@ -2,6 +2,7 @@ package buildscript import ( "errors" + "regexp" "time" "github.com/alecthomas/participle/v2" @@ -10,11 +11,14 @@ import ( "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" - "github.com/ActiveState/cli/internal/rtutils/ptr" ) const atTimeKey = "at_time" +var ErrOutdatedAtTime = errs.New("outdated at_time on top") + +var commitInfoPairRegex = regexp.MustCompile(`(\w+)\s*:\s*([^\n]+)`) + // 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 +35,28 @@ 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) + return nil, ErrOutdatedAtTime + } + + if raw.Info != nil { + for _, matches := range commitInfoPairRegex.FindAllStringSubmatch(*raw.Info, -1) { + key, value := matches[1], matches[2] + switch key { + case "Project": + raw.CommitInfo.Project = value + case "Time": + atTime, err := strfmt.ParseDateTime(value) + if err != nil { + return nil, errs.Wrap(err, "Invalid timestamp: %s", value) + } + raw.CommitInfo.AtTime = time.Time(atTime) + } } - raw.AtTime = ptr.To(time.Time(atTime)) - break } return &BuildScript{raw}, nil diff --git a/pkg/buildscript/unmarshal_buildexpression.go b/pkg/buildscript/unmarshal_buildexpression.go index 7a37afc589..11e1d572ba 100644 --- a/pkg/buildscript/unmarshal_buildexpression.go +++ b/pkg/buildscript/unmarshal_buildexpression.go @@ -48,7 +48,7 @@ const ( // Build expressions ALWAYS set at_time to `$at_time`, which refers to the timestamp on the commit, // while buildscripts encode this timestamp as part of their definition. For this reason we have // to supply the timestamp as a separate argument. -func UnmarshalBuildExpression(data []byte, atTime *time.Time) (*BuildScript, error) { +func UnmarshalBuildExpression(data []byte, commitInfo *CommitInfo) (*BuildScript, error) { expr := make(map[string]interface{}) err := json.Unmarshal(data, &expr) if err != nil { @@ -77,13 +77,13 @@ func UnmarshalBuildExpression(data []byte, atTime *time.Time) (*BuildScript, err } atTimeNode.Str = nil atTimeNode.Ident = ptr.To("at_time") - script.raw.AtTime = ptr.To(time.Time(atTime)) + script.raw.CommitInfo.AtTime = time.Time(atTime) } else if err != nil { return nil, errs.Wrap(err, "Could not get at_time node") } - if atTime != nil { - script.raw.AtTime = atTime + if commitInfo != nil { + script.raw.CommitInfo = *commitInfo } // If the requirements are in legacy object form, e.g. diff --git a/pkg/platform/model/buildplanner/build.go b/pkg/platform/model/buildplanner/build.go index a87cffe152..f5d4845f9f 100644 --- a/pkg/platform/model/buildplanner/build.go +++ b/pkg/platform/model/buildplanner/build.go @@ -11,7 +11,6 @@ import ( "github.com/ActiveState/cli/internal/gqlclient" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" - "github.com/ActiveState/cli/internal/rtutils/ptr" "github.com/ActiveState/cli/pkg/buildplan" "github.com/ActiveState/cli/pkg/buildplan/raw" "github.com/ActiveState/cli/pkg/buildscript" @@ -82,7 +81,7 @@ func (b *BuildPlanner) FetchCommit(commitID strfmt.UUID, owner, project string, return nil, errs.Wrap(err, "failed to unmarshal build plan") } - script, err := buildscript.UnmarshalBuildExpression(commit.Expression, ptr.To(time.Time(commit.AtTime))) + script, err := buildscript.UnmarshalBuildExpression(commit.Expression, buildScriptCommitInfo(owner, project, commitID.String(), time.Time(commit.AtTime))) if err != nil { return nil, errs.Wrap(err, "failed to parse build expression") } diff --git a/pkg/platform/model/buildplanner/buildscript.go b/pkg/platform/model/buildplanner/buildscript.go index 7a29cc8852..54df086a4f 100644 --- a/pkg/platform/model/buildplanner/buildscript.go +++ b/pkg/platform/model/buildplanner/buildscript.go @@ -1,16 +1,39 @@ package buildplanner import ( + "fmt" + "net/url" + "os" "time" + "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/logging" - "github.com/ActiveState/cli/internal/rtutils/ptr" + "github.com/ActiveState/cli/internal/multilog" "github.com/ActiveState/cli/pkg/buildscript" "github.com/ActiveState/cli/pkg/platform/api/buildplanner/request" bpResp "github.com/ActiveState/cli/pkg/platform/api/buildplanner/response" ) +func buildScriptCommitInfo(owner, project, commitID string, atTime time.Time) *buildscript.CommitInfo { + // 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, owner, project)) + if err != nil { + multilog.Error("url parse for project URL failed: %w", err) + return nil + } + q := u.Query() + q.Set("commitID", commitID) + u.RawQuery = q.Encode() + projectURL := u.String() + + return &buildscript.CommitInfo{projectURL, atTime} +} + func (b *BuildPlanner) GetBuildScript(commitID string) (*buildscript.BuildScript, error) { logging.Debug("GetBuildExpression, commitID: %s", commitID) resp := &bpResp.BuildExpressionResponse{} @@ -31,7 +54,7 @@ func (b *BuildPlanner) GetBuildScript(commitID string) (*buildscript.BuildScript return nil, errs.New("Commit does not contain expression") } - script, err := buildscript.UnmarshalBuildExpression(resp.Commit.Expression, ptr.To(time.Time(resp.Commit.AtTime))) + script, err := buildscript.UnmarshalBuildExpression(resp.Commit.Expression, buildScriptCommitInfo("", "", "", time.Time(resp.Commit.AtTime))) if err != nil { return nil, errs.Wrap(err, "failed to parse build expression") } diff --git a/pkg/platform/model/buildplanner/commit.go b/pkg/platform/model/buildplanner/commit.go index 50f6ef98e3..62df15f6bc 100644 --- a/pkg/platform/model/buildplanner/commit.go +++ b/pkg/platform/model/buildplanner/commit.go @@ -45,7 +45,7 @@ func (b *BuildPlanner) StageCommit(params StageCommitParams) (*Commit, error) { } // With the updated build expression call the stage commit mutation - request := request.StageCommit(params.Owner, params.Project, params.ParentCommit, params.Description, script.AtTime(), expression) + request := request.StageCommit(params.Owner, params.Project, params.ParentCommit, params.Description, ptr.To(script.AtTime()), expression) resp := &response.StageCommitResult{} if err := b.client.Run(request, resp); err != nil { return nil, processBuildPlannerError(err, "failed to stage commit") @@ -82,7 +82,7 @@ func (b *BuildPlanner) StageCommit(params StageCommitParams) (*Commit, error) { return nil, errs.Wrap(err, "failed to unmarshal build plan") } - stagedScript, err := buildscript.UnmarshalBuildExpression(resp.Commit.Expression, ptr.To(time.Time(resp.Commit.AtTime))) + stagedScript, err := buildscript.UnmarshalBuildExpression(resp.Commit.Expression, buildScriptCommitInfo(params.Owner, params.Project, resp.Commit.CommitID.String(), time.Time(resp.Commit.AtTime))) if err != nil { return nil, errs.Wrap(err, "failed to parse build expression") } diff --git a/scripts/to-buildscript/main.go b/scripts/to-buildscript/main.go index 6752489c4b..3c5bbc27f5 100644 --- a/scripts/to-buildscript/main.go +++ b/scripts/to-buildscript/main.go @@ -35,16 +35,16 @@ func main() { os.Exit(1) } - var atTime *time.Time + var commitInfo *buildscript.CommitInfo if len(os.Args) == 2 { t, err := time.Parse(strfmt.RFC3339Millis, os.Args[1]) if err != nil { panic(errs.JoinMessage(err)) } - atTime = &t + commitInfo = &buildscript.CommitInfo{"https://platform.activestate.com/org/project?commitID=00000000-0000-0000-0000-000000000000", t} } - bs, err := buildscript.UnmarshalBuildExpression([]byte(input), atTime) + bs, err := buildscript.UnmarshalBuildExpression([]byte(input), commitInfo) if err != nil { panic(errs.JoinMessage(err)) }