Collaboration Store, Part XXIX

As a rule, software systems do not work well until they have been used, and have failed repeatedly, in real applications.”
David Lorge Parnas

Now that we have completed the initial version of the application publishing process, it would be nice to release a new Update Set so that the folks who are inclined to help out with the testing can try it all out. However, we have already received feedback from the earlier testing of the set-up process, and we should really address all of those issues before publishing a new version for further testing. Here are the items discovered during the testing of the first version of the software released earlier:

  • Installation error: Table ‘sys_hub_action_status_metadata’ does not exist
  • Not allowing update of property: x_11556_col_store.store_name
  • Not allowing update of property: x_11556_col_store.host_instance
  • In the setup, the instance name field doesn’t inform you that you only need the instance prefix, not the full url
  • You can only collaborate with one host

I was able to reproduce them all, and here is what I ended up doing for each:

Installation error: Table ‘sys_hub_action_status_metadata’ does not exist

I tried to find out some information on the purpose and use for this table, but I couldn’t really find anything that told me anything of value. I still believe that the ‘sys_hub_action_status_metadata’ table is related to a version or plugin that I have in my instance, but was not present in the instance on which the test installation was being performed. Since it didn’t seem as if it was anything that had anything to do with the operation of the application, I decided to just delete all references to it in the Update Set, just to avoid this issue. I’m not sure if that will cause any issues with any instances that actually do have this table, but it seemed like something worth a try and we’ll see what happens. If it causes a problem, I will not do that in the future and just throw in some release notes that say just ignore the error if it comes up. But let’s see if this works, first.

Not allowing update of property: x_11556_col_store.store_name
Not allowing update of property: x_11556_col_store.host_instance

This one took a little bit of research and a little bit of trial and error (mostly error!), but I eventually solved the problem by moving all updates to these properties into my global utilities so that the command was being executed by a global component instead of a scoped component. I did this once before with the gs.sleep() function, and this worked out just as well. Here is the function that I added to the global utilities:

setProperty: function(name, value) {
	gs.setProperty(name, value);
},

Once I created the function in the global utilities, it was just a matter of changing all gs.setProperty() function calls to csgu.setProperty() and that seemed to have done the trick.

In the setup, the instance name field doesn’t inform you that you only need the instance prefix, not the full url

Since the snh-form-field tag provides for a “help” attribute, which appears underneath the label, I simply updated the definition for that field to include a little help:

<snh-form-field
  snh-model="c.data.host_instance_id"
  snh-name="host_instance_id"
  snh-label="Host Instance ID"
  snh-help="Enter the instance ID only, not the full URL of the instance (https://{instance_id}.servicenow.com))"
  snh-required="c.data.instance_type == 'client'"
  ng-show="c.data.instance_type == 'client'"/>

This renders a little help text underneath the label, and looks like this when the page comes up:

Initial set-up screen with added help text for the Host Instance ID

That should resolve this issue.

You can only collaborate with one host

As I mentioned earlier when this was first reported, this is by design. Allowing multiple Host instances introduces a level of complexity with which I’m not quite ready to deal just yet. At this point, we will just file this one under Will not fix or Future release for now.

Other than these issues, I have not personally encountered or heard of any other issues with the set-up process, but that doesn’t mean that the testing is complete by any means. If anyone wants to join the testing process and you missed out on testing the earlier version, you will still have to work your way through the set-up process for any instances involved, so please report any issues with the set-up process as well as any issues with the application publication process.

Speaking of the application publication process, there is still yet another aspect of this process that remains to be developed. Once a new application has been published to the Host instance, the Host instance will need to push that version out to all of the other Client instances. This version does not include that functionality, but we will need to throw that in at some point. I just did not want to hold up the testing for that particular feature, since it really is a completely independent operation.

For those of you who are interested in participating in the testing, you will need this new Update Set, plus this additional global component that is not included in the scoped application. Also, if this is your first time installing the application, you will also need the latest version of snh-form-fields, which you can find here. As always, if you have any feedback, positive or negative, please leave the details in the comments. All information is welcome and much appreciated. Thanks in advance for your assistance.

