pull/31859/merge
MrJack 7 days ago committed by GitHub
commit 7b6bc9ce17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -36,6 +36,8 @@ type Info struct {
Description string `json:"description,omitempty"`
// Status is the current state of the release
Status common.Status `json:"status,omitempty"`
// RollbackRevision is the revision that was rolled back to. Zero means not a rollback.
RollbackRevision int `json:"rollback_revision,omitempty"`
// Contains the rendered templates/NOTES.txt if available
Notes string `json:"notes,omitempty"`
// Contains the deployed resources information
@ -44,13 +46,14 @@ type Info struct {
// infoJSON is used for custom JSON marshaling/unmarshaling
type infoJSON struct {
FirstDeployed *time.Time `json:"first_deployed,omitempty"`
LastDeployed *time.Time `json:"last_deployed,omitempty"`
Deleted *time.Time `json:"deleted,omitempty"`
Description string `json:"description,omitempty"`
Status common.Status `json:"status,omitempty"`
Notes string `json:"notes,omitempty"`
Resources map[string][]runtime.Object `json:"resources,omitempty"`
FirstDeployed *time.Time `json:"first_deployed,omitempty"`
LastDeployed *time.Time `json:"last_deployed,omitempty"`
Deleted *time.Time `json:"deleted,omitempty"`
Description string `json:"description,omitempty"`
Status common.Status `json:"status,omitempty"`
RollbackRevision int `json:"rollback_revision,omitempty"`
Notes string `json:"notes,omitempty"`
Resources map[string][]runtime.Object `json:"resources,omitempty"`
}
// UnmarshalJSON implements the json.Unmarshaler interface.
@ -95,6 +98,7 @@ func (i *Info) UnmarshalJSON(data []byte) error {
}
i.Description = tmp.Description
i.Status = tmp.Status
i.RollbackRevision = tmp.RollbackRevision
i.Notes = tmp.Notes
i.Resources = tmp.Resources
@ -105,10 +109,11 @@ func (i *Info) UnmarshalJSON(data []byte) error {
// It omits zero-value time fields from the JSON output.
func (i Info) MarshalJSON() ([]byte, error) {
tmp := infoJSON{
Description: i.Description,
Status: i.Status,
Notes: i.Notes,
Resources: i.Resources,
Description: i.Description,
Status: i.Status,
RollbackRevision: i.RollbackRevision,
Notes: i.Notes,
Resources: i.Resources,
}
if !i.FirstDeployed.IsZero() {

@ -87,6 +87,27 @@ func TestInfoMarshalJSON(t *testing.T) {
},
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Uninstalled release","status":"uninstalled"}`,
},
{
name: "with rollback revision",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusDeployed,
RollbackRevision: 2,
Description: "Rollback to 2",
},
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","rollback_revision":2,"description":"Rollback to 2"}`,
},
{
name: "zero rollback revision omitted",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusDeployed,
Description: "Normal install",
},
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","description":"Normal install"}`,
},
}
for _, tt := range tests {
@ -203,6 +224,27 @@ func TestInfoUnmarshalJSON(t *testing.T) {
Status: "",
},
},
{
name: "with rollback revision",
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","rollback_revision":2,"description":"Rollback to 2"}`,
expected: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusDeployed,
RollbackRevision: 2,
Description: "Rollback to 2",
},
},
{
name: "zero rollback revision omitted",
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","description":"Normal install"}`,
expected: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusDeployed,
Description: "Normal install",
},
},
}
for _, tt := range tests {
@ -219,6 +261,7 @@ func TestInfoUnmarshalJSON(t *testing.T) {
assert.Equal(t, tt.expected.Deleted.Unix(), info.Deleted.Unix())
assert.Equal(t, tt.expected.Description, info.Description)
assert.Equal(t, tt.expected.Status, info.Status)
assert.Equal(t, tt.expected.RollbackRevision, info.RollbackRevision)
assert.Equal(t, tt.expected.Notes, info.Notes)
assert.Equal(t, tt.expected.Resources, info.Resources)
})
@ -252,6 +295,61 @@ func TestInfoRoundTrip(t *testing.T) {
assert.Equal(t, original.Notes, decoded.Notes)
}
func TestInfoRollbackRevisionRoundTrip(t *testing.T) {
now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC)
tests := []struct {
name string
info Info
}{
{
name: "with rollback revision",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Description: "Rollback to 2",
Status: common.StatusDeployed,
RollbackRevision: 2,
},
},
{
name: "zero rollback revision",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Description: "Normal install",
Status: common.StatusDeployed,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(&tt.info)
require.NoError(t, err)
var decoded Info
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
assert.Equal(t, tt.info.RollbackRevision, decoded.RollbackRevision)
assert.Equal(t, tt.info.FirstDeployed.Unix(), decoded.FirstDeployed.Unix())
assert.Equal(t, tt.info.LastDeployed.Unix(), decoded.LastDeployed.Unix())
assert.Equal(t, tt.info.Status, decoded.Status)
assert.Equal(t, tt.info.Description, decoded.Description)
// Verify omitempty behavior: zero rollback_revision should not appear in JSON
if tt.info.RollbackRevision == 0 {
var raw map[string]any
err = json.Unmarshal(data, &raw)
require.NoError(t, err)
assert.NotContains(t, raw, "rollback_revision")
}
})
}
}
func TestInfoEmptyStringRoundTrip(t *testing.T) {
// This test specifically verifies that empty string time fields
// are handled correctly during parsing

@ -176,10 +176,11 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
Chart: previousRelease.Chart,
Config: previousRelease.Config,
Info: &release.Info{
FirstDeployed: currentRelease.Info.FirstDeployed,
LastDeployed: time.Now(),
Status: common.StatusPendingRollback,
Notes: previousRelease.Info.Notes,
FirstDeployed: currentRelease.Info.FirstDeployed,
LastDeployed: time.Now(),
Status: common.StatusPendingRollback,
Notes: previousRelease.Info.Notes,
RollbackRevision: previousVersion,
// Because we lose the reference to previous version elsewhere, we set the
// message here, and only override it later if we experience failure.
Description: fmt.Sprintf("Rollback to %d", previousVersion),

@ -83,3 +83,51 @@ func TestRollback_WaitOptionsPassedDownstream(t *testing.T) {
// Verify that WaitOptions were passed to GetWaiter
is.NotEmpty(failer.RecordedWaitOptions, "WaitOptions should be passed to GetWaiter")
}
func TestRollbackSetsRollbackRevision(t *testing.T) {
config := actionConfigFixture(t)
rel1 := releaseStub()
rel1.Name = "rollback-rev-test"
rel1.Version = 1
rel1.Info.Status = "superseded"
rel1.ApplyMethod = "csa"
require.NoError(t, config.Releases.Create(rel1))
rel2 := releaseStub()
rel2.Name = "rollback-rev-test"
rel2.Version = 2
rel2.Info.Status = "deployed"
rel2.ApplyMethod = "csa"
require.NoError(t, config.Releases.Create(rel2))
client := NewRollback(config)
client.Version = 1
client.ServerSideApply = "auto"
require.NoError(t, client.Run("rollback-rev-test"))
reli, err := config.Releases.Get("rollback-rev-test", 3)
require.NoError(t, err)
rel, err := releaserToV1Release(reli)
require.NoError(t, err)
assert.Equal(t, 1, rel.Info.RollbackRevision)
assert.Equal(t, "Rollback to 1", rel.Info.Description)
}
func TestRollbackRevisionZeroForNonRollback(t *testing.T) {
config := actionConfigFixture(t)
rel := releaseStub()
rel.Name = "non-rollback"
rel.Info.Status = "deployed"
require.NoError(t, config.Releases.Create(rel))
reli, err := config.Releases.Get("non-rollback", 1)
require.NoError(t, err)
r, err := releaserToV1Release(reli)
require.NoError(t, err)
assert.Equal(t, 0, r.Info.RollbackRevision)
}

@ -48,11 +48,21 @@ The historical release set is printed as a formatted table, e.g:
2 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Upgraded successfully
3 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Rolled back to 2
4 Mon Oct 3 10:15:13 2016 deployed alpine-0.1.0 1.0 Upgraded successfully
Use '--show-rollback' to include a column showing the revision that was rolled back to:
$ helm history angry-bird --show-rollback
REVISION UPDATED STATUS CHART APP VERSION ROLLBACK DESCRIPTION
1 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Initial install
2 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Upgraded successfully
3 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 2 Rolled back to 2
4 Mon Oct 3 10:15:13 2016 deployed alpine-0.1.0 1.0 Upgraded successfully
`
func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewHistory(cfg)
var outfmt output.Format
var showRollback bool
cmd := &cobra.Command{
Use: "history RELEASE_NAME",
@ -72,34 +82,40 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return err
}
if showRollback {
return outfmt.Write(out, releaseHistoryWithRollback(history))
}
return outfmt.Write(out, history)
},
}
f := cmd.Flags()
f.IntVar(&client.Max, "max", 256, "maximum number of revision to include in history")
f.BoolVar(&showRollback, "show-rollback", false, "show the rollback revision column in table output")
bindOutputFlag(cmd, &outfmt)
return cmd
}
type releaseInfo struct {
Revision int `json:"revision"`
Updated time.Time `json:"updated,omitzero"`
Status string `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
Description string `json:"description"`
Revision int `json:"revision"`
Updated time.Time `json:"updated,omitzero"`
Status string `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
RollbackRevision int `json:"rollback_revision,omitempty"`
Description string `json:"description"`
}
// releaseInfoJSON is used for custom JSON marshaling/unmarshaling
type releaseInfoJSON struct {
Revision int `json:"revision"`
Updated *time.Time `json:"updated,omitempty"`
Status string `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
Description string `json:"description"`
Revision int `json:"revision"`
Updated *time.Time `json:"updated,omitempty"`
Status string `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
RollbackRevision int `json:"rollback_revision,omitempty"`
Description string `json:"description"`
}
// UnmarshalJSON implements the json.Unmarshaler interface.
@ -138,6 +154,7 @@ func (r *releaseInfo) UnmarshalJSON(data []byte) error {
r.Status = tmp.Status
r.Chart = tmp.Chart
r.AppVersion = tmp.AppVersion
r.RollbackRevision = tmp.RollbackRevision
r.Description = tmp.Description
return nil
@ -147,11 +164,12 @@ func (r *releaseInfo) UnmarshalJSON(data []byte) error {
// It omits zero-value time fields from the JSON output.
func (r releaseInfo) MarshalJSON() ([]byte, error) {
tmp := releaseInfoJSON{
Revision: r.Revision,
Status: r.Status,
Chart: r.Chart,
AppVersion: r.AppVersion,
Description: r.Description,
Revision: r.Revision,
Status: r.Status,
Chart: r.Chart,
AppVersion: r.AppVersion,
RollbackRevision: r.RollbackRevision,
Description: r.Description,
}
if !r.Updated.IsZero() {
@ -180,6 +198,30 @@ func (r releaseHistory) WriteTable(out io.Writer) error {
return output.EncodeTable(out, tbl)
}
// releaseHistoryWithRollback wraps releaseHistory to include the rollback column in table output.
type releaseHistoryWithRollback releaseHistory
func (r releaseHistoryWithRollback) WriteJSON(out io.Writer) error {
return output.EncodeJSON(out, releaseHistory(r))
}
func (r releaseHistoryWithRollback) WriteYAML(out io.Writer) error {
return output.EncodeYAML(out, releaseHistory(r))
}
func (r releaseHistoryWithRollback) WriteTable(out io.Writer) error {
tbl := uitable.New()
tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "ROLLBACK", "DESCRIPTION")
for _, item := range r {
rollback := ""
if item.RollbackRevision > 0 {
rollback = strconv.Itoa(item.RollbackRevision)
}
tbl.AddRow(item.Revision, item.Updated.Format(time.ANSIC), item.Status, item.Chart, item.AppVersion, rollback, item.Description)
}
return output.EncodeTable(out, tbl)
}
func getHistory(client *action.History, name string) (releaseHistory, error) {
histi, err := client.Run(name)
if err != nil {
@ -216,11 +258,12 @@ func getReleaseHistory(rls []*release.Release) (history releaseHistory) {
a := formatAppVersion(r.Chart)
rInfo := releaseInfo{
Revision: v,
Status: s,
Chart: c,
AppVersion: a,
Description: d,
Revision: v,
Status: s,
Chart: c,
AppVersion: a,
RollbackRevision: r.Info.RollbackRevision,
Description: d,
}
if !r.Info.LastDeployed.IsZero() {
rInfo.Updated = r.Info.LastDeployed

@ -25,6 +25,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
)
@ -76,6 +77,77 @@ func TestHistoryCmd(t *testing.T) {
runTestCmd(t, tests)
}
func TestHistoryWithRollback(t *testing.T) {
date := time.Unix(242085845, 0).UTC()
ch := &chart.Chart{
Metadata: &chart.Metadata{
Name: "foo",
Version: "0.1.0-beta.1",
AppVersion: "1.0",
},
}
rels := []*release.Release{
{
Name: "angry-bird",
Version: 1,
Info: &release.Info{
FirstDeployed: date,
LastDeployed: date,
Status: common.StatusSuperseded,
Description: "Install complete",
},
Chart: ch,
},
{
Name: "angry-bird",
Version: 2,
Info: &release.Info{
FirstDeployed: date,
LastDeployed: date,
Status: common.StatusSuperseded,
Description: "Upgrade complete",
},
Chart: ch,
},
{
Name: "angry-bird",
Version: 3,
Info: &release.Info{
FirstDeployed: date,
LastDeployed: date,
Status: common.StatusDeployed,
RollbackRevision: 1,
Description: "Rollback to 1",
},
Chart: ch,
},
}
tests := []cmdTestCase{{
name: "history with rollback revision (default, no rollback column)",
cmd: "history angry-bird",
rels: rels,
golden: "output/history-with-rollback-no-flag.txt",
}, {
name: "history with rollback revision and --show-rollback flag",
cmd: "history angry-bird --show-rollback",
rels: rels,
golden: "output/history-with-rollback.txt",
}, {
name: "history with rollback revision json",
cmd: "history angry-bird --output json",
rels: rels,
golden: "output/history-with-rollback.json",
}, {
name: "history with rollback revision yaml",
cmd: "history angry-bird --output yaml",
rels: rels,
golden: "output/history-with-rollback.yaml",
}}
runTestCmd(t, tests)
}
func TestHistoryOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "history")
}
@ -173,6 +245,31 @@ func TestReleaseInfoMarshalJSON(t *testing.T) {
},
expected: `{"revision":0,"updated":"2025-10-08T12:00:00Z","status":"failed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Install failed"}`,
},
{
name: "with rollback revision",
info: releaseInfo{
Revision: 3,
Updated: updated,
Status: "deployed",
Chart: "mychart-1.0.0",
AppVersion: "1.0.0",
RollbackRevision: 1,
Description: "Rollback to 1",
},
expected: `{"revision":3,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","rollback_revision":1,"description":"Rollback to 1"}`,
},
{
name: "without rollback revision",
info: releaseInfo{
Revision: 1,
Updated: updated,
Status: "deployed",
Chart: "mychart-1.0.0",
AppVersion: "1.0.0",
Description: "Initial install",
},
expected: `{"revision":1,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Initial install"}`,
},
}
for _, tt := range tests {
@ -255,6 +352,31 @@ func TestReleaseInfoUnmarshalJSON(t *testing.T) {
Description: "Installing",
},
},
{
name: "with rollback revision",
input: `{"revision":3,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","rollback_revision":1,"description":"Rollback to 1"}`,
expected: releaseInfo{
Revision: 3,
Updated: updated,
Status: "deployed",
Chart: "mychart-1.0.0",
AppVersion: "1.0.0",
RollbackRevision: 1,
Description: "Rollback to 1",
},
},
{
name: "without rollback revision field",
input: `{"revision":1,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Install"}`,
expected: releaseInfo{
Revision: 1,
Updated: updated,
Status: "deployed",
Chart: "mychart-1.0.0",
AppVersion: "1.0.0",
Description: "Install",
},
},
}
for _, tt := range tests {
@ -271,6 +393,7 @@ func TestReleaseInfoUnmarshalJSON(t *testing.T) {
assert.Equal(t, tt.expected.Status, info.Status)
assert.Equal(t, tt.expected.Chart, info.Chart)
assert.Equal(t, tt.expected.AppVersion, info.AppVersion)
assert.Equal(t, tt.expected.RollbackRevision, info.RollbackRevision)
assert.Equal(t, tt.expected.Description, info.Description)
})
}
@ -300,6 +423,7 @@ func TestReleaseInfoRoundTrip(t *testing.T) {
assert.Equal(t, original.Status, decoded.Status)
assert.Equal(t, original.Chart, decoded.Chart)
assert.Equal(t, original.AppVersion, decoded.AppVersion)
assert.Equal(t, original.RollbackRevision, decoded.RollbackRevision)
assert.Equal(t, original.Description, decoded.Description)
}

