First, A Disclaimer…
What I describe below will only work with a public repository. I really wanted it to work with a private repository, but doing so required more pain than I was willing to endure. And by pain, I mean research. If you’re a GitHub guru who knows better, please, enlighten me. I will shower you with glitter and unbridled appreciation. In the meantime, I had to suck it up and use a non-ideal workaround, also described below.
A Little Background
Unfashionable though it may be, I still really enjoy developing desktop applications. Whereas web development necessarily involves a myriad of interrelated technologies, desktop applications are generally more self contained and isolated to a single framework. I guess I find that simplicity attractive. Deploying a desktop app, on the other hand, is as much of a headache as it ever was.
Just recently I got the chance to create a moderately simple WinForms application for a client at work. I was having a grand ol' time, draggin' and droppin' controls into the designer, handlin' all SORTS of events, callin'
whizBangForm.ShowDialog(this) like a boss; but then - deployment. In retrospect, I don’t think we gave adequate consideration to the thorny issue of how to get the thing to the client. The problem we needed to solve was conceptually so simple that I literally think we subconsciously assumed the resulting app would be too trivial to necessitate updates. Hahahahaha. I feel silly just writing that out loud! The first rule of Software Development is: Software always requires updates. The second rule of Software Development is: Software ALWAYS requires updates.* I found myself trying to write perfect software so it would only require a single delivery, an impossibility on two counts. You’d think I was new at this.
When I was in college, a professor of mine told me “Works of writing are never finished; they are just finally abandoned.” And eventually, that’s exactly where I found myself. I deemed my WhizBang WinForms app “pefect enough”, zipped up the executable along with its dependencies, and instant messaged the resulting file to the client. (<- Feeling silly again here.) I’d say maybe 10 minutes passed before I realized there was a bug. It was something fairly minor and the client didn’t even notice. But it got me thinking: What’s the plan here? Am I REALLY going to instant message a zip file every time an update is required? How ridiculous is that? If only there was some sort of technology that would allow me to push changes to a server that would automatically notify the client’s application…
Get To The Point, Already. Sheesh!
I’ve actually used ClickOnce quite a bit over the past several years with positive results. Most of those cases were for internal applications where I could use a private server to host the installer. I’ve also used Azure blob storage to host an installer for an Excel Addin I wrote for my Dad. For this particular client, neither of those options were available. However, they do use GitHub for source control which made me wonder if it’s possible to use GitHub as a host for a ClickOnce installer. Indeed, it is, and this post is intended to fill in some of the details omitted from the StackOverflow answer linked above.
1. Adjust your
.gitignore to allow ClickOnce outputs
Assuming you used the Github Visual Studio ignore template, you’ll need to change it to not ignore ClickOnce outputs. This may be as simple as deleting “
publish/” from the file. However, if you have a global ignore file which excludes the ClickOnce publish directory (as I did), you’ll have to explicitly allow the publish directory for the specific repo you want to deploy by adding this line:
!publish/. (Just add the bang to the beginning of the existing line.)
If you want to use a non-default publish directory, you’ll need to adjust your ignore file to allow its inclusion in the same manner I just described.
2. Adjust your
.gitattributes file to treat ClickOnce files as non-text
This step totally tripped me up. If you’re a git aficionado, this will seem like a silly thing to get stuck on, but personally I’m at the tail end of the noob scale when it comes to the nuances of git. It seemed that no matter what I did, git was screwing with the line endings of the ClickOnce deployment files, which apparently causes some hash mismatch and, consequently, a failed installation. (Grrrr.) To prevent this from happening, add this to your attributes file:
*.manifest binary *.application binary *.deploy binary
As indicated here, this stops git from tweaking the line endings for those file types.
3. Add Publish And Update URLs To Your Project
Next, add the raw GitHub URL for the ClickOnce publish directory in your project. To get the raw URL, go to your repo on github.com, navigate to your
publish directory, select the first file within that directory, then click the “Raw” button. If you’re unaware, this is just the plain text view of the file. But you don’t want the path to the file; you want the path to the parent directory, so copy the URL from your browser omitting the file name segment. You’ll end up with a URL that follows this convention, assuming your solution follows Visual Studio’s default paths:
Paste that path into the “Installation Folder URL” field on the Publish tab of the Properties window for your project. Then click the “Updates…” button, enable updates if you haven’t already, and paste the URL in the “Update Location” field. I am not 100% certain that last step is truly necessary since it’s the same path as the install folder, but it won’t hurt. After that, you should be good to go. Publish your app, commit your changes, and push to the server. To install the application, navigate to the publish directory on GitHub, download the setup.exe, and run it.
Postscript: A Workaround For Private Repositories
The one hitch in my get-along is that raw URLs in private repositories have a user token appended to them, so they end up looking like this:
So when ClickOnce goes a-lookin' for files in your
/publish directory - the first target being
/publish/YourApp.application - guess what! That’s right! 404 - Not Found. Because, of course, there’s no way to tell ClickOnce to append
?token=AHOYragb489Y to all URLs it creates. At least, no way that I could find. (Again - if you know a way, tell me. Glitter + Appreciation. Serious.) In all fairness, hardcoding a user token parameter in an installation path is most definitely a bad idea. What can I say; I was getting desperate.
Anyhoo, I digress. Point is, despite my gallant effort, I failed to get ClickOnce to work in a private GitHub repo.