Cowboy in the desert.

Publishing a package to a local Octopus instance with GitHub Actions

Shawn Sesna

Earlier this year, my colleague Ryan Rousseau wrote a blog post about publishing a package to Octopus Deploy using GitHub Actions. In this post, I'll take that a step further by publishing a package to a local instance of Octopus Deploy with a self-hosted GitHub Actions Runner.

GitHub Actions

GitHub Actions is GitHub's version of a build server. Like many other build tools, such as BitBucket PipeLines and Azure DevOps, GitHub Actions uses Yet Another Markup Language (YAML) to define the build process called a workflow. Below is an example GitHub Actions workflow YAML file.

This example builds the OctoPetShop Sample .NET core application, then pushes the packages to our Octopus Deploy Samples instance:

name: .NET Core 

    branches: [ master ] 
    branches: [ master ]


    runs-on: ubuntu-latest 

    - uses: actions/checkout@v2
    - name: Set Version
      run: echo "::set-env name=PACKAGE_VERSION::$(date +'%Y.%m.%d').$GITHUB_RUN_NUMBER"
    - name: Setup .NET Core
      uses: actions/setup-dotnet@v1
        dotnet-version: 2.2.207
    - name: Install dependencies
      run: dotnet restore
    - name: Build
      run: dotnet build --configuration Release --no-restore
    - name: Test
      run: dotnet test --no-restore --verbosity normal
    - name: Create artifacts folder
      run: |
        mkdir "$GITHUB_WORKSPACE/artifacts"
        mkdir "$GITHUB_WORKSPACE/artifacts/OctopusSamples.OctoPetShop.Database"
        mkdir "$GITHUB_WORKSPACE/artifacts/OctopusSamples.OctoPetShop.Web"
        mkdir "$GITHUB_WORKSPACE/artifacts/OctopusSamples.OctoPetShop.ProductService"
        mkdir "$GITHUB_WORKSPACE/artifacts/OctopusSamples.OctoPetShop.ShoppingCartService"
    - name: Publish OctoPetShopDatabase
      run: dotnet publish OctopusSamples.OctoPetShop.Database/OctopusSamples.OctoPetShop.Database.csproj --configuration Release --no-restore --output "$GITHUB_WORKSPACE/artifacts/OctopusSamples.OctoPetShop.Database"
    - name: Publish OctoPetShopWeb
      run: dotnet publish OctopusSamples.OctoPetShop.Web/OctopusSamples.OctoPetShop.Web.csproj --configuration Release --no-restore --output "$GITHUB_WORKSPACE/artifacts/OctopusSamples.OctoPetShop.Web"
    - name: Publish OctoPetShopProductService
      run: dotnet publish OctopusSamples.OctoPetShop.ProductService/OctopusSamples.OctoPetShop.ProductService.csproj --configuration Release --no-restore --output "$GITHUB_WORKSPACE/artifacts/OctopusSamples.OctoPetShop.ProductService"
    - name: Publish OctoPetShopShoppingCartService
      run: dotnet publish OctopusSamples.OctoPetShop.ShoppingCartService/OctopusSamples.OctoPetShop.ShoppingCartService.csproj --configuration Release --no-restore --output "$GITHUB_WORKSPACE/artifacts/OctopusSamples.OctoPetshop.ShoppingCartService"
    - name: Install Octopus CLI
      uses: OctopusDeploy/install-octocli@v1
        version: 7.4.2
    - name: Package OctoPetShopDatabase
      run: |
        octo pack --id="OctoPetShop.Database" --format="Zip" --version="$PACKAGE_VERSION" --basePath="$GITHUB_WORKSPACE/artifacts/OctopusSamples.OctoPetShop.Database" --outFolder="$GITHUB_WORKSPACE/artifacts"
    - name: Package OctoPetShopWeb
      run: |
        octo pack --id="OctoPetShop.Web" --format="Zip" --version="$PACKAGE_VERSION" --basePath="$GITHUB_WORKSPACE/artifacts/OctopusSamples.OctoPetShop.Web" --outFolder="$GITHUB_WORKSPACE/artifacts"
    - name: Package OctoPetShopProductService
      run: |
        octo pack --id="OctoPetShop.ProductService" --format="Zip" --version="$PACKAGE_VERSION" --basePath="$GITHUB_WORKSPACE/artifacts/OctopusSamples.OctoPetShop.ProductService" --outFolder="$GITHUB_WORKSPACE/artifacts"
    - name: Package OctoPetShopShoppingCartService
      run: |
        octo pack --id="OctoPetShop.ShoppingCartService" --format="Zip" --version="$PACKAGE_VERSION" --basePath="$GITHUB_WORKSPACE/artifacts/OctopusSamples.OctoPetshop.ShoppingCartService" --outFolder="$GITHUB_WORKSPACE/artifacts"
    - name: Push OctoPetShop Database
      run: |
        octo push --package="$GITHUB_WORKSPACE/artifacts/OctoPetShop.Database.$" --server="${{ secrets.OCTOPUSSERVERURL }}" --apiKey="${{ secrets.OCTOPUSSERVERAPIKEY }}" --space="${{ secrets.OCTOPUSSERVERSPACE_HYBRID }}"
    - name: Push OctoPetShop Web
      run: |
        octo push --package="$GITHUB_WORKSPACE/artifacts/OctoPetShop.Web.$" --server="${{ secrets.OCTOPUSSERVERURL }}" --apiKey="${{ secrets.OCTOPUSSERVERAPIKEY }}" --space="${{ secrets.OCTOPUSSERVERSPACE_HYBRID }}"
    - name: Push OctoPetShop ProductService
      run: |
        octo push --package="$GITHUB_WORKSPACE/artifacts/OctoPetShop.ProductService.$" --server="${{ secrets.OCTOPUSSERVERURL }}" --apiKey="${{ secrets.OCTOPUSSERVERAPIKEY }}" --space="${{ secrets.OCTOPUSSERVERSPACE_HYBRID }}"
    - name: Push OctoPetShop ShoppingCartService
      run: |
        octo push --package="$GITHUB_WORKSPACE/artifacts/OctoPetShop.ShoppingCartService.$" --server="${{ secrets.OCTOPUSSERVERURL }}" --apiKey="${{ secrets.OCTOPUSSERVERAPIKEY }}" --space="${{ secrets.OCTOPUSSERVERSPACE_HYBRID }}"

