DEV Community

Hagicode
Hagicode

Posted on • Originally published at docs.hagicode.com

How to Implement Automated Steam Publishing with GitHub Actions

How to Implement Automated Steam Publishing with GitHub Actions

This article shares a complete solution for implementing automated Steam publishing in the HagiCode Desktop project, covering the full automation pipeline from GitHub Release to the Steam platform, including key technical details such as Steam Guard authentication and multi-platform Depot uploads.

Background

The Steam platform's publishing process is actually quite different from traditional application distribution methods. Steam has its own complete update distribution system. Developers need to upload build artifacts to Steam's CDN network using the SteamCMD tool, rather than just throwing out a download link like other platforms.

The HagiCode Desktop project plans to launch on the Steam platform, which has brought some new challenges to our publishing process:

  1. Need to convert existing build artifacts into Steam-compatible format
  2. Must upload to Steam platform via SteamCMD tool
  3. Must handle Steam Guard authentication
  4. Need to support multi-platform (Linux, Windows, macOS) Depot uploads
  5. Need to implement automated flow from GitHub Release to Steam

The project had previously implemented a "portable version mode" that allows the application to detect fixed service payloads packaged in the extra directory. Our goal is to seamlessly integrate this portable version mode with Steam distribution.

About HagiCode

The solution shared in this article comes from our practical experience in the HagiCode project. HagiCode is an AI code assistant project that supports desktop execution. We are working on launching on the Steam platform, which is why we needed to establish a reliable automated publishing process.

Architecture Design

The core of the entire Steam publishing process is a GitHub Actions workflow that divides the process into three main stages:

┌─────────────────────────────────────────────────────────────┐
│ GitHub Actions Workflow (Steam Release)                      │
├─────────────────────────────────────────────────────────────┤
│ 1. Preparation Phase:                                        │
│    - Checkout portable-version code                         │
│    - Download build artifacts from GitHub Release           │
│    - Extract and prepare Steam content directory            │
│                                                             │
│ 2. SteamCMD Setup:                                          │
│    - Install/reuse SteamCMD                                 │
│    - Authenticate using Steam Guard                         │
│                                                             │
│ 3. Publishing Phase:                                        │
│    - Generate Depot VDF configuration files                 │
│    - Generate App Build VDF configuration files             │
│    - Call SteamCMD to upload to Steam                       │
└─────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The advantages of this design are:

  • Reuses existing GitHub Release artifacts, avoiding duplicate builds
  • Achieves security isolation through self-hosted runners
  • Supports preview mode and formal release branch switching
  • Complete error handling and logging

Workflow Implementation

Trigger Parameter Design

Our workflow supports the following key parameters:

inputs:
  release:           # Portable Version release tag
    description: 'Version tag to publish (e.g., v1.0.0)'
    required: true
  steam_preview:     # Whether to generate preview build
    description: 'Whether to enable preview mode'
    required: false
    default: 'false'
  steam_branch:      # Steam branch to set to live
    description: 'Target Steam branch'
    required: false
    default: 'preview'
  steam_description: # Build description override
    description: 'Build description'
    required: false
Enter fullscreen mode Exit fullscreen mode

Self-Hosted Runner Configuration

For security reasons, we use a self-hosted runner with the steam label:

runs-on:
  - self-hosted
  - Linux
  - X64
  - steam
Enter fullscreen mode Exit fullscreen mode

This ensures that Steam publishing is executed on a dedicated runner, maintaining secure isolation of sensitive credentials.

Concurrency Control

To prevent releases of the same version from interfering with each other, we configured concurrency control:

concurrency:
  group: portable-version-steam-${{ github.event.inputs.release }}
  cancel-in-progress: false
Enter fullscreen mode Exit fullscreen mode

Note that cancel-in-progress: false is set here because the Steam publishing process can be lengthy, and we don't want to cancel an ongoing release due to a new trigger.

Core Script Implementation

Preparing Release Input

The prepare-steam-release-input.mjs script is responsible for preparing the input needed for publishing:

