@@ -21,13 +21,9 @@ concurrency:
2121 cancel-in-progress : false
2222
2323jobs :
24- # Single source of truth: the variant matrix lives in .github/variants.json.
25- # This job emits TWO matrices from it:
26- # - sources: one entry per distinct Steam (appId, branch) "source". The
27- # game is downloaded ONCE per source (see download_sources).
28- # - variants: the flat per-variant list (see update_variant), each of which
29- # reuses its source's single download.
30- # To add/remove a variant, edit .github/variants.json only.
24+ # Emits two matrices from .github/variants.json (the single source of truth):
25+ # `sources` (one per Steam appId+branch, downloaded once) and `variants` (flat
26+ # per-variant list). Add/remove variants by editing variants.json.
3127 load :
3228 name : " Load matrices"
3329 runs-on : ubuntu-latest
4339 env :
4440 INPUT_VARIANT : ${{ github.event.inputs.variant }}
4541 EVENT_NAME : ${{ github.event_name }}
46- # Opt-in: set this repo variable to 'true' once a SECOND Steam account
47- # (STEAM_USERNAME_2 / STEAM_PASSWORD_2) exists. Until then the two client
48- # sources stay on one account, serialized — exactly as before.
42+ # Opt-in 2nd Steam account (see ARCHITECTURE.md); empty/false = unchanged.
4943 STEAM_SECOND_ACCOUNT_ENABLED : ${{ vars.STEAM_SECOND_ACCOUNT_ENABLED }}
5044 run : |
5145 set -euo pipefail
@@ -60,22 +54,10 @@ jobs:
6054 base=$(jq -c '.' .github/variants.json)
6155 fi
6256
63- # Group the (possibly filtered) variants by their Steam source. The
64- # source label "<appId>-<branch|default>" is reused as the artifact
65- # name that ties download_sources -> update_variant together.
66- #
67- # Per source we also derive (here, where vars + jq are easy, so
68- # download_sources stays dumb):
69- # steamAccount credential set to log in with: 0=anonymous, 1=primary
70- # (STEAM_USERNAME). The authenticated PREVIEW client source
71- # becomes 2 (STEAM_USERNAME_2) ONLY when a second account is
72- # enabled, so it logs into a DIFFERENT account than the
73- # default client source and the two can run concurrently.
74- # dlgroup download concurrency group. Anonymous sources — and, when
75- # the 2nd account is on, every source — get a unique group
76- # (parallel). Otherwise the two client sources share
77- # "authenticated" (serialized): concurrent logins to ONE
78- # account fail with AlreadyLoggedInElsewhere.
57+ # Group variants by Steam source (appId+branch); the source label doubles as
58+ # the artifact name linking download_sources -> update_variant. Also derive per
59+ # source: steamAccount (0=anon, 1=primary, 2=2nd account for the preview client)
60+ # and dlgroup (download concurrency group). See ARCHITECTURE.md.
7961 second="${STEAM_SECOND_ACCOUNT_ENABLED:-false}"
8062 sources=$(printf '%s' "$base" | jq -c --arg second "$second" '
8163 group_by(.appId + "|" + .branch)
@@ -101,11 +83,9 @@ jobs:
10183 echo "Sources to probe:"
10284 printf '%s' "$sources" | jq -r '.[] | " \(.source) (variants: \(.variants))"'
10385
104- # One job per distinct Steam (appId, branch) source. Probe the manifest once;
105- # if it changed, download the game ONCE and publish just the bits the redist
106- # tool needs (Managed DLLs + appmanifest + Status.json) as an artifact that
107- # every variant of this source reuses. This is where the only Steam logins
108- # happen, so it's the only place that needs serializing.
86+ # One job per Steam source: probe the manifest once; if changed, download the game
87+ # once and upload the bits the tool needs (Managed DLLs + appmanifest + Status.json)
88+ # as an artifact every variant of this source reuses. The only Steam logins happen here.
10989 download_sources :
11090 name : " Download ${{ matrix.source }}"
11191 runs-on : ubuntu-latest
@@ -114,15 +94,9 @@ jobs:
11494 fail-fast : false
11595 matrix :
11696 include : ${{ fromJson(needs.load.outputs.sources) }}
117- # The download concurrency group is precomputed per source by the load job
118- # (matrix.dlgroup). Anonymous (server) sources always get a unique group → run
119- # in parallel. By default the two authenticated (client) sources share ONE Steam
120- # account, so they share the "authenticated" group → serialized (concurrent
121- # logins to one account fail with AlreadyLoggedInElsewhere). With only two
122- # members a concurrency group keeps 1 running + 1 pending and never cancels (the
123- # 3+ case GitHub does cancel cannot occur). When a 2nd Steam account is enabled
124- # (vars.STEAM_SECOND_ACCOUNT_ENABLED == 'true') the preview client source logs
125- # into account 2 and gets its own group → the two client downloads run parallel.
97+ # Per-source download group (matrix.dlgroup): server sources run parallel; the two
98+ # client sources share one account so they're serialized, unless a 2nd account is
99+ # enabled (then each gets its own group). See ARCHITECTURE.md.
126100 concurrency :
127101 group : redist-dl-${{ matrix.dlgroup }}
128102 cancel-in-progress : false
@@ -165,9 +139,7 @@ jobs:
165139 beta_args=(-beta "$APP_BRANCH_NAME")
166140 fi
167141
168- # steamAccount picks the credential set: 2 = the optional second account
169- # (STEAM_USERNAME_2), used by the preview client source so it logs into a
170- # DIFFERENT account than the default client source; otherwise the primary.
142+ # account 2 = optional 2nd Steam account (preview client); see ARCHITECTURE.md.
171143 if [ "$STEAM_ACCOUNT" = "2" ]; then
172144 STEAM_USERNAME="$STEAM_USERNAME_2"
173145 STEAM_PASSWORD="$STEAM_PASSWORD_2"
@@ -185,12 +157,9 @@ jobs:
185157 auth_args+=(-username "$STEAM_USERNAME" -password "$STEAM_PASSWORD")
186158 fi
187159
188- # DepotDownloader can drop its Steam connection mid-probe ("Lost
189- # connection to Steam" / "A task was canceled") and exit WITHOUT a
190- # "Manifest <id>" line — a known transient (SteamRE/DepotDownloader
191- # #223, #691, incl. the pinned 3.4.0), not a real auth/format failure.
192- # Mirror the steamcmd retry in "Download game files": retry the fetch and
193- # gate on a valid id; only fail loudly once all attempts are exhausted.
160+ # DepotDownloader can drop its connection mid-probe and emit no "Manifest <id>"
161+ # line — a known transient (SteamRE/DepotDownloader #223, #691). Retry, gate on
162+ # a valid id, fail loud only when exhausted.
194163 current_manifest=""
195164 manifest_output=""
196165 attempts=4
@@ -205,9 +174,7 @@ jobs:
205174 "${beta_args[@]}" \
206175 -dir redist/temp_depots || true)
207176
208- # Prints e.g. "Manifest 763708736677468005 (date)". Manifest ids are
209- # 64-bit, so their length varies (~17-20 digits) — grab the number
210- # after the "Manifest" keyword rather than assuming a fixed width.
177+ # Grab the number after "Manifest" (64-bit id, ~17-20 digits, variable width).
211178 current_manifest=$(printf '%s\n' "$manifest_output" | grep -oiE 'manifest[[:space:]]+[0-9]+' | grep -oE '[0-9]+' | head -n 1 || true)
212179 if [[ "$current_manifest" =~ ^[0-9]{10,20}$ ]]; then
213180 break
@@ -224,9 +191,7 @@ jobs:
224191 echo "Current manifest: $current_manifest"
225192 echo "current_manifest=$current_manifest" >> "$GITHUB_OUTPUT"
226193
227- # This source needs a download if ANY of its variants' recorded
228- # manifest ids differs from the live one (or is missing). They normally
229- # all match; "any differs" lets a lagging variant catch up.
194+ # Download if ANY variant's recorded manifest id differs from the live one.
230195 changed=false
231196 IFS=',' read -ra source_variants <<< "$VARIANTS"
232197 for v in "${source_variants[@]}"; do
@@ -276,11 +241,8 @@ jobs:
276241 if [ -n "$APP_BRANCH_NAME" ]; then
277242 beta_args=(-beta "$APP_BRANCH_NAME")
278243 fi
279- # steamcmd is flaky: it can exit non-zero on success and fail downloads
280- # transiently. Its exit code isn't reliable, so we RETRY and gate on the
281- # real artifact — steamcmd only writes appmanifest_<appid>.acf once the
282- # update completes (the same "no appmanifest -> force update" remedy
283- # LinuxGSM uses). The redist tool needs that file downstream.
244+ # steamcmd's exit code is unreliable, so retry and gate on the real artifact:
245+ # appmanifest_<appid>.acf, written only on a completed update and needed by the tool.
284246 manifest_present() {
285247 find "$GITHUB_WORKSPACE" -name "appmanifest_${APP_ID}.acf" -print -quit 2>/dev/null | grep -q .
286248 }
@@ -304,9 +266,8 @@ jobs:
304266 if : steps.probe.outputs.source_changed == 'true'
305267 run : echo "${{ steps.probe.outputs.current_manifest }}" > source-manifest.txt
306268
307- # Publish only the small bits the redist tool reads — NOT the multi-GB game.
308- # The artifact's presence is also the "this source changed" signal that
309- # update_variant keys off (a missing artifact => unchanged => skip).
269+ # Upload only the small bits the tool reads (not the multi-GB game). The
270+ # artifact's presence is also the "source changed" signal update_variant keys off.
310271 - name : Upload source files
311272 if : steps.probe.outputs.source_changed == 'true'
312273 uses : actions/upload-artifact@v7
@@ -321,25 +282,20 @@ jobs:
321282 if-no-files-found : warn
322283 retention-days : 1
323284
324- # One job per variant, FULLY PARALLEL: these never touch Steam — each pulls its
325- # source's artifact (the single download above), runs the redist tool, and opens
326- # its rolling PR. No Steam login here means no account contention, so there is
327- # nothing to serialize.
285+ # One job per variant, fully parallel: pull the source artifact, run the tool, open
286+ # the rolling PR. No Steam login here, so nothing to serialize.
328287 update_variant :
329288 name : " Update ${{ matrix.variant }}"
330289 runs-on : ubuntu-latest
331290 needs : [load, download_sources]
332- # Job-level permissions REPLACE the workflow defaults, so this must list
333- # everything GITHUB_TOKEN needs here: contents (checkout), and actions:read
334- # for the "list this run's artifacts" gate. (The PR itself is created with
335- # secrets.PAT, independent of these.)
291+ # Job-level permissions REPLACE workflow defaults: contents (checkout) + actions:read
292+ # (the artifacts gate). The PR is created with secrets.PAT, independent of these.
336293 permissions :
337294 contents : write
338295 pull-requests : write
339296 actions : read
340- # Run even if some source download failed (those variants just find no
341- # artifact and skip); notify-failure still flags the failed source. Skip only
342- # if the load job itself failed (then there's no matrix to run).
297+ # Run even if a source download failed (those variants find no artifact and skip);
298+ # only skip if load itself failed.
343299 if : ${{ !cancelled() && needs.load.result == 'success' }}
344300 strategy :
345301 fail-fast : false
@@ -372,12 +328,8 @@ jobs:
372328 SOURCE_KEY : ${{ steps.ids.outputs.source_key }}
373329 run : |
374330 set -euo pipefail
375- # Fail LOUD on an API error. The ONLY thing that may mean "this source
376- # is unchanged" is a SUCCESSFUL artifacts listing that doesn't contain
377- # our artifact. If the gh call itself fails (transient 5xx / network /
378- # rate-limit), we must NOT treat that as "unchanged" — that would
379- # silently skip a genuinely-changed source under a green run, and
380- # notify-failure (failure()-only) would never fire. Retry, then fail.
331+ # Fail LOUD on API error: only a SUCCESSFUL listing without our artifact means
332+ # "unchanged". A failed gh call must NOT be read as unchanged. Retry, then fail.
381333 ok=false
382334 names=""
383335 for attempt in 1 2 3; do
@@ -469,9 +421,8 @@ jobs:
469421 fi
470422 printf '%s\n' "$tool_output"
471423
472- # Record the manifest id this build was produced from so the next probe
473- # sees "no change". Lives outside REDIST_DIR so the discard step below never
474- # reverts it, even when no package is published.
424+ # Record the manifest id so the next probe sees "no change". Outside REDIST_DIR so
425+ # the discard step never reverts it.
475426 - name : Record source manifest id
476427 if : steps.gate.outputs.changed == 'true'
477428 env :
@@ -498,14 +449,9 @@ jobs:
498449 echo "COMMIT_EOF"
499450 } >> "$GITHUB_OUTPUT"
500451 else
501- # No .commit means the tool did NOT bump the version: the Steam build /
502- # depot manifest changed, but the game version (X.Y.Z.N) did not. We
503- # cannot republish the same NuGet version, so the regenerated DLLs are
504- # unpublishable (would 409). Discard them in the variant dir and keep
505- # ONLY the corrected Steam manifest id (recorded above) so this build is
506- # logged and not re-processed every run. The real DLLs publish on the
507- # next game-version bump. (A tool failure already failed the step above,
508- # so a missing .commit here is a legitimate no-op, not a silent error.)
452+ # No .commit => version unchanged (Steam build moved but game version didn't).
453+ # Can't republish the same NuGet version, so discard the regenerated DLLs and
454+ # keep only the recorded manifest id; the DLLs publish on the next version bump.
509455 git checkout -- "$REDIST_DIR" 2>/dev/null || true
510456 echo "message=Record ${{ matrix.variant }} Steam manifest (game version unchanged; no package update)" >> "$GITHUB_OUTPUT"
511457 fi
@@ -522,9 +468,8 @@ jobs:
522468 echo "has_git_changes=true" >> "$GITHUB_OUTPUT"
523469 fi
524470
525- # peter-evans/create-pull-request commits to a fixed-name branch and keeps
526- # the open PR continually updated until it is merged/closed; delete-branch
527- # cleans it up afterwards. No timestamped branches or existing-PR lookups.
471+ # Commits to a fixed-name branch and keeps the rolling PR updated until merged;
472+ # delete-branch cleans up afterwards.
528473 - name : Create Pull Request
529474 if : steps.gate.outputs.changed == 'true' && steps.check_git_changes.outputs.has_git_changes == 'true'
530475 id : create_pr
@@ -581,9 +526,8 @@ jobs:
581526 fi
582527 } >> "$GITHUB_STEP_SUMMARY"
583528
584- # Make silent breakage visible: if any scheduled source download or variant
585- # update fails, open (or comment on) a tracking issue instead of relying on
586- # someone watching the Actions tab.
529+ # On any scheduled-run failure, open (or comment on) a tracking issue so breakage
530+ # isn't silent.
587531 notify-failure :
588532 name : " Notify on failure"
589533 needs : [load, download_sources, update_variant]
@@ -602,9 +546,7 @@ jobs:
602546 set -euo pipefail
603547 marker="Redist auto-update workflow failed"
604548 body=$(printf '⚠️ The scheduled redist update workflow failed.\n\nRun: %s\n\nA source download (Steam auth/network, DepotDownloader, steamcmd) or a variant update (the redist tool or PR creation) errored. Check the run for the failing job.' "$RUN_URL")
605- # --repo is required: this job has no checkout, so gh can't infer the
606- # repo from a git remote (and gh does NOT read GITHUB_REPOSITORY). Without
607- # it the label is never created and the gh issue create --label below fails.
549+ # --repo is required here: no checkout, and gh doesn't read GITHUB_REPOSITORY.
608550 gh label create update-failure --repo "$REPO" --color B60205 --description "Automated redist update failure" --force >/dev/null 2>&1 || true
609551 existing=$(gh issue list --repo "$REPO" --state open --search "in:title \"$marker\"" --json number --jq '.[0].number // empty')
610552 if [ -n "$existing" ]; then
@@ -616,14 +558,10 @@ jobs:
616558 workflow-keepalive :
617559 if : github.event_name == 'schedule'
618560 runs-on : ubuntu-latest
619- # GitHub disables scheduled workflows after 60 days of repo inactivity; this job
620- # re-enables this workflow on each scheduled run to prevent that. Inlined (was
621- # liskin/gh-workflow-keepalive) so we don't depend on an external, unmaintained
622- # action for a one-line gh-api call that runs with actions:write — same approach
623- # the action used (PUT .../enable), no dummy commits. The default token
624- # occasionally returns a transient 401 here; keepalive is non-critical and
625- # self-healing (the next scheduled run re-enables), so continue-on-error keeps a
626- # blip from reddening an otherwise-green run.
561+ # Re-enable this scheduled workflow (GitHub disables them after 60 days idle).
562+ # Inlined gh-api call, no external dependency. continue-on-error: the default token
563+ # occasionally 401s here; keepalive is non-critical and self-healing, so a blip
564+ # must not redden the run.
627565 continue-on-error : true
628566 permissions :
629567 actions : write
0 commit comments