With the GitHub-Hosted runners pushing to Octopus Cloud, this solution works great. However, without poking holes through the firewall, GitHub-Hosted runners cannot push packages to your self-hosted Octopus Deploy server.

Local build runners

GitHub-Hosted runners are pre-packaged with a lot of functionality, but there are times when you have specific software needs, need the ability to control the version that you are using, or need the runner to have access to on-premises resources such as Octopus Deploy. To solve this problem, the folks over at GitHub developed the ability for Actions to have locally hosted runners. The local runner works in a similar fashion to Octopus Polling Tentacles in that they reach out to GitHub Actions instead of GitHub Actions reaching in.

Set up the runner

Setting up a local runner is incredibly easy. Kudos to GitHub for making it so simple. After you've created your workflow YAML file, make your way over to the Settings in your GitHub repo:

From there, click on Actions:

Scroll down slightly to see the Add Runner button and click it:

The next screen gives you the option to choose the local runner architecture, and then provides the commands necessary to download and configure it:

When running the configuration commands, you'll be asked a couple of basic questions such as runner name, tags, and the work folder location. Pressing Enter at these prompts accepts the default values.

When you are done, you will have a local runner listening for jobs:

Configure the workflow to use a local runner

Configuring the workflow to use a local runner is a one-line change in the YAML. Taking the YAML from above, we change the line runs-on to the tags of our local instance. The current value of runs-on uses the single tag ubuntu-latest. However, when you need multiple tags, you have to place them inside an array, which is designated by the use of square brackets. For our new runner, we want it to use tags self-hosted and linux. To do this, we'll change:

runs-on: ubuntu-latest 


runs-on: [self-hosted, linux]

With our workflow configured to use a local runner, we now can push packages to our local instance of Octopus Deploy. By making the change to the YAML file, it kicked off a build, and I can see that my local runner has picked it up by clicking on Actions:

On my VM, I see the message that it's running the job:

√ Connected to GitHub

2020-06-09 23:47:30Z: Listening for Jobs
2020-06-10 00:27:20Z: Running job: build

Examining the log output, we can see that the version number built was 2020.06.10.6:

On my local Octopus Deploy instance, I find that this has been pushed:


This post demonstrates one method of using a cloud build server to push packages to a self-hosted Octopus Deploy instance by using local build runners or agents. While this focuses on GitHub Actions, the same idea can be extended to TeamCity, Jenkins, or Azure DevOps.