Skip to content

How to Automate Steam Releases with GitHub Actions

火山引擎 Coding Plan
火山引擎提供 Claude API 兼容服务,稳定可靠。订阅折上9折,低至8.9元,订阅越多越划算!
立即订阅
智谱 GLM Coding: 20+ 大编程工具无缝支持 推荐
Claude Code、Cline 等 20+ 大编程工具无缝支持,"码力"全开,越拼越爽!
立即开拼
MiniMax Claude API 兼容服务
MiniMax 提供 Claude API 兼容服务,支持多种模型接入,稳定可靠。
了解更多
阿里云千问 Coding Plan 上线
阿里云千问 Coding Plan 已上线,满足开发日常需求。推荐 + Hagicode,完美实现开发过程中的各项需求。
立即订阅

How to Automate Steam Releases with GitHub Actions

Section titled “How to Automate Steam Releases with GitHub Actions”

This article shares the complete solution we implemented for automated Steam releases in the HagiCode Desktop project, covering the end-to-end automation flow from GitHub Release to the Steam platform, including key technical details such as Steam Guard authentication and multi-platform Depot uploads.

The release workflow on Steam is actually quite different from traditional application distribution. Steam has its own complete update delivery system. Developers need to use SteamCMD to upload build artifacts to Steam’s CDN network, rather than simply dropping in a download link like on other platforms.

The HagiCode Desktop project is preparing for a Steam release, which introduced a few new challenges to our release workflow:

  1. We needed to convert existing build artifacts into a Steam-compatible format.
  2. We had to upload them to Steam through SteamCMD.
  3. We also had to handle Steam Guard authentication.
  4. We needed to support multi-platform Depot uploads for Linux, Windows, and macOS.
  5. We wanted a fully automated flow from GitHub Release to Steam.

The project had already implemented “portable version mode,” which allows the application to detect fixed service payloads packaged in the extra directory. Our goal was to integrate that portable version mode seamlessly with Steam distribution.

The solution shared in this article comes from our practical experience in the HagiCode project. HagiCode is an AI coding assistant that supports desktop usage. Since we are actively working toward launching on Steam, we needed to establish a reliable automated release workflow.

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

┌─────────────────────────────────────────────────────────────┐
│ GitHub Actions Workflow (Steam Release) │
├─────────────────────────────────────────────────────────────┤
│ 1. Preparation Stage: │
│ - Check out portable-version code │
│ - Download build artifacts from GitHub Release │
│ - Extract and prepare the Steam content directory │
│ │
│ 2. SteamCMD Setup: │
│ - Install or reuse SteamCMD │
│ - Authenticate with Steam Guard │
│ │
│ 3. Release Stage: │
│ - Generate Depot VDF configuration files │
│ - Generate App Build VDF configuration files │
│ - Invoke SteamCMD to upload to Steam │
└─────────────────────────────────────────────────────────────┘

This design offers several advantages:

  • It reuses existing GitHub Release artifacts and avoids rebuilding the same outputs.
  • It uses self-hosted runners for secure isolation.
  • It supports switching between preview mode and formal release branches.
  • It includes complete error handling and logging, which makes failures easier to diagnose.

Our workflow supports the following key parameters:

inputs:
release: # Portable Version release tag
description: 'Version tag to release (for example v1.0.0)'
required: true
steam_preview: # Whether to generate a preview build
description: 'Whether to use preview mode'
required: false
default: 'false'
steam_branch: # Steam branch to set live
description: 'Target Steam branch'
required: false
default: 'preview'
steam_description: # Build description override
description: 'Build description'
required: false

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

runs-on:
- self-hosted
- Linux
- X64
- steam

This ensures that Steam releases run on a dedicated runner and keeps sensitive credentials safely isolated.

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

Notice that cancel-in-progress: false is set here because Steam releases can take a while, and we do not want a newly triggered run to cancel one that is already uploading.

The prepare-steam-release-input.mjs script is responsible for preparing the inputs required for release:

// Download the GitHub Release build manifest and artifact inventory
const buildManifest = await downloadBuildManifest(releaseTag);
const artifactInventory = await downloadArtifactInventory(releaseTag);
// Download compressed archives for each platform
for (const platform of ['linux-x64', 'win-x64', 'osx-universal']) {
const artifactUrl = getArtifactUrl(artifactInventory, platform);
await downloadArtifact(artifactUrl, platform);
}
// Extract into the Steam content directory structure
await extractToSteamContent(sources, contentRoot);