// Download build manifest and artifact inventory from GitHub Release
const buildManifest = await downloadBuildManifest(releaseTag);
const artifactInventory = await downloadArtifactInventory(releaseTag);

// Download compressed packages for each platform
for (const platform of ['linux-x64', 'win-x64', 'osx-universal']) {
  const artifactUrl = getArtifactUrl(artifactInventory, platform);
  await downloadArtifact(artifactUrl, platform);
}

// Extract to Steam content directory structure
await extractToSteamContent(sources, contentRoot);
Enter fullscreen mode Exit fullscreen mode

Steam Guard Authentication

Steam requires using Steam Guard to protect accounts. We implemented a code generation algorithm based on shared secrets:

function generateSteamGuardCode(sharedSecret, timestamp = Date.now()) {
  const secret = decodeSharedSecret(sharedSecret);
  const time = Math.floor(timestamp / 1000 / 30);

  const timeBuffer = Buffer.alloc(8);
  timeBuffer.writeBigUInt64BE(BigInt(time));

  // Use HMAC-SHA1 to generate time-based one-time code
  const hash = crypto.createHmac('sha1', secret)
    .update(timeBuffer)
    .digest();

  // Convert to 5-character Steam Guard code
  const code = steamGuardCode(hash);
  return code;
}
Enter fullscreen mode Exit fullscreen mode

This implementation is based on Steam Guard's TOTP (Time-based One-Time Password) mechanism, generating a new verification code every 30 seconds.

VDF Configuration Generation

VDF (Valve Data Format) is the configuration format used by Steam. We need to generate two types of VDF files:

Depot VDF is used to configure content for each platform:

function buildDepotVdf(depotId, contentRoot) {
  return [
    '"DepotBuildConfig"',
    '{',
    `  "DepotID" "${escapeVdf(depotId)}"`,
    `  "ContentRoot" "${escapeVdf(contentRoot)}"`,
    '  "FileMapping"',
    '  {',
    '    "LocalPath" "*"',
    '    "DepotPath" "."',
    '    "recursive" "1"',
    '  }',
    '}'
  ].join('\n');
}
Enter fullscreen mode Exit fullscreen mode

App Build VDF is used to configure the entire application build:

function buildAppBuildVdf(appId, depotBuilds, description, setLive) {
  const vdf = [
    '"appbuild"',
    '{',
    `  "appid" "${appId}"`,
    `  "desc" "${escapeVdf(description)}"`,
    `  "contentroot" "${escapeVdf(contentRoot)}"`,
    '  "buildoutput" "build_output"',
    '  "depots"',
    '  {'
  ];

  for (const [depotId, depotVdfPath] of Object.entries(depotBuilds)) {
    vdf.push(`    "${depotId}" "${depotVdfPath}"`);
  }

  if (setLive) {
    vdf.push(`  }`);
    vdf.push(`  "setlive" "${setLive}"`);
  }

  vdf.push('}');
  return vdf.join('\n');
}
Enter fullscreen mode Exit fullscreen mode

SteamCMD Invocation

Finally, upload is performed by calling SteamCMD:

await runCommand(steamcmdPath, [
  '+login', steamUsername, steamPassword, steamGuardCode,
  '+run_app_build', appBuildPath,
  '+quit'
]);
Enter fullscreen mode Exit fullscreen mode

This step is the final leap of the entire process.

Multi-Platform Depot Handling

Steam uses the Depot system to manage content for different platforms. We support three main Depots:

Platform Depot Identifier Architecture Support
Linux linux-x64 x64_64
Windows win-x64 x64_64
macOS osx-universal universal, x64_64, arm64

Each Depot has an independent content directory and VDF configuration file, ensuring that users on different platforms only download the content they need.

Publishing Process

Step 1: Prepare GitHub Release

First, you need to create a GitHub Release in the portable-version repository, including:

  • Compressed packages for each platform
  • Build manifest ({tag}.build-manifest.json)
  • Artifact inventory ({tag}.artifact-inventory.json)