@ -0,0 +1,4 @@
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Install complete
2 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Upgrade complete
3 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Rollback to 1

@ -0,0 +1 @@
[{"revision":1,"updated":"1977-09-02T22:04:05Z","status":"superseded","chart":"foo-0.1.0-beta.1","app_version":"1.0","description":"Install complete"},{"revision":2,"updated":"1977-09-02T22:04:05Z","status":"superseded","chart":"foo-0.1.0-beta.1","app_version":"1.0","description":"Upgrade complete"},{"revision":3,"updated":"1977-09-02T22:04:05Z","status":"deployed","chart":"foo-0.1.0-beta.1","app_version":"1.0","rollback_revision":1,"description":"Rollback to 1"}]

@ -0,0 +1,4 @@
REVISION UPDATED STATUS CHART APP VERSION ROLLBACK DESCRIPTION
1 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Install complete
2 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Upgrade complete
3 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 1 Rollback to 1

@ -0,0 +1,19 @@
- app_version: "1.0"
chart: foo-0.1.0-beta.1
description: Install complete
revision: 1
status: superseded
updated: "1977-09-02T22:04:05Z"
- app_version: "1.0"
chart: foo-0.1.0-beta.1
description: Upgrade complete
revision: 2
status: superseded
updated: "1977-09-02T22:04:05Z"
- app_version: "1.0"
chart: foo-0.1.0-beta.1
description: Rollback to 1
revision: 3
rollback_revision: 1
status: deployed
updated: "1977-09-02T22:04:05Z"

@ -36,6 +36,8 @@ type Info struct {
Description string `json:"description,omitempty"`
// Status is the current state of the release
Status common.Status `json:"status,omitempty"`
// RollbackRevision is the revision that was rolled back to. Zero means not a rollback.
RollbackRevision int `json:"rollback_revision,omitempty"`
// Contains the rendered templates/NOTES.txt if available
Notes string `json:"notes,omitempty"`
// Contains the deployed resources information
@ -44,13 +46,14 @@ type Info struct {
// infoJSON is used for custom JSON marshaling/unmarshaling
type infoJSON struct {
FirstDeployed *time.Time `json:"first_deployed,omitempty"`
LastDeployed *time.Time `json:"last_deployed,omitempty"`
Deleted *time.Time `json:"deleted,omitempty"`
Description string `json:"description,omitempty"`
Status common.Status `json:"status,omitempty"`
Notes string `json:"notes,omitempty"`
Resources map[string][]runtime.Object `json:"resources,omitempty"`
FirstDeployed *time.Time `json:"first_deployed,omitempty"`
LastDeployed *time.Time `json:"last_deployed,omitempty"`
Deleted *time.Time `json:"deleted,omitempty"`
Description string `json:"description,omitempty"`
Status common.Status `json:"status,omitempty"`
RollbackRevision int `json:"rollback_revision,omitempty"`
Notes string `json:"notes,omitempty"`
Resources map[string][]runtime.Object `json:"resources,omitempty"`
}
// UnmarshalJSON implements the json.Unmarshaler interface.
@ -95,6 +98,7 @@ func (i *Info) UnmarshalJSON(data []byte) error {
}
i.Description = tmp.Description
i.Status = tmp.Status
i.RollbackRevision = tmp.RollbackRevision
i.Notes = tmp.Notes
i.Resources = tmp.Resources
@ -105,10 +109,11 @@ func (i *Info) UnmarshalJSON(data []byte) error {
// It omits zero-value time fields from the JSON output.
func (i Info) MarshalJSON() ([]byte, error) {
tmp := infoJSON{
Description: i.Description,
Status: i.Status,
Notes: i.Notes,
Resources: i.Resources,
Description: i.Description,
Status: i.Status,
RollbackRevision: i.RollbackRevision,
Notes: i.Notes,
Resources: i.Resources,
}
if !i.FirstDeployed.IsZero() {

@ -87,6 +87,27 @@ func TestInfoMarshalJSON(t *testing.T) {
},
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Uninstalled release","status":"uninstalled"}`,
},
{
name: "with rollback revision",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusDeployed,
RollbackRevision: 2,
Description: "Rollback to 2",
},
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","rollback_revision":2,"description":"Rollback to 2"}`,
},
{
name: "zero rollback revision omitted",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusDeployed,
Description: "Normal install",
},
expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","description":"Normal install"}`,
},
}
for _, tt := range tests {
@ -203,6 +224,27 @@ func TestInfoUnmarshalJSON(t *testing.T) {
Status: "",
},
},
{
name: "with rollback revision",
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","rollback_revision":2,"description":"Rollback to 2"}`,
expected: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusDeployed,
RollbackRevision: 2,
Description: "Rollback to 2",
},
},
{
name: "zero rollback revision omitted",
input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed","description":"Normal install"}`,
expected: Info{
FirstDeployed: now,
LastDeployed: later,
Status: common.StatusDeployed,
Description: "Normal install",
},
},
}
for _, tt := range tests {
@ -219,6 +261,7 @@ func TestInfoUnmarshalJSON(t *testing.T) {
assert.Equal(t, tt.expected.Deleted.Unix(), info.Deleted.Unix())
assert.Equal(t, tt.expected.Description, info.Description)
assert.Equal(t, tt.expected.Status, info.Status)
assert.Equal(t, tt.expected.RollbackRevision, info.RollbackRevision)
assert.Equal(t, tt.expected.Notes, info.Notes)
assert.Equal(t, tt.expected.Resources, info.Resources)
})
@ -252,6 +295,61 @@ func TestInfoRoundTrip(t *testing.T) {
assert.Equal(t, original.Notes, decoded.Notes)
}
func TestInfoRollbackRevisionRoundTrip(t *testing.T) {
now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC)
later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC)
tests := []struct {
name string
info Info
}{
{
name: "with rollback revision",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Description: "Rollback to 2",
Status: common.StatusDeployed,
RollbackRevision: 2,
},
},
{
name: "zero rollback revision",
info: Info{
FirstDeployed: now,
LastDeployed: later,
Description: "Normal install",
Status: common.StatusDeployed,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(&tt.info)
require.NoError(t, err)
var decoded Info
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
assert.Equal(t, tt.info.RollbackRevision, decoded.RollbackRevision)
assert.Equal(t, tt.info.FirstDeployed.Unix(), decoded.FirstDeployed.Unix())
assert.Equal(t, tt.info.LastDeployed.Unix(), decoded.LastDeployed.Unix())
assert.Equal(t, tt.info.Status, decoded.Status)
assert.Equal(t, tt.info.Description, decoded.Description)
// Verify omitempty behavior: zero rollback_revision should not appear in JSON
if tt.info.RollbackRevision == 0 {
var raw map[string]any
err = json.Unmarshal(data, &raw)
require.NoError(t, err)
assert.NotContains(t, raw, "rollback_revision")
}
})
}
}
func TestInfoEmptyStringRoundTrip(t *testing.T) {
// This test specifically verifies that empty string time fields
// are handled correctly during parsing

Loading…
Cancel
Save