Steam requires accounts to be protected with Steam Guard, so we implemented a shared-secret-based code generation algorithm:

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 a time-based one-time code
const hash = crypto.createHmac('sha1', secret)
.update(timeBuffer)
.digest();
// Convert it into a 5-character Steam Guard code
const code = steamGuardCode(hash);
return code;
}

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

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');
}

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');
}

Finally, the upload is performed by invoking SteamCMD:

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

This is the final jump in the whole workflow. Once it succeeds, the release is done.

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

PlatformDepot IdentifierArchitecture Support
Linuxlinux-x64x64_64
Windowswin-x64x64_64
macOSosx-universaluniversal, x64_64, arm64

Each Depot has its own content directory and VDF configuration file. This ensures that users on different platforms only download the content they actually need.

First, create a GitHub Release in the portable-version repository that includes:

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

Step 2: Trigger the Steam Release Workflow

Section titled “Step 2: Trigger the Steam Release Workflow”

Trigger the workflow manually through GitHub Actions and fill in the required parameters:

  • release: the version tag to publish, such as v1.0.0
  • steam_branch: the target branch, such as preview or public
  • steam_preview: whether to use preview mode

Step 3: Execute the Release Flow Automatically

Section titled “Step 3: Execute the Release Flow Automatically”

The workflow automatically performs the following steps:

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

Once this sequence completes, the whole release path is covered.

Configure the following secrets in the GitHub repository settings:

Secret NameDescription
STEAM_USERNAMESteam account username
STEAM_PASSWORDSteam account password
STEAM_SHARED_SECRETSteam Guard shared secret (optional)
STEAM_GUARD_CODESteam Guard code (optional)
STEAM_APP_IDSteam application ID
STEAM_DEPOT_ID_LINUXLinux Depot ID
STEAM_DEPOT_ID_WINDOWSWindows Depot ID
STEAM_DEPOT_ID_MACOSmacOS Depot ID

There is nothing especially unusual about these settings. You simply need all of the expected values in place.

Variable NameDescriptionDefault Value
PORTABLE_VERSION_STEAMCMD_ROOTSteamCMD installation directory~/.local/share/portable-version/steamcmd

On the first run, you need to enter the Steam Guard code manually. After that, it is recommended to configure the shared secret so the code can be generated automatically. This avoids manual intervention on every release.

SteamCMD saves the login token and can reuse it on later runs. You still need to keep an eye on token expiration, because re-authentication is required after the token expires.

Make sure 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

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

Preview mode does not set any branch live, so it is suitable for testing and validation:

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

This lets you upload to Steam first for verification, then switch to the formal branch after everything checks out.

The scripts include complete error handling and logging:

  • Validate that the GitHub Release exists.
  • Check required metadata files.
  • Ensure platform content is present.
  • Generate a GitHub Actions summary report.

This information is highly valuable for both debugging and auditing.

The workflow generates two kinds of artifacts:

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

These artifacts can be used for later auditing and debugging. A retention period of 30 days is a practical default.

In the HagiCode project, this automated release workflow has already run successfully across multiple versions. The entire path from GitHub Release to the Steam platform is fully automated and requires no manual intervention.

This significantly improved both release efficiency and reliability. In the past, manually publishing one version took more than 30 minutes. Now the entire process finishes in just a few minutes.

More importantly, the automated workflow reduces the chance of human error. Every release follows the same standardized process, which makes the results more predictable.

With the approach described in this article, we achieved:

  1. Full automation from GitHub Release to the Steam platform.
  2. Multi-platform Depot uploads.
  3. Secure authentication based on Steam Guard.
  4. Flexible switching between preview mode and formal release.
  5. Complete error handling and logging.

This solution is not only suitable for the HagiCode project, but can also serve as a reference for other projects planning to launch on Steam. If you are also considering Steam release automation, I hope this practical experience is useful to you.

Technology can feel complex or simple depending on the path you choose. The key is finding a workflow that fits your needs.

If this article helped you, feel free to star the HagiCode GitHub repository or visit the official website to learn more.

Thank you for reading. If you found this article useful, you are welcome to like, bookmark, and share it. This content was created with AI-assisted collaboration, and the final content was reviewed and confirmed by the author.