Collaboration Store, Part XXVIII

“The best way out is always through.”
Robert Frost

Now that we have completed the functions that send the application record and version record over to the Host instance, the last thing that we need to do is to send over the Update Set XML file attached to the version record. For sending over an attachment, we will need to use the attachment REST API instead of the standard table REST API. Since the contents of the file that we are sending over is plain text, we can use a little hackery to bypass the need to send over an actual file and just place the text content in the body of the request. But first, as usual, we need to grab the GlideRecord that we want to send before we do anything else.

var gsa = new GlideSysAttachment();
var sysAttGR = new GlideRecord('sys_attachment');
if (sysAttGR.get(answer.attachmentId)) {
	...
} else {
	answer = this.processError(answer, 'Invalid attachment record sys_id: ' + answer.attachmentId);
}

Once we have obtained the record, we will need to grab the usual suspects from the System Properties and then construct the appropriate URL for the request.

var host = gs.getProperty('x_11556_col_store.host_instance');
var token = gs.getProperty('x_11556_col_store.active_token');
var url = 'https://';
url += host;
url += '.service-now.com/api/now/attachment/file?table_name=x_11556_col_store_member_application_version&table_sys_id=';
url += answer.hostVerId;
url += '&file_name=';
url += sysAttGR.getDisplayValue('file_name');

Next, we will need to construct and configure our sn_ws.RESTMessageV2 object.

var request  = new sn_ws.RESTMessageV2();
request.setBasicAuth(this.WORKER_ROOT + host, token);
request.setRequestHeader('Content-Type', sysAttGR.getDisplayValue('content_type'));
request.setRequestHeader('Accept', 'application/json');
request.setHttpMethod('post');
request.setEndpoint(url);
request.setRequestBody(gsa.getContent(sysAttGR));

Two things to note on this one: 1) the Content-Type header sets the value of the content_type property of the target instance sys_attachment record, and 2) we use our old friend, the GlideSysAttachment utility to obtain the actual XML of the attachment in lieu of a real file (the file contents are not actually a part of the GlideRecord for the sys_attachment table, hence the need to utilize GlideSysAttachment).

Now all we need to do is to execute the request and check the response.

var response = request.execute();
if (response.haveError()) {
	answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
} else if (response.getStatusCode() != 201) {
	answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
}

Unlike the application and version records, we have no need for any information from the returned JSON string, so there is no need to attempt to parse it and pull out any data. As long as there are no errors, we are good to go.

Putting it all together, the entire function looks like this:

processPhase7: function(answer) {
	var gsa = new GlideSysAttachment();
	var sysAttGR = new GlideRecord('sys_attachment');
	if (sysAttGR.get(answer.attachmentId)) {
		var host = gs.getProperty('x_11556_col_store.host_instance');
		var token = gs.getProperty('x_11556_col_store.active_token');
		var url = 'https://';
		url += host;
		url += '.service-now.com/api/now/attachment/file?table_name=x_11556_col_store_member_application_version&table_sys_id=';
		url += answer.hostVerId;
		url += '&file_name=';
		url += sysAttGR.getDisplayValue('file_name');
		var request  = new sn_ws.RESTMessageV2();
		request.setBasicAuth(this.WORKER_ROOT + host, token);
		request.setRequestHeader('Content-Type', sysAttGR.getDisplayValue('content_type'));
		request.setRequestHeader('Accept', 'application/json');
		request.setHttpMethod('post');
		request.setEndpoint(url);
		request.setRequestBody(gsa.getContent(sysAttGR));
		var response = request.execute();
		if (response.haveError()) {
			answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
		} else if (response.getStatusCode() != 201) {
			answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
		}
	} else {
		answer = this.processError(answer, 'Invalid attachment record sys_id: ' + answer.attachmentId);
	}

	return answer;
},

Unfortunately, when I took it out for a spin, it didn’t work. When I was nosing around to see what the source of the problem was, I figured out that the sys_id value that I was using for the attachment GlideRecord was not a string, but an array of comma-separated sys_id pairs, one for the original attachment and one for the copied attachment. This value came out of the fourth step, where we copied the attachment from the original scoped application record to the version record. Once I realized the actual format of the data returned from the GlideSysAttachment copy function, I did a little rewriting of the processPhase4 function to accommodate the actual structure of the returned data.

processPhase4: function(answer) {
	var gsa = new GlideSysAttachment();
	var values = gsa.copy('sys_app', answer.appSysId, 'x_11556_col_store_member_application_version', answer.versionId);
	gsa.deleteAttachment(answer.attachmentId);
	if (values[0]) {
		var ids = values[0].split(',');
		if (ids[1]) {
			answer.attachmentId = ids[1];
		} else {
			answer = this.processError(answer, 'Unrecognizable response from attachment copy: ' + values);
		}
	} else {
		answer = this.processError(answer, 'Unrecognizable response from attachment copy: ' + values);
	}
		
	return answer;
},

While I was at it, I went ahead and did a little work on the processError function to add a few diagnostic breadcrumbs to the system log whenever there is an error. That function now looks like this:

processError: function(answer, message) {
	gs.info('ApplicationPublisher.processError: ' + message);
	gs.info('ApplicationPublisher.processError: ' + JSON.stringify(answer));
	gs.addErrorMessage(message);
	answer.error = message;
	return answer;
},

Once I straightened all of that out, everything finally worked as intended. This is the last step in the process, so this essentially completes the code for publishing an application to the Collaboration Store. At this point, I should probably cut another Update Set so that the folks who would like to participate in the testing can take things out for a little test drive. I still need to address the issues with the set-up process uncovered by the last round of testing, so I think I will take that on next time out and then release a new Update Set for those of you who are willing to put things through their paces and report your results. As always, all feedback is very much appreciated.

Collaboration Store, Part XXVII

“It does not matter how slowly you go as long as you do not stop.”
Confucius

Last time we pushed the application record to the Host instance and now we have to do basically the same thing with the version record. The only difference really, other than the table and fields, is that a version record will always be a new record, so there is no need to determine if the record exists or not on the Host instance. This simplifies the code quite a bit. We still need to fetch the GlideRecord that will be sent over, so as we did with the application record, this will be the first order of business.

var versionGR = new GlideRecord('x_11556_col_store_member_application_version');
if (versionGR.get(answer.versionId)) {
	...
} else {
	answer = this.processError(answer, 'Invalid version record sys_id: ' + answer.versionId);
}

Once we have the record, we can gather up our System Properties and build the payload for the REST API call.

var host = gs.getProperty('x_11556_col_store.host_instance');
var token = gs.getProperty('x_11556_col_store.active_token');
var payload = {};
payload.member_application = answer.hostAppId;
payload.version = versionGR.getDisplayValue('version');

With our payload in hand, we can now create and configure our sn_ws.RESTMessageV2 object.

var request  = new sn_ws.RESTMessageV2();
request.setBasicAuth(this.WORKER_ROOT + host, token);
request.setRequestHeader("Accept", "application/json");
request.setHttpMethod('post');
request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application_version');
request.setRequestBody(JSON.stringify(payload, null, '\t'));

Now all that is left to do is to execute the request and check the response.

response = request.execute();
if (response.haveError()) {
	answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
} else if (response.getStatusCode() != 201) {
	answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
} else {
	var jsonString = response.getBody();
	var jsonObject = {};
	try {
		jsonObject = JSON.parse(jsonString);
	} catch (e) {
		answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
	}
	if (!answer.error) {
		answer.hostVerId = jsonObject.result.sys_id;
	}
}

Well, that was easy! Here’s the whole thing all put together.

processPhase6: function(answer) {
	var versionGR = new GlideRecord('x_11556_col_store_member_application_version');
	if (versionGR.get(answer.versionId)) {
		var host = gs.getProperty('x_11556_col_store.host_instance');
		var token = gs.getProperty('x_11556_col_store.active_token');
		var payload = {};
		payload.member_application = answer.hostAppId;
		payload.version = versionGR.getDisplayValue('version');
		var request  = new sn_ws.RESTMessageV2();
		request.setBasicAuth(this.WORKER_ROOT + host, token);
		request.setRequestHeader("Accept", "application/json");
		request.setHttpMethod('post');
		request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application_version');
		request.setRequestBody(JSON.stringify(payload, null, '\t'));
		response = request.execute();
		if (response.haveError()) {
			answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
		} else if (response.getStatusCode() != 201) {
			answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
		} else {
			var jsonString = response.getBody();
			var jsonObject = {};
			try {
				jsonObject = JSON.parse(jsonString);
			} catch (e) {
				answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
			}
			if (!answer.error) {
				answer.hostVerId = jsonObject.result.sys_id;
			}
		}
	} else {
		answer = this.processError(answer, 'Invalid version record sys_id: ' + answer.versionId);
	}

	return answer;
},

That takes care of 6 of the 7 steps. The last one that we will need to do will be to push the Update Set XML attachment over to the Host. That one may be a little more involved, so we will save that for next time out.

Collaboration Store, Part XXVI

“It’s not foresight or hindsight we need. We need sight, plain and simple. We need to see what is right in front of us.”
Gordon Atkinson

Last time, we completed the final step in the publication process for apps published on a Host instance. For those apps published on a Client instance, however, there is still more work to do. Everything that we created locally on the instance where the app is being published needs to be transferred over to the Host. To do that, we can utilize the built-in REST API.

The first thing that we will want to do is to fetch the GlideRecord for the app to obtain all of the data to send over. As we have done in other steps, we will check to make sure that we have obtained the record and report an error if we did not.

var mbrAppGR = new GlideRecord('x_11556_col_store_member_application');
if (mbrAppGR.get(answer.mbrAppId)) {
	...
} else {
	answer = this.processError(answer, 'Invalid Member Application sys_id: ' + answer.appSysId);
}

The next thing that we will want to do is to check to see if the app exists on the Host. Although we have already determined whether or not this app is new to the local instance earlier in the process, we will still want to double check to make sure that there isn’t already a version sitting on the Host for some unknown reason. If we find it, we will want to update it; otherwise, we will want to add it. To find the app on the remote Host, we will use both the name of the app and the name of the instance as query arguments (other instances may have published an app with the same name, but we would not want to update any of those). That’s a fairly standard REST API HTTP get operation.

var host = gs.getProperty('x_11556_col_store.host_instance');
var token = gs.getProperty('x_11556_col_store.active_token');
var thisInstance = gs.getProperty('instance_name');
var request  = new sn_ws.RESTMessageV2();
request.setHttpMethod('get');
request.setBasicAuth(this.WORKER_ROOT + host, token);
request.setRequestHeader("Accept", "application/json");
request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application?sysparm_fields=sys_id&sysparm_query=provider.instance%3D' + thisInstance + '%5Ename%3D' + encodeURIComponent(mbrAppGR.getDisplayValue('name')));
var response = request.execute();
if (response.haveError()) {
	answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
} else if (response.getStatusCode() == '200') {
	...
} else {
	answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
}

The HTTP response code should be 200 whether or not any records were returned. To determine if there is a record present on the Host instance, we need to parse the returned JSON string and check the size of the array of records returned.

var jsonString = response.getBody();
var jsonObject = {};
try {
	jsonObject = JSON.parse(jsonString);
} catch (e) {
	answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
}

If we are updating an existing record, the HTTP Method will be a PUT and the URL will include the sys_id of the record. If we are inserting a new record, then the HTTP Method will be a POST. Other than that, the data that we will be sending to the Host instance will be virtually the same, so we can start to build that up before we make the determination as to which method we will use.

var payload = {};
payload.name = mbrAppGR.getDisplayValue('name');
payload.description = mbrAppGR.getDisplayValue('description');
payload.current_version = mbrAppGR.getDisplayValue('current_version');
payload.active = 'true';
request  = new sn_ws.RESTMessageV2();
request.setBasicAuth(this.WORKER_ROOT + host, token);
request.setRequestHeader("Accept", "application/json");

Now we can check the size of the array of records returned and handle the things that will be different depending on whether or not this is a new record on the Host.

if (jsonObject.result && jsonObject.result.length > 0) {
	answer.hostAppId = jsonObject.result[0].sys_id;
	request.setHttpMethod('put');
	request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application/' + answer.hostAppId);
} else {
	request.setHttpMethod('post');
	request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application');
	payload.provider = thisInstance;
}
request.setRequestBody(JSON.stringify(payload, null, '\t'));

Now that we have everything all set up, all this is left to do is to execute the method and check the results.

response = request.execute();
if (response.haveError()) {
	answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
} else if (response.getStatusCode() != 200 && response.getStatusCode() != 201) {
	answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
} else {
	jsonString = response.getBody();
	jsonObject = {};
	try {
		jsonObject = JSON.parse(jsonString);
	} catch (e) {
		answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
	}
	if (!answer.error) {
		answer.hostAppId = jsonObject.result.sys_id;
	}
}

If all goes well, the application record will now be present on the Host instance, which will only leave us with two more things to do, add a new version record to the Host instance and then attach the Update Set XML to the version record. Here is the full function for this step in its entirety.

processPhase5: function(answer) {
	var mbrAppGR = new GlideRecord('x_11556_col_store_member_application');
	if (mbrAppGR.get(answer.mbrAppId)) {
		var host = gs.getProperty('x_11556_col_store.host_instance');
		var token = gs.getProperty('x_11556_col_store.active_token');
		var thisInstance = gs.getProperty('instance_name');
		var request  = new sn_ws.RESTMessageV2();
		request.setHttpMethod('get');
		request.setBasicAuth(this.WORKER_ROOT + host, token);
		request.setRequestHeader("Accept", "application/json");
		request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application?sysparm_fields=sys_id&sysparm_query=provider.instance%3D' + thisInstance + '%5Ename%3D' + encodeURIComponent(mbrAppGR.getDisplayValue('name')));
		var response = request.execute();
		if (response.haveError()) {
			answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
		} else if (response.getStatusCode() == '200') {
			var jsonString = response.getBody();
			var jsonObject = {};
			try {
				jsonObject = JSON.parse(jsonString);
			} catch (e) {
				answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
			}
			if (!answer.error) {
				var payload = {};
				payload.name = mbrAppGR.getDisplayValue('name');
				payload.description = mbrAppGR.getDisplayValue('description');
				payload.current_version = mbrAppGR.getDisplayValue('current_version');
				payload.active = 'true';
				request  = new sn_ws.RESTMessageV2();
				request.setBasicAuth(this.WORKER_ROOT + host, token);
				request.setRequestHeader("Accept", "application/json");
				if (jsonObject.result && jsonObject.result.length > 0) {
					answer.hostAppId = jsonObject.result[0].sys_id;
					request.setHttpMethod('put');
					request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application/' + answer.hostAppId);
				} else {
					request.setHttpMethod('post');
					request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application');
					payload.provider = thisInstance;
				}
				request.setRequestBody(JSON.stringify(payload, null, '\t'));
				response = request.execute();
				if (response.haveError()) {
					answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
				} else if (response.getStatusCode() != 200 && response.getStatusCode() != 201) {
					answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
				} else {
					jsonString = response.getBody();
					jsonObject = {};
					try {
						jsonObject = JSON.parse(jsonString);
					} catch (e) {
						answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
					}
					if (!answer.error) {
						answer.hostAppId = jsonObject.result.sys_id;
					}
				}
			}
		} else {
			answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
		}
	} else {
		answer = this.processError(answer, 'Invalid Member Application sys_id: ' + answer.appSysId);
	}

	return answer;
},

Next time, we will code out the insertion of the version record, which should be very similar to this step, although it should be a little simpler since we do not have to check to see if a record exists or not. Version records are always new records for each new version published. That should simplify things quite a bit.

Collaboration Store, Part XXV

“Controlling complexity is the essence of computer programming.”
Brian Kernighan