Step 2: Trigger Steam Publishing Workflow

Manually trigger the workflow through GitHub Actions and fill in the necessary parameters:

  • release: Version tag to publish (e.g., v1.0.0)
  • steam_branch: Target branch (e.g., preview or public)
  • steam_preview: Whether to enable preview mode

Step 3: Automatic Publishing Process

The workflow will automatically execute the following steps:

  1. Download and extract GitHub Release artifacts
  2. Install/update SteamCMD
  3. Generate Steam VDF configuration files
  4. Authenticate using Steam Guard
  5. Upload content to Steam CDN
  6. Set specified branch to live

Configuration Guide

Required Secrets Configuration

Configure the following secrets in GitHub repository settings:

Secret Name Description
STEAM_USERNAME Steam account username
STEAM_PASSWORD Steam account password
STEAM_SHARED_SECRET Steam Guard shared secret (optional)
STEAM_GUARD_CODE Steam Guard code (optional)
STEAM_APP_ID Steam application ID
STEAM_DEPOT_ID_LINUX Linux Depot ID
STEAM_DEPOT_ID_WINDOWS Windows Depot ID
STEAM_DEPOT_ID_MACOS macOS Depot ID

Environment Variable Configuration

Variable Name Description Default Value
PORTABLE_VERSION_STEAMCMD_ROOT SteamCMD installation directory ~/.local/share/portable-version/steamcmd

Best Practices

Steam Guard Authentication Management

First-time run requires manually entering the Steam Guard code. After that, it's recommended to configure a shared secret for automatic code generation. This avoids the need for manual intervention with each publish.

SteamCMD will save the login token for subsequent reuse. However, note the token's validity period - it will need re-authentication after expiration.

Content Directory Structure

Ensure the Steam content directory structure is correct:

steam-content/
├── linux-x64/     # Linux platform content
├── win-x64/       # Windows platform content
└── osx-universal/ # macOS universal binary content
Enter fullscreen mode Exit fullscreen mode

Each directory should contain the complete application files for the corresponding platform.

Using Preview Mode

Preview mode does not set any branch to live, making it suitable for testing and verification:

if [ "$STEAM_PREVIEW_INPUT" = 'true' ]; then
  cmd+=(--preview)
fi
Enter fullscreen mode Exit fullscreen mode

This allows uploading to the Steam platform for verification first, then switching to the formal branch after confirmation.

Error Handling and Logging

The script includes comprehensive error handling and logging:

  • Verify GitHub Release existence
  • Check required metadata files
  • Ensure platform content exists
  • Generate GitHub Actions summary reports

This information is very valuable for debugging and auditing.

Artifact Management

The workflow generates two types of artifacts:

  • portable-steam-release-preparation-{tag}: Publishing preparation metadata
  • portable-steam-build-metadata-{tag}: Steam build metadata

These artifacts can be used for subsequent auditing and debugging. It's recommended to set the retention time to 30 days.

Practical Application

In the HagiCode project, this automated publishing process has successfully run for multiple versions. The entire pipeline from GitHub Release to Steam platform is fully automated without manual intervention.

This has significantly improved our publishing efficiency and reliability. Previously, manually publishing a version took over 30 minutes, but now the entire process can be completed in just a few minutes.

More importantly, the automated process reduces the possibility of human error. Each publish follows a standardized process with more predictable results.

Summary

Through the solution shared in this article, we have achieved:

  1. Full automation from GitHub Release to Steam platform
  2. Support for multi-platform Depot uploads
  3. Security authentication based on Steam Guard
  4. Flexible switching between preview mode and formal publishing
  5. Comprehensive error handling and logging

This solution is not only applicable to the HagiCode project but can also provide reference for other projects planning to launch on the Steam platform. If you're also considering Steam automated publishing, I hope the practices shared in this article can be helpful to you.

If this article helps you, feel free to give a Star on HagiCode's GitHub repository or visit the official website for more information.

References

Original Article & License

Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.
This article was created with AI assistance and reviewed by the author before publication.

Top comments (0)