Build private mercurial subrepos on Appveyor

Appveyor is a great CI service for Windows apps. It's simple, free (for open-source) and easy to setup. Sometimes even public, open source projects may want to have private subrepositories. Appveyor supports such a setup and in this post I will show you, how to configure private subrepo for mercruial.

The Git way

There already is a good guide for private git subrepos. Let's try and do the same for mercurial. The git guide references GitHub as hosting platform, and for mercurial I will use BitBucket, wich has similar est of features but support both git and mercurial (and has unlimited number of free private repositories, yay!).

The Hg way

In case of mercurial, the solution is similar to git, but configuriaton may not be as straightforward.

We will split the process in three steps: 1. Configure ssh clone on local machine 2. Do the same in AppVeyor with an arbitrary repository 3. Configure private hg subrepo and check it out in AppVeyor

Cloning HG over SSH (from Bitbucket)

Let's start with a simple thing: clone a repository over ssh. I'll use BitBucket for mercurial hosting and Appveyor for cloning and building. BitBucket has a guide on setting up ssh: https://confluence.atlassian.com/bitbucket/set-up-ssh-for-mercurial-728138122.html. Unfortunatelly, the Windows guide uses Putty and Pageant for managing SSH keys, which requires a GUI and isn't commandline-friendly. We cannot use it from Appveyor scripts (plink can also be run in batch mode, but I will stick to plain ssh).

