This repository owns Steam publication for Portable Version and DLC releases. It still provides the build-plan / handoff helpers used by external automation, but packaging plus Azure publication now execute directly in repos/steam_packer.
The actual packaging-safe workspace assembly, payload injection, toolchain staging, archive repacking, artifact inventory generation, checksum generation, Azure Blob upload, and root hagicode-steam/index.json refresh now execute in steam_packer.
This repository now keeps only the Steam publication workflows:
.github/workflows/portable-version-steam-release.yml(portable-version-steam-release) manually publishes an existing Azure-hosted Portable Version release to Steam..github/workflows/portable-version-steam-dlc-release.yml(portable-version-steam-dlc-release) manually discovers every DLC from the dedicated DLC Azure container and publishes the latest version of each DLC to Steam.
Packaging and Azure publication are no longer triggered from portable-version. If an external caller still wants the normalized selector and handoff contract, call scripts/resolve-build-plan.mjs directly and hand the resulting plan to repos/steam_packer.
The base Steam workflow is workflow_dispatch only and accepts an optional release tag. If you leave it blank, the workflow resolves the latest Azure-published Portable Version release from hagicode-steam/index.json. The DLC Steam workflow is also workflow_dispatch, but it does not accept a release input because it always discovers the latest version for every DLC from the DLC root index.
portable-version-steam-release accepts these workflow_dispatch inputs:
release: optional Portable Version release tag to hydrate and publish to Steam. Leave it blank to publish the latest version entry from the Portable Version Azure root index.steam_preview: generate a Steam preview build instead of publishing and setting the beta branch live.steam_branch: Steam branch to set live for non-preview uploads. This defaults tobeta.steam_description: optional Steam build description override.
portable-version-steam-dlc-release accepts these workflow_dispatch inputs:
steam_preview: generate a Steam preview build instead of publishing and setting the beta branch live.steam_branch: Steam branch to set live for non-preview uploads. This defaults tobeta.steam_description: optional Steam build description override.
The DLC workflow does not ask for dlc_name or release. It always downloads the DLC root index.json, enumerates every dlcs[] entry, resolves the latest version of each DLC, and publishes that latest-per-DLC set as one non-interactive Steam run.
Portable Version releases use a readable Web-driven tag:
- canonical format:
<web-tag> - current mapping:
web-tagmeans the selectedservice_tag/PCode.Webpayload version desktop_tagremains available as an optional source-selection override, but it only affects Desktop asset resolution and provenance- normalization rule:
refs/tags/v0.1.0-beta.35,v0.1.0-beta.35, and0.1.0-beta.35all normalize tov0.1.0-beta.35 - direct helper example:
node scripts/resolve-build-plan.mjs --event-name workflow_dispatch --event-path <event-json>still normalizesservice_tag=0.1.0-beta.35anddesktop_tag=refs/tags/v0.1.34into release tagv0.1.0-beta.35
The same Web-only tag is reused for duplicate detection, Azure version directories, root-index entries, dry-run metadata filenames, and Steam hydration.
Portable Version uses four Azure-backed data surfaces:
- Desktop index:
https://index.hagicode.com/desktop/index.json - Server index:
https://index.hagicode.com/server/index.json - Portable Version publication container:
hagicode-steam - DLC publication container: a dedicated Azure Blob container addressed by
PORTABLE_VERSION_DLC_AZURE_SAS_URL
The build-plan helper reads the Desktop and Server manifests, picks the selected version entries, records the matched platform assets, and emits a delegated handoff payload for steam_packer. The delegated workflow then downloads the raw archives by combining:
- the asset
pathfrom the index manifest - the Desktop Azure Blob SAS container URL from
PORTABLE_VERSION_DESKTOP_AZURE_SAS_URL - the Server Azure Blob SAS container URL from
PORTABLE_VERSION_SERVICE_AZURE_SAS_URL
The delegated publication step in steam_packer writes these assets into hagicode-steam/<releaseTag>/:
- Portable Version platform archives such as
hagicode-portable-linux-x64.zip <releaseTag>.build-manifest.json<releaseTag>.artifact-inventory.json<releaseTag>.checksums.txt
After the versioned blobs are visible, steam_packer refreshes hagicode-steam/index.json. Each version entry in that root index carries:
versionmetadata.buildManifestPathmetadata.artifactInventoryPathmetadata.checksumsPathsteamDepotIds.linuxsteamDepotIds.windowssteamDepotIds.macosartifacts[]with per-platform blob-relative paths
Steam publication now treats hagicode-steam/index.json as the only source of truth for hydration and depot resolution.
The DLC publication workflow uses its own root-level index.json as the only source of truth. The document must expose:
updatedAtdlcs[]dlcs[].dlcNamedlcs[].versions[]dlcs[].versions[].versiondlcs[].versions[].steamAppIddlcs[].versions[].steamDepotIds.linuxdlcs[].versions[].steamDepotIds.windowsdlcs[].versions[].steamDepotIds.macosdlcs[].versions[].artifacts[]
Example:
{
"updatedAt": "2026-04-21T03:09:32.3804912Z",
"dlcs": [
{
"dlcName": "turbo-engine",
"versions": [
{
"version": "0.1.0-beta.50",
"steamAppId": "4635479",
"steamDepotIds": {
"linux": "4635482",
"windows": "4635480",
"macos": "4635481"
},
"artifacts": [
{
"name": "hagicode-dlc-turbo-engine-0.1.0-beta.50-linux-x64-nort.zip",
"path": "turbo-engine/0.1.0-beta.50/hagicode-dlc-turbo-engine-0.1.0-beta.50-linux-x64-nort.zip"
},
{
"name": "hagicode-dlc-turbo-engine-0.1.0-beta.50-win-x64-nort.zip",
"path": "turbo-engine/0.1.0-beta.50/hagicode-dlc-turbo-engine-0.1.0-beta.50-win-x64-nort.zip"
},
{
"name": "hagicode-dlc-turbo-engine-0.1.0-beta.50-osx-universal-nort.zip",
"path": "turbo-engine/0.1.0-beta.50/hagicode-dlc-turbo-engine-0.1.0-beta.50-osx-universal-nort.zip"
}
]
}
]
}
]
}Field semantics:
dlcName: stable Steam/DLC identifier. This becomes the per-DLC staging directory name understeam-dlc-content/<dlcName>/.versions[].steamAppId: version-scoped Steam AppID. The DLC Steam workflow groups builds by this value and emits oneapp-build.vdfper AppID.versions[].steamDepotIds: version-scoped Steam depot mapping. The DLC Steam workflow refuses to guess depot ids from repository secrets.artifacts[]: download inventory for that DLC version. The workflow deriveswindows,linux, andmacosstaging from artifact names/paths.
Artifact selection rules:
windowsrequires exactly onewin-x64artifact.linuxrequires exactly onelinux-x64artifact.macosprefers exactly oneosx-universalartifact.- If
osx-universalis absent,macosrequires bothosx-x64andosx-arm64, and both archives are extracted into the samesteam-dlc-content/<dlcName>/macoscontent root.
Any missing latest version, missing depot mapping, or missing required artifact causes the DLC workflow to fail before SteamCMD authentication or upload.
Recommended repository secrets:
PORTABLE_VERSION_DESKTOP_AZURE_SAS_URL: Desktop Azure Blob container SAS URL with at leastReadandListpermissions.PORTABLE_VERSION_SERVICE_AZURE_SAS_URL: Server Azure Blob container SAS URL with at leastReadandListpermissions.PORTABLE_VERSION_STEAM_AZURE_SAS_URL: Azure Blob SAS URL for thehagicode-steamcontainer.portable-versionSteam workflows only needReadandList; packaging/publication writes now happen insteam_packer.PORTABLE_VERSION_DLC_AZURE_SAS_URL: Azure Blob SAS URL for the dedicated DLC container. The DLC Steam workflow needsReadandList.STEAM_USERNAME: Steam build account name.STEAM_PASSWORD: Steam build account password.STEAM_SHARED_SECRET: optional Steam Guard shared secret for fully unattended uploads.STEAM_GUARD_CODE: optional fallback Steam Guard code when a shared secret is not available.
Optional repository variables:
PORTABLE_VERSION_STEAMCMD_ROOT: absolute path on the self-hosted runner where SteamCMD and its persistentconfig/config.vdfshould be stored. Defaults to$HOME/.local/share/portable-version/steamcmd.
PORTABLE_VERSION_STEAMCMD_ROOT is the single durable root for SteamCMD authentication state in both Steam workflows. The workflows install steamcmd.sh into that directory and expect any reusable authentication files to live under the same root, including:
config/config.vdfconfig/loginusers.vdfwhen SteamCMD writes account metadata- root-level
ssfn*files when Steam Guard persistence is available
The publication script now probes that root as a set instead of hard-coding only config/config.vdf. Successful runs write the probe result, initial/final authentication mode, fallback usage, and any failure stage into steam-build-manifest.json, and the workflow summary prefers that manifest output before falling back to a direct root probe.
Directory relocation is supported as long as the entire SteamCMD root moves together. If a self-hosted runner is replaced or the storage mount changes, copy the full PORTABLE_VERSION_STEAMCMD_ROOT directory, then update STEAMCMD_PATH or the repository variable to point to the new absolute location. The script resolves the root from the current steamcmd.sh path, so it does not depend on the old absolute path.
If Steam authentication starts prompting unexpectedly, check these items first:
- The workflow summary or uploaded
steam-build-manifest.jsonforsteamAuthentication.detectedStatePaths,detectionReason,initialMode,finalMode, andfailureStage. - Whether
PORTABLE_VERSION_STEAMCMD_ROOTstill points at the intended persistent directory on the self-hosted runner. - Whether the root still contains
config/config.vdforssfn*after any runner cleanup, home-directory reset, or storage migration. - Whether
STEAM_PASSWORDis still configured, because the script only performs one credentialed refresh attempt when saved-login reuse fails.
Workflow permissions are set to:
portable-version-steam-release:contents: readportable-version-steam-dlc-release:contents: read
The Steam workflow is pinned to a dedicated self-hosted runner with labels self-hosted, Linux, X64, and steam.
The automation currently assumes:
- scheduled builds default to the full platform matrix:
linux-x64,win-x64, andosx-universal - Desktop assets are selected from index
assets[]by platform-specific naming rules. Linux prefers zip fixtures when present and otherwise falls back to the indexed AppImage; Windows uses the published*-unpacked.zip; macOS uses the published zip archives. - Server assets follow the framework-dependent naming contract used by HagiCode releases, for example
hagicode-0.1.0-beta.35-linux-x64-nort.zip. - the selected Server asset extracts to a structure that contains
manifest.json,config/,lib/PCode.Web.dll,lib/PCode.Web.runtimeconfig.json, andlib/PCode.Web.deps.json. - the downloaded Desktop asset already contains
resources/extra/portable-fixed/orContents/Resources/extra/portable-fixed/, and the workflow injects the runtime intocurrent/inside that directory. - delegated packaging in
steam_packerpins the portable toolchain per platform and stages it underportable-fixed/toolchain/, includingnode/,npm-global/,bin/openspec,bin/opsx,env/activate.*, andtoolchain-manifest.json.
Steam publication hydrates its input from an existing Azure-hosted Portable Version release instead of package-job artifacts or GitHub Release assets. portable-version-steam-release now:
- resolves the requested
releaseinput againsthagicode-steam/index.json, or picks the latest available version entry whenreleaseis omitted - downloads the Azure-hosted build manifest, artifact inventory, and checksums referenced by the matched version entry
- downloads each published Portable Version archive referenced by the root index and artifact inventory
- reconstructs
steam-content/<platform>from those archives, usingsteam-content/osx-universalfor the unified macOS depot when available - installs
steamcmdon the dedicated self-hosted runner - generates app and depot VDF scripts under
steam-build/scripts/ - saves the initial SteamCMD login token under the persistent SteamCMD root and reuses that token on future runs
- probes the configured SteamCMD root, records which authentication files were detected, and prefers saved-login reuse before any password-based bootstrap
- derives a Steam Guard code from
STEAM_SHARED_SECRETwhen available, otherwise usesSTEAM_GUARD_CODEif provided - writes
metadata/steam-release-input.jsonwith both the requested selector and the resolved effective release - runs
steamcmd +run_app_buildin preview or publish mode, retrying once with a credentialed refresh if saved-login reuse fails andSTEAM_PASSWORDis available
steam_preview=false uploads the build while setting beta live unless you override steam_branch. steam_preview=true keeps the Steam upload in preview mode so you can validate depot mappings and authentication without pushing a live update; preview runs do not pass setlive even if steam_branch is populated.
If the selected release is missing the build manifest, merged artifact inventory, depot mapping, or one of the required platform archives, the workflow fails before any Steam login happens. The same fail-fast rule applies when release is omitted but the Azure root index is empty or malformed. That usually means the Azure root index entry is incomplete or the Azure version directory is only partially published and should be republished first.
The DLC Steam publication flow is separate and latest-driven. portable-version-steam-dlc-release now:
- downloads the dedicated DLC root
index.json - enumerates every
dlcs[]entry and resolves the latest version per DLC - validates that each latest version contains
steamAppId, completesteamDepotIds, and requiredartifacts[] - downloads and extracts the selected DLC archives into
steam-dlc-content/<dlcName>/linux,steam-dlc-content/<dlcName>/windows, andsteam-dlc-content/<dlcName>/macos - writes
metadata/steam-dlc-release-input.jsonwithdlcName,dlcVersion,steamAppId,steamDepotIds,selectedArtifacts,preparedPlatforms, and content roots for every DLC - groups discovered DLCs by
steamAppId, generates oneapp-build.vdfper AppID, and publishes those builds sequentially - fails before SteamCMD login whenever any discovered DLC is incomplete
The generated DLC release-input metadata is intentionally explicit so operators can inspect exactly which DLC versions were discovered and which archives were selected for each platform family.
Run the helper tests from the repository root for portable-version:
npm testThese tests cover version resolution, Azure index hydration, Steam/DLC preparation, and publication helpers that still live in portable-version.
Use repos/steam_packer for delegated packaging and Azure publication verification:
cd ../steam_packer
npm test
npm run verify:dry-runPortable Version publication no longer treats GitHub Release as a source of truth.
Portable Version also no longer owns the packaging or Azure publication implementation. Those responsibilities moved to steam_packer, while this repository keeps version resolution, handoff generation, trigger orchestration, and Steam publication entrypoints.
Removed assumptions:
- Portable Version release hydration from GitHub Release assets
- GitHub Release duplicate detection for the primary build workflow
- DLC root-index lookup for the main application's Steam depot mappings
- repository-level
STEAM_DEPOT_ID_*secrets at Steam publication time - manual
dlc_nameselection for DLC Steam publication
If you have external tooling that consumed GitHub Release assets, migrate it to:
hagicode-steam/index.jsonhagicode-steam/<releaseTag>/<releaseTag>.build-manifest.jsonhagicode-steam/<releaseTag>/<releaseTag>.artifact-inventory.jsonhagicode-steam/<releaseTag>/<releaseTag>.checksums.txthagicode-steam/<releaseTag>/<portable-archive>.zip
Use these recovery paths when a workflow run fails or must be replayed:
- For packaging or Azure publication replay, run the corresponding workflow or script in
repos/steam_packer. - If a specific upstream build must be replayed with normalized selectors, regenerate the handoff plan via
node scripts/resolve-build-plan.mjsand pass that plan intosteam_packer. - Inspect the uploaded workflow artifacts:
portable-release-metadata-<release-tag>portable-steam-release-preparation-<release-tag>portable-steam-build-metadata-<release-tag>
- For delegated packaging failures, inspect the
steam_packerworkflow jobs and delegated summary output before changing anything inportable-version. - Review the workflow summary for the exact selector mismatch, delegated packaging failure, Azure upload failure, root-index refresh failure, archive hydration failure, Steam authentication issue, or SteamCMD publication error.
Each successful build publishes:
- one deterministic Portable Version version directory in the
<web-tag>namespace underhagicode-steam/ - repacked Desktop artifacts copied to deterministic asset names such as
hagicode-portable-linux-x64.zip - the normalized build manifest
- merged artifact inventory metadata
- merged SHA-256 checksums
- one root-index entry containing
metadata.*,steamDepotIds.*, andartifacts[] - one toolchain validation report per platform, proving the bundled
node,openspec, andopsxcommands executed successfully before publication For DLC publication, no repository-level app id secret is used. Each DLC latest version must provide its ownsteamAppIdin the DLC rootindex.json.