Last time, we realized that we had a little bit of rework to do, and it turns out that we actually have to do a couple of things: 1) insert the missing step (attaching the XML to the version record), and 2) modify the ending point if the instance is the Host instance (there is no need to send the records to the Host instance if you are the Host instance). The first part is easy enough; just insert one more DIV for our missing step and then renumber all of the ones that follow:

<div class="row" id="phase_4" style="visibility: hidden; display: none;">
	<image id="loading_4" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
	<image id="success_4" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<image id="error_4" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<span style="margin-left: 10px; font-weight:bold;">
		Attaching the Update Set XML to the Version record
	</span>
</div>
<div class="row" id="phase_5" style="visibility: hidden; display: none;">
	<image id="loading_5" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
	<image id="success_5" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<image id="error_5" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<span style="margin-left: 10px; font-weight:bold;">
		Sending the Application record to the Host instance
	</span>
</div>
<div class="row" id="phase_6" style="visibility: hidden; display: none;">
	<image id="loading_6" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
	<image id="success_6" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<image id="error_6" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<span style="margin-left: 10px; font-weight:bold;">
		Sending the Version record to the Host instance
	</span>
</div>
<div class="row" id="phase_7" style="visibility:hidden; display: none;">
	<image id="loading_7" src="/images/loading_anim4.gif" style="width: 16px; height: 16px;"/>
	<image id="success_7" src="/images/check32.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<image id="error_7" src="/images/delete_row.gif" style="width: 16px; height: 16px; visibility: hidden; display: none;"/>
	<span style="margin-left: 10px; font-weight:bold;">
		Sending the Update Set XML to the Host instance
	</span>
</div>

And of course, we have to insert the missing step in our Script Include, which at this point is just another empty placeholder like all of the others. We’ll build out the details later as we come to that step.

For controlling the point at which we stop doing stuff, we will need to know if this instance is the Host instance or one of the Client instances. The easiest way to do that is to compare our scoped Host instance property with the stock instance_name property.

var isHost = gs.getProperty('instance_name') == gs.getProperty('x_11556_col_store.host_instance');

Then we just need to modify our original conditional statement that only assumed 6 steps and looked like this:

if (answer.phase < 7) {

… to one that will do all 7 steps for a Client instance and only the first 4 steps for a Host instance.

if (answer.phase < 5 || (answer.phase < 8 && !answer.isHost)) {

With that out of the way, we can return to building out the missing steps in the Script Include, starting with the newly added fourth step, which is to attach the Update Set XML to the version record. As you may recall, we already created an attachment record when we generated the XML, so now all we need to do is to transfer that attachment to our new version record. For that, we can return to our old friend, the GlideSysAttachment. This time, instead of creating the attachment record, we will be copying the attachment from one record to another.

var gsa = new GlideSysAttachment();
var newSysId = gsa.copy('sys_app', answer.appSysId, 'x_11556_col_store_member_application_version', answer.versionId);

Once we have copied the attachment from the Scoped Application record to the version record, we will want to delete the attachment record linked to the Scoped Application.

gsa.deleteAttachment(answer.attachmentId);

The last thing that we will need to do will be to update the attachment sys_id in our transfer object so that we will have the ID of the right attachment later on when we go to send it over to the Host.

answer.attachmentId = newSysId;

That makes this a relatively simple step in terms of code. The whole thing looks like this:

processPhase4: function(answer) {
	var gsa = new GlideSysAttachment();
	var newSysId = gsa.copy('sys_app', answer.appSysId, 'x_11556_col_store_member_application_version', answer.versionId);
	gsa.deleteAttachment(answer.attachmentId);
	answer.attachmentId = newSysId;

	return answer;
},

If the instance doing the publishing is a Host instance, this would actually be the end of the process, as far as publishing is concerned. We will still have to notify all of the other instances of the new version, but that’s an entirely separate process that we will deal with at some point in the future. But for our new Publish to Collaboration Store action, this is all that needs to be done if you are the Host. For all of the other instances, the application, version, and attachment records will all have to be sent over to the Host as a part of this process. We’ll get started on those steps of the process next time out.