Lucky for me, a similar guide for git (https://confluence.atlassian.com/bitbucket/set-up-ssh-for-git-728138079.html) doesn't include putty at all. We can use the same steps to configure mercurials ssh.

  1. Install Git for Windows:

    > choco install -y git
    
  2. Make sure you have ssh.exe on PATH (it will most probably be in 'c:\Program Files\Git\usr\bin')

  3. List the content of $env:USERPROFILE/.ssh directory

    > ls $env:USERPROFILE/.ssh
    

If you have a default identity already, you'll some id_* files.

  1. Generate a ssh key (or use an existing one)

    > ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
    
  2. Set up SSH key on Bitbucket:

    1. Open a browser and log in to Bitbucket.
    2. Choose avatar > Bitbucket settings from the menu bar, then click SSH Settings on the left.
    3. Add a new key. This is a public key, which value is the content of $env:USERPROFILE/.ssh/id_rsa.pub (will probably start with "ssh-rsa ...")
  3. create a private HG repo and clone it over SSH:

    > hg clone ssh://hg@bitbucket.org/heavymetaldev/top-secret
    

    If you see remote: Permission denied (publickey)., then there is something wrong with SSH key, i.e.: 1. Mercurial doesn't use the private key from $env:USERPROFILE/.ssh/id_rsa 2. Public SSH key is not properly configured in BitBucket

    You can add --debug switch to see the commands that are invoked undearneath. You will see that mercurial calls:

    ssh hg@bitbucket.org "hg -R heavymetaldev/top-secret serve --stdio"

    You can use this command to further debug ssh issues.

Private HG subprepos on Appveyor

Knowing that SSH clone works locally, we can configure AppVeyor to do the same.

These are general steps we need to take: 1. Generate a new SSH key pair for AppVeyor access to Bitbucket repo 2. Save private key in AppVeyor's encrypted environment variable

In the build script (during install phase), we need to: 1. Extract private key from environment variable to file $env:USERPROFILE/.ssh/id_rsa 2. Add Bitbucket's SSL certificate fingerprint to $env:USERPROFILE/.ssh/known_hosts

First, generate a new SSH key that will be used by AppVeyor and add it to Bitbucket (like in the previous paragraph).

> ssh-keygen -t rsa -b 4096 -C "your_email@example.com" -f "id_rsa_appveyor_top-secret"

Instead of configuring it at account level, add it as a deployment key to specific repo that you will be cloning.

Now, we need to configure the SSH key in AppVeyor. The process is very similar to the git way.

Open the generated private key and copy base-64 body of the key between -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- into clipboard (without these BEGIN / END lines).

Copy the contents of private key to clipboard as shown above and open Encrypt data tool in AppVeyor. Encrypt the value of clipboard using that page.

Paste the encrypted value into the build script (or configure it in web UI). It will look something like this:

appveyor.yml:

environment:
  priv_key:
    secure: <encryped-value>
  subrepo_owner: heavymetaldev
  subrepo_name: top-secret
  subrepo_branch: default
install:
  - ps: .\clone-subrepo.ps1

The additional environment variables (subrepo_*) are used to determine repository url and branch name to checkout. clone-subrepo.ps1 is where the real job is done:

# get repo url and branch from env variables 
$owner = $env:subrepo_owner
$repoName = $env:subrepo_name
$repo = "$owner/$repoName"
$branch = $env:subrepo_branch

if ($branch -eq $null) {
    $branch = "default"
    write-host "will use default branch '$branch'"
} else {
    write-host "will use configured branch '$branch'"
}

write-host "testing if ssh is available"
get-command "ssh.exe" -ErrorAction Stop

# use ssh.exe available on PATH
'[ui]' | out-file  "$env:USERPROFILE/mercurial.ini" -Append -Encoding utf8
'ssh=ssh.exe' | out-file "$env:USERPROFILE/mercurial.ini" -Append -Encoding utf8

# add Bitbucket host fingerprint to known_hosts
$bbhostkey = @"
bitbucket.org,104.192.143.3 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAubiN81eDcafrgMeLzaFPsw2kNvEcqTKl/VqLat/MaB33pZy0y3rJZtnqwR2qOOvbwKZYKiEO1O6VqNEBxKvJJelCq0dTXWT5pbO2gDXC6h6QDXCaHo6pOHGPUy+YBaGQRGuSusMEASYiWunYN0vCAI8QaXnWMXNMdFP3jHAJH0eDsoiGnLPBlBp4TNm6rYI74nMzgz3B9IikW4WVK+dc8KZJZWYjAuORU3jc1c/NPskD2ASinf8v3xnfXeukU0sJ5N6m5E8VLjObPEO+mN2t/FZTMZLiFqPWc/ALSqnMnnhwrNi2rbfg/rd/IpL8Le3pSBne8+seeFVBoGqzHM9yXw==
"@

write-host "adding bitbucket to known_hosts"
$bbhostkey | out-file "$env:USERPROFILE/.ssh/known_hosts" -Append -Encoding utf8

# add private key to id_rsa
write-host "adding private key"
$fileContent = "-----BEGIN RSA PRIVATE KEY-----`n"
$fileContent += $env:priv_key.Replace(' ', "`n")
$fileContent += "`n-----END RSA PRIVATE KEY-----`n"
Set-Content "$env:USERPROFILE\.ssh\id_rsa" $fileContent

#clone private repo
write-host "cloning"
hg clone --verbose ssh://hg@bitbucket.org/$repo $repoName

#update private repo to specified branch, get status
try {
    pushd

    cd $repoName

    write-host "updating to $branch"
    hg update $branch 

    hg summary


    $message = hg log -r . -T "{desc}"
    $id = hg log -r . -T "{node}"
    $ts = hg log -r . -T "{date|isodate}"
    $ts = [DateTime]::Parse($ts)
    $authorname = hg log -r . -T "{author|person}"
    $authormail = hg log -r . -T "{author|email}"
    $br = hg log -r . -T "{branch}"

    write-host "id:$id branch:$br msg:$message date:$ts author:$authorname mail:$authormail"
} 
finally {
    popd
}

This is everything you need to get this working. Commit appveyor.yml and clone-subrepo.ps1 to a new, public repository and add it to appveyor.

Changing Appveyor build info

You may also want to include some information about the status of your subrepo in Appveyor's build message. Update-AppveyorBuild can update build details. Add the following code to clone-subrepo.ps1:

if ($env:appveyor -ne $null) {
    Update-AppveyorBuild -message "subrepo [$br](https://bitbucket.org/$repo/commits/$id): $message" -Committed $ts -CommitterName $authorname -CommitterEmail $authorEmail 
    #-CommitId $id
} 

A real subrepo

Until now, the inner repository was not a real hg subrepo - the script determined it's location and branch. Let's now make it a subrepo and tie the exact revision to parent repository revision.

Add .hgsub to your public repo (this will be the "parent"):

top-secret = top-secret

[subpaths]
https://bitbucket\.org/([^/]*)/([^/]*)/([^/]*)$ = ssh://hg@bitbucket.org/\1/\3
ssh://hg@bitbucket\.org/([^/]*)/([^/]*)/([^/]*)$ = ssh://hg@bitbucket.org/\1/\3

top-secret is the name of the private repository. The subpaths section is needed, because by default mercurial constructs subrepo url by adding it's name after slash, so we need to remap: https://bitbucket.org/heavymetaldev/appveyor-wrapper/top-secret to ssh://hg@bitbucket.org/heavymetaldev/top-secret. Appveyor clones repos over https, but private subrepo needs to be accessed over ssh.

After commiting, do a clean update:

> hg update -C

This will create top-secret directory and set it's default url to ssh://hg@bitbucket.com/heavymetaldev/top-secret. Go to top-secret folder, update the subrepo to desired revision and commit changes in the parent repo.

One last thing we need to do is to move id_rsa initalization directly to appveyor.yml, to init phase. The reason for this is the chicken-egg problem we now have: install phase takes place after repo clone and update, but mercurial (unlike git) updates all subrepos on parent repo update, so it needs the ssh credentials before doing the update. Fortunatelly, appveyor is clever enough to read appveyor.yml content before cloning, so it can execute init script without the repo being checked out.

appveyor.yml will now look like this (note that we don't need subrepo_* ariables any more):

environment:
  priv_key:
    secure: <encryped-value>
init: 
  - ps: $fileContent = "-----BEGIN RSA PRIVATE KEY-----`n"
  - ps: $fileContent += $env:priv_key.Replace(' ', "`n")
  - ps: $fileContent += "`n-----END RSA PRIVATE KEY-----`n"
  - ps: Set-Content c:\users\appveyor\.ssh\id_rsa $fileContent

Finally, commit changes and push the parent repo. Appveyor should now detect a new commit and start building. Hopefully, everything will be built smoothly.

Hapy hacking!

Notes and resources

  • You can find sample repo at: https://bitbucket.org/heavymetaldev/appveyor-wrapper
  • The build status at https://ci.appveyor.com/project/qbikez/appveyor-wrapper.
  • The private repo is at https://bitbucket.org/heavymetaldev/top-secret, but you won't find it there, because, well.. it's private :)