Skip to content

feat: add concurrency token support for partial variable snapshot updates - #465

Merged
bec-callow-oct merged 3 commits into
mainfrom
bec/si-340-partial-variable-client-concurrency
Aug 24, 2026
Merged

feat: add concurrency token support for partial variable snapshot updates#465
bec-callow-oct merged 3 commits into
mainfrom
bec/si-340-partial-variable-client-concurrency

Conversation

@bec-callow-oct

@bec-callow-oct bec-callow-oct commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Background

Adds client support for release and runbook variable snapshot concurrency. Relates to the changes in https://github.com/OctopusDeploy/OctopusDeploy/pull/46160 and https://github.com/OctopusDeploy/OctopusDeploy/pull/46136

Results

The concurrency token can be retrieved from a a release:

release, err := client.Releases.Add(releases.NewRelease(channel.GetID(), proj.GetID(), "1.0.0"))
token := release.VariableSnapshotConcurrencyToken

A new token is generated by the snapshot by name endpoint:

identifiers := []core.VariableIdentifier{{Name: variableName, OwnerID: variableOwnerId}}
afterByName, err := releases.SnapshotVariablesByName(client, release, identifiers, currentToken)

An exception occurs when the concurrency token on the request does not equal the token on the release:

_, err := releases.SnapshotVariablesByName(client, release, identifiers, staleToken)
apiErr := err.(*core.APIError) // apiErr.StatusCode == http.StatusConflict, apiErr.ErrorMessage == "This variable snapshot was changed by someone else since you last loaded it. Please reload and try again."

Code samples for testing

func TestReleaseSnapshotByName(octopusClient *client.Client, project *projects.Project, variable *variables.Variable, release *releases.Release, variableIdentifiers []core.VariableIdentifier) {
	variable.Value = "UpdatedValue1"
	_, err := variables.UpdateSingle(octopusClient, project.SpaceID, project.GetID(), variable)
	if err != nil {
		fmt.Printf("Failed to update the snapshot for release %s. Error: %v\n", release.GetID(), err)
		return
	}

	staleToken := release.VariableSnapshotConcurrencyToken
	updated, err := releases.SnapshotVariablesByName(octopusClient, release, variableIdentifiers, staleToken)
	if err != nil {
		fmt.Printf("Failed to update the snapshot for release %s. Error: %v\n", release.GetID(), err)
		return
	}
	fmt.Printf("Updated the snapshot for release %s\n", updated.GetID())

	variable.Value = "UpdatedValue2"
	_, err = variables.UpdateSingle(octopusClient, project.SpaceID, project.GetID(), variable)
	if err != nil {
		fmt.Printf("Failed to update the snapshot for release %s. Error: %v\n", release.GetID(), err)
		return
	}

	_, err = releases.SnapshotVariablesByName(octopusClient, updated, variableIdentifiers, staleToken)
	if err != nil {
		fmt.Printf("Failed to update the snapshot for release %s. Error: %v\n", release.GetID(), err)
		return
	}
	fmt.Printf("Failed to update the snapshot for release %s. Error: expected an error but none was returned\n", release.GetID())
}
func TestRunbookSnapshotByName(octopusClient *client.Client, project *projects.Project, variable *variables.Variable, runbookSnapshot *runbooks.RunbookSnapshot, variableIdentifiers []core.VariableIdentifier) {
	variable.Value = "UpdatedValue1"
	_, err := variables.UpdateSingle(octopusClient, project.SpaceID, project.GetID(), variable)
	if err != nil {
		fmt.Printf("Failed to update the snapshot for runbook snapshot %s. Error: %v\n", runbookSnapshot.GetID(), err)
		return
	}

	staleToken := runbookSnapshot.VariableSnapshotConcurrencyToken
	updated, err := runbooks.SnapshotVariablesByName(octopusClient, runbookSnapshot, variableIdentifiers, staleToken)
	if err != nil {
		fmt.Printf("Failed to update the snapshot for runbook snapshot %s. Error: %v\n", runbookSnapshot.GetID(), err)
		return
	}
	fmt.Printf("Updated the snapshot for runbook snapshot %s\n", updated.GetID())

	variable.Value = "UpdatedValue2"
	_, err = variables.UpdateSingle(octopusClient, project.SpaceID, project.GetID(), variable)
	if err != nil {
		fmt.Printf("Failed to update the snapshot for runbook snapshot %s. Error: %v\n", runbookSnapshot.GetID(), err)
		return
	}

	_, err = runbooks.SnapshotVariablesByName(octopusClient, updated, variableIdentifiers, staleToken)
	if err != nil {
		fmt.Printf("Failed to update the snapshot for runbook snapshot %s. Error: %v\n", runbookSnapshot.GetID(), err)
		return
	}
	fmt.Printf("Failed to update the snapshot for runbook snapshot %s. Error: expected an error but none was returned\n", runbookSnapshot.GetID())
}

@bec-callow-oct
bec-callow-oct force-pushed the bec/si-340-partial-variable-client-concurrency branch from f65fa49 to a42958d Compare August 24, 2026 01:23
@bec-callow-oct
bec-callow-oct requested a review from a team August 24, 2026 01:45
Comment thread pkg/releases/release_service.go Outdated

// SnapshotVariablesByName requires octopus feature toggle partial-updates-on-variables = true
func SnapshotVariablesByName(client newclient.Client, release *Release, variables []core.VariableIdentifier) (*Release, error) {
func SnapshotVariablesByName(client newclient.Client, release *Release, variables []core.VariableIdentifier, concurrencyToken ...string) (*Release, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to support an array of concurrency tokens here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have learnt that this is a method of making the parameter optional, and is not uncommon in Go

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

making it an *string would allow it to be optionally null, or a value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll change it to *string, that looks cleaner

@benPearce1 benPearce1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good. Ignore previous comments about the ... operator.

@bec-callow-oct
bec-callow-oct enabled auto-merge (squash) August 24, 2026 23:13
@bec-callow-oct
bec-callow-oct merged commit 7bc0e78 into main Aug 24, 2026
5 checks passed
@bec-callow-oct
bec-callow-oct deleted the bec/si-340-partial-variable-client-concurrency branch August 24, 2026 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants