Deployment Callbacks?

Does the deployment package offer any api/callback for triggering/reacting to a deploy?

i have 2 use cases right now that would greatly benefit from this, but i just cant find then:

  • i have all my items as normal SOs with custom fields, and i press a button to convert all of then into inventory files and THEN i click the deploy button

  • when i deploy my c# module i have a post build action to copy the dll of my DTOS into a project folder - but then i have to manually refresh the assetdatabase(by saving/doing some changes or whatever) to trigger a solution refresh and detect the dll

1 Like

Hi!

So my news are sort of mixed. First, callbacks were considered them, had them in alpha, and then removed them to await further feedback, and here you are with further feedback :slight_smile: . We also considered and spiked out a full easy-to-use public-facing editor deployment API, but since we had the CLI, we opted to have a small API footprint instead…

Second, while we don’t have exactly UI bound callback system, we do have the API services register publicaly exposed, and this can be used for automation.

You have a few alternatives

  1. Call the deployment code yourself (its public)
  2. Listen to a change of status
  3. Implement your own provider
  4. Set up automation with the CLI which supports deploy

If you reference “Unity.Services.DeploymentApi” in your assembly, you could do this

  1. Create your own automation (will require your own button)
using Unity.Services.DeploymentApi.Editor;

// ... stuff

// run my SO -> item code
var cc = Deployments.Instance.DeploymentProviders.First(s => s.Service == "Cloud Code"); // you may have to disambiguate between C# and JS here :-(
var item = cc.DeploymentItems.First(d => d.Name == "MyModule.ccmr");
await cc.DeployCommand.ExecuteAsync(new IDeploymentItem[] { item });

// run my copying code
  1. Listen on the item for its change of status (will work with our UI, but only after)
using Unity.Services.DeploymentApi.Editor;

// ... stuff

// run my SO -> item code
var cc = Deployments.Instance.DeploymentProviders.First(s => s.Service == "Cloud Code");
var item = cc.DeploymentItems.First(d => d.Name == "MyModule.ccmr");
item.PropertyChanged += (sender,
    e) =>
{
    if (e.PropertyName == nameof(IDeploymentItem.Status)
        && ((IDeploymentItem)sender).Status.MessageSeverity == SeverityLevel.Success)
    {
        //deployment is done
    }
};

// run my copying code
  1. You can implement your own provider that wraps a different provider
(I wont go into too much detail about that one, unless you're interested)
  1. Use the CLI

For more longterm automation integrated with CI, consider this option. Let me know if you want more details

Now, for your first use-case, that seems like something we should be supporting by default (generating SO from inventory items or vice-versa), and something we’ve had in consideration for other services (analytics and CloudCode).

For your second one, I have a better recommendation… Keep your code in the editor and use the csproj to reference that assembly.

<ItemGroup>
    <!-- relative path here depends on where your csproj is -->
    <Compile Include="../Assets/DTOs/**/*.cs" />
</ItemGroup>

Now, I know that doesnt remove the need for callbacks, so if you have any ideas on what they’d be implemented on, I’m all ears, we’ll take it into account.

As a simple extension of the Deployment API?
As a binding you put on a deployment definition? (we thought of keeping callbacks scoped to a deployment definition)

Thanks, and sorry for the delay, I need to update my forum watching setup :slight_smile:

Cheers!

Gab

Hi Gab! Thanks so much for the detailed answer!

It seems that having that assembly will already make my life 10x easier… i will probably just create my own button, i would love to just use the deployment window - but even the item status listener would still need me to do stuff.

Regarding your suggestions for my usecase!

I Considered that, but at the end of the day i would love my module project to be separate so that backend people can work completely separated - and even to some degree cross project, but by either listening to the item change/having custom automation, i can simply trigger the asset refresh :slight_smile:

That said, please note that the Docs and Tutorials on Cloud Code Modules do guide you through using DLLs and split projects.

i dont quite understand if you are telling me its something that should already be there but isn’t, or is that i’m failing to use it :stuck_out_tongue:

I know you can quite simple go Create → Economy File → Inventory, but that file is not extensible in any way, so i have no choice but to keep a Custom Item SO + Inventory File pair.

Lastly, regarding the deployment

Now, if i could somehow extend a deployment definition class, or simple have a few events - that would already do the trick, a simple “BeforeFileDeploy” “AfterFileDeploy” would already give me the window i need to refresh/create assets and react to any project changes

In theory i can - indeed - do that with the status change you mentioned, but its on a item level and not scope level, it just feels that having a IDeploymentDefinition would fit right in.:sweat_smile: i would not need to create any custom editor code or anything - other than extending a class and writing the logic.

Create → Economy File → Inventory, but that file is not extensible in any way, so i have no choice but to keep a Custom Item SO + Inventory File pair.

We should include something like this by default, we’ll take the feedback into consideration !

Now, if i could somehow extend a deployment definition class, or simple have a few events - that would already do the trick, a simple “BeforeFileDeploy” “AfterFileDeploy” would already give me the window i need to refresh/create assets and react to any project changes

Thanks for the feedback :slight_smile: We’ll be keeping this in mind when we look at a new version of the deployment api.

1 Like

Hey!

New version of deployment API (1.1, requires deployment 1.4) supports events as a first class citizen.
https://docs.unity3d.com/Packages/com.unity.services.deployment.api@1.1/api/Unity.Services.DeploymentApi.Editor.IDeploymentWindow.html#events

I recommend you take a look at the API and give us feedback in our release thread:

Cheers

Hey!

Thanks for the update - i’m finally back at this!!

It seems to be exactly what i needed - although the docs are quite abstract. Im struggling a little to use it - it requires the Window to be open, and it is LOL so im figuring it out!

The new generate bindings shortcut was supposed to solve this, but its lacking so much that i have to go back and use these events!

The API has a function to open the window that works even if the window is not open.

We’d need a tiny bit more work decoupling to make it work, but most of the feedback is streamed through the UI, and it controls initialization of the underlying service clients. My team prioritizes a feedback-based iterative approach, your feedback tells us we’re moving in the right direction, so please keep it coming as it’ll help orient our work. Thanks!

Hey! yeah - i manage to make it work by kind of openning the window and then hiding it away LOL

its not done yet as i have to do error handling and some automation on the DLL…but it did the trick :slight_smile:

Code
using Backend.Config.Cache;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Unity.Services.Core.Editor.Environments;
using Unity.Services.DeploymentApi.Editor;
using UnityEditor;
using UnityEngine;

namespace Backend.Config
{
    public class DeployConfigTab : ConfigTab<DeployConfig>
    {
        protected override void Setup()
        {
            base.Setup();
            pathListProp = configProp.FindPropertyRelative(nameof(DeployConfig.DllPaths));
            targetFolderProp = configProp.FindPropertyRelative(nameof(DeployConfig.TargetFolder));
        }

        private SerializedProperty pathListProp;
        private SerializedProperty targetFolderProp;

        public override void Draw()
        {
            if (GUILayout.Button("Upload CloudCode"))
            {
                UploadBackend();
            }

            if(GUILayout.Button("Import DLL"))
            {
                CopyDllFiles();
            }
            EditorGUILayout.Space(5);
            EditorGUILayout.PropertyField(targetFolderProp);
            EditorGUILayout.PropertyField(pathListProp);
        }

        private void CopyDllFiles()
        {
            window.ToggleProcessing(true);
            config.CopyDllFiles();
            window.ToggleProcessing(false);
        }

        private async void UploadBackend()
        {
            window.ToggleProcessing(true);
            await config.UploadBackend();
            window.ToggleProcessing(false);
        }
    }

    public class DeployConfig : EditorConfig
    {
        public List<string> DllPaths = new List<string>();
        public string TargetFolder;

        public async Task UploadBackend()
        {
            Debug.Log("Starting deploying proccess");
            await EnvironmentsApi.Instance.RefreshAsync();
            //EnvironmentsApi.Instance.SetActiveEnvironment("development");
            EditorWindow window = Deployments.Instance.DeploymentWindow.OpenWindow();

            if (Deployments.Instance.DeploymentWindow == null)
            {
                while (Deployments.Instance.DeploymentWindow == null)
                {
                    await Task.Yield();
                }
            }

            var deployment = Deployments.Instance.DeploymentWindow;

            var oldPos = window.position;
            window.position = new Rect(10000, 10000, 10, 10);

            Deployments.Instance.DeploymentWindow.DeploymentEnded -= OnDeploymentCompleted;
            Deployments.Instance.DeploymentWindow.DeploymentEnded += OnDeploymentCompleted;

            var items = deployment.GetAllDeploymentItems();
            await deployment.Deploy(items);

            window.position = oldPos;
            window.Close();
            Debug.Log("Deploying proccess completed");
        }

        internal void CopyDllFiles()
        {
            foreach (var path in DllPaths)
            {
                try
                {
                    if (File.Exists(TargetFolder))
                    {
                        File.Delete(TargetFolder);
                    }

                    if (File.Exists(path))
                    {
                        FileUtil.CopyFileOrDirectory(path, TargetFolder);
                        Debug.Log($"Copied Dll from {path}");
                    }
                }
                catch (Exception e)
                {
                    Debug.LogException(e);
                }
            }
            AssetDatabase.Refresh();
        }

        private void OnDeploymentCompleted(IReadOnlyList<IDeploymentItem> list)
        {
            CopyDllFiles();
        }
    }
}

Also, feedbacks and problems found:

  • you NEED to refresh the envs
  • you need to do this openWindow check - because even if its open, it might not detect or might be somehow a different reference…so i needed all this null checks

Thanks for the feedback. For the first one, the DW will init environments, but there’s no choice but to refresh environments, because we need the ID internally, but we only have the name.
Saving the ID is not an option as you can delete/create and environment with the same name.

For the latter, it seems like its good reasons to do a bit of work to decouple the rest of the API and allow you to use deployment functionality while the window is closed.

Will keep this in mind

first point is not really as much of a problem as the lack of clarity on it! its not clear anywhere that i could find that you NEED to do it

Good point, I guess my tests didnt hit that. I think normally the DW will initialize environments so you shouldnt need it, but there may be an edge case im missing. Either way we can by-pass this by allowing using the API without opening the window. Doable, it just requires a bit more of de-coupling on our side. Thanks!